package service import ( "database/sql" "testing" "time" "cmautobuy/admin/model" "cmautobuy/admin/repository" ) const collectedThreeDimensions = `{ "goods_id":"737116531267", "price_granularity":"sku", "dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}], "skus":[ {"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true}, {"options":{"style":"加绒","size":"L码","color":"白色"},"price_cent":4590,"available":true}, {"options":{"style":"下架","size":"S码","color":"红色"},"price_cent":2990,"available":false} ] }` func seedPurchasableWorkflow(t *testing.T, db *sql.DB, sybID string) string { t.Helper() if err := repository.UpsertShopeeProduct(db, "SP-1", "测试上衣", "正常", ""); err != nil { t.Fatal(err) } seedWorkflowSKU(t, db, "SKU-1", "SP-1", "黑色,M") order := seedWorkflowOrder(t, db, sybID, "SP-1", "黑色,M") order.PriceTwdCent = 999900 if _, err := repository.UpsertSybOrder(db, order); err != nil { t.Fatal(err) } if err := AssignSybShopeeSKU(db, sybID, "SKU-1"); err != nil { t.Fatal(err) } if _, err := AssociateShopeePdd(db, "SP-1", pddURLA, false); err != nil { t.Fatal(err) } if err := repository.SetCollectResult(db, "737116531267", "PDD 上衣", "店铺", collectedThreeDimensions); err != nil { t.Fatal(err) } key, _ := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"}) return key } func TestSybMapping_动态维度保存并复用(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") detail, err := GetSybProcessingDetail(db, "SYB-1") if err != nil { t.Fatal(err) } if len(detail.PddDimensionNames) != 3 || detail.PddDimensionNames[2] != "款式" || len(detail.PddChoices) != 2 { t.Fatalf("动态维度/可用组合不对: names=%v choices=%+v", detail.PddDimensionNames, detail.PddChoices) } if err := SaveSybMapping(db, "SYB-1", key, "USR-1"); err != nil { t.Fatal(err) } detail, _ = GetSybProcessingDetail(db, "SYB-1") if !detail.MappingValid { t.Fatal("保存后相同蝦皮 SKU + 当前 PDD 商品应自动复用") } list, _ := ListSybOrdersView(db, "", SybStagePurchaseReady, 1) if list.Total != 1 || !list.Rows[0].CanPurchase || list.Rows[0].DefaultMaxPrice != "39.90" { t.Fatalf("可采购阶段不对: %+v", list) } } func TestSybMapping_换品和选项消失都会失效(t *testing.T) { t.Run("换 PDD 商品隔离旧映射", func(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") SaveSybMapping(db, "SYB-1", key, "USR-1") if _, err := AssociateShopeePdd(db, "SP-1", pddURLB, true); err != nil { t.Fatal(err) } repository.SetCollectResult(db, "937122477375", "另一个商品", "店铺", stringsReplaceGoodsID(collectedThreeDimensions)) detail, _ := GetSybProcessingDetail(db, "SYB-1") if detail.MappingValid { t.Fatal("更换 PDD 商品后不得复用旧映射") } }) t.Run("最新采集结果删除原选项", func(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") SaveSybMapping(db, "SYB-1", key, "USR-1") changed := `{"goods_id":"737116531267","dimensions":[{"key":"color","name":"颜色"}],"skus":[{"options":{"color":"白色"},"price_cent":1000,"available":true}]}` repository.SetCollectResult(db, "737116531267", "PDD 上衣", "店铺", changed) detail, _ := GetSybProcessingDetail(db, "SYB-1") if detail.MappingValid || detail.Stage != SybStageMappingPending { t.Fatalf("失效映射应回到待匹配: valid=%t stage=%s", detail.MappingValid, detail.Stage) } }) } func stringsReplaceGoodsID(raw string) string { return `{"goods_id":"937122477375","price_granularity":"sku","dimensions":[{"key":"color","name":"颜色"},{"key":"size","name":"尺码"},{"key":"style","name":"款式"}],"skus":[{"options":{"style":"常规","size":"M码","color":"黑色"},"price_cent":3990,"available":true}]}` } func TestCreatePurchaseTasks_安全字段和重复保护(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") if err := SaveSybMapping(db, "SYB-1", key, "USR-1"); err != nil { t.Fatal(err) } admin, _, _ := prepareClientAssignmentUsers(t, db) if err := RegisterClient(db, model.Client{ClientID: "CLIENT-1", Name: "采购机"}, true); err != nil { t.Fatal(err) } result, err := CreatePurchaseTasks(db, admin, []PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4200}}, "CLIENT-1") if err != nil || result.Created != 1 || len(result.Failures) != 0 { t.Fatalf("创建结果=%+v err=%v", result, err) } var task model.Task var status string err = db.QueryRow(`SELECT status, assigned_client, syb_id, order_no, goods_id, shopee_sku_id, pdd_goods_url, pdd_goods_id, pdd_options, quantity, max_price_cent FROM tasks WHERE task_type='purchase' AND syb_id='SYB-1'`).Scan( &status, &task.AssignedClient, &task.SybID, &task.OrderNo, &task.GoodsID, &task.ShopeeSKUID, &task.PddGoodsURL, &task.PddGoodsID, &task.PddOptions, &task.Quantity, &task.MaxPriceCent) if err != nil { t.Fatal(err) } if status != "assigned" || task.MaxPriceCent != 4200 || task.MaxPriceCent == 999900 || task.Quantity != 1 || task.PddOptions != key || task.AssignedClient != "CLIENT-1" { t.Fatalf("采购任务字段不安全或不完整: status=%s task=%+v", status, task) } claimed, err := repository.ClaimNextTask(db, "CLIENT-1", []string{"purchase"}) if err != nil || claimed == nil || claimed.PddOptions != key || claimed.Quantity != 1 || claimed.MaxPriceCent != 4200 { t.Fatalf("现有 Client 领取契约读不到完整采购参数: task=%+v err=%v", claimed, err) } second, err := CreatePurchaseTasks(db, admin, []PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4300}}, "CLIENT-1") if err != nil || second.Created != 0 || len(second.Failures) != 1 { t.Fatalf("重复提交应逐条拒绝: result=%+v err=%v", second, err) } } func TestCreatePurchaseTasks_客户端权限和逐条失败(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") SaveSybMapping(db, "SYB-1", key, "USR-1") admin, buyerA, buyerB := prepareClientAssignmentUsers(t, db) RegisterClient(db, model.Client{ClientID: "CLIENT-A"}, true) AssignClient(db, admin, "CLIENT-A", buyerA.UserID, time.Now()) if _, err := CreatePurchaseTasks(db, buyerB, []PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 4200}}, "CLIENT-A"); err == nil { t.Fatal("采购员不得使用其他采购员的客户端") } result, err := CreatePurchaseTasks(db, buyerA, []PurchaseTaskRequest{{SybID: "SYB-1", MaxPriceCent: 0}, {SybID: "NOT-FOUND", MaxPriceCent: 100}}, "CLIENT-A") if err != nil || result.Created != 0 || len(result.Failures) != 2 { t.Fatalf("业务失败应逐条返回且不创建: result=%+v err=%v", result, err) } } func TestCreatePurchaseTasks_部分业务失败不影响有效明细(t *testing.T) { db := newTestDB(t) key := seedPurchasableWorkflow(t, db, "SYB-1") SaveSybMapping(db, "SYB-1", key, "USR-1") admin, _, _ := prepareClientAssignmentUsers(t, db) RegisterClient(db, model.Client{ClientID: "CLIENT-1"}, true) result, err := CreatePurchaseTasks(db, admin, []PurchaseTaskRequest{ {SybID: "NOT-FOUND", MaxPriceCent: 100}, {SybID: "SYB-1", MaxPriceCent: 4200}, }, "CLIENT-1") if err != nil || result.Created != 1 || len(result.Failures) != 1 || result.Failures[0].SybID != "NOT-FOUND" { t.Fatalf("部分失败边界不对: result=%+v err=%v", result, err) } } func TestParsePriceYuanToCent_精确转换(t *testing.T) { for raw, want := range map[string]int64{"39.90": 3990, "1": 100, "0.01": 1, "12.3": 1230} { got, err := ParsePriceYuanToCent(raw) if err != nil || got != want { t.Fatalf("%s => %d, %v; want %d", raw, got, err, want) } } for _, raw := range []string{"", "0", "-1", "1.234", "abc"} { if _, err := ParsePriceYuanToCent(raw); err == nil { t.Fatalf("应拒绝 %q", raw) } } }