feat: 完成规格映射与采购任务创建 (#69)

This commit is contained in:
chengma
2026-08-09 22:55:14 +08:00
parent 610e9e3749
commit 4433e3af37
18 changed files with 976 additions and 135 deletions
+20
View File
@@ -184,6 +184,26 @@ func ListClientsForUser(db *sql.DB, keyword, visibleUserID string) ([]ClientWith
return listClientsForUserPage(db, keyword, visibleUserID, -1, 0)
}
// ClientVisibleToUser 校验客户端是否存在且位于当前账号可见范围。
// visibleUserID 为空表示管理员,可选择任意已登记客户端。
func ClientVisibleToUser(q Execer, clientID, visibleUserID string) (bool, error) {
query := `SELECT COUNT(*) FROM clients c`
args := []any{}
if visibleUserID != "" {
query += ` JOIN client_user_assignments a
ON a.client_id = c.client_id AND a.ended_at IS NULL
AND a.user_id = ?`
args = append(args, visibleUserID)
}
query += ` WHERE c.client_id = ?`
args = append(args, clientID)
var count int
if err := q.QueryRow(query, args...).Scan(&count); err != nil {
return false, fmt.Errorf("校验客户端可见范围失败: %w", err)
}
return count == 1, nil
}
// ListClientsForUserPage 返回一页客户端和同一可见范围内的总数。
func ListClientsForUserPage(db *sql.DB, keyword, visibleUserID string, limit, offset int) ([]ClientWithAssignee, int, error) {
where, args := clientListFilter(keyword, visibleUserID)
+52
View File
@@ -0,0 +1,52 @@
package repository
import (
"database/sql"
"errors"
"fmt"
"cmautobuy/admin/model"
)
// GetSKUMapping 读取某个蝦皮 SKU 在指定 PDD 商品下的映射。
func GetSKUMapping(q Execer, shopeeSKUID, pddGoodsID string) (*model.SKUMapping, error) {
var m model.SKUMapping
var mappedBy sql.NullString
err := q.QueryRow(`
SELECT shopee_sku_id, pdd_goods_id, pdd_option_key, pdd_options,
goods_id, mapped_at, mapped_by
FROM sku_mappings
WHERE shopee_sku_id = ? AND pdd_goods_id = ?`,
shopeeSKUID, pddGoodsID).Scan(
&m.ShopeeSKUID, &m.PddGoodsID, &m.PddOptionKey, &m.PddOptions,
&m.GoodsID, &m.MappedAt, &mappedBy)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("查询规格映射失败: %w", err)
}
m.MappedBy = mappedBy.String
return &m, nil
}
// UpsertSKUMapping 保存可复用规格映射,唯一键包含当前 PDD 商品。
func UpsertSKUMapping(q Execer, m model.SKUMapping) error {
_, err := q.Exec(`
INSERT INTO sku_mappings
(shopee_sku_id, pdd_goods_id, pdd_option_key, pdd_options,
goods_id, mapped_at, mapped_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(shopee_sku_id, pdd_goods_id) DO UPDATE SET
pdd_option_key = excluded.pdd_option_key,
pdd_options = excluded.pdd_options,
goods_id = excluded.goods_id,
mapped_at = excluded.mapped_at,
mapped_by = excluded.mapped_by`,
m.ShopeeSKUID, m.PddGoodsID, m.PddOptionKey, m.PddOptions,
m.GoodsID, m.MappedAt, nullableText(m.MappedBy))
if err != nil {
return fmt.Errorf("保存规格映射失败: %w", err)
}
return nil
}
+26 -5
View File
@@ -320,7 +320,9 @@ func sybOrderFilterClause(filter SybOrderFilter) (string, []any) {
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 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`
// SybOrderContext 是顺运宝明细及其当前蝦皮/PDD 处理上下文。
// 处理阶段由 service 计算,Repository 只提供数据库事实。
@@ -331,6 +333,10 @@ type SybOrderContext struct {
PddGoodsURL string
PddCollectStatus string
PddCollectMsg string
PddSkusJSON string
MappingOptionKey string
MappingOptions string
HasActiveTask bool
}
func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
@@ -338,12 +344,15 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
var title, productSpec, shopeeGoodsID, shopeeSKUID, imageURL sql.NullString
var priceCent sql.NullInt64
var shopeeExists int
var pddGoodsID, pddGoodsURL, collectStatus, collectMsg sql.NullString
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.Quantity, &priceCent, &imageURL, &c.Order.SybData,
&c.Order.CreatedAt, &c.Order.UpdatedAt, &shopeeExists,
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg,
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg, &skusJSON,
&mappingKey, &mappingOptions, &hasActiveTask,
)
c.Order.Title = title.String
c.Order.ProductSpec = productSpec.String
@@ -356,6 +365,10 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
c.PddGoodsURL = pddGoodsURL.String
c.PddCollectStatus = collectStatus.String
c.PddCollectMsg = collectMsg.String
c.PddSkusJSON = skusJSON.String
c.MappingOptionKey = mappingKey.String
c.MappingOptions = mappingOptions.String
c.HasActiveTask = hasActiveTask != 0
return c, err
}
@@ -367,7 +380,11 @@ func ListSybOrderContexts(q Execer, filter SybOrderFilter, limit, offset int) ([
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` +
sp.pdd_goods_id, sp.pdd_goods_url, pp.collect_status, pp.collect_msg,
pp.skus_json, sm.pdd_option_key, sm.pdd_options,
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
AND t.syb_id = so.syb_id
AND t.status IN ('pending', 'assigned', 'claimed'))` +
sybOrderContextFrom + where + `
ORDER BY so.updated_at DESC, so.syb_id DESC LIMIT ? OFFSET ?`
args = append(args, limit, offset)
@@ -394,7 +411,11 @@ func GetSybOrderContext(q Execer, sybID string) (*SybOrderContext, error) {
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`+
sp.pdd_goods_id, sp.pdd_goods_url, pp.collect_status, pp.collect_msg,
pp.skus_json, sm.pdd_option_key, sm.pdd_options,
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
AND t.syb_id = so.syb_id
AND t.status IN ('pending', 'assigned', 'claimed'))`+
sybOrderContextFrom+` WHERE so.syb_id = ?`, sybID))
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
+37
View File
@@ -445,3 +445,40 @@ func InsertCollectTask(q Execer, taskID, goodsID, goodsURL string) error {
}
return nil
}
// HasActivePurchaseTask 判断顺运宝明细是否已有尚未结束的采购任务。
func HasActivePurchaseTask(q Execer, sybID string) (bool, error) {
var count int
err := q.QueryRow(`
SELECT COUNT(*) FROM tasks
WHERE task_type = 'purchase' AND syb_id = ?
AND status IN ('pending', 'assigned', 'claimed')`, sybID).Scan(&count)
if err != nil {
return false, fmt.Errorf("检查顺运宝明细 %s 的进行中采购任务失败: %w", sybID, err)
}
return count > 0, nil
}
// InsertPurchaseTask 插入一条已分配、等待指定客户端领取的采购任务。
func InsertPurchaseTask(q Execer, task model.Task) error {
if task.TaskID == "" || task.AssignedClient == "" || task.SybID == "" ||
task.PddGoodsURL == "" || task.PddGoodsID == "" || task.PddOptions == "" ||
task.Quantity <= 0 || task.MaxPriceCent <= 0 {
return fmt.Errorf("采购任务缺少客户端、商品、规格、数量或人民币价格上限")
}
now := model.NowISO()
_, err := q.Exec(`
INSERT INTO tasks
(task_id, task_type, status, assigned_client,
syb_id, order_no, goods_id, shopee_sku_id,
pdd_goods_url, pdd_goods_id, pdd_options,
quantity, max_price_cent, created_at, updated_at)
VALUES (?, 'purchase', 'assigned', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
task.TaskID, task.AssignedClient, task.SybID, task.OrderNo,
task.GoodsID, task.ShopeeSKUID, task.PddGoodsURL, task.PddGoodsID,
task.PddOptions, task.Quantity, task.MaxPriceCent, now, now)
if err != nil {
return fmt.Errorf("创建顺运宝明细 %s 的采购任务失败: %w", task.SybID, err)
}
return nil
}