feat: 支持顺运宝批量创建采集任务 (#153)

This commit is contained in:
chengma
2026-08-11 14:59:26 +08:00
parent 2629e41211
commit b527204204
12 changed files with 341 additions and 14 deletions
+21
View File
@@ -611,6 +611,27 @@ func (h *Handler) SybCollectPdd(c *gin.Context) {
h.sybRedirect(c, service.FormatCollectTaskMessage(result)) h.sybRedirect(c, service.FormatCollectTaskMessage(result))
} }
// SybCollectPddBatch 为勾选的顺运宝明细批量创建 PDD 采集任务。
// Service 会重新校验阶段、按 PDD 商品去重并核对指定客户端的可见范围。
func (h *Handler) SybCollectPddBatch(c *gin.Context) {
ids := c.PostFormArray("ids")
if len(ids) == 0 {
h.sybRedirect(c, "没有选择可采集的顺运宝明细,没有创建任务。")
return
}
result, err := service.CreateSybPddCollectTasksForUser(
h.db, currentUser(c), ids, c.PostForm("client_id"))
if err != nil {
message := "批量采集任务未创建:系统没有改动这一批数据,请稍后重试。"
if service.IsValidationError(err) {
message = "批量采集任务未创建:" + err.Error()
}
h.sybRedirect(c, message)
return
}
h.sybRedirect(c, service.FormatCollectTaskMessage(result))
}
// SybMatch 保存规格匹配结果。 // SybMatch 保存规格匹配结果。
// //
// 关键:保存的是**可复用的商品规格映射**(写 spec_mappings), // 关键:保存的是**可复用的商品规格映射**(写 spec_mappings),
+23
View File
@@ -166,3 +166,26 @@ func TestPurchaseTaskOptionsFromForm_忽略篡改模式并固定Live(t *testing.
t.Fatalf("采购客户端读取错误: %+v", options) t.Fatalf("采购客户端读取错误: %+v", options)
} }
} }
func TestSybCollectPddBatch_未选择时保留列表筛选(t *testing.T) {
context, recorder := sybPostContext(t, url.Values{
"order_no": {"ORDER-1"}, "shop": {"qwg8fkb044"},
"stage": {"pdd_pending"}, "page": {"2"},
})
(&Handler{}).SybCollectPddBatch(context)
location, err := url.Parse(recorder.Header().Get("Location"))
if err != nil {
t.Fatal(err)
}
query := location.Query()
for name, want := range map[string]string{
"order_no": "ORDER-1", "shop": "qwg8fkb044", "stage": "pdd_pending", "page": "2",
} {
if got := query.Get(name); got != want {
t.Errorf("批量采集返回丢失 %s:got=%q want=%q Location=%s", name, got, want, location.String())
}
}
if !strings.Contains(query.Get("msg"), "没有选择") {
t.Fatalf("未选择应返回可读提示:%s", location.String())
}
}
+1
View File
@@ -79,6 +79,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
pages.POST("/syb/login-and-sync", h.SybLoginAndSync) pages.POST("/syb/login-and-sync", h.SybLoginAndSync)
pages.POST("/syb/associate-pdd", h.SybAssociatePdd) pages.POST("/syb/associate-pdd", h.SybAssociatePdd)
pages.POST("/syb/collect-pdd", h.SybCollectPdd) pages.POST("/syb/collect-pdd", h.SybCollectPdd)
pages.POST("/syb/collect-pdd-batch", h.SybCollectPddBatch)
pages.POST("/syb/match", h.SybMatch) pages.POST("/syb/match", h.SybMatch)
pages.POST("/syb/create-task", h.SybCreateTask) pages.POST("/syb/create-task", h.SybCreateTask)
pages.POST("/syb/delete", h.SybDelete) pages.POST("/syb/delete", h.SybDelete)
+31
View File
@@ -65,6 +65,37 @@ func TestPurchaseModal_未选规格行保持隐藏且不提交(t *testing.T) {
} }
} }
func TestSybBulkActions_按服务端动作能力隔离勾选项(t *testing.T) {
files := map[string][]string{
"templates/syb/list.html": {
`data-need-checked="collect"`, `data-need-checked="purchase"`,
`data-select-action="{{if .CanCollect}}collect{{else if .CanPurchase}}purchase{{end}}"`,
`{{if .CanCollect}}form="syb-collect-form"{{end}}`,
`action="/syb/collect-pdd-batch"`, `id="syb-collect-modal"`,
},
"static/js/app.js": {
`supportsSelectAction(box, action)`,
`checkedCount(action)`,
`supportsSelectAction(box, "purchase")`,
`getAttribute("data-collect-action")`,
},
"handler/web/web.go": {
`pages.POST("/syb/collect-pdd-batch", h.SybCollectPddBatch)`,
},
}
for path, wants := range files {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, want := range wants {
if !strings.Contains(string(content), want) {
t.Errorf("%s 缺少动作隔离约束 %q", path, want)
}
}
}
}
func TestPddDetailRecollect_使用独立入口并可选客户端(t *testing.T) { func TestPddDetailRecollect_使用独立入口并可选客户端(t *testing.T) {
content, err := os.ReadFile("templates/pdd/edit_modal.html") content, err := os.ReadFile("templates/pdd/edit_modal.html")
if err != nil { if err != nil {
+28
View File
@@ -157,3 +157,31 @@ func CreateSybPddCollectTaskForUser(db *sql.DB, actor *model.User, sybID string)
} }
return CreateShopeePddCollectTaskForUser(db, actor, context.Order.ShopeeGoodsID) return CreateShopeePddCollectTaskForUser(db, actor, context.Order.ShopeeGoodsID)
} }
// CreateSybPddCollectTasksForUser 把勾选的顺运宝明细转换成 PDD 商品后批量建采集任务。
// 同一 PDD 商品可能对应多条货运单明细,因此先按 goods_id 去重;任何明细不存在或
// 已不处于可采集阶段时整批拒绝,避免浏览器过期或参数篡改造成部分创建。
func CreateSybPddCollectTasksForUser(db *sql.DB, actor *model.User, sybIDs []string, clientID string) (CollectTaskResult, error) {
sybIDs = dedupe(sybIDs)
if len(sybIDs) == 0 {
return CollectTaskResult{}, invalidInput("没有选择可采集的顺运宝明细")
}
goodsIDs := make([]string, 0, len(sybIDs))
for _, sybID := range sybIDs {
context, err := repository.GetSybOrderContext(db, sybID)
if err != nil {
return CollectTaskResult{}, err
}
if context == nil {
return CollectTaskResult{}, invalidInput("顺运宝明细 %s 不存在,请刷新页面后重新选择", sybID)
}
stage, _, _, _ := sybStageFor(*context)
if stage != SybStagePddPending && stage != SybStagePddFailed {
return CollectTaskResult{}, invalidInput("顺运宝明细 %s 已不处于 PDD 待采集或采集失败状态,请刷新页面后重新选择", sybID)
}
goodsIDs = append(goodsIDs, context.PddGoodsID)
}
return CreatePddCollectTasksForUser(db, actor, dedupe(goodsIDs), clientID)
}
+92
View File
@@ -1,9 +1,12 @@
package service package service
import ( import (
"database/sql"
"errors" "errors"
"testing" "testing"
"time"
"cmautobuy/admin/model"
"cmautobuy/admin/repository" "cmautobuy/admin/repository"
) )
@@ -116,3 +119,92 @@ func TestCreateShopeePddCollectTaskForUser_记录当前用户(t *testing.T) {
t.Fatalf("蝦皮入口任务创建人=%q err=%v", creator, err) t.Fatalf("蝦皮入口任务创建人=%q err=%v", creator, err)
} }
} }
func TestCreateSybPddCollectTasksForUser_同一PDD商品去重(t *testing.T) {
db := newTestDB(t)
admin, _, _ := insertTaskUsers(t, db)
seedShopeeProduct(t, db, "1001", "商品一")
seedShopeeProduct(t, db, "1002", "商品二")
if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil {
t.Fatal(err)
}
if _, err := AssociateShopeePdd(db, "1002", pddURLA, false); err != nil {
t.Fatal(err)
}
seedSybCollectOrder(t, db, "SYB-1", "1001")
seedSybCollectOrder(t, db, "SYB-2", "1002")
result, err := CreateSybPddCollectTasksForUser(
db, admin, []string{"SYB-1", "SYB-2", "SYB-1"}, "")
if err != nil || result.Created != 1 {
t.Fatalf("批量创建结果=%+v err=%v", result, err)
}
var taskCount int
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)
}
}
func TestCreateSybPddCollectTasksForUser_不可采集明细整批拒绝(t *testing.T) {
db := newTestDB(t)
admin, _, _ := insertTaskUsers(t, db)
seedShopeeProduct(t, db, "1001", "商品一")
seedShopeeProduct(t, db, "1002", "商品二")
AssociateShopeePdd(db, "1001", pddURLA, false)
AssociateShopeePdd(db, "1002", pddURLB, false)
seedSybCollectOrder(t, db, "SYB-PENDING", "1001")
seedSybCollectOrder(t, db, "SYB-COLLECTED", "1002")
if _, err := db.Exec(`UPDATE pdd_products SET collect_status='collected' WHERE goods_id='937122477375'`); err != nil {
t.Fatal(err)
}
_, err := CreateSybPddCollectTasksForUser(
db, admin, []string{"SYB-PENDING", "SYB-COLLECTED"}, "")
if !IsValidationError(err) {
t.Fatalf("不可采集明细应返回校验错误,实际 %v", err)
}
var taskCount int
db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE task_type='collect'`).Scan(&taskCount)
if taskCount != 0 {
t.Fatalf("整批拒绝后不应产生部分任务,实际 %d", taskCount)
}
pending, _ := repository.GetPddProductByGoodsID(db, "737116531267")
if pending.CollectStatus != model.CollectPending {
t.Fatalf("整批拒绝后待采集商品状态被改变为 %s", pending.CollectStatus)
}
}
func TestCreateSybPddCollectTasksForUser_拒绝不可见客户端(t *testing.T) {
db := newTestDB(t)
admin, buyerA, buyerB := prepareClientAssignmentUsers(t, db)
if err := RegisterClient(db, model.Client{ClientID: "client-b", Name: "B 的机器"}, true); err != nil {
t.Fatal(err)
}
if _, _, err := AssignClient(db, admin, "client-b", buyerB.UserID,
time.Date(2026, 8, 11, 1, 0, 0, 0, time.UTC)); err != nil {
t.Fatal(err)
}
seedShopeeProduct(t, db, "1001", "商品一")
AssociateShopeePdd(db, "1001", pddURLA, false)
seedSybCollectOrder(t, db, "SYB-1", "1001")
_, err := CreateSybPddCollectTasksForUser(db, buyerA, []string{"SYB-1"}, "client-b")
if !IsValidationError(err) {
t.Fatalf("不可见客户端应返回校验错误,实际 %v", err)
}
var taskCount int
db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE task_type='collect'`).Scan(&taskCount)
if taskCount != 0 {
t.Fatalf("客户端权限校验失败后不应建任务,实际 %d", taskCount)
}
}
func seedSybCollectOrder(t *testing.T, db *sql.DB, sybID, shopeeGoodsID string) {
t.Helper()
if _, err := repository.UpsertSybOrder(db, model.SybOrder{
SybID: sybID, OrderNo: "ORDER-" + sybID, Title: "测试商品", ProductSpec: "黑色,M",
ShopeeGoodsID: shopeeGoodsID, Quantity: 1, SybData: `{}`,
}); err != nil {
t.Fatalf("插入顺运宝测试明细失败: %v", err)
}
}
+11
View File
@@ -939,7 +939,9 @@ type SybOrderView struct {
StageHelp string StageHelp string
ActionText string ActionText string
NeedsAttention bool NeedsAttention bool
CanCollect bool
CanPurchase bool CanPurchase bool
SelectionHint string
DefaultMaxPrice string DefaultMaxPrice string
MappedPddChoice string MappedPddChoice string
MappingOptionKey string MappingOptionKey string
@@ -1129,7 +1131,16 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
Quantity: o.Quantity, ImageURL: o.ImageURL, Quantity: o.Quantity, ImageURL: o.ImageURL,
UpdatedAt: formatLocalTime(o.UpdatedAt)} UpdatedAt: formatLocalTime(o.UpdatedAt)}
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context) v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
v.CanCollect = v.Stage == SybStagePddPending || v.Stage == SybStagePddFailed
v.CanPurchase = v.Stage == SybStagePurchaseReady v.CanPurchase = v.Stage == SybStagePurchaseReady
switch {
case v.CanCollect:
v.SelectionHint = "可批量创建 PDD 采集任务"
case v.CanPurchase:
v.SelectionHint = "可批量创建采购任务"
default:
v.SelectionHint = "当前阶段不能批量操作:" + v.StageHelp
}
if v.CanPurchase { if v.CanPurchase {
v.MappingOptionKey = context.MappingOptionKey v.MappingOptionKey = context.MappingOptionKey
v.ContextVersion = mappingContextVersion(context) v.ContextVersion = mappingContextVersion(context)
+41
View File
@@ -130,3 +130,44 @@ func TestSybStageFor_全部筛选阶段与逐行推导一致(t *testing.T) {
} }
} }
} }
func TestSybOrderViewFor_批量动作能力按阶段隔离(t *testing.T) {
base := repository.SybOrderContext{
Order: model.SybOrder{SybID: "SYB-1", SpecKey: "黑色,M", Quantity: 1},
PddGoodsID: "737116531267",
}
cases := []struct {
status model.CollectStatus
mappingKey string
wantCollect, wantPurchase bool
}{
{status: model.CollectPending, wantCollect: true},
{status: model.CollectFailed, wantCollect: true},
{status: model.CollectCollecting},
{status: model.CollectCollected},
}
for _, tc := range cases {
context := base
context.PddCollectStatus = string(tc.status)
context.MappingOptionKey = tc.mappingKey
view := sybOrderViewFor(context)
if view.CanCollect != tc.wantCollect || view.CanPurchase != tc.wantPurchase {
t.Errorf("status=%s: CanCollect=%v CanPurchase=%v", tc.status, view.CanCollect, view.CanPurchase)
}
if view.SelectionHint == "" {
t.Errorf("status=%s: 应给出选择能力说明", tc.status)
}
}
key, err := OptionKey(map[string]string{"color": "黑色", "size": "M码", "style": "常规"})
if err != nil {
t.Fatal(err)
}
ready := base
ready.PddCollectStatus = string(model.CollectCollected)
ready.PddSkusJSON = collectedThreeDimensions
ready.MappingOptionKey = key
view := sybOrderViewFor(ready)
if view.CanCollect || !view.CanPurchase {
t.Fatalf("采购就绪行应只允许采购:CanCollect=%v CanPurchase=%v", view.CanCollect, view.CanPurchase)
}
}
+16 -8
View File
@@ -40,18 +40,25 @@
} }
/* ── 选中数量 ────────────────────────────── */ /* ── 选中数量 ────────────────────────────── */
function checkedCount() { function supportsSelectAction(box, action) {
return document.querySelectorAll( if (!action) return true;
"tbody input[type=checkbox]:checked:not(:disabled)" var actions = (box.getAttribute("data-select-action") || "").split(/\s+/);
return actions.indexOf(action) !== -1;
}
function checkedCount(action) {
return Array.prototype.filter.call(
document.querySelectorAll("tbody input[type=checkbox]:checked:not(:disabled)"),
function (box) { return supportsSelectAction(box, action); }
).length; ).length;
} }
/* ── 需要勾选才能点的按钮 ────────────────── /* ── 需要勾选才能点的按钮 ──────────────────
标了 data-need-checked 的按钮,没勾选任何行时禁用。 */ 标了 data-need-checked 的按钮,没勾选任何行时禁用。 */
function syncButtons() { function syncButtons() {
var n = checkedCount();
document.querySelectorAll("[data-need-checked]").forEach(function (btn) { document.querySelectorAll("[data-need-checked]").forEach(function (btn) {
btn.disabled = n === 0; var action = btn.getAttribute("data-need-checked") || "";
btn.disabled = checkedCount(action) === 0;
}); });
} }
@@ -148,8 +155,8 @@
if (singleID) { if (singleID) {
selected[singleID] = true; selected[singleID] = true;
} else { } else {
document.querySelectorAll("tbody input[name=ids]:checked").forEach(function (box) { document.querySelectorAll("tbody input[name=ids]:checked:not(:disabled)").forEach(function (box) {
selected[box.value] = true; if (supportsSelectAction(box, "purchase")) selected[box.value] = true;
}); });
} }
var shown = 0; var shown = 0;
@@ -183,11 +190,12 @@
if (!form || !modal) return; if (!form || !modal) return;
var count = modal.querySelector("[data-collect-count]"); var count = modal.querySelector("[data-collect-count]");
var submitButton = modal.querySelector("[data-collect-submit]"); var submitButton = modal.querySelector("[data-collect-submit]");
var action = openButton.getAttribute("data-collect-action") || "";
if (!count || !submitButton) return; if (!count || !submitButton) return;
openButton.addEventListener("click", function () { openButton.addEventListener("click", function () {
var fixedCount = openButton.getAttribute("data-collect-count-value"); var fixedCount = openButton.getAttribute("data-collect-count-value");
count.textContent = fixedCount === null ? checkedCount() : fixedCount; count.textContent = fixedCount === null ? checkedCount(action) : fixedCount;
}); });
form.addEventListener("submit", function (event) { form.addEventListener("submit", function (event) {
if (event.submitter !== submitButton) return; if (event.submitter !== submitButton) return;
+16
View File
@@ -30,6 +30,13 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
SybID: "SYB-PDD", OrderNo: "ORDER-PDD", Title: "待关联商品", SybID: "SYB-PDD", OrderNo: "ORDER-PDD", Title: "待关联商品",
Stage: service.SybStagePddMissing, StageText: "未关联 PDD", Stage: service.SybStagePddMissing, StageText: "未关联 PDD",
StageHelp: "下一步关联采购商品", ActionText: "关联 PDD", StageHelp: "下一步关联采购商品", ActionText: "关联 PDD",
SelectionHint: "当前阶段不能批量操作:下一步关联采购商品",
},
{
SybID: "SYB-COLLECT", OrderNo: "ORDER-COLLECT", Title: "待采集商品",
Stage: service.SybStagePddPending, StageText: "PDD 待采集",
StageHelp: "尚未创建采集任务", ActionText: "创建采集任务",
CanCollect: true, SelectionHint: "可批量创建 PDD 采集任务",
}, },
} }
var output bytes.Buffer var output bytes.Buffer
@@ -50,6 +57,15 @@ func TestSybListTemplate_区分标题图片与阶段动作(t *testing.T) {
`href="/tasks?type=purchase`, `href="/tasks?type=purchase`,
`q=ORDER-TASK"`, `q=ORDER-TASK"`,
`data-detail-open-id="SYB-PDD"`, `data-detail-open-id="SYB-PDD"`,
`action="/syb/collect-pdd-batch"`,
`data-need-checked="collect"`,
`data-need-checked="purchase"`,
`data-collect-action="collect"`,
`data-select-action="collect"`,
`form="syb-collect-form"`,
`id="syb-collect-modal"`,
`同一 PDD 商品关联多条货运单时只创建一条任务`,
`title="当前阶段不能批量操作:下一步关联采购商品"`,
} { } {
if !strings.Contains(body, want) { if !strings.Contains(body, want) {
t.Errorf("顺运宝列表缺少 %q", want) t.Errorf("顺运宝列表缺少 %q", want)
+49 -3
View File
@@ -22,7 +22,18 @@
<button type="button" data-modal-open="sync-history-modal">同步记录</button> <button type="button" data-modal-open="sync-history-modal">同步记录</button>
<button type="button" data-need-checked data-modal-open="purchase-modal" data-purchase-open>创建采购任务</button> <form id="syb-collect-form" method="post" action="/syb/collect-pdd-batch" hidden></form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" form="syb-collect-form">
<input type="hidden" name="order_no" value="{{.Keyword}}" form="syb-collect-form">
<input type="hidden" name="shop" value="{{.ShopFilter}}" form="syb-collect-form">
<input type="hidden" name="stage" value="{{.StageFilter}}" form="syb-collect-form">
<input type="hidden" name="page" value="{{.CurrentPage}}" form="syb-collect-form">
<button type="button" data-modal-open="syb-collect-modal" data-collect-open
data-collect-form="syb-collect-form" data-collect-action="collect"
data-need-checked="collect">创建 PDD 采集任务</button>
<button type="button" data-need-checked="purchase" data-modal-open="purchase-modal"
data-purchase-open>创建采购任务</button>
<form class="inline grow" method="get" action="/syb"> <form class="inline grow" method="get" action="/syb">
<label for="stage">处理阶段</label> <label for="stage">处理阶段</label>
@@ -109,8 +120,13 @@
{{range .Rows}} {{range .Rows}}
<tr {{if .NeedsAttention}}class="row-warn"{{end}}> <tr {{if .NeedsAttention}}class="row-warn"{{end}}>
<td class="col-check"> <td class="col-check">
<input type="checkbox" value="{{.SybID}}" name="ids" {{if not .CanPurchase}}disabled{{end}} <span title="{{.SelectionHint}}">
aria-label="选择货运单明细 {{.SybID}}"> <input type="checkbox" value="{{.SybID}}" name="ids"
data-select-action="{{if .CanCollect}}collect{{else if .CanPurchase}}purchase{{end}}"
{{if .CanCollect}}form="syb-collect-form"{{end}}
{{if not (or .CanCollect .CanPurchase)}}disabled{{end}}
aria-label="选择货运单明细 {{.SybID}}:{{.SelectionHint}}">
</span>
</td> </td>
<td>{{.SybID}}</td> <td>{{.SybID}}</td>
<td>{{.OrderNo}}</td> <td>{{.OrderNo}}</td>
@@ -163,6 +179,36 @@
</table> </table>
</div> </div>
<div class="modal-backdrop" id="syb-collect-modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="syb-collect-modal-title">
<div class="modal-head">
<h2 id="syb-collect-modal-title">创建 PDD 采集任务</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<div class="modal-body">
<p>将处理已选择的 <strong data-collect-count>0</strong> 条可采集货运单明细。</p>
<div class="field">
<label for="syb-collect-client">执行客户端</label>
<select id="syb-collect-client" name="client_id" form="syb-collect-form" autofocus>
<option value="">不指定(任意客户端可领取)</option>
{{range .AssignableClients}}
<option value="{{.ClientID}}">{{.Name}}({{.ClientID}},{{.Status}})</option>
{{end}}
</select>
</div>
<p class="hint">
同一 PDD 商品关联多条货运单时只创建一条任务。通常保持不指定即可;
指定后任务只等待该客户端领取。
</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" form="syb-collect-form" class="primary"
data-collect-submit>确认创建</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="purchase-modal" hidden> <div class="modal-backdrop" id="purchase-modal" hidden>
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="purchase-modal-title"> <div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="purchase-modal-title">
<div class="modal-head"> <div class="modal-head">
+12 -3
View File
@@ -449,7 +449,7 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
```text ```text
开始日期 [2026-08-07] 结束日期 [2026-08-09] [同步] [同步记录] 开始日期 [2026-08-07] 结束日期 [2026-08-09] [同步] [同步记录]
[创建采购任务] 处理阶段 [全部] 店铺 [___] 订单号 [___] [搜索] [删除] [创建 PDD 采集任务] [创建采购任务] 处理阶段 [全部] 店铺 [___] 订单号 [___] [搜索] [删除]
``` ```
日期始终显示在主工具条,按 UTC+8 解释且两端都包含。通常默认最近 3 天;如果 日期始终显示在主工具条,按 UTC+8 解释且两端都包含。通常默认最近 3 天;如果
@@ -501,7 +501,7 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。 `[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
店铺是独立的模糊搜索条件,可与处理阶段、订单号/商品标题组合;输入会去除首尾空白。 店铺是独立的模糊搜索条件,可与处理阶段、订单号/商品标题组合;输入会去除首尾空白。
翻页、同步、删除、详情内写操作和创建采购任务返回列表时都要保留店铺条件。 翻页、同步、删除、详情内写操作、创建 PDD 采集任务和创建采购任务返回列表时都要保留店铺条件。
### 6.2 表格列 ### 6.2 表格列
@@ -524,6 +524,15 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
- 规格原文为空时不显示匹配表单,明确提示“顺运宝未提供规格”,并禁止建立映射或创建采购任务。 - 规格原文为空时不显示匹配表单,明确提示“顺运宝未提供规格”,并禁止建立映射或创建采购任务。
- 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。 - 完整货运单 JSON 不作为列显示,落在 `syb_data` 里,供后续排查用。
复选框按批量动作能力启用,不再等同于“可采购”:PDD 待采集、PDD 采集失败行可用于
“创建 PDD 采集任务”,可创建采购任务行可用于“创建采购任务”,其他阶段继续禁用并通过
`title` 和无障碍标签说明原因。混合勾选时两个按钮分别统计和提交自身兼容的行,不能把
待采集商品带进采购确认,也不能把采购就绪商品带进采集请求。
批量创建 PDD 采集任务的确认弹窗显示可采集明细数,并允许不指定客户端或选择当前账号
可见客户端。同一 PDD 商品可能对应多条顺运宝明细,服务端按 PDD 商品 ID 去重,只创建
一条采集任务;浏览器提交的全部明细必须重新校验阶段,参数过期或被篡改时整批拒绝。
### 6.3 采购处理与规格匹配弹窗(点击商品标题或处理类下一步打开) ### 6.3 采购处理与规格匹配弹窗(点击商品标题或处理类下一步打开)
弹窗按当前处理阶段渐进展示。它直接以顺运宝规格原文及其 `spec_key` 弹窗按当前处理阶段渐进展示。它直接以顺运宝规格原文及其 `spec_key`
@@ -566,7 +575,7 @@ HTML 片段,只替换弹窗内部的表格、总数和分页。主货运单表
### 6.4 创建采购任务 ### 6.4 创建采购任务
只有“可创建采购任务”的行可勾选。勾选后打开服务端渲染的确认弹窗:选择当前账号 只有“可创建采购任务”的行能参与采购批量动作。勾选后打开服务端渲染的确认弹窗:选择当前账号
可见的任意客户端,并逐行确认人民币价格上限。离线或尚未就绪的 Client 也可提前 可见的任意客户端,并逐行确认人民币价格上限。离线或尚未就绪的 Client 也可提前
指派,待其就绪后领取;选项继续显示在线状态但不因能力状态禁用。默认价格来自映射规格 指派,待其就绪后领取;选项继续显示在线状态但不因能力状态禁用。默认价格来自映射规格
的 PDD 采集价;没有价格时留空强制填写。新任务固定为真实下单(不支付),不显示执行 的 PDD 采集价;没有价格时留空强制填写。新任务固定为真实下单(不支付),不显示执行