feat: 展示任务PDD店铺和客户端名称 (#158)

This commit is contained in:
chengma
2026-08-11 15:49:01 +08:00
parent 3e52e188f5
commit df1287f8eb
7 changed files with 175 additions and 28 deletions
+8 -1
View File
@@ -189,8 +189,15 @@ func TestTaskPage_创建人筛选列表列和详情字段齐全(t *testing.T) {
files := map[string][]string{ files := map[string][]string{
"templates/task/list.html": { "templates/task/list.html": {
`{{if .ShowCreator}}`, `name="creator"`, `{{.CreatorText}}`, `>筛选</button>`, `{{if .ShowCreator}}`, `name="creator"`, `{{.CreatorText}}`, `>筛选</button>`,
`<th>PDD 店铺</th>`, `title="{{.PddShopText}}"`,
`title="{{.ClientTitle}}"`, `{{if .ShowCreator}}10{{else}}9{{end}}`,
},
"templates/task/detail_modal.html": {
`<dt>创建人</dt><dd>{{.CreatorText}}</dd>`,
`<dt>客户端名称</dt><dd>{{.ClientText}}</dd>`,
`<dt>客户端 ID</dt><dd>{{.ClientIDText}}</dd>`,
`<dt>PDD 店铺</dt><dd>{{.PddShopText}}</dd>`,
}, },
"templates/task/detail_modal.html": {`<dt>创建人</dt><dd>{{.CreatorText}}</dd>`},
} }
for path, wants := range files { for path, wants := range files {
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
+39 -11
View File
@@ -176,16 +176,37 @@ func TaskVisibleToUser(q Execer, taskID, visibleUserID string) (bool, error) {
return count == 1, nil return count == 1, nil
} }
// GetTaskCreatorUsername 返回任务创建人的用户名;历史任务或账号不存在时为空。 // TaskDisplayContext 是任务详情按当前关联数据展示的名称。
func GetTaskCreatorUsername(q Execer, taskID string) (string, error) { // 名称可能被修改或对应记录被删除,因此 tasks 仍只保存稳定 ID。
var username sql.NullString type TaskDisplayContext struct {
if err := q.QueryRow(`SELECT u.username FROM tasks t LEFT JOIN users u ON u.user_id=t.created_by_user_id WHERE t.task_id=?`, taskID).Scan(&username); err != nil { CreatorUsername string
ClientName string
PddShopName string
}
// GetTaskDisplayContext 返回详情需要的当前创建人、客户端名称和 PDD 店铺。
// PDD 商品软删除后不再作为当前商品档案展示,店铺名自然为空。
func GetTaskDisplayContext(q Execer, taskID string) (TaskDisplayContext, error) {
var result TaskDisplayContext
var creatorUsername, clientName, pddShopName sql.NullString
err := q.QueryRow(`
SELECT u.username, c.name, p.shop_name
FROM tasks t
LEFT JOIN users u ON u.user_id = t.created_by_user_id
LEFT JOIN clients c ON c.client_id = t.assigned_client
LEFT JOIN pdd_products p
ON p.goods_id = t.pdd_goods_id AND p.deleted_at IS NULL
WHERE t.task_id = ?`, taskID).Scan(&creatorUsername, &clientName, &pddShopName)
if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return "", nil return result, nil
} }
return "", fmt.Errorf("读取任务创建人失败: %w", err) return result, fmt.Errorf("读取任务展示信息失败: %w", err)
} }
return username.String, nil result.CreatorUsername = creatorUsername.String
result.ClientName = clientName.String
result.PddShopName = pddShopName.String
return result, nil
} }
// RecordClaim 记一笔"某客户端领过某任务"。 // RecordClaim 记一笔"某客户端领过某任务"。
@@ -323,6 +344,7 @@ type TaskListRow struct {
AssignedClient string // 空表示无主任务 AssignedClient string // 空表示无主任务
CreatedByUserID string CreatedByUserID string
CreatedByUsername string CreatedByUsername string
ClientName string
OrderNo string OrderNo string
PddGoodsID string PddGoodsID string
@@ -336,6 +358,7 @@ type TaskListRow struct {
// 没有对应商品、或商品已被软删除时都是空。 // 没有对应商品、或商品已被软删除时都是空。
// `[必须]` service 层退回显示 PddGoodsID,不能显示空,见 #19。 // `[必须]` service 层退回显示 PddGoodsID,不能显示空,见 #19。
PddTitle string PddTitle string
PddShopName string
} }
// taskFilterClause 把三项筛选条件拼成 WHERE 子句,供 ListTasks 和 // taskFilterClause 把三项筛选条件拼成 WHERE 子句,供 ListTasks 和
@@ -387,11 +410,13 @@ func ListTasks(q Execer, filter TaskFilter, limit, offset int) ([]TaskListRow, e
sqlText := ` sqlText := `
SELECT t.task_id, t.task_type, t.status, t.execution_mode, t.assigned_client, SELECT t.task_id, t.task_type, t.status, t.execution_mode, t.assigned_client,
t.order_no, t.pdd_goods_id, t.pdd_options, t.quantity, t.max_price_cent, t.order_no, t.pdd_goods_id, t.pdd_options, t.quantity, t.max_price_cent,
t.updated_at, p.title, t.created_by_user_id, u.username t.updated_at, p.title, p.shop_name,
t.created_by_user_id, u.username, c.name
FROM tasks t FROM tasks t
LEFT JOIN pdd_products p LEFT JOIN pdd_products p
ON p.goods_id = t.pdd_goods_id AND p.deleted_at IS NULL ON p.goods_id = t.pdd_goods_id AND p.deleted_at IS NULL
LEFT JOIN users u ON u.user_id = t.created_by_user_id` + LEFT JOIN users u ON u.user_id = t.created_by_user_id
LEFT JOIN clients c ON c.client_id = t.assigned_client` +
where + ` ORDER BY t.updated_at DESC, t.task_id DESC LIMIT ? OFFSET ?` where + ` ORDER BY t.updated_at DESC, t.task_id DESC LIMIT ? OFFSET ?`
args = append(args, limit, offset) args = append(args, limit, offset)
@@ -404,13 +429,14 @@ func ListTasks(q Execer, filter TaskFilter, limit, offset int) ([]TaskListRow, e
list := make([]TaskListRow, 0, 16) list := make([]TaskListRow, 0, 16)
for rows.Next() { for rows.Next() {
var r TaskListRow var r TaskListRow
var assigned, orderNo, pddGoodsID, pddOptions, title, creatorID, creatorName sql.NullString var assigned, orderNo, pddGoodsID, pddOptions, title, shopName sql.NullString
var creatorID, creatorName, clientName sql.NullString
var quantity, maxPrice sql.NullInt64 var quantity, maxPrice sql.NullInt64
if err := rows.Scan( if err := rows.Scan(
&r.TaskID, &r.TaskType, &r.Status, &r.ExecutionMode, &assigned, &r.TaskID, &r.TaskType, &r.Status, &r.ExecutionMode, &assigned,
&orderNo, &pddGoodsID, &pddOptions, &quantity, &maxPrice, &orderNo, &pddGoodsID, &pddOptions, &quantity, &maxPrice,
&r.UpdatedAt, &title, &creatorID, &creatorName, &r.UpdatedAt, &title, &shopName, &creatorID, &creatorName, &clientName,
); err != nil { ); err != nil {
return nil, fmt.Errorf("读取任务列表失败: %w", err) return nil, fmt.Errorf("读取任务列表失败: %w", err)
} }
@@ -422,8 +448,10 @@ func ListTasks(q Execer, filter TaskFilter, limit, offset int) ([]TaskListRow, e
r.Quantity = int(quantity.Int64) r.Quantity = int(quantity.Int64)
r.MaxPriceCent = maxPrice.Int64 r.MaxPriceCent = maxPrice.Int64
r.PddTitle = title.String r.PddTitle = title.String
r.PddShopName = shopName.String
r.CreatedByUserID = creatorID.String r.CreatedByUserID = creatorID.String
r.CreatedByUsername = creatorName.String r.CreatedByUsername = creatorName.String
r.ClientName = clientName.String
list = append(list, r) list = append(list, r)
} }
return list, rows.Err() return list, rows.Err()
+39 -5
View File
@@ -130,12 +130,38 @@ func isTaskWarn(s model.TaskStatus) bool {
// `[必须]` 无主任务显示 `—`,不是空白也不是 `<nil>`——#17 之后 // `[必须]` 无主任务显示 `—`,不是空白也不是 `<nil>`——#17 之后
// 采集任务默认无主,这一列会大量为空,显示不出来会让人以为页面坏了。 // 采集任务默认无主,这一列会大量为空,显示不出来会让人以为页面坏了。
func clientText(assignedClient string) string { func clientText(assignedClient string) string {
if assignedClient == "" { if strings.TrimSpace(assignedClient) == "" {
return placeholder return placeholder
} }
return assignedClient return assignedClient
} }
// clientDisplayName 优先使用操作员看得懂的客户端名称。
// 名称为空或客户端记录已删除时回退稳定 ID,不能把已分配任务显示成空白。
func clientDisplayName(clientID, clientName string) string {
if name := strings.TrimSpace(clientName); name != "" {
return name
}
return clientText(clientID)
}
// clientDisplayTitle 给列表截断单元格提供完整名称和稳定 ID。
func clientDisplayTitle(clientID, clientName string) string {
name := strings.TrimSpace(clientName)
id := strings.TrimSpace(clientID)
if name != "" && id != "" {
return name + "(" + id + ")"
}
return clientDisplayName(id, name)
}
func optionalDisplayText(value string) string {
if value = strings.TrimSpace(value); value != "" {
return value
}
return placeholder
}
// ---------- 目标列 ---------- // ---------- 目标列 ----------
// buildTarget 把一行任务的业务字段拼成「目标」列要显示的一句话。 // buildTarget 把一行任务的业务字段拼成「目标」列要显示的一句话。
@@ -236,9 +262,11 @@ type TaskView struct {
TypeText string TypeText string
ExecutionModeText string ExecutionModeText string
Target string Target string
PddShopText string
StatusText string StatusText string
IsWarn bool // 失败 / 需人工,标黄提醒 IsWarn bool // 失败 / 需人工,标黄提醒
ClientText string ClientText string
ClientTitle string
CreatorText string CreatorText string
UpdatedAt string UpdatedAt string
} }
@@ -321,9 +349,11 @@ func ListTasksView(db *sql.DB, filter repository.TaskFilter, requestedPage int)
TypeText: taskTypeText(r.TaskType), TypeText: taskTypeText(r.TaskType),
ExecutionModeText: taskExecutionModeText(r.TaskType, r.ExecutionMode), ExecutionModeText: taskExecutionModeText(r.TaskType, r.ExecutionMode),
Target: buildTarget(r), Target: buildTarget(r),
PddShopText: optionalDisplayText(r.PddShopName),
StatusText: taskStatusText(r.Status), StatusText: taskStatusText(r.Status),
IsWarn: isTaskWarn(r.Status), IsWarn: isTaskWarn(r.Status),
ClientText: clientText(r.AssignedClient), ClientText: clientDisplayName(r.AssignedClient, r.ClientName),
ClientTitle: clientDisplayTitle(r.AssignedClient, r.ClientName),
CreatorText: taskCreatorText(r.CreatedByUsername), CreatorText: taskCreatorText(r.CreatedByUsername),
UpdatedAt: formatLocalTime(r.UpdatedAt), UpdatedAt: formatLocalTime(r.UpdatedAt),
}) })
@@ -421,11 +451,13 @@ type TaskDetailView struct {
ExecutionModeText string ExecutionModeText string
StatusText string StatusText string
ClientText string ClientText string
ClientIDText string
CreatorText string CreatorText string
ClaimedAt string ClaimedAt string
FinishedAt string FinishedAt string
PddGoodsURL string PddGoodsURL string
PddShopText string
// IsPurchase 为 false 时,模板要把下面四个采购专有字段整段隐藏, // IsPurchase 为 false 时,模板要把下面四个采购专有字段整段隐藏,
// 不能显示空行——见 #19。 // 不能显示空行——见 #19。
@@ -458,7 +490,7 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
if err != nil || t == nil { if err != nil || t == nil {
return nil, err return nil, err
} }
creatorUsername, err := repository.GetTaskCreatorUsername(db, taskID) displayContext, err := repository.GetTaskDisplayContext(db, taskID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -468,11 +500,13 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
TypeText: taskTypeText(t.TaskType), TypeText: taskTypeText(t.TaskType),
ExecutionModeText: taskExecutionModeText(t.TaskType, t.ExecutionMode), ExecutionModeText: taskExecutionModeText(t.TaskType, t.ExecutionMode),
StatusText: taskStatusText(t.Status), StatusText: taskStatusText(t.Status),
ClientText: clientText(t.AssignedClient), ClientText: clientDisplayName(t.AssignedClient, displayContext.ClientName),
CreatorText: taskCreatorText(creatorUsername), ClientIDText: clientText(t.AssignedClient),
CreatorText: taskCreatorText(displayContext.CreatorUsername),
ClaimedAt: formatLocalTime(t.ClaimedAt), ClaimedAt: formatLocalTime(t.ClaimedAt),
FinishedAt: formatLocalTime(t.FinishedAt), FinishedAt: formatLocalTime(t.FinishedAt),
PddGoodsURL: t.PddGoodsURL, PddGoodsURL: t.PddGoodsURL,
PddShopText: optionalDisplayText(displayContext.PddShopName),
IsPurchase: t.TaskType == model.TaskPurchase, IsPurchase: t.TaskType == model.TaskPurchase,
} }
if v.IsPurchase { if v.IsPurchase {
+70
View File
@@ -285,6 +285,76 @@ func TestListTasksView_无主任务客户端列显示占位符(t *testing.T) {
} }
} }
func TestListTasksView_显示PDD店铺和客户端名称并保留ID提示(t *testing.T) {
db := newTestDB(t)
now := model.NowISO()
if _, err := db.Exec(`
INSERT INTO pdd_products (goods_id, url, title, shop_name, collect_status, created_at, updated_at)
VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
'测试商品', '测试旗舰店', 'collected', ?, ?)`, now, now); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`
INSERT INTO clients (client_id, name, last_seen_at, created_at, updated_at)
VALUES ('client-001', '办公室手机', ?, ?, ?)`, now, now, now); err != nil {
t.Fatal(err)
}
insertTestTask(t, db, testTaskParams{
taskID: "COL-SHOP", taskType: model.TaskCollect, status: model.TaskAssigned,
pddGoodsID: "737116531267", assignedClient: "client-001",
})
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatal(err)
}
if len(r.Rows) != 1 || r.Rows[0].PddShopText != "测试旗舰店" {
t.Fatalf("列表应显示 PDD 店铺,实际 %+v", r.Rows)
}
if r.Rows[0].ClientText != "办公室手机" ||
r.Rows[0].ClientTitle != "办公室手机(client-001)" {
t.Errorf("客户端应显示名称并在提示保留 ID,实际 text=%q title=%q",
r.Rows[0].ClientText, r.Rows[0].ClientTitle)
}
if _, err := db.Exec(`UPDATE clients SET name='仓库手机' WHERE client_id='client-001'`); err != nil {
t.Fatal(err)
}
renamed, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatal(err)
}
if renamed.Rows[0].ClientText != "仓库手机" {
t.Errorf("客户端重命名后列表应显示当前名称,实际 %q", renamed.Rows[0].ClientText)
}
detail, err := GetTaskDetail(db, "COL-SHOP")
if err != nil {
t.Fatal(err)
}
if detail.PddShopText != "测试旗舰店" || detail.ClientText != "仓库手机" ||
detail.ClientIDText != "client-001" {
t.Errorf("详情展示信息不完整:%+v", detail)
}
}
func TestListTasksView_关联缺失时店铺占位且客户端回退ID(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{
taskID: "COL-FALLBACK", taskType: model.TaskCollect, status: model.TaskAssigned,
pddGoodsID: "missing-product", assignedClient: "deleted-client",
})
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatal(err)
}
if r.Rows[0].PddShopText != "—" || r.Rows[0].ClientText != "deleted-client" ||
r.Rows[0].ClientTitle != "deleted-client" {
t.Errorf("关联缺失回退错误:%+v", r.Rows[0])
}
}
// ── 统计跟随筛选 ────────────────────────────────────── // ── 统计跟随筛选 ──────────────────────────────────────
func TestListTasksView_统计跟随当前筛选(t *testing.T) { func TestListTasksView_统计跟随当前筛选(t *testing.T) {
+3 -1
View File
@@ -16,7 +16,8 @@
<dt>类型</dt><dd>{{.TypeText}}</dd> <dt>类型</dt><dd>{{.TypeText}}</dd>
<dt>执行模式</dt><dd>{{.ExecutionModeText}}</dd> <dt>执行模式</dt><dd>{{.ExecutionModeText}}</dd>
<dt>状态</dt><dd>{{.StatusText}}</dd> <dt>状态</dt><dd>{{.StatusText}}</dd>
<dt>分配客户端</dt><dd>{{.ClientText}}</dd> <dt>客户端名称</dt><dd>{{.ClientText}}</dd>
<dt>客户端 ID</dt><dd>{{.ClientIDText}}</dd>
<dt>创建人</dt><dd>{{.CreatorText}}</dd> <dt>创建人</dt><dd>{{.CreatorText}}</dd>
<dt>领取时间</dt><dd>{{.ClaimedAt}}</dd> <dt>领取时间</dt><dd>{{.ClaimedAt}}</dd>
<dt>完成时间</dt><dd>{{.FinishedAt}}</dd> <dt>完成时间</dt><dd>{{.FinishedAt}}</dd>
@@ -26,6 +27,7 @@
<dl class="detail"> <dl class="detail">
<dt>PDD 链接</dt> <dt>PDD 链接</dt>
<dd><a href="{{.PddGoodsURL}}" target="_blank" rel="noopener noreferrer">{{.PddGoodsURL}}</a></dd> <dd><a href="{{.PddGoodsURL}}" target="_blank" rel="noopener noreferrer">{{.PddGoodsURL}}</a></dd>
<dt>PDD 店铺</dt><dd>{{.PddShopText}}</dd>
{{/* 下面三项只有采购任务才有;采集任务这里整段不出现,不显示空行 */}} {{/* 下面三项只有采购任务才有;采集任务这里整段不出现,不显示空行 */}}
{{if .IsPurchase}} {{if .IsPurchase}}
+4 -2
View File
@@ -66,6 +66,7 @@
<th>类型</th> <th>类型</th>
<th>执行模式</th> <th>执行模式</th>
<th>目标</th> <th>目标</th>
<th>PDD 店铺</th>
<th>状态</th> <th>状态</th>
<th>客户端</th> <th>客户端</th>
{{if .ShowCreator}}<th>创建人</th>{{end}} {{if .ShowCreator}}<th>创建人</th>{{end}}
@@ -84,15 +85,16 @@
<td>{{.TypeText}}</td> <td>{{.TypeText}}</td>
<td>{{.ExecutionModeText}}</td> <td>{{.ExecutionModeText}}</td>
<td class="truncate" title="{{.Target}}">{{.Target}}</td> <td class="truncate" title="{{.Target}}">{{.Target}}</td>
<td class="truncate" title="{{.PddShopText}}">{{.PddShopText}}</td>
<td>{{.StatusText}}</td> <td>{{.StatusText}}</td>
<td>{{.ClientText}}</td> <td class="truncate" title="{{.ClientTitle}}">{{.ClientText}}</td>
{{if $.ShowCreator}}<td>{{.CreatorText}}</td>{{end}} {{if $.ShowCreator}}<td>{{.CreatorText}}</td>{{end}}
<td>{{.UpdatedAt}}</td> <td>{{.UpdatedAt}}</td>
</tr> </tr>
{{else}} {{else}}
{{/* 空状态要分情况:从没建过任务 和 筛选没结果,下一步动作完全不同 */}} {{/* 空状态要分情况:从没建过任务 和 筛选没结果,下一步动作完全不同 */}}
<tr class="empty"> <tr class="empty">
<td colspan="{{if .ShowCreator}}9{{else}}8{{end}}"> <td colspan="{{if .ShowCreator}}10{{else}}9{{end}}">
{{if .IsFiltered}} {{if .IsFiltered}}
当前筛选条件下没有任务。<br> 当前筛选条件下没有任务。<br>
<small>换个类型、状态或关键词再试。<a href="/tasks">查看全部</a></small> <small>换个类型、状态或关键词再试。<a href="/tasks">查看全部</a></small>
+11 -7
View File
@@ -649,20 +649,22 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
### 7.2 表格列 ### 7.2 表格列
☐ / 任务编号 / 类型 / **执行模式** / **目标** / 状态 / 客户端 / 创建人(管理员) / 更新时间 ☐ / 任务编号 / 类型 / **执行模式** / **目标** / PDD 店铺 / 状态 / 客户端 / 创建人(管理员) / 更新时间
```text ```text
☐ │ 任务编号 │ 类型 │ 执行模式 │ 目标 │ 状态 │ 客户端 │ 更新时间 ☐ │ 任务编号 │ 类型 │ 执行模式 │ 目标 │ PDD 店铺 │ 状态 │ 客户端 │ 更新时间
☐ │ PDD-20260807-01 │ 采集 │ 采集 │ PDD 737116531267 │ 已领取 │ 办公室-01 │ 15:20 ☐ │ PDD-20260807-01 │ 采集 │ 采集 │ PDD 737116531267 │ 某某店铺 │ 已领取 │ 办公室-01 │ 15:20
☐ │ PDD-20260807-02 │ 采购 │ 真实下单(不支付)│ SO-001 · 黑色/M · 2件 · ≤¥42.00 │ 待领取 │ 办公室-02 │ 15:22 ☐ │ PDD-20260807-02 │ 采购 │ 真实下单(不支付)│ SO-001 · 黑色/M · 2件 · ≤¥42.00 │ 某某店铺 │ 待领取 │ 办公室-02 │ 15:22
``` ```
- `[必须]` **目标列**:采集显示 `PDD <pdd_goods_id>`(能 join 到未删除商品 - `[必须]` **目标列**:采集显示 `PDD <pdd_goods_id>`(能 join 到未删除商品
的标题时追加显示);采购显示 `<order_no> · <颜色/尺码> · <数量>件 · ≤<价格上限>`。 的标题时追加显示);采购显示 `<order_no> · <颜色/尺码> · <数量>件 · ≤<价格上限>`。
拼接逻辑在 service 层组装成一个字符串,模板只负责显示。 拼接逻辑在 service 层组装成一个字符串,模板只负责显示。
- `[必须]` 价格上限显示成 `¥42.00`(分转元),底层存的是整数分。 - `[必须]` 价格上限显示成 `¥42.00`(分转元),底层存的是整数分。
- `[必须]` **客户端列**:无主任务显示 `—`,不是空白或 `<nil>`——#17 之后 - `[必须]` **PDD 店铺列**:按任务的 `pdd_goods_id` 关联当前未删除
采集任务默认无主,这一列会大量为空。 `pdd_products.shop_name`;没有商品、尚未采集、店铺为空或商品已软删除时显示 `—`。
- `[必须]` **客户端列**:优先显示当前 `clients.name`;名称为空或客户端记录不存在时
回退稳定客户端 ID;无主任务显示 `—`。名称和店铺过长时省略,完整内容放在 `title`。
- `[必须]` 类型和状态都是文字,不能只靠颜色区分。 - `[必须]` 类型和状态都是文字,不能只靠颜色区分。
- `[必须]` 管理员显示创建人列,NULL 显示“历史任务”;采购员省略该列。 - `[必须]` 管理员显示创建人列,NULL 显示“历史任务”;采购员省略该列。
@@ -679,13 +681,15 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
│ 类型 采集 │ │ 类型 采集 │
│ 执行模式 采集 │ │ 执行模式 采集 │
│ 状态 已领取 │ │ 状态 已领取 │
│ 分配客户端 办公室-01 │ │ 客户端名称 办公室-01 │
│ 客户端 ID client-001 │
│ 创建人 buyer-a │ │ 创建人 buyer-a │
│ 领取时间 2026-08-07 15:20:31 │ │ 领取时间 2026-08-07 15:20:31 │
│ 完成时间 — │ │ 完成时间 — │
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ 执行参数 │ │ 执行参数 │
│ PDD 链接 https://mobile.yangkeduo.com/... │ │ PDD 链接 https://mobile.yangkeduo.com/... │
│ PDD 店铺 某某店铺 │
│ 目标规格 黑色 / M (采购任务才有) │ │ 目标规格 黑色 / M (采购任务才有) │
│ 数量 2 (采购任务才有) │ │ 数量 2 (采购任务才有) │
│ 价格上限 ¥42.00 (采购任务才有) │ │ 价格上限 ¥42.00 (采购任务才有) │