feat: 建立全局店铺跨渠道管理 (#208 #209 #210 #211)

This commit is contained in:
chengma
2026-08-13 11:15:54 +08:00
parent 901b37a8cb
commit 8f707722ba
28 changed files with 1160 additions and 500 deletions
+17 -13
View File
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
@@ -553,21 +554,22 @@ func RunSybSync(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.
// 局部历史补拉只 upsert 数据、不动游标,否则会让未覆盖的订单永久漏掉。
func RunSybSyncWithOptions(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.SybConfig, now time.Time, options SybSyncOptions) SyncReport {
report := SyncReport{StartedAt: now, Specified: options.IsSpecified()}
allowedNames, err := repository.ListEnabledSybShopNames(db)
allowedShops, err := repository.ListEnabledSybShopMappings(db)
if err != nil {
report.Err = fmt.Errorf("读取顺运宝允许店铺失败: %w", err)
report.FinishedAt = time.Now().UTC()
return report
}
if len(allowedNames) == 0 {
report.Err = fmt.Errorf("没有启用的顺运宝同步店铺,请先由管理员在“同步店铺”中配置并启用至少一个店铺")
if len(allowedShops) == 0 {
report.Err = fmt.Errorf("没有启用的顺运宝同步店铺,请先由管理员在“店铺管理”中配置并启用至少一个 SYB 店铺")
report.FinishedAt = time.Now().UTC()
return report
}
allowedShops := make(map[string]struct{}, len(allowedNames))
for _, name := range allowedNames {
allowedShops[name] = struct{}{}
allowedNames := make([]string, 0, len(allowedShops))
for name := range allowedShops {
allowedNames = append(allowedNames, name)
}
sort.Strings(allowedNames)
report.ShopFilterHash = fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(allowedNames, "\x00"))))
pageSize := cfg.PageSize
@@ -754,7 +756,7 @@ func RunSybSyncWithOptions(ctx context.Context, db *sql.DB, client *syb.Client,
report.StockCount += len(rawIDs)
orderedIDs := make([]int64, 0, len(rawIDs))
for _, id := range rawIDs {
if !sybShopAllowed(allowedShops, stockByID[id].Raw, nil) {
if _, ok := sybShopID(allowedShops, stockByID[id].Raw, nil); !ok {
report.ShopSkipped++
continue
}
@@ -782,12 +784,13 @@ func RunSybSyncWithOptions(ctx context.Context, db *sql.DB, client *syb.Client,
}
for _, d := range details {
stockRow := stockByID[d.ID]
if !sybShopAllowed(allowedShops, stockRow.Raw, d.Raw) {
shopID, ok := sybShopID(allowedShops, stockRow.Raw, d.Raw)
if !ok {
report.AcceptedCount--
report.ShopSkipped++
continue
}
if err := writeStockDetail(db, cfg.BaseURL, stockRow, d, &report); err != nil {
if err := writeStockDetail(db, cfg.BaseURL, shopID, stockRow, d, &report); err != nil {
report.Err = fmt.Errorf("写入货运单 %s(id=%d)失败(本次同步整体作废,"+
"已写入的数据保留): %w", d.Code, d.ID, err)
report.FinishedAt = time.Now().UTC()
@@ -820,10 +823,10 @@ func RunSybSyncWithOptions(ctx context.Context, db *sql.DB, client *syb.Client,
// sybShopAllowed 用明细字段覆盖列表字段后再核对,防止列表通过但明细在同步
// 期间已变成其他店铺。detail 为空时只检查列表快照。
func sybShopAllowed(allowed map[string]struct{}, listRaw, detailRaw map[string]any) bool {
func sybShopID(allowed map[string]string, listRaw, detailRaw map[string]any) (string, bool) {
name := trimmedStringField(mergeRaw(listRaw, detailRaw), "shopName")
_, ok := allowed[name]
return ok
shopID, ok := allowed[name]
return shopID, ok
}
// validateDetailBatch 确认批量明细响应与请求 ID 一一对应。任何缺失、重复、
@@ -856,7 +859,7 @@ func validateDetailBatch(requested []int64, details []syb.StockDetail) error {
// writeStockDetail 把一张货运单的全部商品明细写进 syb_orders,
// 一张货运单一个事务(工单 #46「按货运单为单位提交」)。
func writeStockDetail(db *sql.DB, baseURL string, stockRow syb.StockRow, detail syb.StockDetail, report *SyncReport) error {
func writeStockDetail(db *sql.DB, baseURL, shopID string, stockRow syb.StockRow, detail syb.StockDetail, report *SyncReport) error {
if len(detail.Details) == 0 {
return nil
}
@@ -888,6 +891,7 @@ func writeStockDetail(db *sql.DB, baseURL string, stockRow syb.StockRow, detail
order := model.SybOrder{
SybID: sybID,
OrderNo: detail.Code,
ShopID: shopID,
ShopName: trimmedStringField(stockRaw, "shopName"),
Title: item.ProductTitle,
ProductSpec: item.ProductSpec,