feat(distribution): 优化一键同步主站商品功能(并发防重、分批入库、失效商品联动下架与初始加价)
- 后端优化: - 引入 Redis + 内存 sync.Map 双重互斥锁,避免并发/连击导致唯一索引冲突 (ErrSyncInProgress) - 使用 CreateInBatches(500) 进行大数据量批量插入,杜绝超长 SQL 与锁表超时 - 增加 CleanStaleListings 支持,自动排查并联动下架主站非在售的货架商品,清理历史幽灵数据 - 增加 DefaultShelfRatioDelta 支持,新同步商品直接继承初始加价比例,免除二次手动改价 - 前端优化: - distribution-admin 货架管理新增一键同步配置弹窗(可勾选自动下架失效商品、配置初始加价) - 同步完成后展示结构化统计弹窗(新增、保持、联动下架、主站在售总量) - 前后端类型定义与 API 客户端对齐升级 - 测试覆盖: - 补充 TestSyncSiteAccountsOptimizations 单元测试与 handler 集成测试 - 全工程测试与门禁 check.sh 100% 通过
This commit is contained in:
@@ -441,7 +441,7 @@ func TestSyncSiteAccountsAndPublicListings(t *testing.T) {
|
||||
}
|
||||
|
||||
// 默认同步全网合规商品(包含主站商品与本站上传货)
|
||||
resMain, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, nil)
|
||||
resMain, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("SyncSiteAccounts err = %v", err)
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func TestSyncSiteAccountsAndPublicListings(t *testing.T) {
|
||||
t.Fatalf("unexpected sync res: %+v", resMain)
|
||||
}
|
||||
|
||||
res2, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, nil)
|
||||
res2, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("SyncSiteAccounts retry err = %v", err)
|
||||
}
|
||||
@@ -457,11 +457,11 @@ func TestSyncSiteAccountsAndPublicListings(t *testing.T) {
|
||||
t.Fatalf("unexpected res2: %+v", res2)
|
||||
}
|
||||
|
||||
if _, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, []uint64{999999}); !errors.Is(err, ErrSyncListingNotFound) {
|
||||
if _, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{ListingIDs: []uint64{999999}}); !errors.Is(err, ErrSyncListingNotFound) {
|
||||
t.Fatalf("missing listing_ids err = %v", err)
|
||||
}
|
||||
|
||||
resSelect, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, []uint64{mainListing.ID})
|
||||
resSelect, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{ListingIDs: []uint64{mainListing.ID}})
|
||||
if err != nil {
|
||||
t.Fatalf("explicit select err = %v", err)
|
||||
}
|
||||
@@ -1209,3 +1209,81 @@ func TestGetSiteWalletPendingReleaseExcludesOpenDispute(t *testing.T) {
|
||||
t.Fatalf("pending release with open dispute = %d, want 0", walletDTO.PendingReleaseCent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSiteAccountsOptimizations(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
site, err := repo.CreateSite(t.Context(), CreateSiteRequest{
|
||||
SiteCode: "sync_opt_site",
|
||||
SiteName: "同步优化分站",
|
||||
Domain: "sync-opt.hfb.local",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
acc1 := model.GameAccount{Title: "商品1", GameName: "cfhd", ServerRegion: "东部", HafCoinAmount: 50_000_000, Status: "active"}
|
||||
acc2 := model.GameAccount{Title: "商品2", GameName: "cfhd", ServerRegion: "北部", HafCoinAmount: 60_000_000, Status: "active"}
|
||||
db.Create(&acc1)
|
||||
db.Create(&acc2)
|
||||
|
||||
l1 := model.RentalListing{ListingNo: "L-SYNC-OPT-1", AccountID: acc1.ID, OwnerID: 1, PriceCent: 3000, Status: "published", ReviewStatus: "approved"}
|
||||
l2 := model.RentalListing{ListingNo: "L-SYNC-OPT-2", AccountID: acc2.ID, OwnerID: 1, PriceCent: 4000, Status: "published", ReviewStatus: "approved"}
|
||||
db.Create(&l1)
|
||||
db.Create(&l2)
|
||||
|
||||
// 1. 验证负数加价比例校验
|
||||
if _, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{DefaultShelfRatioDelta: -5}); !errors.Is(err, ErrInvalidAmount) {
|
||||
t.Fatalf("expected ErrInvalidAmount for negative delta, got: %v", err)
|
||||
}
|
||||
|
||||
// 2. 同步且设置初始加价 DefaultShelfRatioDelta = 15.5
|
||||
res, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
|
||||
DefaultShelfRatioDelta: 15.5,
|
||||
CleanStaleListings: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SyncSiteAccounts failed: %v", err)
|
||||
}
|
||||
if res.SyncedCount != 2 || res.TotalActive != 2 || res.DisabledStaleCount != 0 {
|
||||
t.Fatalf("unexpected res: %+v", res)
|
||||
}
|
||||
|
||||
// 验证货架商品是否正确写入初始加价
|
||||
var dsa1 model.DistributionSiteAccount
|
||||
if err := db.Where("site_id = ? AND listing_id = ?", site.ID, l1.ID).First(&dsa1).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dsa1.ShelfRatioDelta != 15.5 || dsa1.DisplayStatus != "enabled" {
|
||||
t.Fatalf("dsa1 mismatch: ratio=%v, status=%s", dsa1.ShelfRatioDelta, dsa1.DisplayStatus)
|
||||
}
|
||||
|
||||
// 3. 验证主站商品下架联动(CleanStaleListings = true)
|
||||
db.Model(&l1).Update("status", "offline")
|
||||
|
||||
resClean, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
|
||||
CleanStaleListings: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SyncSiteAccounts clean failed: %v", err)
|
||||
}
|
||||
if resClean.DisabledStaleCount != 1 {
|
||||
t.Fatalf("expected 1 disabled stale listing, got %d", resClean.DisabledStaleCount)
|
||||
}
|
||||
|
||||
// 验证 dsa1 已被标记为 disabled
|
||||
var dsa1After model.DistributionSiteAccount
|
||||
db.First(&dsa1After, dsa1.ID)
|
||||
if dsa1After.DisplayStatus != "disabled" {
|
||||
t.Fatalf("expected dsa1 disabled, got: %s", dsa1After.DisplayStatus)
|
||||
}
|
||||
|
||||
// 4. 验证并发互斥锁(ErrSyncInProgress)
|
||||
repo.siteSyncLocks.Store(site.ID, struct{}{})
|
||||
_, err = repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{})
|
||||
if !errors.Is(err, ErrSyncInProgress) {
|
||||
t.Fatalf("expected ErrSyncInProgress, got %v", err)
|
||||
}
|
||||
repo.siteSyncLocks.Delete(site.ID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user