diff --git a/admin/handler/web/others.go b/admin/handler/web/others.go index 2012fa6..edde1ab 100644 --- a/admin/handler/web/others.go +++ b/admin/handler/web/others.go @@ -207,8 +207,13 @@ func (h *Handler) SybDetail(c *gin.Context) { fail(c, http.StatusNotFound, "顺运宝货运单明细不存在。") return } + assignableClients, err := service.ListAssignableClients(h.db, currentUser(c), h.onlineThreshold) + if err != nil { + fail(c, http.StatusInternalServerError, "读取可分配客户端失败,数据没有被改动。") + return + } c.HTML(http.StatusOK, "syb/detail_modal", gin.H{ - "D": detail, "CSRFToken": csrfToken(c), + "D": detail, "CSRFToken": csrfToken(c), "AssignableClients": assignableClients, "Keyword": c.Query("order_no"), "ShopFilter": c.Query("shop"), "StageFilter": c.Query("stage"), "CurrentPage": service.ParsePage(c.Query("page")), }) @@ -603,7 +608,8 @@ func (h *Handler) SybAssociatePdd(c *gin.Context) { // SybCollectPdd 为货运单当前关联的 PDD 商品创建采集任务。 func (h *Handler) SybCollectPdd(c *gin.Context) { - result, err := service.CreateSybPddCollectTaskForUser(h.db, currentUser(c), c.PostForm("syb_id")) + result, err := service.CreateSybPddCollectTasksForUser( + h.db, currentUser(c), []string{c.PostForm("syb_id")}, c.PostForm("client_id")) if err != nil { h.sybRedirect(c, "采集任务未创建:"+err.Error()) return diff --git a/admin/service/shopee_pdd_test.go b/admin/service/shopee_pdd_test.go index ab85875..e36fcb4 100644 --- a/admin/service/shopee_pdd_test.go +++ b/admin/service/shopee_pdd_test.go @@ -143,6 +143,15 @@ func TestCreateSybPddCollectTasksForUser_同一PDD商品去重(t *testing.T) { if err := db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE task_type='collect'`).Scan(&taskCount); err != nil || taskCount != 1 { t.Fatalf("同一 PDD 商品应只建一条任务,count=%d err=%v", taskCount, err) } + var status model.TaskStatus + var assignedClient sql.NullString + if err := db.QueryRow(`SELECT status,assigned_client FROM tasks WHERE task_type='collect'`). + Scan(&status, &assignedClient); err != nil { + t.Fatal(err) + } + if status != model.TaskPending || assignedClient.Valid { + t.Fatalf("不指定客户端时应为无主待领,status=%q client=%+v", status, assignedClient) + } var sourceCount int if err := db.QueryRow(`SELECT COUNT(*) FROM task_syb_sources`).Scan(&sourceCount); err != nil || sourceCount != 2 { t.Fatalf("去重任务应保留两条顺运宝来源,count=%d err=%v", sourceCount, err) @@ -158,6 +167,34 @@ func TestCreateSybPddCollectTasksForUser_同一PDD商品去重(t *testing.T) { } } +func TestCreateSybPddCollectTasksForUser_指定可见客户端(t *testing.T) { + db := newTestDB(t) + admin, _, _ := insertTaskUsers(t, db) + if err := RegisterClient(db, model.Client{ClientID: "client-chosen", Name: "仓库手机"}, true); err != nil { + t.Fatal(err) + } + seedShopeeProduct(t, db, "1001", "商品一") + if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil { + t.Fatal(err) + } + seedSybCollectOrder(t, db, "SYB-CHOSEN", "1001") + + result, err := CreateSybPddCollectTasksForUser( + db, admin, []string{"SYB-CHOSEN"}, "client-chosen") + if err != nil || result.Created != 1 { + t.Fatalf("指定客户端创建结果=%+v err=%v", result, err) + } + var status model.TaskStatus + var assignedClient string + if err := db.QueryRow(`SELECT status,assigned_client FROM tasks WHERE task_type='collect' AND pdd_goods_id='737116531267'`). + Scan(&status, &assignedClient); err != nil { + t.Fatal(err) + } + if status != model.TaskAssigned || assignedClient != "client-chosen" { + t.Fatalf("指定客户端任务状态=%q client=%q", status, assignedClient) + } +} + func TestCreateSybPddCollectTasksForUser_无任务的采集中状态可恢复(t *testing.T) { db := newTestDB(t) admin, _, _ := insertTaskUsers(t, db) diff --git a/admin/syb_interaction_template_test.go b/admin/syb_interaction_template_test.go index 0e9ea4f..847f108 100644 --- a/admin/syb_interaction_template_test.go +++ b/admin/syb_interaction_template_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "cmautobuy/admin/model" "cmautobuy/admin/service" ) @@ -87,3 +88,37 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) { t.Error("顺运宝列表不应继续使用含义模糊的整行双击入口") } } + +func TestSybDetailTemplate_单条采集可选执行客户端(t *testing.T) { + tmpl, err := template.ParseFS(templateFS, "templates/*/*.html") + if err != nil { + t.Fatal(err) + } + data := map[string]any{ + "D": &service.SybProcessingDetail{ + SybID: "SYB-1", OrderNo: "ORDER-1", Title: "待采集商品", + PddGoodsID: "737116531267", CanCollect: true, + }, + "CSRFToken": "test-csrf", + "AssignableClients": []service.ClientView{{ + Client: model.Client{ClientID: "CLIENT-1", Name: "仓库手机"}, Status: "在线", + }}, + } + var output bytes.Buffer + if err := tmpl.ExecuteTemplate(&output, "syb/detail_modal", data); err != nil { + t.Fatalf("渲染顺运宝详情失败: %v", err) + } + body := output.String() + for _, want := range []string{ + `action="/syb/collect-pdd"`, + `label for="syb-detail-collect-client"`, + `select id="syb-detail-collect-client" name="client_id"`, + `option value="">不指定(任意客户端可领取)`, + `option value="CLIENT-1">仓库手机(CLIENT-1,在线)`, + `指定后任务只等待该客户端领取`, + } { + if !strings.Contains(body, want) { + t.Errorf("顺运宝详情缺少 %q", want) + } + } +} diff --git a/admin/templates/syb/detail_modal.html b/admin/templates/syb/detail_modal.html index ef516bd..f37c68a 100644 --- a/admin/templates/syb/detail_modal.html +++ b/admin/templates/syb/detail_modal.html @@ -39,7 +39,7 @@ {{if .PddGoodsID}} -
+ @@ -47,7 +47,17 @@ {{if .CanCollect}} - +
+ + + 通常保持不指定即可;指定后任务只等待该客户端领取。 +
+
{{else}} {{if eq .CollectStatus "collecting"}}采集任务正在执行,请稍后刷新。{{else}}PDD 数据已采集,下一步匹配采购规格。{{end}} {{end}} diff --git a/docs/admin/05-ui-specification.md b/docs/admin/05-ui-specification.md index 0f7e8fc..fd08a9b 100644 --- a/docs/admin/05-ui-specification.md +++ b/docs/admin/05-ui-specification.md @@ -561,6 +561,12 @@ PDD 采集中(超时)行可用于 标准商品链接并按 `goods_id` 复用已有档案;换成其他商品需要明确确认。待采集或采集失败时显示 “创建采集任务”或“重新创建采集任务”,采集中和已采集状态只显示说明。 +单条创建和重新创建采集任务时,按钮上方显示有可见标签的“执行客户端” +下拉框,只列出当前账号可见的 Client,并显示名称、Client ID 和在线状态。 +默认为“不指定(任意客户端可领取)”;指定后只等待该 Client 领取。持续 +帮助文字必须说清两种选择的后果,不依赖 placeholder 或颜色。该入口与列表 +顶部批量创建共用同一套服务端阶段、客户端可见范围和重复任务校验。 + ```text ┌──────────────────────────────────────────────────┐ │ 规格匹配 · 订单 SO-20260806-001 │