问题:分站上架商品审核通过后主站能搜到、分站看不到,且没有自动同步。 根因:分站前台/搜索只认 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 站点码回退,站点探测完成后再挂载应用, 避免分站发布因缺少站点码被记成主站商品;同步弹窗文案与统计项同步更新 - 补充分销货架自动铺货、清理口径、建站零值落库与审核铺货回归测试
145 lines
4.9 KiB
Go
145 lines
4.9 KiB
Go
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
|
||
}
|