feat: 支持顺运宝批量创建采集任务 (#153)
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 采集价;没有价格时留空强制填写。新任务固定为真实下单(不支付),不显示执行
|
||||||
|
|||||||
Reference in New Issue
Block a user