fix: 修复顺运宝采购完成状态与重复采购 (#169)

This commit is contained in:
chengma
2026-08-11 18:05:27 +08:00
parent c61a028d9c
commit 5528b4b874
9 changed files with 160 additions and 26 deletions
+6
View File
@@ -268,6 +268,12 @@ func validatePurchaseRequest(q repository.Execer, request PurchaseTaskRequest) (
if context == nil {
return "顺运宝明细不存在", nil, "", nil
}
if context.HasSucceededPurchaseTask {
return "该顺运宝明细已有成功采购任务,禁止重复采购", context, "", nil
}
if context.HasManualReviewPurchaseTask {
return "该顺运宝明细有待人工核对的采购任务,请先核对订单,禁止重复采购", context, "", nil
}
if context.Order.SpecKey == "" {
return "顺运宝未提供规格", context, "", nil
}
+58
View File
@@ -341,6 +341,64 @@ func TestCreatePurchaseTasks_安全字段和重复保护(t *testing.T) {
}
}
func TestCreatePurchaseTasks_成功或待核对任务禁止重复采购(t *testing.T) {
cases := []struct {
status model.TaskStatus
wantStage string
wantReason string
wantAttention bool
}{
{model.TaskSucceeded, SybStagePurchaseCompleted, "已有成功采购任务", false},
{model.TaskManualReview, SybStagePurchaseReview, "待人工核对", true},
}
for _, tc := range cases {
t.Run(string(tc.status), func(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-LOCKED")
if err := SaveSybMapping(db, "SYB-LOCKED", 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)
}
created, err := CreatePurchaseTasks(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-LOCKED", MaxPriceCent: 4200}}, "CLIENT-1")
if err != nil || created.Created != 1 {
t.Fatalf("准备采购任务失败: result=%+v err=%v", created, err)
}
if _, err := db.Exec(`UPDATE tasks SET status=? WHERE task_type='purchase' AND syb_id=?`, tc.status, "SYB-LOCKED"); err != nil {
t.Fatal(err)
}
before := countPurchaseTasksForSyb(t, db, "SYB-LOCKED")
result, err := CreatePurchaseTasks(db, admin,
[]PurchaseTaskRequest{{SybID: "SYB-LOCKED", MaxPriceCent: 4300}}, "CLIENT-1")
if err != nil || result.Created != 0 || len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0].Reason, tc.wantReason) {
t.Fatalf("%s 任务必须阻止重复采购: result=%+v err=%v", tc.status, result, err)
}
if after := countPurchaseTasksForSyb(t, db, "SYB-LOCKED"); after != before {
t.Fatalf("拒绝后任务数量从 %d 变为 %d", before, after)
}
list, err := ListSybOrdersView(db, "", "", tc.wantStage, 1)
if err != nil || list.Total != 1 || len(list.Rows) != 1 ||
list.Rows[0].Stage != tc.wantStage || list.Rows[0].NeedsAttention != tc.wantAttention {
t.Fatalf("%s 阶段筛选错误: result=%+v err=%v", tc.status, list, err)
}
})
}
}
func countPurchaseTasksForSyb(t *testing.T, db *sql.DB, sybID string) int {
t.Helper()
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE task_type='purchase' AND syb_id=?`, sybID).Scan(&count); err != nil {
t.Fatal(err)
}
return count
}
func TestCreatePurchaseTasks_客户端权限和逐条失败(t *testing.T) {
db := newTestDB(t)
key := seedPurchasableWorkflow(t, db, "SYB-1")
+16 -6
View File
@@ -959,6 +959,8 @@ const (
SybStageMappingPending = "mapping_pending"
SybStagePurchaseReady = "purchase_ready"
SybStageTaskCreated = "task_created"
SybStagePurchaseCompleted = "purchase_completed"
SybStagePurchaseReview = "purchase_review"
SybStagePurchaseBlocked = "purchase_blocked"
)
@@ -980,6 +982,8 @@ func SybStageOptions() []SybStageOption {
{Value: SybStageMappingPending, Text: "规格待匹配"},
{Value: SybStagePurchaseReady, Text: "可创建采购任务"},
{Value: SybStageTaskCreated, Text: "已创建采购任务"},
{Value: SybStagePurchaseCompleted, Text: "采购完成"},
{Value: SybStagePurchaseReview, Text: "采购待人工核对"},
{Value: SybStagePurchaseBlocked, Text: "采购数据异常"},
}
}
@@ -1004,6 +1008,12 @@ func SybStageLabel(stage string) string {
func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string) {
switch {
case c.HasSucceededPurchaseTask:
return SybStagePurchaseCompleted, "采购完成", "采购任务已成功完成,请到采集采购页面查看 PDD 订单编号和下单时间。", "查看采购结果"
case c.HasManualReviewPurchaseTask:
return SybStagePurchaseReview, "采购待人工核对", "任务可能已经下单,必须先核对订单,禁止重新创建采购任务。", "查看采购任务"
case c.HasActiveTask:
return SybStageTaskCreated, "已创建采购任务", "已有未结束的采购任务,请到采集采购页面查看。", "查看采购任务"
case c.Order.SpecKey == "":
return SybStageSpecMissing, "采购数据异常:顺运宝未提供规格", "缺少规格原文,不能匹配或创建采购任务。", "核对数据"
case c.PddGoodsID == "" || c.PddCollectStatus == "":
@@ -1020,8 +1030,6 @@ func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string
help += " 原因:" + c.PddCollectMsg
}
return SybStagePddFailed, "PDD 采集失败", help, "重新采集"
case c.HasActiveTask:
return SybStageTaskCreated, "已创建采购任务", "已有未结束的采购任务,请到采集采购页面查看。", "查看采购任务"
case !mappingIsValid(c):
return SybStageMappingPending, "规格待匹配", "PDD 数据已采集,下一步匹配采购规格。", "匹配规格"
case c.Order.Quantity <= 0:
@@ -1059,7 +1067,8 @@ func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybL
stage = ParseSybStage(stage)
if stage == SybStagePddCollecting || stage == SybStagePddCollectingStale ||
stage == SybStageMappingPending || stage == SybStagePurchaseReady ||
stage == SybStageTaskCreated || stage == SybStagePurchaseBlocked {
stage == SybStageTaskCreated || stage == SybStagePurchaseCompleted ||
stage == SybStagePurchaseReview || stage == SybStagePurchaseBlocked {
return listAdvancedSybStage(db, keyword, shop, stage, page)
}
filter := repository.SybOrderFilter{Keyword: keyword, Shop: shop, Stage: stage}
@@ -1096,8 +1105,8 @@ func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybL
return result, nil
}
// listAdvancedSybStage 在 Service 解析最新 skus_json 后筛选映射相关阶段。
// 这四个阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
// listAdvancedSybStage 在 Service 解析最新 skus_json 后筛选实时推导阶段。
// 这些阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
// OptionKey 规范化逻辑。当前同步量是百到千级,先保证采购判断正确;普通列表
// 和其余阶段仍在数据库分页。
func listAdvancedSybStage(db *sql.DB, keyword, shop, stage string, page int) (*SybListResult, error) {
@@ -1166,7 +1175,8 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
}
}
}
v.NeedsAttention = v.Stage != SybStagePddCollecting && v.Stage != SybStageTaskCreated
v.NeedsAttention = v.Stage != SybStagePddCollecting && v.Stage != SybStageTaskCreated &&
v.Stage != SybStagePurchaseCompleted
if o.PriceTwdCent > 0 {
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
} else {
+19
View File
@@ -125,6 +125,14 @@ func TestSybStageFor_全部筛选阶段与逐行推导一致(t *testing.T) {
{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 })},
{SybStagePurchaseCompleted, "查看采购结果", copyWith(func(c *repository.SybOrderContext) {
c.HasSucceededPurchaseTask, c.HasManualReviewPurchaseTask = true, true
c.Order.SpecKey = ""
})},
{SybStagePurchaseReview, "查看采购任务", copyWith(func(c *repository.SybOrderContext) {
c.HasManualReviewPurchaseTask = true
c.MappingOptionKey = ""
})},
{SybStagePurchaseBlocked, "核对数据", copyWith(func(c *repository.SybOrderContext) { c.Order.Quantity = 0 })},
{SybStagePurchaseReady, "创建采购任务", ready},
}
@@ -182,6 +190,17 @@ func TestSybOrderViewFor_批量动作能力按阶段隔离(t *testing.T) {
if view.CanCollect || !view.CanPurchase {
t.Fatalf("采购就绪行应只允许采购:CanCollect=%v CanPurchase=%v", view.CanCollect, view.CanPurchase)
}
for _, change := range []func(*repository.SybOrderContext){
func(c *repository.SybOrderContext) { c.HasSucceededPurchaseTask = true },
func(c *repository.SybOrderContext) { c.HasManualReviewPurchaseTask = true },
} {
locked := ready
change(&locked)
lockedView := sybOrderViewFor(locked)
if lockedView.CanCollect || lockedView.CanPurchase {
t.Fatalf("采购完成或待核对行不得再提供批量动作:%+v", lockedView)
}
}
}
func TestSybCollectingIsStale_有效任务但时间损坏时允许恢复(t *testing.T) {