fix(distribution): 修正商品归属回填、禁止全站灌货并拒绝无效站点头
用货架写入时间与上传来源回滚误标产地,不再依赖绝对时间戳;同步仅收本站上传与显式勾选。
This commit is contained in:
@@ -55,6 +55,8 @@ func writeListingError(c *gin.Context, err error) {
|
||||
response.BadRequest(c, "外部商品调整参数不正确")
|
||||
case errors.Is(err, ErrSaleRatioOutOfRange):
|
||||
response.BadRequest(c, "发布比例必须在参考比例至参考比例加 10 之间")
|
||||
case errors.Is(err, ErrDistributionSiteInvalid):
|
||||
response.BadRequest(c, "分销站点无效或已停用")
|
||||
case isUploadValidationError(err):
|
||||
response.BadRequest(c, err.Error())
|
||||
case isFireLevelTooLow(err):
|
||||
|
||||
@@ -22,6 +22,22 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
|
||||
if err := r.ensureCreateCooldown(tx, ownerID, publishCooldown, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var site model.DistributionSite
|
||||
var siteID *uint64
|
||||
originChannel := ""
|
||||
if len(siteCode) > 0 && strings.TrimSpace(siteCode[0]) != "" {
|
||||
trimmedSiteCode := strings.TrimSpace(siteCode[0])
|
||||
if err := tx.Where("site_code = ? AND status = 'active'", trimmedSiteCode).First(&site).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrDistributionSiteInvalid
|
||||
}
|
||||
return err
|
||||
}
|
||||
siteID = &site.ID
|
||||
originChannel = site.SiteCode
|
||||
}
|
||||
|
||||
listingNo, err := r.nextListingNo(tx, now)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -35,6 +51,7 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
|
||||
return err
|
||||
}
|
||||
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
|
||||
|
||||
account := model.GameAccount{
|
||||
OwnerID: ownerID,
|
||||
GameName: "delta_force",
|
||||
@@ -52,15 +69,6 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
|
||||
return err
|
||||
}
|
||||
|
||||
var site model.DistributionSite
|
||||
var siteID *uint64
|
||||
if len(siteCode) > 0 && strings.TrimSpace(siteCode[0]) != "" {
|
||||
trimmedSiteCode := strings.TrimSpace(siteCode[0])
|
||||
if err := tx.Where("site_code = ? AND status = 'active'", trimmedSiteCode).First(&site).Error; err == nil {
|
||||
siteID = &site.ID
|
||||
}
|
||||
}
|
||||
|
||||
priceCent := normalizedListingPriceCent(req)
|
||||
listing := model.RentalListing{
|
||||
ListingNo: listingNo,
|
||||
@@ -72,6 +80,7 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
|
||||
ReviewStatus: reviewStatus,
|
||||
PublishedAt: publishedAt,
|
||||
DistributionSiteID: siteID,
|
||||
SourceChannel: originChannel,
|
||||
}
|
||||
if err := tx.Create(&listing).Error; err != nil {
|
||||
return err
|
||||
@@ -203,6 +212,7 @@ func (r *Repository) CreateFromExternalUpload(ctx context.Context, upload extern
|
||||
HandoffMode: "platform",
|
||||
SettlementMode: "platform_managed",
|
||||
ManagedAdminID: &admin.ID,
|
||||
SourceChannel: upload.SourceChannel,
|
||||
}
|
||||
if err := tx.Create(&listing).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,7 +18,7 @@ type listingRow struct {
|
||||
Title string
|
||||
OwnerPhone string
|
||||
OwnerNickname string
|
||||
SourceChannel string `gorm:"column:source_channel"`
|
||||
DisplaySourceChannel string `gorm:"column:display_source_channel"`
|
||||
IsExternalUpload bool `gorm:"column:is_external_upload"`
|
||||
DistributionSiteCode string `gorm:"column:distribution_site_code"`
|
||||
DistributionSiteName string `gorm:"column:distribution_site_name"`
|
||||
@@ -171,7 +171,7 @@ func (row listingRow) toDTO() ListingDTO {
|
||||
OwnerID: row.OwnerID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
OwnerNickname: row.OwnerNickname,
|
||||
SourceChannel: row.SourceChannel,
|
||||
SourceChannel: row.DisplaySourceChannel,
|
||||
IsExternalUpload: row.IsExternalUpload,
|
||||
Title: row.Title,
|
||||
Description: row.Description,
|
||||
|
||||
@@ -233,7 +233,7 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
WHEN lu.id IS NULL THEN ?
|
||||
WHEN COALESCE(lu.source_channel, '') = '' THEN ?
|
||||
ELSE lu.source_channel
|
||||
END AS source_channel`, sourceChannelWebsite, sourceChannelExternalUnknown).
|
||||
END AS display_source_channel`, sourceChannelWebsite, sourceChannelExternalUnknown).
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
||||
Joins("LEFT JOIN users AS u ON u.id = l.owner_id").
|
||||
Joins("LEFT JOIN distribution_sites AS src_site ON src_site.id = l.distribution_site_id").
|
||||
|
||||
@@ -32,6 +32,7 @@ var (
|
||||
ErrExternalListingRequired = errors.New("external listing required")
|
||||
ErrExternalAdjustment = errors.New("invalid external listing adjustment")
|
||||
ErrSaleRatioOutOfRange = errors.New("sale ratio out of range")
|
||||
ErrDistributionSiteInvalid = errors.New("invalid distribution site")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
||||
@@ -891,6 +891,14 @@ func TestCreateListingWithDistributionSiteCode(t *testing.T) {
|
||||
t.Fatalf("expected enabled, got %s", dsa.DisplayStatus)
|
||||
}
|
||||
|
||||
var stored model.RentalListing
|
||||
if err := db.First(&stored, dto.ID).Error; err != nil {
|
||||
t.Fatalf("load listing: %v", err)
|
||||
}
|
||||
if stored.SourceChannel != site.SiteCode {
|
||||
t.Fatalf("stored source_channel = %q, want %q", stored.SourceChannel, site.SiteCode)
|
||||
}
|
||||
|
||||
// 验证审核工作台列表展示分销站来源
|
||||
pending, err := repo.ListPendingReview(context.Background())
|
||||
if err != nil {
|
||||
@@ -904,6 +912,43 @@ func TestCreateListingWithDistributionSiteCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateListingInvalidDistributionSiteCode(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||
t.Fatalf("migrate err: %v", err)
|
||||
}
|
||||
repo := NewRepository(db, nil)
|
||||
|
||||
user := model.User{Phone: "18800000011", Nickname: "分销卖家", RealnameStatus: "verified", Status: "active"}
|
||||
db.Create(&user)
|
||||
|
||||
inactive := model.DistributionSite{
|
||||
SiteCode: "dist_inactive",
|
||||
SiteName: "已停用分销站",
|
||||
Domain: "inactive.example.com",
|
||||
Status: "disabled",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
db.Create(&inactive)
|
||||
|
||||
req := CreateRequest{Title: "无效站点商品", ServerRegion: "QQ", LoginPlatform: "PC", PriceCent: 1000}
|
||||
|
||||
if _, err := repo.Create(context.Background(), user.ID, req, true, 0, "does_not_exist"); !errors.Is(err, ErrDistributionSiteInvalid) {
|
||||
t.Fatalf("unknown site err = %v, want ErrDistributionSiteInvalid", err)
|
||||
}
|
||||
if _, err := repo.Create(context.Background(), user.ID, req, true, 0, "dist_inactive"); !errors.Is(err, ErrDistributionSiteInvalid) {
|
||||
t.Fatalf("inactive site err = %v, want ErrDistributionSiteInvalid", err)
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.Model(&model.RentalListing{}).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("invalid site must not create listing, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMineSiteIsolation(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user