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
+24 -99
View File
@@ -12,100 +12,10 @@ import (
"fmt"
"strings"
"github.com/go-sql-driver/mysql"
"cmautobuy/admin/model"
"cmautobuy/admin/spec"
)
var ErrSybAllowedShopExists = errors.New("顺运宝允许店铺已经存在")
// ListSybAllowedShops 返回全部准入项;管理页需要同时看到已停用项。
func ListSybAllowedShops(q Execer) ([]model.SybAllowedShop, error) {
rows, err := q.Query(`SELECT shop_id,shop_name,normalized_name,enabled,created_by_user_id,created_at,updated_at
FROM syb_allowed_shops ORDER BY enabled DESC,normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询顺运宝允许店铺失败: %w", err)
}
defer rows.Close()
var result []model.SybAllowedShop
for rows.Next() {
var item model.SybAllowedShop
var enabled int
if err := rows.Scan(&item.ShopID, &item.ShopName, &item.NormalizedName, &enabled,
&item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, fmt.Errorf("读取顺运宝允许店铺失败: %w", err)
}
item.Enabled = enabled == 1
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取顺运宝允许店铺失败: %w", err)
}
return result, nil
}
func ListEnabledSybShopNames(q Execer) ([]string, error) {
rows, err := q.Query(`SELECT normalized_name FROM syb_allowed_shops WHERE enabled=1 ORDER BY normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询启用的顺运宝店铺失败: %w", err)
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, fmt.Errorf("读取启用的顺运宝店铺失败: %w", err)
}
names = append(names, name)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取启用的顺运宝店铺失败: %w", err)
}
return names, nil
}
func InsertSybAllowedShop(q Execer, item model.SybAllowedShop) error {
_, err := q.Exec(`INSERT INTO syb_allowed_shops
(shop_id,shop_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
VALUES(?,?,?,?,?,?,?)`, item.ShopID, item.ShopName, item.NormalizedName, item.Enabled,
item.CreatedByUserID, item.CreatedAt, item.UpdatedAt)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return ErrSybAllowedShopExists
}
return fmt.Errorf("新增顺运宝允许店铺失败: %w", err)
}
return nil
}
func SetSybAllowedShopEnabled(q Execer, shopID string, enabled bool, updatedAt string) (bool, error) {
result, err := q.Exec(`UPDATE syb_allowed_shops SET enabled=?,updated_at=? WHERE shop_id=?`, enabled, updatedAt, shopID)
if err != nil {
return false, fmt.Errorf("更新顺运宝允许店铺失败: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return false, fmt.Errorf("确认顺运宝允许店铺更新结果失败: %w", err)
}
return affected == 1, nil
}
// DeleteDisabledSybAllowedShop 只删除提交瞬间仍处于停用状态的配置项。
// enabled 条件是最终并发保护,不能只依赖管理页隐藏启用项的删除按钮。
func DeleteDisabledSybAllowedShop(q Execer, shopID string) (bool, error) {
result, err := q.Exec(`DELETE FROM syb_allowed_shops WHERE shop_id=? AND enabled=0`, shopID)
if err != nil {
return false, fmt.Errorf("删除已停用的顺运宝店铺失败: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return false, fmt.Errorf("确认顺运宝店铺删除结果失败: %w", err)
}
return affected == 1, nil
}
// ---------- 会话缓存 ----------
// SaveSybSession 写入或更新顺运宝登录会话缓存(按用户名 upsert)。
@@ -333,6 +243,12 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
if o.SybID == "" {
return false, fmt.Errorf("syb_id 不能为空")
}
if strings.TrimSpace(o.ShopID) == "" && strings.TrimSpace(o.ShopName) != "" {
o.ShopID, err = FindShopIDByAlias(q, "syb", o.ShopName)
if err != nil {
return false, fmt.Errorf("解析顺运宝明细 %s 的业务店铺失败: %w", o.SybID, err)
}
}
var specKey any
if strings.TrimSpace(o.ProductSpec) != "" {
key, keyErr := spec.SpecKey(o.ProductSpec)
@@ -353,17 +269,18 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
now := model.NowISO()
if strings.TrimSpace(o.ShopeeGoodsID) != "" {
if err := upsertSybShopeeProduct(q, o.ShopeeGoodsID, o.Title, o.ShopName, o.ImageURL, now); err != nil {
if err := upsertSybShopeeProduct(q, o.ShopeeGoodsID, o.ShopID, o.Title, o.ShopName, o.ImageURL, now); err != nil {
return false, fmt.Errorf("为顺运宝明细 %s 补建蝦皮商品骨架失败: %w", o.SybID, err)
}
}
_, err = q.Exec(`
INSERT INTO syb_orders
(syb_id, order_no, shop_name, title, product_spec, spec_key, shopee_goods_id,
(syb_id, order_no, shop_id, shop_name, title, product_spec, spec_key, shopee_goods_id,
quantity, price_twd_cent, image_url, syb_data, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, NULLIF(?,''), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
order_no = VALUES(order_no),
shop_id = VALUES(shop_id),
shop_name = VALUES(shop_name),
title = VALUES(title),
product_spec = VALUES(product_spec),
@@ -374,7 +291,7 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
image_url = VALUES(image_url),
syb_data = VALUES(syb_data),
updated_at = VALUES(updated_at)`,
o.SybID, o.OrderNo, nullableText(o.ShopName), o.Title, nullableText(o.ProductSpec), specKey, nullableText(o.ShopeeGoodsID),
o.SybID, o.OrderNo, strings.TrimSpace(o.ShopID), nullableText(o.ShopName), o.Title, nullableText(o.ProductSpec), specKey, nullableText(o.ShopeeGoodsID),
o.Quantity, o.PriceTwdCent, nullableText(o.ImageURL),
o.SybData, now, now)
if err != nil {
@@ -387,20 +304,24 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
//
// 店铺和图片是字段级低优先级数据:只能补空值,或更新原本同样来自 syb 的值;
// 人工字段和商品目录等权威来源永远不被顺运宝覆盖。空值也不能清除已有内容。
func upsertSybShopeeProduct(q Execer, goodsID, title, shopName, imageURL, observedAt string) error {
func upsertSybShopeeProduct(q Execer, goodsID, shopID, title, shopName, imageURL, observedAt string) error {
_, err := q.Exec(`
INSERT INTO shopee_products
(goods_id,title,image_url,shopee_shop_name,
(goods_id,shop_id,title,image_url,shopee_shop_name,
image_source,image_observed_at,image_is_manual,
shop_name_source,shop_name_observed_at,shop_name_is_manual,
source,source_observed_at,created_at,updated_at)
VALUES (?, ?, NULLIF(TRIM(?),''), NULLIF(TRIM(?),''),
VALUES (?, NULLIF(TRIM(?),''), ?, NULLIF(TRIM(?),''), NULLIF(TRIM(?),''),
CASE WHEN TRIM(?)='' THEN NULL ELSE 'syb' END,
CASE WHEN TRIM(?)='' THEN NULL ELSE ? END, 0,
CASE WHEN TRIM(?)='' THEN NULL ELSE 'syb' END,
CASE WHEN TRIM(?)='' THEN NULL ELSE ? END, 0,
'syb', ?, ?, ?)
ON DUPLICATE KEY UPDATE
shop_id = CASE
WHEN VALUES(shop_id) IS NOT NULL AND (shop_id IS NULL OR shop_name_source='syb') THEN VALUES(shop_id)
ELSE shop_id
END,
title = CASE
WHEN source='syb' AND TRIM(VALUES(title))<>'' THEN VALUES(title)
ELSE title
@@ -445,7 +366,7 @@ func upsertSybShopeeProduct(q Execer, goodsID, title, shopName, imageURL, observ
OR (shop_name_source='syb' AND TRIM(VALUES(shopee_shop_name))<>'') THEN VALUES(updated_at)
ELSE updated_at
END`,
goodsID, title, imageURL, shopName,
goodsID, shopID, title, imageURL, shopName,
imageURL, imageURL, observedAt,
shopName, shopName, observedAt,
observedAt, observedAt, observedAt)
@@ -490,7 +411,11 @@ func backfillSybProductMetadata(db *sql.DB) error {
return fmt.Errorf("读取历史顺运宝图片失败: %w", err)
}
for goodsID, item := range items {
if err := upsertSybShopeeProduct(db, goodsID, "", item.shopName, item.imageURL, item.observedAt); err != nil {
shopID, err := FindShopIDByAlias(db, "syb", item.shopName)
if err != nil {
return fmt.Errorf("解析历史顺运宝店铺失败: %w", err)
}
if err := upsertSybShopeeProduct(db, goodsID, shopID, "", item.shopName, item.imageURL, item.observedAt); err != nil {
return fmt.Errorf("回填蝦皮商品 %s 的顺运宝元数据失败: %w", goodsID, err)
}
}