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
@@ -0,0 +1,144 @@
package distribution
import (
"strings"
"sync"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 货架下架来源标记:写入 distribution_site_accounts.display_disabled_by。
// 所有站点共用同一批已审核商品,因此「上架成功后自动铺货」只允许恢复系统下架的行,
// 站长人工下架(选品)必须保持不动;两个来源必须可区分,否则同步会覆盖人工操作。
const (
displayDisabledByOperator = "operator"
displayDisabledBySystem = "system"
)
// shelfDisabledBySystem 判断某条下架行是否允许被自动铺货恢复。
// 历史数据没有来源标记(空串),按系统下架处理,使存量被误下架的商品可自动恢复。
func shelfDisabledBySystem(disabledBy string) bool {
switch strings.TrimSpace(disabledBy) {
case "", displayDisabledBySystem:
return true
default:
return false
}
}
// explicitZeroSiteColumns 收集建站请求里「显式传了零值」但模型带 default 标签的列。
//
// GORM 对带 default 标签的字段会忽略零值(false/0)并回落数据库默认值,
// 例如 display_account_enabled 默认 true:站长建站时明确关闭自动展示,
// 若不显式补写就会出现「配置被静默改成开启」。
func explicitZeroSiteColumns(req CreateSiteRequest) map[string]any {
columns := map[string]any{}
if req.DisplayAccountEnabled != nil && !*req.DisplayAccountEnabled {
columns["display_account_enabled"] = false
}
if req.TaxEnabled != nil && !*req.TaxEnabled {
columns["tax_enabled"] = false
}
if req.SettlementHoldDays != nil && *req.SettlementHoldDays == 0 {
columns["settlement_hold_days"] = 0
}
return columns
}
// displayDisabledBy 把站长人工上下架动作翻译成下架来源标记:下架记 operator,恢复展示则清空标记。
func displayDisabledBy(status string) string {
if strings.TrimSpace(status) == "enabled" {
return ""
}
return displayDisabledByOperator
}
// autoDisplaySiteIDs 返回开启「自动展示账号」的可用站点 ID。
// display_account_enabled=false 表示该站不参与自动铺货,需要站长手动同步。
func autoDisplaySiteIDs(tx *gorm.DB) ([]uint64, error) {
var ids []uint64
err := tx.Model(&model.DistributionSite{}).
Where("status = ? AND display_account_enabled = ?", "active", true).
Pluck("id", &ids).Error
return ids, err
}
// shelfTablePresent 探测分销货架表是否存在,仅首次访问查询 information_schema,
// 兼容尚未执行分销迁移的旧环境,避免商品上架事务因缺表而失败。
var (
shelfTablePresent bool
shelfTableCheckOnce sync.Once
)
func shelfTablesPresent(tx *gorm.DB) bool {
shelfTableCheckOnce.Do(func() {
shelfTablePresent = tx.Migrator().HasTable(&model.DistributionSiteAccount{})
})
return shelfTablePresent
}
// EnsureListingShelvedTx 在商品审核通过(published + approved)后自动铺到各分站货架。
//
// 站点货架与主站共用同一套货源(rental_listings / game_accounts),因此无需站长手动
// 「一键同步」才能看到新商品:
// - 缺少货架行:新增 enabled 行,加价保持 0(等于主站原价,站长可再自主加价)
// - 已有系统下架/历史无标记的下架行:恢复为 enabled
// - 已有站长人工下架的行:保持 disabled,尊重人工选品
//
// 本方法在调用方事务内执行,幂等:重复调用不会产生重复货架行。
func (r *Repository) EnsureListingShelvedTx(tx *gorm.DB, listingID, accountID uint64) error {
if tx == nil || listingID == 0 || accountID == 0 {
return nil
}
if !shelfTablesPresent(tx) {
return nil
}
siteIDs, err := autoDisplaySiteIDs(tx)
if err != nil || len(siteIDs) == 0 {
return err
}
var existing []model.DistributionSiteAccount
if err := tx.Where("site_id IN ? AND account_id = ?", siteIDs, accountID).Find(&existing).Error; err != nil {
return err
}
bySite := make(map[uint64]model.DistributionSiteAccount, len(existing))
for _, row := range existing {
bySite[row.SiteID] = row
}
toInsert := make([]model.DistributionSiteAccount, 0, len(siteIDs))
for _, siteID := range siteIDs {
row, ok := bySite[siteID]
if !ok {
toInsert = append(toInsert, model.DistributionSiteAccount{
SiteID: siteID,
AccountID: accountID,
ListingID: listingID,
DisplayStatus: "enabled",
})
continue
}
updates := map[string]any{}
if row.ListingID != listingID {
updates["listing_id"] = listingID
}
if row.DisplayStatus != "enabled" && shelfDisabledBySystem(row.DisplayDisabledBy) {
updates["display_status"] = "enabled"
updates["display_disabled_by"] = ""
}
if len(updates) == 0 {
continue
}
if err := tx.Model(&model.DistributionSiteAccount{}).Where("id = ?", row.ID).Updates(updates).Error; err != nil {
return err
}
}
if len(toInsert) == 0 {
return nil
}
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&toInsert).Error
}