fix: 修复顺运宝采购完成状态与重复采购 (#169)
This commit is contained in:
+15
-2
@@ -406,6 +406,8 @@ type SybOrderContext struct {
|
|||||||
PddUpdatedAt string
|
PddUpdatedAt string
|
||||||
MappingOptionKey string
|
MappingOptionKey string
|
||||||
MappingOptions string
|
MappingOptions string
|
||||||
|
HasSucceededPurchaseTask bool
|
||||||
|
HasManualReviewPurchaseTask bool
|
||||||
HasActiveTask bool
|
HasActiveTask bool
|
||||||
HasActiveCollectTask bool
|
HasActiveCollectTask bool
|
||||||
}
|
}
|
||||||
@@ -417,13 +419,14 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
|||||||
var shopeeExists int
|
var shopeeExists int
|
||||||
var pddGoodsID, pddGoodsURL, collectStatus, collectMsg, skusJSON, pddUpdatedAt sql.NullString
|
var pddGoodsID, pddGoodsURL, collectStatus, collectMsg, skusJSON, pddUpdatedAt sql.NullString
|
||||||
var mappingKey, mappingOptions sql.NullString
|
var mappingKey, mappingOptions sql.NullString
|
||||||
var hasActiveTask, hasActiveCollectTask int
|
var hasSucceededTask, hasManualReviewTask, hasActiveTask, hasActiveCollectTask int
|
||||||
err := s.Scan(
|
err := s.Scan(
|
||||||
&c.Order.SybID, &c.Order.OrderNo, &shopName, &title, &productSpec, &specKey, &shopeeGoodsID,
|
&c.Order.SybID, &c.Order.OrderNo, &shopName, &title, &productSpec, &specKey, &shopeeGoodsID,
|
||||||
&c.Order.Quantity, &priceCent, &imageURL, &c.Order.SybData,
|
&c.Order.Quantity, &priceCent, &imageURL, &c.Order.SybData,
|
||||||
&c.Order.CreatedAt, &c.Order.UpdatedAt, &shopeeExists,
|
&c.Order.CreatedAt, &c.Order.UpdatedAt, &shopeeExists,
|
||||||
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg, &skusJSON, &pddUpdatedAt,
|
&pddGoodsID, &pddGoodsURL, &collectStatus, &collectMsg, &skusJSON, &pddUpdatedAt,
|
||||||
&mappingKey, &mappingOptions, &hasActiveTask, &hasActiveCollectTask,
|
&mappingKey, &mappingOptions, &hasSucceededTask, &hasManualReviewTask,
|
||||||
|
&hasActiveTask, &hasActiveCollectTask,
|
||||||
)
|
)
|
||||||
c.Order.ShopName = shopName.String
|
c.Order.ShopName = shopName.String
|
||||||
c.Order.Title = title.String
|
c.Order.Title = title.String
|
||||||
@@ -441,6 +444,8 @@ func scanSybOrderContext(s rowScanner) (SybOrderContext, error) {
|
|||||||
c.PddUpdatedAt = pddUpdatedAt.String
|
c.PddUpdatedAt = pddUpdatedAt.String
|
||||||
c.MappingOptionKey = mappingKey.String
|
c.MappingOptionKey = mappingKey.String
|
||||||
c.MappingOptions = mappingOptions.String
|
c.MappingOptions = mappingOptions.String
|
||||||
|
c.HasSucceededPurchaseTask = hasSucceededTask != 0
|
||||||
|
c.HasManualReviewPurchaseTask = hasManualReviewTask != 0
|
||||||
c.HasActiveTask = hasActiveTask != 0
|
c.HasActiveTask = hasActiveTask != 0
|
||||||
c.HasActiveCollectTask = hasActiveCollectTask != 0
|
c.HasActiveCollectTask = hasActiveCollectTask != 0
|
||||||
return c, err
|
return c, err
|
||||||
@@ -456,6 +461,10 @@ func ListSybOrderContexts(q Execer, filter SybOrderFilter, limit, offset int) ([
|
|||||||
CASE WHEN sp.goods_id IS NULL THEN 0 ELSE 1 END,
|
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, pp.updated_at, sm.pdd_option_key, sm.pdd_options,
|
pp.skus_json, pp.updated_at, 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 = 'succeeded'),
|
||||||
|
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
||||||
|
AND t.syb_id = so.syb_id AND t.status = 'manual_review'),
|
||||||
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
||||||
AND t.syb_id = so.syb_id
|
AND t.syb_id = so.syb_id
|
||||||
AND t.status IN ('pending', 'assigned', 'claimed')),
|
AND t.status IN ('pending', 'assigned', 'claimed')),
|
||||||
@@ -493,6 +502,10 @@ func GetSybOrderContext(q Execer, sybID string) (*SybOrderContext, error) {
|
|||||||
CASE WHEN sp.goods_id IS NULL THEN 0 ELSE 1 END,
|
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, pp.updated_at, sm.pdd_option_key, sm.pdd_options,
|
pp.skus_json, pp.updated_at, 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 = 'succeeded'),
|
||||||
|
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
||||||
|
AND t.syb_id = so.syb_id AND t.status = 'manual_review'),
|
||||||
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
EXISTS(SELECT 1 FROM tasks t WHERE t.task_type = 'purchase'
|
||||||
AND t.syb_id = so.syb_id
|
AND t.syb_id = so.syb_id
|
||||||
AND t.status IN ('pending', 'assigned', 'claimed')),
|
AND t.status IN ('pending', 'assigned', 'claimed')),
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ func validatePurchaseRequest(q repository.Execer, request PurchaseTaskRequest) (
|
|||||||
if context == nil {
|
if context == nil {
|
||||||
return "顺运宝明细不存在", nil, "", nil
|
return "顺运宝明细不存在", nil, "", nil
|
||||||
}
|
}
|
||||||
|
if context.HasSucceededPurchaseTask {
|
||||||
|
return "该顺运宝明细已有成功采购任务,禁止重复采购", context, "", nil
|
||||||
|
}
|
||||||
|
if context.HasManualReviewPurchaseTask {
|
||||||
|
return "该顺运宝明细有待人工核对的采购任务,请先核对订单,禁止重复采购", context, "", nil
|
||||||
|
}
|
||||||
if context.Order.SpecKey == "" {
|
if context.Order.SpecKey == "" {
|
||||||
return "顺运宝未提供规格", context, "", nil
|
return "顺运宝未提供规格", context, "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestCreatePurchaseTasks_客户端权限和逐条失败(t *testing.T) {
|
||||||
db := newTestDB(t)
|
db := newTestDB(t)
|
||||||
key := seedPurchasableWorkflow(t, db, "SYB-1")
|
key := seedPurchasableWorkflow(t, db, "SYB-1")
|
||||||
|
|||||||
+16
-6
@@ -959,6 +959,8 @@ const (
|
|||||||
SybStageMappingPending = "mapping_pending"
|
SybStageMappingPending = "mapping_pending"
|
||||||
SybStagePurchaseReady = "purchase_ready"
|
SybStagePurchaseReady = "purchase_ready"
|
||||||
SybStageTaskCreated = "task_created"
|
SybStageTaskCreated = "task_created"
|
||||||
|
SybStagePurchaseCompleted = "purchase_completed"
|
||||||
|
SybStagePurchaseReview = "purchase_review"
|
||||||
SybStagePurchaseBlocked = "purchase_blocked"
|
SybStagePurchaseBlocked = "purchase_blocked"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -980,6 +982,8 @@ func SybStageOptions() []SybStageOption {
|
|||||||
{Value: SybStageMappingPending, Text: "规格待匹配"},
|
{Value: SybStageMappingPending, Text: "规格待匹配"},
|
||||||
{Value: SybStagePurchaseReady, Text: "可创建采购任务"},
|
{Value: SybStagePurchaseReady, Text: "可创建采购任务"},
|
||||||
{Value: SybStageTaskCreated, Text: "已创建采购任务"},
|
{Value: SybStageTaskCreated, Text: "已创建采购任务"},
|
||||||
|
{Value: SybStagePurchaseCompleted, Text: "采购完成"},
|
||||||
|
{Value: SybStagePurchaseReview, Text: "采购待人工核对"},
|
||||||
{Value: SybStagePurchaseBlocked, Text: "采购数据异常"},
|
{Value: SybStagePurchaseBlocked, Text: "采购数据异常"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1008,12 @@ func SybStageLabel(stage string) string {
|
|||||||
|
|
||||||
func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string) {
|
func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string) {
|
||||||
switch {
|
switch {
|
||||||
|
case c.HasSucceededPurchaseTask:
|
||||||
|
return SybStagePurchaseCompleted, "采购完成", "采购任务已成功完成,请到采集采购页面查看 PDD 订单编号和下单时间。", "查看采购结果"
|
||||||
|
case c.HasManualReviewPurchaseTask:
|
||||||
|
return SybStagePurchaseReview, "采购待人工核对", "任务可能已经下单,必须先核对订单,禁止重新创建采购任务。", "查看采购任务"
|
||||||
|
case c.HasActiveTask:
|
||||||
|
return SybStageTaskCreated, "已创建采购任务", "已有未结束的采购任务,请到采集采购页面查看。", "查看采购任务"
|
||||||
case c.Order.SpecKey == "":
|
case c.Order.SpecKey == "":
|
||||||
return SybStageSpecMissing, "采购数据异常:顺运宝未提供规格", "缺少规格原文,不能匹配或创建采购任务。", "核对数据"
|
return SybStageSpecMissing, "采购数据异常:顺运宝未提供规格", "缺少规格原文,不能匹配或创建采购任务。", "核对数据"
|
||||||
case c.PddGoodsID == "" || c.PddCollectStatus == "":
|
case c.PddGoodsID == "" || c.PddCollectStatus == "":
|
||||||
@@ -1020,8 +1030,6 @@ func sybStageFor(c repository.SybOrderContext) (stage, text, help, action string
|
|||||||
help += " 原因:" + c.PddCollectMsg
|
help += " 原因:" + c.PddCollectMsg
|
||||||
}
|
}
|
||||||
return SybStagePddFailed, "PDD 采集失败", help, "重新采集"
|
return SybStagePddFailed, "PDD 采集失败", help, "重新采集"
|
||||||
case c.HasActiveTask:
|
|
||||||
return SybStageTaskCreated, "已创建采购任务", "已有未结束的采购任务,请到采集采购页面查看。", "查看采购任务"
|
|
||||||
case !mappingIsValid(c):
|
case !mappingIsValid(c):
|
||||||
return SybStageMappingPending, "规格待匹配", "PDD 数据已采集,下一步匹配采购规格。", "匹配规格"
|
return SybStageMappingPending, "规格待匹配", "PDD 数据已采集,下一步匹配采购规格。", "匹配规格"
|
||||||
case c.Order.Quantity <= 0:
|
case c.Order.Quantity <= 0:
|
||||||
@@ -1059,7 +1067,8 @@ func ListSybOrdersView(db *sql.DB, keyword, shop, stage string, page int) (*SybL
|
|||||||
stage = ParseSybStage(stage)
|
stage = ParseSybStage(stage)
|
||||||
if stage == SybStagePddCollecting || stage == SybStagePddCollectingStale ||
|
if stage == SybStagePddCollecting || stage == SybStagePddCollectingStale ||
|
||||||
stage == SybStageMappingPending || stage == SybStagePurchaseReady ||
|
stage == SybStageMappingPending || stage == SybStagePurchaseReady ||
|
||||||
stage == SybStageTaskCreated || stage == SybStagePurchaseBlocked {
|
stage == SybStageTaskCreated || stage == SybStagePurchaseCompleted ||
|
||||||
|
stage == SybStagePurchaseReview || stage == SybStagePurchaseBlocked {
|
||||||
return listAdvancedSybStage(db, keyword, shop, stage, page)
|
return listAdvancedSybStage(db, keyword, shop, stage, page)
|
||||||
}
|
}
|
||||||
filter := repository.SybOrderFilter{Keyword: keyword, Shop: shop, Stage: stage}
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// listAdvancedSybStage 在 Service 解析最新 skus_json 后筛选映射相关阶段。
|
// listAdvancedSybStage 在 Service 解析最新 skus_json 后筛选实时推导阶段。
|
||||||
// 这四个阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
|
// 这些阶段不能只靠 SQL 判断:同一个 option key 是否仍存在,需要走唯一的
|
||||||
// OptionKey 规范化逻辑。当前同步量是百到千级,先保证采购判断正确;普通列表
|
// OptionKey 规范化逻辑。当前同步量是百到千级,先保证采购判断正确;普通列表
|
||||||
// 和其余阶段仍在数据库分页。
|
// 和其余阶段仍在数据库分页。
|
||||||
func listAdvancedSybStage(db *sql.DB, keyword, shop, stage string, page int) (*SybListResult, error) {
|
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 {
|
if o.PriceTwdCent > 0 {
|
||||||
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
v.PriceText = fmt.Sprintf("NT$%.2f", float64(o.PriceTwdCent)/100)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -125,6 +125,14 @@ func TestSybStageFor_全部筛选阶段与逐行推导一致(t *testing.T) {
|
|||||||
{SybStagePddFailed, "重新采集", copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectFailed) })},
|
{SybStagePddFailed, "重新采集", copyWith(func(c *repository.SybOrderContext) { c.PddCollectStatus = string(model.CollectFailed) })},
|
||||||
{SybStageMappingPending, "匹配规格", copyWith(func(c *repository.SybOrderContext) { c.MappingOptionKey = "" })},
|
{SybStageMappingPending, "匹配规格", copyWith(func(c *repository.SybOrderContext) { c.MappingOptionKey = "" })},
|
||||||
{SybStageTaskCreated, "查看采购任务", copyWith(func(c *repository.SybOrderContext) { c.HasActiveTask = true })},
|
{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 })},
|
{SybStagePurchaseBlocked, "核对数据", copyWith(func(c *repository.SybOrderContext) { c.Order.Quantity = 0 })},
|
||||||
{SybStagePurchaseReady, "创建采购任务", ready},
|
{SybStagePurchaseReady, "创建采购任务", ready},
|
||||||
}
|
}
|
||||||
@@ -182,6 +190,17 @@ func TestSybOrderViewFor_批量动作能力按阶段隔离(t *testing.T) {
|
|||||||
if view.CanCollect || !view.CanPurchase {
|
if view.CanCollect || !view.CanPurchase {
|
||||||
t.Fatalf("采购就绪行应只允许采购:CanCollect=%v CanPurchase=%v", 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) {
|
func TestSybCollectingIsStale_有效任务但时间损坏时允许恢复(t *testing.T) {
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
|
|||||||
Stage: service.SybStageTaskCreated, StageText: "已创建采购任务",
|
Stage: service.SybStageTaskCreated, StageText: "已创建采购任务",
|
||||||
StageHelp: "已有任务", ActionText: "查看采购任务",
|
StageHelp: "已有任务", ActionText: "查看采购任务",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
SybID: "SYB-DONE", OrderNo: "ORDER-DONE", Title: "采购完成商品",
|
||||||
|
Stage: service.SybStagePurchaseCompleted, StageText: "采购完成",
|
||||||
|
StageHelp: "采购任务已成功完成", ActionText: "查看采购结果",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SybID: "SYB-REVIEW", OrderNo: "ORDER-REVIEW", Title: "待核对商品",
|
||||||
|
Stage: service.SybStagePurchaseReview, StageText: "采购待人工核对",
|
||||||
|
StageHelp: "禁止重新采购", ActionText: "查看采购任务",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
SybID: "SYB-PDD", OrderNo: "ORDER-PDD", Title: "待关联商品",
|
SybID: "SYB-PDD", OrderNo: "ORDER-PDD", Title: "待关联商品",
|
||||||
Stage: service.SybStagePddMissing, StageText: "未关联 PDD",
|
Stage: service.SybStagePddMissing, StageText: "未关联 PDD",
|
||||||
@@ -56,6 +66,8 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
|
|||||||
`data-purchase-id="SYB-READY"`,
|
`data-purchase-id="SYB-READY"`,
|
||||||
`href="/tasks?type=purchase`,
|
`href="/tasks?type=purchase`,
|
||||||
`q=ORDER-TASK"`,
|
`q=ORDER-TASK"`,
|
||||||
|
`q=ORDER-DONE"`,
|
||||||
|
`q=ORDER-REVIEW"`,
|
||||||
`data-detail-open-id="SYB-PDD"`,
|
`data-detail-open-id="SYB-PDD"`,
|
||||||
`action="/syb/collect-pdd-batch"`,
|
`action="/syb/collect-pdd-batch"`,
|
||||||
`data-need-checked="collect"`,
|
`data-need-checked="collect"`,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
{{if eq .Stage "purchase_ready"}}
|
{{if eq .Stage "purchase_ready"}}
|
||||||
<button type="button" data-modal-open="purchase-modal" data-purchase-open
|
<button type="button" data-modal-open="purchase-modal" data-purchase-open
|
||||||
data-purchase-id="{{.SybID}}">{{.ActionText}}</button>
|
data-purchase-id="{{.SybID}}">{{.ActionText}}</button>
|
||||||
{{else if eq .Stage "task_created"}}
|
{{else if or (eq .Stage "task_created") (eq .Stage "purchase_completed") (eq .Stage "purchase_review")}}
|
||||||
<a class="button-link" href="/tasks?type=purchase&q={{urlquery .OrderNo}}">{{.ActionText}}</a>
|
<a class="button-link" href="/tasks?type=purchase&q={{urlquery .OrderNo}}">{{.ActionText}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" data-detail-open-id="{{.SybID}}">{{.ActionText}}</button>
|
<button type="button" data-detail-open-id="{{.SybID}}">{{.ActionText}}</button>
|
||||||
|
|||||||
@@ -374,7 +374,12 @@ Admin 本地时区,付款状态只是 Client 核单上报时的快照,Admin
|
|||||||
5. **价格上限已填且大于 0** —— 默认从当前映射的 PDD 规格价格带出,操作员可改,但不允许为空;
|
5. **价格上限已填且大于 0** —— 默认从当前映射的 PDD 规格价格带出,操作员可改,但不允许为空;
|
||||||
6. 已选择分配的客户端;
|
6. 已选择分配的客户端;
|
||||||
7. 所选客户端存在且在当前账号可见范围内;其在线状态和 `purchase_mode` 只影响何时领取,不阻止提前指派;
|
7. 所选客户端存在且在当前账号可见范围内;其在线状态和 `purchase_mode` 只影响何时领取,不阻止提前指派;
|
||||||
8. 同一顺运宝明细没有 `pending` / `assigned` / `claimed` 的采购任务。
|
8. 同一顺运宝明细没有 `pending` / `assigned` / `claimed` 的采购任务;
|
||||||
|
9. 同一顺运宝明细没有 `succeeded` 的采购任务;成功采购是终态,禁止重复采购;
|
||||||
|
10. 同一顺运宝明细没有 `manual_review` 的采购任务;该状态可能已经下单,必须先人工核对,禁止重新采购。
|
||||||
|
|
||||||
|
`failed` 和 `cancelled` 继续沿用现有人工判断后重新创建策略。本规则必须在服务端校验,
|
||||||
|
不能只依赖顺运宝列表的复选框和按钮状态。
|
||||||
|
|
||||||
Admin 新建采购任务固定使用不可变的 `execution_mode=live`,不向采购员提供模式选择:
|
Admin 新建采购任务固定使用不可变的 `execution_mode=live`,不向采购员提供模式选择:
|
||||||
|
|
||||||
@@ -421,6 +426,11 @@ Admin 新建采购任务固定使用不可变的 `execution_mode=live`,不向
|
|||||||
| `failed` | 失败 | 客户端报告失败 |
|
| `failed` | 失败 | 客户端报告失败 |
|
||||||
| `cancelled` | 已取消 | 人工取消 |
|
| `cancelled` | 已取消 | 人工取消 |
|
||||||
|
|
||||||
|
顺运宝页面根据关联采购任务实时推导采购阶段:`succeeded` 显示“采购完成”,
|
||||||
|
`manual_review` 显示“采购待人工核对”,活动任务显示“已创建采购任务”。优先级按
|
||||||
|
成功、待人工核对、活动任务、其他处理阶段排列;完成和待核对行都只能查看任务,
|
||||||
|
不能再次创建采购任务。
|
||||||
|
|
||||||
`[必须]` **Admin 看不到客户端执行到哪一步**(没有心跳,是有意的)。
|
`[必须]` **Admin 看不到客户端执行到哪一步**(没有心跳,是有意的)。
|
||||||
`claimed` 之后就只能等结果。想知道细节看 Client 那边的界面。
|
`claimed` 之后就只能等结果。想知道细节看 Client 那边的界面。
|
||||||
|
|
||||||
|
|||||||
@@ -527,7 +527,9 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
|
|||||||
图片加载失败时显示可读提示。`[必须]` 只存 URL,不代理、缓存或把图片塞进数据库。
|
图片加载失败时显示可读提示。`[必须]` 只存 URL,不代理、缓存或把图片塞进数据库。
|
||||||
- 处理阶段是按当前关联事实**算出来的**,不是存一份容易过期的状态字段。阶段至少区分:
|
- 处理阶段是按当前关联事实**算出来的**,不是存一份容易过期的状态字段。阶段至少区分:
|
||||||
采购数据异常(顺运宝未提供规格)、未关联 PDD、PDD 待采集、采集中、采集中(超时)、
|
采购数据异常(顺运宝未提供规格)、未关联 PDD、PDD 待采集、采集中、采集中(超时)、
|
||||||
采集失败、规格待匹配、可采购和已创建任务。
|
采集失败、规格待匹配、可采购、已创建任务、采购完成和采购待人工核对。
|
||||||
|
- 成功采购任务显示“采购完成”和“查看采购结果”;`manual_review` 显示“采购待人工核对”
|
||||||
|
和“查看采购任务”,并明确提示可能已经下单、必须先核对。两种状态都不允许勾选或重新采购。
|
||||||
- `collecting` 已超过 15 分钟,或已找不到 `pending/assigned/claimed` 采集任务时,
|
- `collecting` 已超过 15 分钟,或已找不到 `pending/assigned/claimed` 采集任务时,
|
||||||
显示“PDD 采集中(超时)”和“重新创建采集任务”,不能继续伪装成正常进行中。
|
显示“PDD 采集中(超时)”和“重新创建采集任务”,不能继续伪装成正常进行中。
|
||||||
- 每行显示一个有文字的“下一步”按钮。商品标题是服务端渲染处理弹窗的明确入口,
|
- 每行显示一个有文字的“下一步”按钮。商品标题是服务端渲染处理弹窗的明确入口,
|
||||||
@@ -605,7 +607,9 @@ grid 样式不得覆盖 `hidden`,关闭后改变勾选再打开也不得残留
|
|||||||
`[必须]` 校验不过的**不要静默跳过**,要列出来告诉操作员缺什么:
|
`[必须]` 校验不过的**不要静默跳过**,要列出来告诉操作员缺什么:
|
||||||
|
|
||||||
- 同一批中合法行可以创建,失败行逐条显示明细 ID 和原因;数据库错误则整批回滚。
|
- 同一批中合法行可以创建,失败行逐条显示明细 ID 和原因;数据库错误则整批回滚。
|
||||||
- 同一明细已有 `pending`、`assigned` 或 `claimed` 的采购任务时拒绝重复创建。
|
- 同一明细已有 `pending`、`assigned`、`claimed`、`succeeded` 或 `manual_review` 的采购任务时
|
||||||
|
拒绝重复创建;成功任务是采购终态,待人工核对任务可能已经下单。`failed/cancelled` 的现有
|
||||||
|
人工判断后重新创建策略不变。
|
||||||
- 顺运宝列表继续显示台币售价,但绝不把它带入人民币价格上限。
|
- 顺运宝列表继续显示台币售价,但绝不把它带入人民币价格上限。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -620,8 +624,10 @@ grid 样式不得覆盖 `hidden`,关闭后改变勾选再打开也不得残留
|
|||||||
可采购行的“下一步”显示“创建采购任务”。点击后复用同一个确认弹窗并只带入当前明细,
|
可采购行的“下一步”显示“创建采购任务”。点击后复用同一个确认弹窗并只带入当前明细,
|
||||||
不改变表格复选框状态;顶部批量入口仍按已勾选明细打开。客户端选项显示名称和 Client
|
不改变表格复选框状态;顶部批量入口仍按已勾选明细打开。客户端选项显示名称和 Client
|
||||||
ID 和在线状态;所有可见 Client 都可选择且只有一个时自动选中。没有可见 Client 时禁用
|
ID 和在线状态;所有可见 Client 都可选择且只有一个时自动选中。没有可见 Client 时禁用
|
||||||
提交并说明需要管理员先绑定客户端。已有未
|
提交并说明需要管理员先绑定客户端。已有未结束任务的行显示“查看采购任务”并进入
|
||||||
结束任务的行显示“查看采购任务”并进入“采集采购”页,不得再次创建。服务端仍必须重新
|
“采集采购”页,不得再次创建;采购完成行显示
|
||||||
|
“查看采购结果”,待人工核对行显示“查看采购任务”,两者同样进入按订单号筛选的采集采购页。
|
||||||
|
服务端仍必须重新
|
||||||
校验映射、数量、人民币价格上限、客户端权限、固定 live 模式、创建审计和重复任务,不能
|
校验映射、数量、人民币价格上限、客户端权限、固定 live 模式、创建审计和重复任务,不能
|
||||||
信任列表页的旧状态。
|
信任列表页的旧状态。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user