Files
yml2213 766c923a6a 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 站点码回退,站点探测完成后再挂载应用,
  避免分站发布因缺少站点码被记成主站商品;同步弹窗文案与统计项同步更新
- 补充分销货架自动铺货、清理口径、建站零值落库与审核铺货回归测试
2026-09-30 21:11:09 +08:00

145 lines
4.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}