feat: 重构顺运宝规格主链路 (#88)
This commit is contained in:
+42
-64
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
// ---------- 会话缓存 ----------
|
||||
@@ -224,11 +225,8 @@ func CountSybSyncRuns(q Execer) (int, error) {
|
||||
|
||||
// UpsertSybOrder 写入或更新一条顺运宝货运单明细行。
|
||||
//
|
||||
// `[必须]` DO UPDATE SET 里绝不允许出现 shopee_sku_id。它是规格匹配的
|
||||
// 结果(人工确认或自动匹配产生),顺运宝那边根本没有这个值——写进去
|
||||
// 就是写 NULL,把人工攒的匹配成果洗掉,而且不报错。这和 #38 里
|
||||
// pdd_goods_url 不能被 Excel 导入覆盖是同一类问题,见工单 #46、
|
||||
// docs/admin/08-顺运宝接口.md §6.2。
|
||||
// `[必须]` shopee_sku_id 已退出采购主链路,INSERT 和 UPDATE 都不再写它。
|
||||
// 旧列只为只追加迁移兼容保留。
|
||||
//
|
||||
// `[必须]` shopee_goods_id **可以**被覆盖——它就是顺运宝
|
||||
// detail.productId,来自顺运宝,不是人工填的。
|
||||
@@ -238,6 +236,14 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
|
||||
if o.SybID == "" {
|
||||
return false, fmt.Errorf("syb_id 不能为空")
|
||||
}
|
||||
var specKey any
|
||||
if strings.TrimSpace(o.ProductSpec) != "" {
|
||||
key, keyErr := spec.SpecKey(o.ProductSpec)
|
||||
if keyErr != nil {
|
||||
return false, fmt.Errorf("顺运宝明细 %s 的规格不能生成身份键: %w", o.SybID, keyErr)
|
||||
}
|
||||
specKey = key
|
||||
}
|
||||
|
||||
var exists int
|
||||
err = q.QueryRow(`SELECT 1 FROM syb_orders WHERE syb_id = ?`, o.SybID).Scan(&exists)
|
||||
@@ -249,26 +255,33 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
|
||||
}
|
||||
|
||||
now := model.NowISO()
|
||||
if strings.TrimSpace(o.ShopeeGoodsID) != "" {
|
||||
if _, err := q.Exec(`
|
||||
INSERT INTO shopee_products (goods_id, title, source, created_at, updated_at)
|
||||
VALUES (?, ?, 'syb', ?, ?)
|
||||
ON DUPLICATE KEY UPDATE goods_id=VALUES(goods_id)`,
|
||||
o.ShopeeGoodsID, o.Title, now, now); err != nil {
|
||||
return false, fmt.Errorf("为顺运宝明细 %s 补建蝦皮商品骨架失败: %w", o.SybID, err)
|
||||
}
|
||||
}
|
||||
_, err = q.Exec(`
|
||||
INSERT INTO syb_orders
|
||||
(syb_id, order_no, title, product_spec, shopee_goods_id, shopee_sku_id,
|
||||
(syb_id, order_no, title, product_spec, spec_key, shopee_goods_id,
|
||||
quantity, price_twd_cent, image_url, syb_data, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
order_no = VALUES(order_no),
|
||||
title = VALUES(title),
|
||||
product_spec = VALUES(product_spec),
|
||||
spec_key = VALUES(spec_key),
|
||||
shopee_goods_id = VALUES(shopee_goods_id),
|
||||
quantity = VALUES(quantity),
|
||||
price_twd_cent = VALUES(price_twd_cent),
|
||||
image_url = VALUES(image_url),
|
||||
syb_data = VALUES(syb_data),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
// 注意:shopee_sku_id 只出现在 INSERT 的列清单里(新建行时写 o.ShopeeSKUID,
|
||||
// 同步永远传空字符串),完全不出现在 DO UPDATE SET 里——已存在的行
|
||||
// 这一列不受本语句影响,见上面的函数注释。
|
||||
o.SybID, o.OrderNo, o.Title, nullableText(o.ProductSpec), nullableText(o.ShopeeGoodsID),
|
||||
nullableText(o.ShopeeSKUID), o.Quantity, o.PriceTwdCent, nullableText(o.ImageURL),
|
||||
o.SybID, o.OrderNo, o.Title, nullableText(o.ProductSpec), specKey, nullableText(o.ShopeeGoodsID),
|
||||
o.Quantity, o.PriceTwdCent, nullableText(o.ImageURL),
|
||||
o.SybData, now, now)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("写入顺运宝货运单明细 %s 失败: %w", o.SybID, err)
|
||||
@@ -301,14 +314,12 @@ func sybOrderFilterClause(filter SybOrderFilter) (string, []any) {
|
||||
args = append(args, like, like)
|
||||
}
|
||||
switch filter.Stage {
|
||||
case "missing_shopee":
|
||||
clauses = append(clauses, `sp.goods_id IS NULL`)
|
||||
case "sku_pending":
|
||||
clauses = append(clauses, `sp.goods_id IS NOT NULL AND so.shopee_sku_id IS NULL`)
|
||||
case "spec_missing":
|
||||
clauses = append(clauses, `so.spec_key IS NULL`)
|
||||
case "pdd_missing":
|
||||
clauses = append(clauses, `so.shopee_sku_id IS NOT NULL AND (sp.pdd_goods_id IS NULL OR sp.pdd_goods_id = '' OR pp.goods_id IS NULL)`)
|
||||
clauses = append(clauses, `so.spec_key IS NOT NULL AND (sp.pdd_goods_id IS NULL OR sp.pdd_goods_id = '' OR pp.goods_id IS NULL)`)
|
||||
case "pdd_pending", "pdd_collecting", "pdd_failed", "pdd_collected":
|
||||
clauses = append(clauses, `so.shopee_sku_id IS NOT NULL AND pp.collect_status = ?`)
|
||||
clauses = append(clauses, `so.spec_key IS NOT NULL AND pp.collect_status = ?`)
|
||||
args = append(args, strings.TrimPrefix(filter.Stage, "pdd_"))
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
@@ -321,8 +332,9 @@ const sybOrderContextFrom = `
|
||||
FROM syb_orders so
|
||||
LEFT JOIN shopee_products sp ON sp.goods_id = so.shopee_goods_id
|
||||
LEFT JOIN pdd_products pp ON pp.goods_id = sp.pdd_goods_id AND pp.deleted_at IS NULL
|
||||
LEFT JOIN sku_mappings sm
|
||||
ON sm.shopee_sku_id = so.shopee_sku_id AND sm.pdd_goods_id = sp.pdd_goods_id`
|
||||
LEFT JOIN spec_mappings sm
|
||||
ON sm.shopee_goods_id = so.shopee_goods_id
|
||||
AND sm.spec_key = so.spec_key AND sm.pdd_goods_id = sp.pdd_goods_id`
|
||||
|
||||
// SybOrderContext 是顺运宝明细及其当前蝦皮/PDD 处理上下文。
|
||||
// 处理阶段由 service 计算,Repository 只提供数据库事实。
|
||||
@@ -341,14 +353,14 @@ type SybOrderContext struct {
|
||||
|
||||
func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
||||
var c SybOrderContext
|
||||
var title, productSpec, shopeeGoodsID, shopeeSKUID, imageURL sql.NullString
|
||||
var title, productSpec, specKey, shopeeGoodsID, imageURL sql.NullString
|
||||
var priceCent sql.NullInt64
|
||||
var shopeeExists int
|
||||
var pddGoodsID, pddGoodsURL, collectStatus, collectMsg, skusJSON sql.NullString
|
||||
var mappingKey, mappingOptions sql.NullString
|
||||
var hasActiveTask int
|
||||
err := s.Scan(
|
||||
&c.Order.SybID, &c.Order.OrderNo, &title, &productSpec, &shopeeGoodsID, &shopeeSKUID,
|
||||
&c.Order.SybID, &c.Order.OrderNo, &title, &productSpec, &specKey, &shopeeGoodsID,
|
||||
&c.Order.Quantity, &priceCent, &imageURL, &c.Order.SybData,
|
||||
&c.Order.CreatedAt, &c.Order.UpdatedAt, &shopeeExists,
|
||||
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg, &skusJSON,
|
||||
@@ -356,8 +368,8 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
||||
)
|
||||
c.Order.Title = title.String
|
||||
c.Order.ProductSpec = productSpec.String
|
||||
c.Order.SpecKey = specKey.String
|
||||
c.Order.ShopeeGoodsID = shopeeGoodsID.String
|
||||
c.Order.ShopeeSKUID = shopeeSKUID.String
|
||||
c.Order.PriceTwdCent = priceCent.Int64
|
||||
c.Order.ImageURL = imageURL.String
|
||||
c.ShopeeExists = shopeeExists != 0
|
||||
@@ -376,8 +388,8 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
||||
func ListSybOrderContexts(q Execer, filter SybOrderFilter, limit, offset int) ([]SybOrderContext, error) {
|
||||
where, args := sybOrderFilterClause(filter)
|
||||
query := `
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec,
|
||||
so.shopee_goods_id, so.shopee_sku_id, so.quantity,
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec, so.spec_key,
|
||||
so.shopee_goods_id, so.quantity,
|
||||
so.price_twd_cent, so.image_url, so.syb_data, so.created_at, so.updated_at,
|
||||
CASE WHEN sp.goods_id IS NULL THEN 0 ELSE 1 END,
|
||||
sp.pdd_goods_id, sp.pdd_goods_url, pp.collect_status, pp.collect_msg,
|
||||
@@ -410,8 +422,8 @@ func ListSybOrderContexts(q Execer, filter SybOrderFilter, limit, offset int) ([
|
||||
// GetSybOrderContext 按明细 ID 读取一条处理上下文。
|
||||
func GetSybOrderContext(q Execer, sybID string) (*SybOrderContext, error) {
|
||||
row, err := scanSybOrderContext(q.QueryRow(`
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec,
|
||||
so.shopee_goods_id, so.shopee_sku_id, so.quantity,
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec, so.spec_key,
|
||||
so.shopee_goods_id, so.quantity,
|
||||
so.price_twd_cent, so.image_url, so.syb_data, so.created_at, so.updated_at,
|
||||
CASE WHEN sp.goods_id IS NULL THEN 0 ELSE 1 END,
|
||||
sp.pdd_goods_id, sp.pdd_goods_url, pp.collect_status, pp.collect_msg,
|
||||
@@ -429,45 +441,11 @@ func GetSybOrderContext(q Execer, sybID string) (*SybOrderContext, error) {
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// SetSybShopeeSKUIfEmpty 只为尚未确认规格的明细写入自动识别结果。
|
||||
func SetSybShopeeSKUIfEmpty(q Execer, sybID, skuID string) (bool, error) {
|
||||
result, err := q.Exec(`
|
||||
UPDATE syb_orders
|
||||
SET shopee_sku_id = ?, updated_at = ?
|
||||
WHERE syb_id = ? AND shopee_sku_id IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM shopee_skus sk
|
||||
WHERE sk.sku_id = ? AND sk.goods_id = syb_orders.shopee_goods_id
|
||||
)`, skuID, model.NowISO(), sybID, skuID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("自动确认顺运宝明细 %s 的蝦皮规格失败: %w", sybID, err)
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
return n > 0, err
|
||||
}
|
||||
|
||||
// SetSybShopeeSKU 在调用方完成商品归属校验后保存人工选择。
|
||||
func SetSybShopeeSKU(q Execer, sybID, skuID string) error {
|
||||
result, err := q.Exec(`UPDATE syb_orders SET shopee_sku_id = ?, updated_at = ? WHERE syb_id = ?`,
|
||||
skuID, model.NowISO(), sybID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存顺运宝明细 %s 的蝦皮规格失败: %w", sybID, err)
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("顺运宝明细 %s 不存在", sybID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSybOrders 按筛选条件分页查货运单明细列表,按更新时间倒序。
|
||||
func ListSybOrders(q Execer, filter SybOrderFilter, limit, offset int) ([]model.SybOrder, error) {
|
||||
where, args := sybOrderFilterClause(filter)
|
||||
sqlText := `
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec, so.shopee_goods_id, so.shopee_sku_id,
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec, so.spec_key, so.shopee_goods_id,
|
||||
so.quantity, so.price_twd_cent, so.image_url, so.syb_data, so.created_at, so.updated_at` +
|
||||
sybOrderContextFrom + where + `
|
||||
ORDER BY so.updated_at DESC, so.syb_id DESC LIMIT ? OFFSET ?`
|
||||
@@ -482,18 +460,18 @@ func ListSybOrders(q Execer, filter SybOrderFilter, limit, offset int) ([]model.
|
||||
var list []model.SybOrder
|
||||
for rows.Next() {
|
||||
var o model.SybOrder
|
||||
var title, productSpec, shopeeGoodsID, shopeeSKUID, imageURL sql.NullString
|
||||
var title, productSpec, specKey, shopeeGoodsID, imageURL sql.NullString
|
||||
var priceCent sql.NullInt64
|
||||
if err := rows.Scan(
|
||||
&o.SybID, &o.OrderNo, &title, &productSpec, &shopeeGoodsID, &shopeeSKUID,
|
||||
&o.SybID, &o.OrderNo, &title, &productSpec, &specKey, &shopeeGoodsID,
|
||||
&o.Quantity, &priceCent, &imageURL, &o.SybData, &o.CreatedAt, &o.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("读取顺运宝货运单列表失败: %w", err)
|
||||
}
|
||||
o.Title = title.String
|
||||
o.ProductSpec = productSpec.String
|
||||
o.SpecKey = specKey.String
|
||||
o.ShopeeGoodsID = shopeeGoodsID.String
|
||||
o.ShopeeSKUID = shopeeSKUID.String
|
||||
o.PriceTwdCent = priceCent.Int64
|
||||
o.ImageURL = imageURL.String
|
||||
list = append(list, o)
|
||||
|
||||
Reference in New Issue
Block a user