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 }