feat: 支持采购任务追溯并安全重新创建 (#186)
This commit is contained in:
@@ -158,6 +158,27 @@ func seedPurchasableWorkflow(t *testing.T, db *sql.DB, sybID string) string {
|
||||
return key
|
||||
}
|
||||
|
||||
func TestGetSybOrderView_按明细ID准备单条采购确认(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
key := seedPurchasableWorkflow(t, db, "SYB-DEEP-LINK")
|
||||
if err := SaveSybMapping(db, "SYB-DEEP-LINK", key, "USR-1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
view, err := GetSybOrderView(db, " SYB-DEEP-LINK ")
|
||||
if err != nil || view == nil {
|
||||
t.Fatalf("精确读取失败: view=%+v err=%v", view, err)
|
||||
}
|
||||
if !view.CanPurchase || view.SybID != "SYB-DEEP-LINK" ||
|
||||
view.MappingOptionKey != key || view.DefaultUnitPrice != "39.90" {
|
||||
t.Fatalf("深链接采购确认数据不完整: %+v", view)
|
||||
}
|
||||
missing, err := GetSybOrderView(db, "NOT-FOUND")
|
||||
if err != nil || missing != nil {
|
||||
t.Fatalf("不存在明细应返回 nil: view=%+v err=%v", missing, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSybMapping_动态维度保存并复用(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
key := seedPurchasableWorkflow(t, db, "SYB-1")
|
||||
|
||||
@@ -1197,6 +1197,17 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
|
||||
return v
|
||||
}
|
||||
|
||||
// GetSybOrderView 按稳定明细 ID 读取一行完整视图,供任务来源深链接在分页之外
|
||||
// 准备采购确认数据。查不到返回 (nil, nil)。
|
||||
func GetSybOrderView(db *sql.DB, sybID string) (*SybOrderView, error) {
|
||||
context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID))
|
||||
if err != nil || context == nil {
|
||||
return nil, err
|
||||
}
|
||||
view := sybOrderViewFor(*context)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
// SybProcessingDetail 是顺运宝“下一步”弹窗第一阶段需要的上下文。
|
||||
type SybProcessingDetail struct {
|
||||
SybID string
|
||||
|
||||
+41
-6
@@ -12,6 +12,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -456,7 +457,8 @@ const resultDataLimit = 4000
|
||||
|
||||
// TaskDetailView 是双击弹窗要显示的全部内容。
|
||||
//
|
||||
// `[必须]` 只读——本工单不做改派、重试、取消,弹窗里不应该有对应的表单。
|
||||
// `[必须]` 字段只读——弹窗不直接改派、重试、取消或创建任务;安全恢复只导航
|
||||
// 回原顺运宝明细,真正创建仍走原确认表单和服务端门禁。
|
||||
type TaskDetailView struct {
|
||||
TaskID string
|
||||
TypeText string
|
||||
@@ -471,12 +473,20 @@ type TaskDetailView struct {
|
||||
PddGoodsURL string
|
||||
PddShopText string
|
||||
|
||||
// IsPurchase 为 false 时,模板要把下面四个采购专有字段整段隐藏,
|
||||
// IsPurchase 为 false 时,模板要把下面的采购专有字段整段隐藏,
|
||||
// 不能显示空行——见 #19。
|
||||
IsPurchase bool
|
||||
SpecText string
|
||||
QuantityText string
|
||||
PriceLimitText string
|
||||
IsPurchase bool
|
||||
ShopeeOrderNo string
|
||||
SybID string
|
||||
ShopeeGoodsID string
|
||||
PddGoodsID string
|
||||
HasSybSource bool
|
||||
ViewSybURL string
|
||||
CanRecreatePurchase bool
|
||||
RecreatePurchaseURL string
|
||||
SpecText string
|
||||
QuantityText string
|
||||
PriceLimitText string
|
||||
|
||||
// PDD 核单结果只在采购任务详情显示。付款状态是 Client 核单上报时的快照,
|
||||
// 不是 Admin 对 PDD 的实时查询结果。
|
||||
@@ -522,6 +532,18 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
|
||||
IsPurchase: t.TaskType == model.TaskPurchase,
|
||||
}
|
||||
if v.IsPurchase {
|
||||
v.ShopeeOrderNo = orPlaceholder(t.OrderNo)
|
||||
v.SybID = orPlaceholder(t.SybID)
|
||||
v.ShopeeGoodsID = orPlaceholder(t.GoodsID)
|
||||
v.PddGoodsID = orPlaceholder(t.PddGoodsID)
|
||||
v.HasSybSource = strings.TrimSpace(t.SybID) != ""
|
||||
if v.HasSybSource {
|
||||
v.ViewSybURL = taskSybURL(*t, false)
|
||||
v.CanRecreatePurchase = t.Status == model.TaskFailed || t.Status == model.TaskCancelled
|
||||
if v.CanRecreatePurchase {
|
||||
v.RecreatePurchaseURL = taskSybURL(*t, true)
|
||||
}
|
||||
}
|
||||
v.SpecText = specText(t.PddOptions)
|
||||
v.QuantityText = quantityText(t.Quantity)
|
||||
v.PriceLimitText = priceLimitText(t.MaxPriceCent)
|
||||
@@ -541,6 +563,19 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// taskSybURL 生成采购任务来源的稳定深链接。syb_id 用于精确定位;订单号只负责
|
||||
// 让跳转后的列表保留有意义的上下文,不能拿它代替明细主键。
|
||||
func taskSybURL(task model.Task, purchaseIntent bool) string {
|
||||
values := url.Values{"open_syb_id": {task.SybID}}
|
||||
if strings.TrimSpace(task.OrderNo) != "" {
|
||||
values.Set("order_no", task.OrderNo)
|
||||
}
|
||||
if purchaseIntent {
|
||||
values.Set("purchase_intent", "1")
|
||||
}
|
||||
return "/syb?" + values.Encode()
|
||||
}
|
||||
|
||||
// GetTaskDetailForUser 对采购员隐藏其他人和历史任务,统一表现为不存在。
|
||||
func GetTaskDetailForUser(db *sql.DB, actor *model.User, taskID string) (*TaskDetailView, error) {
|
||||
visibleUserID, err := visibleClientUserID(actor)
|
||||
|
||||
@@ -44,7 +44,9 @@ type testTaskParams struct {
|
||||
taskType model.TaskType
|
||||
status model.TaskStatus
|
||||
assignedClient string
|
||||
sybID string
|
||||
orderNo string
|
||||
goodsID string
|
||||
pddGoodsID string
|
||||
pddOptions string
|
||||
quantity int
|
||||
@@ -61,13 +63,19 @@ func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
|
||||
t.Helper()
|
||||
now := model.NowISO()
|
||||
|
||||
var assigned, orderNo, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
|
||||
var assigned, sybID, orderNo, goodsID, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
|
||||
if p.assignedClient != "" {
|
||||
assigned = p.assignedClient
|
||||
}
|
||||
if p.orderNo != "" {
|
||||
orderNo = p.orderNo
|
||||
}
|
||||
if p.sybID != "" {
|
||||
sybID = p.sybID
|
||||
}
|
||||
if p.goodsID != "" {
|
||||
goodsID = p.goodsID
|
||||
}
|
||||
if p.pddGoodsID != "" {
|
||||
pddGoodsID = p.pddGoodsID
|
||||
}
|
||||
@@ -100,14 +108,14 @@ func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO tasks (task_id, task_type, status, assigned_client, claimed_at,
|
||||
order_no, pdd_goods_url, pdd_goods_id, pdd_options,
|
||||
syb_id, order_no, goods_id, pdd_goods_url, pdd_goods_id, pdd_options,
|
||||
quantity, max_price_cent,
|
||||
error_code, error_message, result_data, finished_at, created_by_user_id,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
p.taskID, string(p.taskType), string(p.status), assigned, claimedAt,
|
||||
orderNo, pddGoodsID, pddOptions,
|
||||
sybID, orderNo, goodsID, pddGoodsID, pddOptions,
|
||||
quantity, maxPrice,
|
||||
errCode, errMsg, resultData, finishedAt, nullableString(p.createdByUserID),
|
||||
now, now)
|
||||
@@ -425,8 +433,9 @@ func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
insertTestTask(t, db, testTaskParams{
|
||||
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskClaimed,
|
||||
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
|
||||
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
|
||||
sybID: "SYB-001", orderNo: "SO-001", goodsID: "SHOPEE-001", pddGoodsID: "PDD-001",
|
||||
pddOptions: `{"color":"黑色","size":"M"}`,
|
||||
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
|
||||
claimedAt: model.NowISO(),
|
||||
})
|
||||
|
||||
@@ -446,6 +455,40 @@ func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
|
||||
if d.ClientText != "client-001" {
|
||||
t.Errorf("分配客户端 = %q, want client-001", d.ClientText)
|
||||
}
|
||||
if d.ShopeeOrderNo != "SO-001" || d.SybID != "SYB-001" ||
|
||||
d.ShopeeGoodsID != "SHOPEE-001" || d.PddGoodsID != "PDD-001" || !d.HasSybSource {
|
||||
t.Errorf("采购来源字段不完整: %+v", d)
|
||||
}
|
||||
if d.ViewSybURL != "/syb?open_syb_id=SYB-001&order_no=SO-001" || d.CanRecreatePurchase || d.RecreatePurchaseURL != "" {
|
||||
t.Errorf("执行中的采购任务只应允许查看来源: view=%q recreate=%t url=%q",
|
||||
d.ViewSybURL, d.CanRecreatePurchase, d.RecreatePurchaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_只有失败和取消允许进入重新创建确认(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
statuses := []model.TaskStatus{
|
||||
model.TaskPending, model.TaskAssigned, model.TaskClaimed, model.TaskSucceeded,
|
||||
model.TaskManualReview, model.TaskFailed, model.TaskCancelled,
|
||||
}
|
||||
for _, status := range statuses {
|
||||
taskID := "PUR-" + string(status)
|
||||
insertTestTask(t, db, testTaskParams{
|
||||
taskID: taskID, taskType: model.TaskPurchase, status: status,
|
||||
sybID: "SYB A&B", orderNo: "ORDER/1", goodsID: "SHOPEE-1", pddGoodsID: "PDD-1",
|
||||
})
|
||||
detail, err := GetTaskDetail(db, taskID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := status == model.TaskFailed || status == model.TaskCancelled
|
||||
if detail.CanRecreatePurchase != want {
|
||||
t.Errorf("状态 %s CanRecreatePurchase=%t,want %t", status, detail.CanRecreatePurchase, want)
|
||||
}
|
||||
if want && detail.RecreatePurchaseURL != "/syb?open_syb_id=SYB+A%26B&order_no=ORDER%2F1&purchase_intent=1" {
|
||||
t.Errorf("状态 %s 的深链接编码错误: %q", status, detail.RecreatePurchaseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_采购成功独立显示PDD核单结果(t *testing.T) {
|
||||
@@ -525,6 +568,9 @@ func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) {
|
||||
if d.HasPddOrder || d.PddOrderResultText != "" {
|
||||
t.Error("采集任务不应生成 PDD 核单结果展示")
|
||||
}
|
||||
if d.HasSybSource || d.ShopeeOrderNo != "" || d.CanRecreatePurchase {
|
||||
t.Error("采集任务不应生成采购来源和重新创建入口")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskDetail_有错误信息时显示(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user