feat: 建立顺运宝采购处理状态 (#67)
This commit is contained in:
+150
-10
@@ -8,6 +8,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
)
|
||||
@@ -288,25 +289,164 @@ func nullableText(s string) any {
|
||||
// SybOrderFilter 是货运单列表页支持的筛选条件。
|
||||
type SybOrderFilter struct {
|
||||
Keyword string // 匹配订单号或商品标题
|
||||
Stage string // 处理阶段;空字符串表示全部
|
||||
}
|
||||
|
||||
func sybOrderFilterClause(filter SybOrderFilter) (string, []any) {
|
||||
kw := filter.Keyword
|
||||
if kw == "" {
|
||||
return "", nil
|
||||
var clauses []string
|
||||
var args []any
|
||||
if kw := filter.Keyword; kw != "" {
|
||||
like := "%" + escapeLike(kw) + "%"
|
||||
clauses = append(clauses, `(so.order_no LIKE ? ESCAPE '\' OR so.title LIKE ? ESCAPE '\')`)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
like := "%" + escapeLike(kw) + "%"
|
||||
return ` WHERE (order_no LIKE ? ESCAPE '\' OR title LIKE ? ESCAPE '\')`, []any{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 "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)`)
|
||||
case "pdd_pending", "pdd_collecting", "pdd_failed", "pdd_collected":
|
||||
clauses = append(clauses, `so.shopee_sku_id IS NOT NULL AND pp.collect_status = ?`)
|
||||
args = append(args, strings.TrimPrefix(filter.Stage, "pdd_"))
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
return "", args
|
||||
}
|
||||
return ` WHERE ` + strings.Join(clauses, ` AND `), args
|
||||
}
|
||||
|
||||
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`
|
||||
|
||||
// SybOrderContext 是顺运宝明细及其当前蝦皮/PDD 处理上下文。
|
||||
// 处理阶段由 service 计算,Repository 只提供数据库事实。
|
||||
type SybOrderContext struct {
|
||||
Order model.SybOrder
|
||||
ShopeeExists bool
|
||||
PddGoodsID string
|
||||
PddGoodsURL string
|
||||
PddCollectStatus string
|
||||
PddCollectMsg string
|
||||
}
|
||||
|
||||
func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
||||
var c SybOrderContext
|
||||
var title, productSpec, shopeeGoodsID, shopeeSKUID, imageURL sql.NullString
|
||||
var priceCent sql.NullInt64
|
||||
var shopeeExists int
|
||||
var pddGoodsID, pddGoodsURL, collectStatus, collectMsg sql.NullString
|
||||
err := s.Scan(
|
||||
&c.Order.SybID, &c.Order.OrderNo, &title, &productSpec, &shopeeGoodsID, &shopeeSKUID,
|
||||
&c.Order.Quantity, &priceCent, &imageURL, &c.Order.SybData,
|
||||
&c.Order.CreatedAt, &c.Order.UpdatedAt, &shopeeExists,
|
||||
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg,
|
||||
)
|
||||
c.Order.Title = title.String
|
||||
c.Order.ProductSpec = productSpec.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
|
||||
c.PddGoodsID = pddGoodsID.String
|
||||
c.PddGoodsURL = pddGoodsURL.String
|
||||
c.PddCollectStatus = collectStatus.String
|
||||
c.PddCollectMsg = collectMsg.String
|
||||
return c, err
|
||||
}
|
||||
|
||||
// ListSybOrderContexts 分页读取采购处理工作台所需的关联事实。
|
||||
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,
|
||||
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` +
|
||||
sybOrderContextFrom + where + `
|
||||
ORDER BY so.updated_at DESC, so.syb_id DESC LIMIT ? OFFSET ?`
|
||||
args = append(args, limit, offset)
|
||||
rows, err := q.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询顺运宝采购处理列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
list := make([]SybOrderContext, 0)
|
||||
for rows.Next() {
|
||||
row, err := scanSybOrderContext(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取顺运宝采购处理列表失败: %w", err)
|
||||
}
|
||||
list = append(list, row)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// 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,
|
||||
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`+
|
||||
sybOrderContextFrom+` WHERE so.syb_id = ?`, sybID))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询顺运宝明细 %s 失败: %w", sybID, err)
|
||||
}
|
||||
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 syb_id, order_no, title, product_spec, shopee_goods_id, shopee_sku_id,
|
||||
quantity, price_twd_cent, image_url, syb_data, created_at, updated_at
|
||||
FROM syb_orders` + where + `
|
||||
ORDER BY updated_at DESC, syb_id DESC LIMIT ? OFFSET ?`
|
||||
SELECT so.syb_id, so.order_no, so.title, so.product_spec, so.shopee_goods_id, so.shopee_sku_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 ?`
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := q.Query(sqlText, args...)
|
||||
@@ -343,7 +483,7 @@ func ListSybOrders(q Execer, filter SybOrderFilter, limit, offset int) ([]model.
|
||||
// 靠它,写成两份筛选条件迟早有一天会不一致(工单 #43 的教训)。
|
||||
func CountSybOrders(q Execer, filter SybOrderFilter) (int, error) {
|
||||
where, args := sybOrderFilterClause(filter)
|
||||
sqlText := `SELECT COUNT(*) FROM syb_orders` + where
|
||||
sqlText := `SELECT COUNT(*)` + sybOrderContextFrom + where
|
||||
var n int
|
||||
if err := q.QueryRow(sqlText, args...).Scan(&n); err != nil {
|
||||
return 0, fmt.Errorf("统计顺运宝货运单数量失败: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user