fix(distribution): 修正商品归属回填、禁止全站灌货并拒绝无效站点头

用货架写入时间与上传来源回滚误标产地,不再依赖绝对时间戳;同步仅收本站上传与显式勾选。
This commit is contained in:
yml2213
2026-09-08 17:30:12 +08:00
parent 96bc6a4db6
commit 31bf6dd3b6
15 changed files with 287 additions and 84 deletions
@@ -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):
+19 -9
View File
@@ -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,
+1 -1
View File
@@ -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 {