feat: 支持蝦皮批量创建 PDD 采集任务 (#185)
This commit is contained in:
+44
-15
@@ -75,6 +75,12 @@ func (h *Handler) renderShopeeList(c *gin.Context, keyword, shopName, statusRaw,
|
||||
"读取蝦皮数据失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
return
|
||||
}
|
||||
assignableClients, err := service.ListAssignableClients(h.db, currentUser(c), h.onlineThreshold)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"读取可选客户端失败,数据没有被改动。刷新页面重试。")
|
||||
return
|
||||
}
|
||||
|
||||
status := msg
|
||||
if status == "" {
|
||||
@@ -99,21 +105,22 @@ func (h *Handler) renderShopeeList(c *gin.Context, keyword, shopName, statusRaw,
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "shopee/list", page(c, "shopee", "蝦皮数据", gin.H{
|
||||
"Keyword": keyword,
|
||||
"ShopNameKeyword": filter.ShopName,
|
||||
"StatusFilter": filter.Status,
|
||||
"StatusOptions": service.ShopeeStatusOptions(),
|
||||
"ShopFilter": filter.Shop,
|
||||
"ShopOptions": service.ShopeePresenceOptions("有店铺", "无店铺"),
|
||||
"ImageFilter": filter.Image,
|
||||
"ImageOptions": service.ShopeePresenceOptions("有图片", "无图片"),
|
||||
"Rows": result.Rows,
|
||||
"Status": status,
|
||||
"HasAnyProducts": result.HasAnyProducts,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"CurrentPage": result.Page,
|
||||
"DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page),
|
||||
"Keyword": keyword,
|
||||
"ShopNameKeyword": filter.ShopName,
|
||||
"StatusFilter": filter.Status,
|
||||
"StatusOptions": service.ShopeeStatusOptions(),
|
||||
"ShopFilter": filter.Shop,
|
||||
"ShopOptions": service.ShopeePresenceOptions("有店铺", "无店铺"),
|
||||
"ImageFilter": filter.Image,
|
||||
"ImageOptions": service.ShopeePresenceOptions("有图片", "无图片"),
|
||||
"Rows": result.Rows,
|
||||
"Status": status,
|
||||
"HasAnyProducts": result.HasAnyProducts,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"CurrentPage": result.Page,
|
||||
"DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page),
|
||||
"AssignableClients": assignableClients,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -180,6 +187,28 @@ func (h *Handler) ShopeeCollect(c *gin.Context) {
|
||||
h.shopeeRedirect(c, service.FormatCollectTaskMessage(result))
|
||||
}
|
||||
|
||||
// ShopeeCollectBatch 为勾选的蝦皮商品按当前 PDD 关联批量创建采集任务。
|
||||
func (h *Handler) ShopeeCollectBatch(c *gin.Context) {
|
||||
ids := c.PostFormArray("ids")
|
||||
if len(ids) == 0 {
|
||||
h.shopeeRedirect(c, "没有勾选任何蝦皮商品,没有创建任务")
|
||||
return
|
||||
}
|
||||
result, err := service.CreateShopeePddCollectTasksForUser(
|
||||
h.db, currentUser(c), ids, c.PostForm("client_id"))
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
message := "创建采集任务失败,系统没有改动这一批数据。请稍后重试。"
|
||||
if service.IsValidationError(err) {
|
||||
status = http.StatusBadRequest
|
||||
message = err.Error() + "。这一批任务整体没有创建,请重新选择。"
|
||||
}
|
||||
fail(c, status, message)
|
||||
return
|
||||
}
|
||||
h.shopeeRedirect(c, service.FormatCollectTaskMessage(result))
|
||||
}
|
||||
|
||||
func (h *Handler) shopeeRedirect(c *gin.Context, msg string) {
|
||||
params := url.Values{}
|
||||
if value := strings.TrimSpace(c.PostForm("list_goods_id")); value != "" {
|
||||
|
||||
@@ -59,6 +59,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
pages.POST("/shopee/save", h.ShopeeSave)
|
||||
pages.POST("/shopee/delete", h.ShopeeDelete)
|
||||
pages.POST("/shopee/collect", h.ShopeeCollect)
|
||||
pages.POST("/shopee/collect-batch", h.ShopeeCollectBatch)
|
||||
|
||||
// 2. PDD 商品
|
||||
pages.GET("/pdd", h.PddList)
|
||||
|
||||
@@ -234,6 +234,24 @@ func TestShopeePage_移除Excel入口且保留目录记录入口(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopeePage_批量创建Pdd采集任务入口(t *testing.T) {
|
||||
content, err := os.ReadFile("templates/shopee/list.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page := string(content)
|
||||
for _, want := range []string{
|
||||
`action="/shopee/collect-batch"`, `data-modal-open="shopee-collect-modal"`,
|
||||
`data-collect-form="shopee-collect-form"`, `form="shopee-collect-form"`,
|
||||
`<label for="shopee-collect-client">执行客户端</label>`,
|
||||
`不指定(任意客户端可领取)`, `.AssignableClients`, `data-collect-submit`,
|
||||
} {
|
||||
if !strings.Contains(page, want) {
|
||||
t.Errorf("蝦皮批量采集入口缺少 %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopeeDetail_区分顺运宝观测与正式SKU(t *testing.T) {
|
||||
content, err := os.ReadFile("templates/shopee/detail_modal.html")
|
||||
if err != nil {
|
||||
|
||||
@@ -301,6 +301,37 @@ func GetShopeeProductByGoodsID(q Execer, goodsID string) (*model.ShopeeProduct,
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListShopeePddLinksByGoodsIDs 批量读取蝦皮商品当前关联的 PDD 商品 ID。
|
||||
// 返回结果只包含仍存在的蝦皮商品;值为空表示商品存在但尚未关联 PDD。
|
||||
func ListShopeePddLinksByGoodsIDs(q Execer, goodsIDs []string) (map[string]string, error) {
|
||||
links := make(map[string]string, len(goodsIDs))
|
||||
if len(goodsIDs) == 0 {
|
||||
return links, nil
|
||||
}
|
||||
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(goodsIDs)), ",")
|
||||
args := make([]any, len(goodsIDs))
|
||||
for i, goodsID := range goodsIDs {
|
||||
args[i] = goodsID
|
||||
}
|
||||
rows, err := q.Query(`SELECT goods_id, COALESCE(pdd_goods_id, '')
|
||||
FROM shopee_products WHERE goods_id IN (`+placeholders+`)`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("批量查询蝦皮商品 PDD 关联失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var shopeeGoodsID, pddGoodsID string
|
||||
if err := rows.Scan(&shopeeGoodsID, &pddGoodsID); err != nil {
|
||||
return nil, fmt.Errorf("读取蝦皮商品 PDD 关联失败: %w", err)
|
||||
}
|
||||
links[shopeeGoodsID] = pddGoodsID
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历蝦皮商品 PDD 关联失败: %w", err)
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
// UpdateShopeePddLink 更新蝦皮商品当前关联的 PDD 商品。
|
||||
// 调用方必须先在同一事务里保证 PDD 商品存在,避免留下悬空关联。
|
||||
func UpdateShopeePddLink(q Execer, shopeeGoodsID, pddGoodsID, pddURL string) error {
|
||||
|
||||
+12
-4
@@ -543,9 +543,11 @@ func DeletePddProducts(db *sql.DB, goodsIDs []string) (int64, error) {
|
||||
type CollectTaskResult struct {
|
||||
Created int
|
||||
|
||||
SkippedCollecting int // 正在采集且**没超时**,得等客户端来领/提交
|
||||
SkippedCollected int // 已经采集完成,本来就不需要重新采集
|
||||
SkippedDeleted int // 商品不存在或已被软删除
|
||||
SkippedShopeeMissing int // 浏览器提交的蝦皮商品已不存在
|
||||
SkippedUnlinked int // 蝦皮商品尚未关联 PDD
|
||||
SkippedCollecting int // 正在采集且**没超时**,得等客户端来领/提交
|
||||
SkippedCollected int // 已经采集完成,本来就不需要重新采集
|
||||
SkippedDeleted int // 商品不存在或已被软删除
|
||||
|
||||
// RetryWaitText 是 SkippedCollecting 里最快能重试的还需等待时长,
|
||||
// 形如 "12 分钟";SkippedCollecting 为 0 时是空串。
|
||||
@@ -555,7 +557,7 @@ type CollectTaskResult struct {
|
||||
// Skipped 是跳过总数,供测试和粗粒度统计用;
|
||||
// 界面提示要按原因分开说,见 FormatCollectTaskMessage,不要直接显示这个数。
|
||||
func (r CollectTaskResult) Skipped() int {
|
||||
return r.SkippedCollecting + r.SkippedCollected + r.SkippedDeleted
|
||||
return r.SkippedShopeeMissing + r.SkippedUnlinked + r.SkippedCollecting + r.SkippedCollected + r.SkippedDeleted
|
||||
}
|
||||
|
||||
// CreatePddCollectTasks 为勾选的 PDD 商品创建不指定客户端的采集任务。
|
||||
@@ -784,6 +786,12 @@ func formatRetryWait(remain time.Duration) string {
|
||||
// (比如还要等多久),见 #24。
|
||||
func FormatCollectTaskMessage(r CollectTaskResult) string {
|
||||
var reasons []string
|
||||
if r.SkippedShopeeMissing > 0 {
|
||||
reasons = append(reasons, fmt.Sprintf("%d 个蝦皮商品已不存在", r.SkippedShopeeMissing))
|
||||
}
|
||||
if r.SkippedUnlinked > 0 {
|
||||
reasons = append(reasons, fmt.Sprintf("%d 个未关联 PDD", r.SkippedUnlinked))
|
||||
}
|
||||
if r.SkippedCollecting > 0 {
|
||||
reason := fmt.Sprintf("%d 个正在采集中", r.SkippedCollecting)
|
||||
if r.Created > 0 {
|
||||
|
||||
@@ -112,6 +112,43 @@ func CreateShopeePddCollectTaskForUser(db *sql.DB, actor *model.User, shopeeGood
|
||||
return CreatePddCollectTasksForUser(db, actor, []string{product.PddGoodsID}, "")
|
||||
}
|
||||
|
||||
// CreateShopeePddCollectTasksForUser 把勾选的蝦皮商品转换成当前 PDD 商品后批量建采集任务。
|
||||
// 蝦皮与 PDD 可以多对一,因此交给统一 PDD 服务前先按 PDD goods_id 去重;
|
||||
// 商品不存在和未关联按原因跳过,其余状态、防重和客户端权限仍由统一服务处理。
|
||||
func CreateShopeePddCollectTasksForUser(db *sql.DB, actor *model.User, shopeeGoodsIDs []string, clientID string) (CollectTaskResult, error) {
|
||||
shopeeGoodsIDs = dedupe(shopeeGoodsIDs)
|
||||
if len(shopeeGoodsIDs) == 0 {
|
||||
return CollectTaskResult{}, invalidInput("没有选择蝦皮商品")
|
||||
}
|
||||
links, err := repository.ListShopeePddLinksByGoodsIDs(db, shopeeGoodsIDs)
|
||||
if err != nil {
|
||||
return CollectTaskResult{}, err
|
||||
}
|
||||
|
||||
var skipped CollectTaskResult
|
||||
pddGoodsIDs := make([]string, 0, len(shopeeGoodsIDs))
|
||||
for _, shopeeGoodsID := range shopeeGoodsIDs {
|
||||
pddGoodsID, exists := links[shopeeGoodsID]
|
||||
if !exists {
|
||||
skipped.SkippedShopeeMissing++
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(pddGoodsID) == "" {
|
||||
skipped.SkippedUnlinked++
|
||||
continue
|
||||
}
|
||||
pddGoodsIDs = append(pddGoodsIDs, pddGoodsID)
|
||||
}
|
||||
|
||||
result, err := CreatePddCollectTasksForUser(db, actor, dedupe(pddGoodsIDs), clientID)
|
||||
if err != nil {
|
||||
return CollectTaskResult{}, err
|
||||
}
|
||||
result.SkippedShopeeMissing = skipped.SkippedShopeeMissing
|
||||
result.SkippedUnlinked = skipped.SkippedUnlinked
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AssociateSybPdd 从顺运宝明细解析出可信的蝦皮商品,再复用统一关联逻辑。
|
||||
func AssociateSybPdd(db *sql.DB, sybID, rawURL string, confirmReplace bool) (string, error) {
|
||||
context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID))
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -120,6 +121,92 @@ func TestCreateShopeePddCollectTaskForUser_记录当前用户(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateShopeePddCollectTasksForUser_按Pdd去重并分类跳过(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, _, _ := insertTaskUsers(t, db)
|
||||
for _, goodsID := range []string{"S-A", "S-B", "S-UNLINKED", "S-DELETED"} {
|
||||
seedShopeeProduct(t, db, goodsID, goodsID)
|
||||
}
|
||||
for _, goodsID := range []string{"S-A", "S-B"} {
|
||||
if _, err := AssociateShopeePdd(db, goodsID, pddURLA, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := AssociateShopeePdd(db, "S-DELETED", pddURLB, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repository.SoftDeletePddProduct(db, "937122477375"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := CreateShopeePddCollectTasksForUser(db, admin,
|
||||
[]string{"S-A", "S-B", "S-A", "S-UNLINKED", "S-DELETED", "S-MISSING"}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Created != 1 || result.SkippedUnlinked != 1 || result.SkippedShopeeMissing != 1 || result.SkippedDeleted != 1 {
|
||||
t.Fatalf("批量分类或 PDD 去重错误: %+v", result)
|
||||
}
|
||||
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)
|
||||
}
|
||||
message := FormatCollectTaskMessage(result)
|
||||
for _, want := range []string{"已创建 1 个", "1 个蝦皮商品已不存在", "1 个未关联 PDD", "1 个已删除的"} {
|
||||
if !strings.Contains(message, want) {
|
||||
t.Errorf("批量结果提示 %q 缺少 %q", message, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateShopeePddCollectTasksForUser_已采集和正常采集中分别跳过(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, _, _ := insertTaskUsers(t, db)
|
||||
seedShopeeProduct(t, db, "S-COLLECTED", "已采集")
|
||||
seedShopeeProduct(t, db, "S-COLLECTING", "采集中")
|
||||
AssociateShopeePdd(db, "S-COLLECTED", pddURLA, false)
|
||||
AssociateShopeePdd(db, "S-COLLECTING", pddURLB, false)
|
||||
if err := repository.SetCollectResult(db, "737116531267", "商品", "店铺", collectedThreeDimensions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result, err := CreatePddCollectTasksForUser(db, admin, []string{"937122477375"}, ""); err != nil || result.Created != 1 {
|
||||
t.Fatalf("准备采集中任务失败: result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
result, err := CreateShopeePddCollectTasksForUser(db, admin,
|
||||
[]string{"S-COLLECTED", "S-COLLECTING"}, "")
|
||||
if err != nil || result.Created != 0 || result.SkippedCollected != 1 || result.SkippedCollecting != 1 {
|
||||
t.Fatalf("状态分类错误: result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateShopeePddCollectTasksForUser_拒绝不可见客户端且整批不创建(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, buyerA, buyerB := prepareClientAssignmentUsers(t, db)
|
||||
if err := RegisterClient(db, model.Client{ClientID: "client-batch-b", Name: "B 的客户端"}, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := AssignClient(db, admin, "client-batch-b", buyerB.UserID, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedShopeeProduct(t, db, "S-PERM", "权限商品")
|
||||
AssociateShopeePdd(db, "S-PERM", pddURLA, false)
|
||||
|
||||
_, err := CreateShopeePddCollectTasksForUser(db, buyerA, []string{"S-PERM"}, "client-batch-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)
|
||||
}
|
||||
product, _ := repository.GetPddProductByGoodsID(db, "737116531267")
|
||||
if product.CollectStatus != model.CollectPending {
|
||||
t.Fatalf("权限失败后商品状态应保持 pending,实际 %s", product.CollectStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSybPddCollectTasksForUser_同一PDD商品去重(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
admin, _, _ := insertTaskUsers(t, db)
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
{{/* ── 第一段:顶部工具条 ────────────────────────────── */}}
|
||||
<div class="toolbar">
|
||||
{{if .CurrentUser.IsAdmin}}<a class="button" href="/integrations/catalog">导入记录</a>{{end}}
|
||||
<form id="shopee-collect-form" method="post" action="/shopee/collect-batch" hidden></form>
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="list_goods_id" value="{{.Keyword}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="list_shop_name" value="{{.ShopNameKeyword}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="status" value="{{.StatusFilter}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="shop" value="{{.ShopFilter}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="image" value="{{.ImageFilter}}" form="shopee-collect-form">
|
||||
<input type="hidden" name="page" value="{{.CurrentPage}}" form="shopee-collect-form">
|
||||
<button type="button" data-modal-open="shopee-collect-modal" data-collect-open
|
||||
data-collect-form="shopee-collect-form" data-need-checked>创建 PDD 采集任务</button>
|
||||
<form class="inline grow" method="get" action="/shopee">
|
||||
{{/* 状态筛选是刚需(找待补规格 / 找没填链接的),不是锦上添花,
|
||||
见 docs/admin/05-ui-specification.md §4.1、工单 #43。
|
||||
@@ -77,7 +87,7 @@
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
<tr data-detail-id="{{.GoodsID}}"{{if gt .PendingCount 0}} class="row-warn"{{end}}>
|
||||
<td class="col-check"><input type="checkbox" name="ids" value="{{.GoodsID}}"></td>
|
||||
<td class="col-check"><input type="checkbox" name="ids" value="{{.GoodsID}}" form="shopee-collect-form"></td>
|
||||
<td>{{.GoodsID}}</td>
|
||||
<td>{{if .ImageURL}}<button type="button" class="thumb-action" data-image-preview-url="{{.ImageURL}}" data-image-preview-title="{{.Title}}" title="查看蝦皮主图"><img src="{{.ImageURL}}" alt="{{.Title}} 缩略图" class="thumb" width="32" height="32" loading="lazy" referrerpolicy="no-referrer" data-image-thumb><span class="thumb-fallback" hidden data-image-thumb-fallback>加载失败</span></button>{{else}}<span class="thumb-placeholder">无图</span>{{end}}</td>
|
||||
{{/* 商品名称列收窄,用独立的 .col-title 类设 max-width(具体数值见 app.css),
|
||||
@@ -111,6 +121,32 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" id="shopee-collect-modal" hidden>
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="shopee-collect-modal-title">
|
||||
<div class="modal-head">
|
||||
<h2 id="shopee-collect-modal-title">创建 PDD 采集任务</h2>
|
||||
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>将按当前 PDD 关联处理已选择的 <strong data-collect-count>0</strong> 个蝦皮商品。</p>
|
||||
<div class="field">
|
||||
<label for="shopee-collect-client">执行客户端</label>
|
||||
<select id="shopee-collect-client" name="client_id" form="shopee-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="shopee-collect-form" class="primary" data-collect-submit>确认创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" id="image-preview-modal" hidden><div class="modal image-preview-modal" role="dialog" aria-modal="true" aria-labelledby="image-preview-title"><div class="modal-head"><h2 id="image-preview-title" data-image-preview-title>蝦皮商品主图</h2><button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button></div><div class="modal-body image-preview-body"><p class="hint image-preview-status" data-image-preview-status role="status">正在加载原图…</p><img data-image-preview-image alt="" hidden></div><div class="modal-foot"><a class="button-link" data-image-preview-link href="#" target="_blank" rel="noopener noreferrer">新窗口打开原图</a><button type="button" data-modal-close>关闭</button></div></div></div>
|
||||
|
||||
<p class="hint">
|
||||
|
||||
@@ -87,8 +87,8 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
|
||||
|
||||
### 4.1 蝦皮数据模块
|
||||
|
||||
**顶部工具条:** 导入按钮、状态筛选(全部 / 待补规格 / 未填 PDD 链接 / 已填链接)、
|
||||
商品 ID 搜索框、搜索按钮、删除按钮。采集从商品详情中发起,避免脱离商品上下文。
|
||||
**顶部工具条:** 导入按钮、批量创建 PDD 采集任务按钮、状态筛选
|
||||
(全部 / 待补规格 / 未填 PDD 链接 / 已填链接)、商品 ID 搜索框、搜索按钮、删除按钮。
|
||||
|
||||
`[必须]` 状态筛选是**刚需**,不是锦上添花(工单 #43):本页的主要用途是维护
|
||||
(找出需要补规格的商品、找出还没关联 PDD 链接的商品),只靠商品 ID 搜索的话,
|
||||
@@ -100,7 +100,7 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
|
||||
|
||||
| 列 | 说明 |
|
||||
|---|---|
|
||||
| 勾选 | 支持批量删除 |
|
||||
| 勾选 | 支持批量删除、批量创建 PDD 采集任务 |
|
||||
| 商品 ID | 蝦皮商品编号 |
|
||||
| 商品名称 | |
|
||||
| 颜色 / 尺码 / 建议 | 从规格原文解析,解析不出来留空并标记 |
|
||||
@@ -114,7 +114,11 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
|
||||
- 保存链接时从完整链接解析 `goods_id`,在同一事务内复用或复活 PDD 商品档案并更新当前关联;
|
||||
- 更换成不同 `goods_id` 时必须明确勾选确认,旧规格映射保留但不会被当前关联查询误用;
|
||||
- 弹窗内提供【创建采集任务】按钮,复用 PDD 商品模块的幂等与超时重试规则;
|
||||
- `[必须]` 采集按钮**在本页只出现在弹窗里**,不要放在每个表格行——
|
||||
- 蝦皮列表支持勾选商品后批量创建 PDD 采集任务。系统按蝦皮商品当前关联取得
|
||||
PDD 商品并按 PDD 商品 ID 去重,可选当前账号可见客户端;未关联、PDD 已删除、
|
||||
已采集和正常采集中的商品按原因统计并跳过。采集失败、采集超时或采集中但已无
|
||||
有效任务的商品可以恢复创建。该入口不修改 PDD 关联,不替代商品详情里的单商品入口;
|
||||
- `[必须]` 采集操作**只出现在顶部批量入口和商品详情弹窗里**,不要放在每个表格行——
|
||||
一个商品有 N 个 SKU 行,放行上就是 N 个按钮干同一件事,还会建出 N 个重复任务。
|
||||
|
||||
`[注意]` PDD 商品模块(§4.2)继续作为商品档案、采集状态和故障诊断页;
|
||||
@@ -527,7 +531,7 @@ MVP 之后:
|
||||
- PDD 链接**人工填写**,不自动抓取。
|
||||
- PDD 商品是**独立模块**,创建商品和创建采集任务都在那里;
|
||||
蝦皮数据模块的编辑弹窗负责的是「这个蝦皮商品对应哪个 PDD 商品」。
|
||||
- **采集任务不指定客户端**,谁领到算谁的;采购任务仍可分配,也允许留空。
|
||||
- 采集任务和采购任务都允许指定当前账号可见的客户端,也允许留空;留空时由符合条件的客户端领取。
|
||||
- 任务**分配给指定客户端**,Client 只领分给自己的。
|
||||
- **不加心跳接口**;设置页显式登记,领取接口保留兼容登记,在线状态由最近活动时间派生。
|
||||
- 规格映射**独立成表且可复用**,按 `(蝦皮商品 ID, spec_key, PDD 商品 ID)` 隔离;换 PDD 商品不会误用旧映射,换回时可继续复用。
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
### 4.1 工具条
|
||||
|
||||
```text
|
||||
[导入记录] 状态[全部▾] 店铺[全部▾] 图片[全部▾] 商品 ID / 名称 [____] 店铺名称 [____] [搜索] [删除]
|
||||
[导入记录] [创建 PDD 采集任务] 状态[全部▾] 店铺[全部▾] 图片[全部▾] 商品 ID / 名称 [____] 店铺名称 [____] [搜索] [删除]
|
||||
```
|
||||
|
||||
- **导入记录**:管理员进入统一商品目录导入记录页;采购员无系统级审计入口。
|
||||
@@ -166,6 +166,9 @@
|
||||
- `[必须]` 店铺、图片使用独立三态下拉框,不使用含义不清的复选框;两个条件可互相
|
||||
组合,也可与状态和商品 ID/标题搜索叠加。未知参数按“全部”处理,翻页、详情弹窗
|
||||
和写操作返回时保留全部条件。工具条在窄窗口沿用公共 `flex-wrap` 自然换行。
|
||||
- **创建 PDD 采集任务**(工单 #185):勾选蝦皮商品后打开批量确认弹窗,显示选择
|
||||
数量及带可见标签的“执行客户端”,默认不指定。服务端按当前 PDD 关联和 PDD 商品 ID
|
||||
去重;未关联、已删除、已采集、正常采集中分别统计并跳过。商品详情的单商品入口保留。
|
||||
|
||||
### 4.2 表格列
|
||||
|
||||
|
||||
Reference in New Issue
Block a user