diff --git a/admin/main_test.go b/admin/main_test.go index f3826fb..b16e5d8 100644 --- a/admin/main_test.go +++ b/admin/main_test.go @@ -84,6 +84,22 @@ func TestPddDetailRecollect_使用独立入口并可选客户端(t *testing.T) { } } +func TestTaskDetail_采购核单结果使用独立只读分组(t *testing.T) { + content, err := os.ReadFile("templates/task/detail_modal.html") + if err != nil { + t.Fatal(err) + } + page := string(content) + for _, want := range []string{ + "PDD 核单结果", "PDD 订单编号", "下单时间", "上报时付款状态", + "{{if .IsPurchase}}", "{{if .HasPddOrder}}", "完整结果(result_data)", + } { + if !strings.Contains(page, want) { + t.Errorf("采购任务详情缺少 %q", want) + } + } +} + func TestTemplatesAndRoutesCanBeBuiltWithoutDatabaseConnection(t *testing.T) { if _, err := newRouter(nil); err != nil { t.Fatalf("模板或路由组装失败: %v", err) diff --git a/admin/service/task.go b/admin/service/task.go index 9890fa2..3f97bbb 100644 --- a/admin/service/task.go +++ b/admin/service/task.go @@ -345,6 +345,14 @@ type TaskDetailView struct { QuantityText string PriceLimitText string + // PDD 核单结果只在采购任务详情显示。付款状态是 Client 核单上报时的快照, + // 不是 Admin 对 PDD 的实时查询结果。 + HasPddOrder bool + PddOrderNo string + PddOrderedAt string + PddPaymentStatusText string + PddOrderResultText string + HasError bool ErrorCode string ErrorMessage string @@ -377,6 +385,7 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) { v.SpecText = specText(t.PddOptions) v.QuantityText = quantityText(t.Quantity) v.PriceLimitText = priceLimitText(t.MaxPriceCent) + setPddOrderResult(v, *t) } if t.ErrorCode != "" || t.ErrorMessage != "" { @@ -392,6 +401,61 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) { return v, nil } +// purchaseResult 只声明详情页需要展示的字段,避免把 Client 的完整结果结构 +// 复制到 Admin。其余原始字段仍保存在 result_data 并可在详情底部展开查看。 +type purchaseResult struct { + Purchase *struct { + OrderNo string `json:"order_no"` + OrderedAt string `json:"ordered_at"` + PaymentStatus string `json:"payment_status"` + MatchStatus string `json:"match_status"` + OrderSubmitted bool `json:"order_submitted"` + } `json:"purchase"` +} + +// setPddOrderResult 把采购结果翻成模板可直接显示的文字。 +// 解析失败只影响结构化摘要,不能阻止详情页打开,也不能隐藏原始 result_data。 +func setPddOrderResult(v *TaskDetailView, task model.Task) { + raw := strings.TrimSpace(task.ResultData) + if raw == "" { + switch task.Status { + case model.TaskManualReview: + v.PddOrderResultText = "订单结果不确定,请结合错误信息人工核对。" + case model.TaskFailed, model.TaskCancelled: + v.PddOrderResultText = "任务未产生可展示的 PDD 核单结果。" + default: + v.PddOrderResultText = "等待 Client 核单上报。" + } + return + } + + var result purchaseResult + if err := json.Unmarshal([]byte(raw), &result); err != nil || result.Purchase == nil { + v.PddOrderResultText = "采购结果格式异常,请展开完整结果进行诊断。" + return + } + p := result.Purchase + if p.MatchStatus != "matched" || !p.OrderSubmitted { + v.PddOrderResultText = "本次结果没有已匹配的 PDD 订单。" + return + } + if strings.TrimSpace(p.OrderNo) == "" || p.PaymentStatus != "unpaid" { + v.PddOrderResultText = "采购结果格式异常,请展开完整结果进行诊断。" + return + } + orderedAt, ok := model.ParseISO(strings.TrimSpace(p.OrderedAt)) + if !ok { + v.PddOrderResultText = "采购结果中的下单时间格式异常,请展开完整结果进行诊断。" + return + } + + v.HasPddOrder = true + v.PddOrderNo = strings.TrimSpace(p.OrderNo) + v.PddOrderedAt = orderedAt.Local().Format("2006-01-02 15:04") + v.PddPaymentStatusText = "未付款(Client 上报时)" + v.PddOrderResultText = "Admin 不会查询 PDD 实时付款状态。" +} + func orPlaceholder(s string) string { if s == "" { return placeholder diff --git a/admin/service/task_test.go b/admin/service/task_test.go index 21af163..a16e2f1 100644 --- a/admin/service/task_test.go +++ b/admin/service/task_test.go @@ -3,6 +3,7 @@ package service import ( "database/sql" "fmt" + "strings" "testing" "cmautobuy/admin/model" @@ -368,6 +369,66 @@ func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) { } } +func TestGetTaskDetail_采购成功独立显示PDD核单结果(t *testing.T) { + db := newTestDB(t) + insertTestTask(t, db, testTaskParams{ + taskID: "PUR-ORDER", taskType: model.TaskPurchase, status: model.TaskSucceeded, + resultData: `{"purchase":{"order_no":"PDD-20260810-001","ordered_at":"2026-08-10T11:12:13Z","payment_status":"unpaid","match_status":"matched","order_submitted":true}}`, + }) + + d, err := GetTaskDetail(db, "PUR-ORDER") + if err != nil { + t.Fatal(err) + } + if !d.HasPddOrder || d.PddOrderNo != "PDD-20260810-001" { + t.Fatalf("PDD 订单编号没有结构化显示: %+v", d) + } + if d.PddOrderedAt != formatLocalTime("2026-08-10T11:12:13Z") { + t.Errorf("UTC 下单时间应转为本地时间,实际 %q", d.PddOrderedAt) + } + if d.PddPaymentStatusText != "未付款(Client 上报时)" || + d.PddOrderResultText != "Admin 不会查询 PDD 实时付款状态。" { + t.Errorf("付款状态说明不明确: status=%q help=%q", + d.PddPaymentStatusText, d.PddOrderResultText) + } + if !d.HasResult || !strings.Contains(d.ResultData, `"order_no":"PDD-20260810-001"`) { + t.Error("结构化展示后仍必须保留原始结果") + } +} + +func TestGetTaskDetail_核单结果状态分别显示(t *testing.T) { + db := newTestDB(t) + tests := []struct { + name string + status model.TaskStatus + resultData string + want string + }{ + {"等待上报", model.TaskClaimed, "", "等待 Client 核单上报。"}, + {"人工处理", model.TaskManualReview, "", "订单结果不确定,请结合错误信息人工核对。"}, + {"非法JSON", model.TaskSucceeded, `{`, "采购结果格式异常,请展开完整结果进行诊断。"}, + {"缺少purchase", model.TaskSucceeded, `{}`, "采购结果格式异常,请展开完整结果进行诊断。"}, + {"尚未匹配", model.TaskSucceeded, `{"purchase":{"match_status":"not_found"}}`, "本次结果没有已匹配的 PDD 订单。"}, + {"时间非法", model.TaskSucceeded, `{"purchase":{"order_no":"PDD-1","ordered_at":"2026-08-10 11:12","payment_status":"unpaid","match_status":"matched","order_submitted":true}}`, "采购结果中的下单时间格式异常,请展开完整结果进行诊断。"}, + } + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + taskID := fmt.Sprintf("PUR-STATE-%d", i) + insertTestTask(t, db, testTaskParams{ + taskID: taskID, taskType: model.TaskPurchase, + status: tc.status, resultData: tc.resultData, + }) + d, err := GetTaskDetail(db, taskID) + if err != nil { + t.Fatal(err) + } + if d.HasPddOrder || d.PddOrderResultText != tc.want { + t.Errorf("状态说明不对: HasPddOrder=%t text=%q", d.HasPddOrder, d.PddOrderResultText) + } + }) + } +} + func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) { db := newTestDB(t) insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"}) @@ -382,6 +443,9 @@ func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) { if d.SpecText != "" || d.QuantityText != "" || d.PriceLimitText != "" { t.Errorf("采集任务不该填充采购专有字段: spec=%q qty=%q price=%q", d.SpecText, d.QuantityText, d.PriceLimitText) } + if d.HasPddOrder || d.PddOrderResultText != "" { + t.Error("采集任务不应生成 PDD 核单结果展示") + } } func TestGetTaskDetail_有错误信息时显示(t *testing.T) { diff --git a/admin/templates/task/detail_modal.html b/admin/templates/task/detail_modal.html index b756aaa..06196bd 100644 --- a/admin/templates/task/detail_modal.html +++ b/admin/templates/task/detail_modal.html @@ -34,6 +34,20 @@ {{end}} + {{if .IsPurchase}} +

PDD 核单结果

+ {{if .HasPddOrder}} +
+
PDD 订单编号
{{.PddOrderNo}}
+
下单时间
{{.PddOrderedAt}}
+
上报时付款状态
{{.PddPaymentStatusText}}
+
+

{{.PddOrderResultText}}

+ {{else}} +

{{.PddOrderResultText}}

+ {{end}} + {{end}} + {{if .HasError}}

错误

diff --git a/docs/admin/01-requirements.md b/docs/admin/01-requirements.md index 74a9f38..41e08e9 100644 --- a/docs/admin/01-requirements.md +++ b/docs/admin/01-requirements.md @@ -306,6 +306,11 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立 (PDD 链接,采购任务额外显示目标规格、数量、价格上限);错误信息 (错误码、错误说明,没有错误时不显示这一段)。 +采购任务额外显示独立的“PDD 核单结果”:PDD 订单编号、下单时间和 +“上报时付款状态”。这些字段从现有 `result_data.purchase` 读取;下单时间转换为 +Admin 本地时区,付款状态只是 Client 核单上报时的快照,Admin 不查询 PDD 实时状态。 +尚未上报、订单结果不确定和结果格式异常必须使用不同的明确文字;异常时仍保留原始结果。 + `[必须]` 采购专有字段(数量、价格上限、目标规格)在采集任务的弹窗里 **整段隐藏**,不显示空行。 diff --git a/docs/admin/05-ui-specification.md b/docs/admin/05-ui-specification.md index 9e4f89f..23dcd97 100644 --- a/docs/admin/05-ui-specification.md +++ b/docs/admin/05-ui-specification.md @@ -656,6 +656,12 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD │ 数量 2 (采购任务才有) │ │ 价格上限 ¥42.00 (采购任务才有) │ ├──────────────────────────────────────────────┤ +│ PDD 核单结果 (采购任务才有) │ +│ PDD 订单编号 PDD-20260810-001 │ +│ 下单时间 2026-08-10 19:12 │ +│ 上报时付款状态 未付款(Client 上报时) │ +│ Admin 不会查询 PDD 实时付款状态。 │ +├──────────────────────────────────────────────┤ │ 错误 │ │ 错误码 PDD_PAGE_TIMEOUT │ │ 错误说明 商品页加载超时 │ @@ -666,6 +672,10 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD - `[必须]` 采购专有字段(数量、价格上限、目标规格)在采集任务的弹窗里 **整段隐藏**,不显示空行。 +- PDD 核单结果只在采购任务显示,订单编号使用可选择复制的普通文本;付款状态标签 + 必须包含“上报时”,不能暗示为实时状态。 +- 尚未上报显示“等待 Client 核单上报”;需人工时显示“订单结果不确定”;结果结构或 + 带时区时间非法时显示格式异常,并引导展开完整结果诊断。 - 错误段没有错误码/说明时整段不显示。 - `[建议]` `result_data`(Client 提交的完整结果)默认折叠,展开查看; 过长时截断显示。