fix(distribution): 修复分站搜不到已审核商品并实现审核通过自动铺货

问题:分站上架商品审核通过后主站能搜到、分站看不到,且没有自动同步。

根因:分站前台/搜索只认 distribution_site_accounts 中 display_status='enabled'
的货架行,而主站前台不按站点过滤。货架行此前只由「号主在该站上架」或站长手动
「一键同步主站商品」写入,审核通过不回写;同时一键同步的失效清理把「非
published+approved」的货架行一律置为 disabled,商品重新在售后也不会恢复。

- 新增 EnsureListingShelvedTx:商品变为 published+approved 时自动铺到所有
  display_account_enabled=true 的站点货架(缺行新增、系统下架恢复、人工下架不动),
  经 ListingShelfSyncer 接口注入,避免 listing 反向依赖 distribution
- 新增 distribution_site_accounts.display_disabled_by(operator/system/空)区分下架
  来源;人工上下架各入口写入来源标记
- 一键同步的失效清理收紧为只清理「货源记录已删除」的孤儿行,并支持恢复系统下架行,
  结果新增 reenabled_count
- 新增迁移 000074 回填存量缺失货架行,部署后无需逐站手动同步
- 修复 CreateSite 显式传 false/0 的配置(不允许自动展示、结算冷却 0 天)被 GORM
  default 标签忽略而静默回落默认值
- 前端:去掉会跨站串扰的 localStorage 站点码回退,站点探测完成后再挂载应用,
  避免分站发布因缺少站点码被记成主站商品;同步弹窗文案与统计项同步更新
- 补充分销货架自动铺货、清理口径、建站零值落库与审核铺货回归测试
This commit is contained in:
yml2213
2026-09-30 21:04:05 +08:00
parent 940510a826
commit f8d3053b5f
21 changed files with 716 additions and 47 deletions
@@ -1308,7 +1308,50 @@ func TestSyncSiteAccountsOptimizations(t *testing.T) {
t.Fatalf("dsa1 mismatch: ratio=%v, status=%s", dsa1.ShelfRatioDelta, dsa1.DisplayStatus)
}
// 3. 验证主站商品下架联动(CleanStaleListings = true)
// 3. 站长人工下架属于选品决策,再次同步不得覆盖
var dsa1After model.DistributionSiteAccount
if _, err := repo.UpdateAccountDisplay(t.Context(), 0, dsa1.ID, "disabled"); err != nil {
t.Fatalf("UpdateAccountDisplay failed: %v", err)
}
resKeep, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
CleanStaleListings: true,
})
if err != nil {
t.Fatalf("SyncSiteAccounts keep failed: %v", err)
}
if resKeep.ReenabledCount != 0 {
t.Fatalf("人工下架不应被自动恢复: %+v", resKeep)
}
if err := db.First(&dsa1After, dsa1.ID).Error; err != nil {
t.Fatal(err)
}
if dsa1After.DisplayStatus != "disabled" || dsa1After.DisplayDisabledBy != displayDisabledByOperator {
t.Fatalf("人工下架标记丢失: status=%s by=%s", dsa1After.DisplayStatus, dsa1After.DisplayDisabledBy)
}
// 3.1 系统下架(含历史无来源标记的数据)应被铺货恢复
if err := db.Model(&model.DistributionSiteAccount{}).Where("id = ?", dsa1.ID).
Updates(map[string]any{"display_status": "disabled", "display_disabled_by": ""}).Error; err != nil {
t.Fatal(err)
}
resRepair, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
CleanStaleListings: true,
})
if err != nil {
t.Fatalf("SyncSiteAccounts repair failed: %v", err)
}
if resRepair.ReenabledCount != 1 {
t.Fatalf("expected 1 reenabled shelf row, got %+v", resRepair)
}
if err := db.First(&dsa1After, dsa1.ID).Error; err != nil {
t.Fatal(err)
}
if dsa1After.DisplayStatus != "enabled" || dsa1After.DisplayDisabledBy != "" {
t.Fatalf("系统下架应被恢复: status=%s by=%s", dsa1After.DisplayStatus, dsa1After.DisplayDisabledBy)
}
// 3.2 商品下架不清理货架行:分站前台本来就要求 published+approved 才展示,
// 一旦把货架行写成 disabled,商品重新上架后就再也不会出现在分站。
db.Model(&l1).Update("status", "offline")
resClean, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
@@ -1317,15 +1360,35 @@ func TestSyncSiteAccountsOptimizations(t *testing.T) {
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)
if resClean.DisabledStaleCount != 0 || resClean.ReenabledCount != 0 {
t.Fatalf("下架商品不应清理货架行: %+v", resClean)
}
if err := db.First(&dsa1After, dsa1.ID).Error; err != nil {
t.Fatal(err)
}
if dsa1After.DisplayStatus != "enabled" {
t.Fatalf("商品下架后货架行应保持 enabled,got: %s", dsa1After.DisplayStatus)
}
// 验证 dsa1 已被标记为 disabled
var dsa1After model.DistributionSiteAccount
db.First(&dsa1After, dsa1.ID)
if dsa1After.DisplayStatus != "disabled" {
t.Fatalf("expected dsa1 disabled, got: %s", dsa1After.DisplayStatus)
// 3.3 只有货源已被删除的孤儿货架行才会被清理
if err := db.Delete(&model.RentalListing{}, l2.ID).Error; err != nil {
t.Fatal(err)
}
resStale, err := repo.SyncSiteAccounts(t.Context(), 0, site.ID, SyncSiteAccountsRequest{
CleanStaleListings: true,
})
if err != nil {
t.Fatalf("SyncSiteAccounts stale failed: %v", err)
}
if resStale.DisabledStaleCount != 1 {
t.Fatalf("expected 1 disabled stale listing, got %+v", resStale)
}
var dsa2 model.DistributionSiteAccount
if err := db.Where("site_id = ? AND listing_id = ?", site.ID, l2.ID).First(&dsa2).Error; err != nil {
t.Fatal(err)
}
if dsa2.DisplayStatus != "disabled" || dsa2.DisplayDisabledBy != displayDisabledBySystem {
t.Fatalf("已删除货源应被系统下架: status=%s by=%s", dsa2.DisplayStatus, dsa2.DisplayDisabledBy)
}
// 4. 验证并发互斥锁(ErrSyncInProgress)