feat: 重构顺运宝规格主链路 (#88)

This commit is contained in:
chengma
2026-08-10 12:24:23 +08:00
parent 5bb1258462
commit d1165b9e17
28 changed files with 985 additions and 531 deletions
+67 -92
View File
@@ -8,11 +8,11 @@ import (
"cmautobuy/admin/repository"
)
func seedWorkflowOrder(t *testing.T, db *sql.DB, sybID, goodsID, spec string) model.SybOrder {
func seedWorkflowOrder(t *testing.T, db *sql.DB, sybID, goodsID, rawSpec string) model.SybOrder {
t.Helper()
order := model.SybOrder{
SybID: sybID, OrderNo: "ORDER-" + sybID, Title: "测试商品",
ProductSpec: spec, ShopeeGoodsID: goodsID, Quantity: 1, SybData: `{}`,
ProductSpec: rawSpec, ShopeeGoodsID: goodsID, Quantity: 1, SybData: `{}`,
}
if _, err := repository.UpsertSybOrder(db, order); err != nil {
t.Fatalf("准备顺运宝明细失败: %v", err)
@@ -20,106 +20,41 @@ func seedWorkflowOrder(t *testing.T, db *sql.DB, sybID, goodsID, spec string) mo
return order
}
func seedWorkflowSKU(t *testing.T, db *sql.DB, skuID, goodsID, spec string) {
t.Helper()
color, size, advice, ok := ParseSpec(spec)
if err := repository.UpsertShopeeSKU(db, skuID, goodsID, spec, color, size, advice, ok, ""); err != nil {
t.Fatalf("准备蝦皮规格失败: %v", err)
}
}
func readOrderSKU(t *testing.T, db *sql.DB, sybID string) string {
t.Helper()
var sku sql.NullString
if err := db.QueryRow(`SELECT shopee_sku_id FROM syb_orders WHERE syb_id = ?`, sybID).Scan(&sku); err != nil {
t.Fatal(err)
}
return sku.String
}
func TestAutoResolveSybShopeeSKU_只采用唯一精确候选(t *testing.T) {
t.Run("规格原文唯一一致", func(t *testing.T) {
db := newSyncTestDB(t)
if err := repository.UpsertShopeeProduct(db, "SP-1", "商品", "", ""); err != nil {
t.Fatal(err)
}
seedWorkflowSKU(t, db, "SKU-1", "SP-1", "黑色,M")
order := seedWorkflowOrder(t, db, "SYB-1", "SP-1", "黑色,M")
got, err := AutoResolveSybShopeeSKU(db, order)
if err != nil || got != "SKU-1" || readOrderSKU(t, db, "SYB-1") != "SKU-1" {
t.Fatalf("应自动确认唯一原文候选: got=%q stored=%q err=%v", got, readOrderSKU(t, db, "SYB-1"), err)
}
})
t.Run("解析后颜色尺码唯一一致", func(t *testing.T) {
db := newSyncTestDB(t)
if err := repository.UpsertShopeeProduct(db, "SP-2", "商品", "", ""); err != nil {
t.Fatal(err)
}
seedWorkflowSKU(t, db, "SKU-2", "SP-2", "黑色,M【建議40-50公斤】")
order := seedWorkflowOrder(t, db, "SYB-2", "SP-2", "黑色,M")
got, err := AutoResolveSybShopeeSKU(db, order)
if err != nil || got != "SKU-2" {
t.Fatalf("应采用唯一颜色尺码候选: got=%q err=%v", got, err)
}
})
t.Run("多个颜色尺码候选不猜测", func(t *testing.T) {
db := newSyncTestDB(t)
if err := repository.UpsertShopeeProduct(db, "SP-3", "商品", "", ""); err != nil {
t.Fatal(err)
}
seedWorkflowSKU(t, db, "SKU-3A", "SP-3", "黑色,M【建議40-50公斤】")
seedWorkflowSKU(t, db, "SKU-3B", "SP-3", "黑色,M【建議50-60公斤】")
order := seedWorkflowOrder(t, db, "SYB-3", "SP-3", "黑色,M")
got, err := AutoResolveSybShopeeSKU(db, order)
if err != nil || got != "" || readOrderSKU(t, db, "SYB-3") != "" {
t.Fatalf("多候选必须留给人工确认: got=%q stored=%q err=%v", got, readOrderSKU(t, db, "SYB-3"), err)
}
})
}
func TestAssignSybShopeeSKU_拒绝其他商品的规格(t *testing.T) {
func TestUpsertSybOrder_补建骨架并由报表提升来源(t *testing.T) {
db := newSyncTestDB(t)
for _, goodsID := range []string{"SP-A", "SP-B"} {
if err := repository.UpsertShopeeProduct(db, goodsID, goodsID, "", ""); err != nil {
t.Fatal(err)
}
}
seedWorkflowSKU(t, db, "SKU-A", "SP-A", "黑色,M")
seedWorkflowSKU(t, db, "SKU-B", "SP-B", "白色,L")
seedWorkflowOrder(t, db, "SYB-A", "SP-A", "未知规格")
seedWorkflowOrder(t, db, "SYB-1", "SP-1", "黑色, M")
if err := AssignSybShopeeSKU(db, "SYB-A", "SKU-B"); err == nil {
t.Fatal("不得把其他蝦皮商品的 SKU 保存到当前顺运宝明细")
product, err := repository.GetShopeeProductByGoodsID(db, "SP-1")
if err != nil || product == nil || product.Source != "syb" || product.Title != "测试商品" {
t.Fatalf("顺运宝骨架不正确: product=%+v err=%v", product, err)
}
if got := readOrderSKU(t, db, "SYB-A"); got != "" {
t.Fatalf("越权选择后不应写库,实际 %q", got)
context, err := repository.GetSybOrderContext(db, "SYB-1")
if err != nil || context == nil || context.Order.SpecKey != "黑色, M" {
t.Fatalf("规格身份键不正确: context=%+v err=%v", context, err)
}
if err := AssignSybShopeeSKU(db, "SYB-A", "SKU-A"); err != nil {
t.Fatalf("同商品 SKU 应保存成功: %v", err)
if err := repository.UpsertShopeeProduct(db, "SP-1", "报表标题", "正常", "MAIN-1"); err != nil {
t.Fatal(err)
}
product, err = repository.GetShopeeProductByGoodsID(db, "SP-1")
if err != nil || product.Source != "report" || product.Title != "报表标题" {
t.Fatalf("报表导入应提升骨架来源: product=%+v err=%v", product, err)
}
seedWorkflowOrder(t, db, "SYB-2", "SP-1", "白色,L")
product, _ = repository.GetShopeeProductByGoodsID(db, "SP-1")
if product.Source != "report" || product.Title != "报表标题" {
t.Fatalf("后续同步不得覆盖报表商品: %+v", product)
}
}
func TestListSybOrdersView_阶段筛选区分蝦皮识别和PDD准备(t *testing.T) {
func TestListSybOrdersView_空规格阻断且普通规格直接进入PDD阶段(t *testing.T) {
db := newSyncTestDB(t)
seedWorkflowOrder(t, db, "SYB-MISSING", "NOT-IMPORTED", "黑色,M")
if err := repository.UpsertShopeeProduct(db, "SP-READY", "商品", "", ""); err != nil {
t.Fatal(err)
}
seedWorkflowSKU(t, db, "SKU-READY", "SP-READY", "黑色,M")
seedWorkflowOrder(t, db, "SYB-SKU", "SP-READY", "无法自动匹配")
order := seedWorkflowOrder(t, db, "SYB-PDD", "SP-READY", "黑色,M")
if _, err := AutoResolveSybShopeeSKU(db, order); err != nil {
t.Fatal(err)
}
seedWorkflowOrder(t, db, "SYB-EMPTY", "SP-EMPTY", "")
seedWorkflowOrder(t, db, "SYB-PDD", "SP-PDD", "黑色,M")
cases := []struct{ stage, wantID string }{
{SybStageMissingShopee, "SYB-MISSING"},
{SybStageSKUPending, "SYB-SKU"},
{SybStageSpecMissing, "SYB-EMPTY"},
{SybStagePddMissing, "SYB-PDD"},
}
for _, tc := range cases {
@@ -135,3 +70,43 @@ func TestListSybOrdersView_阶段筛选区分蝦皮识别和PDD准备(t *testing
}
}
}
func TestSybStageFor_全部筛选阶段与逐行推导一致(t *testing.T) {
key, err := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"})
if err != nil {
t.Fatal(err)
}
ready := repository.SybOrderContext{
Order: model.SybOrder{SpecKey: "黑色,M", Quantity: 1},
PddGoodsID: "737116531267", PddCollectStatus: string(model.CollectCollected),
PddSkusJSON: collectedThreeDimensions, MappingOptionKey: key,
}
copyWith := func(change func(*repository.SybOrderContext)) repository.SybOrderContext {
value := ready
change(&value)
return value
}
cases := []struct {
want string
context repository.SybOrderContext
}{
{SybStageSpecMissing, copyWith(func(c *repository.SybOrderContext) { c.Order.SpecKey = "" })},
{SybStagePddMissing, copyWith(func(c *repository.SybOrderContext) { c.PddGoodsID, c.PddCollectStatus = "", "" })},
{SybStagePddPending, copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectPending) })},
{SybStagePddCollecting, copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectCollecting) })},
{SybStagePddFailed, copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectFailed) })},
{SybStageMappingPending, copyWith(func(c *repository.SybOrderContext) { c.MappingOptionKey = "" })},
{SybStageTaskCreated, copyWith(func(c *repository.SybOrderContext) { c.HasActiveTask = true })},
{SybStagePurchaseBlocked, copyWith(func(c *repository.SybOrderContext) { c.Order.Quantity = 0 })},
{SybStagePurchaseReady, ready},
}
if got := len(SybStageOptions()) - 1; got != len(cases) {
t.Fatalf("筛选项有 %d 个业务阶段,但测试只覆盖 %d 个", got, len(cases))
}
for _, tc := range cases {
stage, text, _, action := sybStageFor(tc.context)
if stage != tc.want || text != SybStageLabel(tc.want) || action == "" {
t.Fatalf("阶段 %s 推导错误: stage=%s text=%q action=%q", tc.want, stage, text, action)
}
}
}