feat: 明确顺运宝复用 PDD 关联 (#183)

This commit is contained in:
chengma
2026-08-12 15:13:08 +08:00
parent e32041e794
commit 4c5f0610fd
8 changed files with 105 additions and 25 deletions
+17
View File
@@ -247,6 +247,23 @@ func TestShopeeDetail_区分顺运宝观测与正式SKU(t *testing.T) {
} }
} }
func TestSybPage_明确展示蝦皮商品Pdd关联来源(t *testing.T) {
list, err := os.ReadFile("templates/syb/list.html")
if err != nil {
t.Fatal(err)
}
detail, err := os.ReadFile("templates/syb/detail_modal.html")
if err != nil {
t.Fatal(err)
}
content := string(list) + string(detail)
for _, want := range []string{".PddAssociationText", "仅在需要更换采购商品时修改下面的关联"} {
if !strings.Contains(content, want) {
t.Errorf("顺运宝页面缺少关联复用提示 %q", want)
}
}
}
func TestCatalogHistory_展示更新策略和分级统计(t *testing.T) { func TestCatalogHistory_展示更新策略和分级统计(t *testing.T) {
list, err := os.ReadFile("templates/catalog/list.html") list, err := os.ReadFile("templates/catalog/list.html")
if err != nil { if err != nil {
+38
View File
@@ -44,6 +44,44 @@ func TestAssociateSybPdd_兼容商品ID并复用已有采集结果(t *testing.T)
} }
} }
func TestUpsertSybOrder_同步后自动复用蝦皮商品Pdd关联(t *testing.T) {
db := newTestDB(t)
if err := repository.UpsertShopeeProduct(db, "SP-REUSE", "目录商品", "正常", "MAIN-1"); err != nil {
t.Fatal(err)
}
if _, err := AssociateShopeePdd(db, "SP-REUSE", pddURLA, false); err != nil {
t.Fatal(err)
}
seedWorkflowOrder(t, db, "SYB-REUSE", "SP-REUSE", "黑色,M")
detail, err := GetSybProcessingDetail(db, "SYB-REUSE")
if err != nil || detail == nil {
t.Fatalf("读取同步明细失败: detail=%+v err=%v", detail, err)
}
if detail.Stage != SybStagePddPending || detail.PddGoodsID != "737116531267" || detail.PddAssociationText == "" {
t.Fatalf("新明细应直接复用已有 PDD 关联并进入待采集阶段: %+v", detail)
}
list, err := ListSybOrdersView(db, "", "", SybStagePddPending, 1)
if err != nil || list.Total != 1 || list.Rows[0].PddAssociationText == "" {
t.Fatalf("列表应显示关联来源: list=%+v err=%v", list, err)
}
// 重复同步仍复用同一商品关系,不覆盖主数据上的 PDD 关联。
seedWorkflowOrder(t, db, "SYB-REUSE", "SP-REUSE", "黑色,M")
product, err := repository.GetShopeeProductByGoodsID(db, "SP-REUSE")
if err != nil || product == nil || product.PddGoodsID != "737116531267" {
t.Fatalf("重复同步覆盖了蝦皮商品的 PDD 关联: product=%+v err=%v", product, err)
}
if err := repository.SoftDeletePddProduct(db, "737116531267"); err != nil {
t.Fatal(err)
}
detail, err = GetSybProcessingDetail(db, "SYB-REUSE")
if err != nil || detail.Stage != SybStagePddMissing || detail.PddAssociationText != "" {
t.Fatalf("PDD 软删除后必须回到关联失效状态且不显示复用提示: detail=%+v err=%v", detail, err)
}
}
func TestAssociateSybPdd_新商品ID生成标准链接且完整链接保持兼容(t *testing.T) { func TestAssociateSybPdd_新商品ID生成标准链接且完整链接保持兼容(t *testing.T) {
t.Run("纯商品ID", func(t *testing.T) { t.Run("纯商品ID", func(t *testing.T) {
db := newTestDB(t) db := newTestDB(t)
+13
View File
@@ -937,6 +937,7 @@ type SybOrderView struct {
Stage string Stage string
StageText string StageText string
StageHelp string StageHelp string
PddAssociationText string
ActionText string ActionText string
NeedsAttention bool NeedsAttention bool
CanCollect bool CanCollect bool
@@ -1154,6 +1155,7 @@ 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.PddAssociationText = sybPddAssociationText(context)
v.CanCollect = v.Stage == SybStagePddPending || v.Stage == SybStagePddFailed || v.CanCollect = v.Stage == SybStagePddPending || v.Stage == SybStagePddFailed ||
v.Stage == SybStagePddCollectingStale v.Stage == SybStagePddCollectingStale
v.CanPurchase = v.Stage == SybStagePurchaseReady v.CanPurchase = v.Stage == SybStagePurchaseReady
@@ -1209,6 +1211,7 @@ type SybProcessingDetail struct {
ShopeeExists bool ShopeeExists bool
PddGoodsID string PddGoodsID string
PddURL string PddURL string
PddAssociationText string
CollectStatus string CollectStatus string
CollectMsg string CollectMsg string
CanCollect bool CanCollect bool
@@ -1231,6 +1234,7 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
ShopeeGoodsID: o.ShopeeGoodsID, ShopeeGoodsID: o.ShopeeGoodsID,
Quantity: o.Quantity, ShopeeExists: context.ShopeeExists, Quantity: o.Quantity, ShopeeExists: context.ShopeeExists,
PddGoodsID: context.PddGoodsID, PddURL: context.PddGoodsURL, PddGoodsID: context.PddGoodsID, PddURL: context.PddGoodsURL,
PddAssociationText: sybPddAssociationText(*context),
CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg, CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg,
} }
d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context) d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context)
@@ -1258,6 +1262,15 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
return d, nil return d, nil
} }
// sybPddAssociationText 只在当前蝦皮商品关联能找到有效、未删除的 PDD 商品时显示。
// PDD 关联的唯一事实来源是 shopee_products;顺运宝明细不复制这份关系。
func sybPddAssociationText(context repository.SybOrderContext) string {
if context.PddGoodsID == "" || context.PddCollectStatus == "" {
return ""
}
return "使用蝦皮商品的 PDD 关联:" + context.PddGoodsID + ",无需重复输入链接"
}
// SaveSybLoginSession 登录成功后把会话缓存进数据库。 // SaveSybLoginSession 登录成功后把会话缓存进数据库。
// //
// `[必须]` 缓存写失败不能让已经登录的会话失效——本函数把错误原样 // `[必须]` 缓存写失败不能让已经登录的会话失效——本函数把错误原样
+1
View File
@@ -20,6 +20,7 @@
<p class="missing" role="alert">顺运宝没有提供规格原文,系统无法安全判断要采购的颜色和尺码。请先核对上游货运单数据。</p> <p class="missing" role="alert">顺运宝没有提供规格原文,系统无法安全判断要采购的颜色和尺码。请先核对上游货运单数据。</p>
{{else}} {{else}}
<h3>关联采购商品</h3> <h3>关联采购商品</h3>
{{if .PddAssociationText}}<p class="hint" role="status">{{.PddAssociationText}};仅在需要更换采购商品时修改下面的关联。</p>{{end}}
<form method="post" action="/syb/associate-pdd" class="form-stack"> <form method="post" action="/syb/associate-pdd" class="form-stack">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<input type="hidden" name="syb_id" value="{{.SybID}}"> <input type="hidden" name="syb_id" value="{{.SybID}}">
+1 -1
View File
@@ -149,7 +149,7 @@
</button> </button>
{{else}}—{{end}} {{else}}—{{end}}
</td> </td>
<td><span class="status-pill">{{.StageText}}</span><br><small>{{.StageHelp}}</small></td> <td><span class="status-pill">{{.StageText}}</span><br><small>{{.StageHelp}}</small>{{if .PddAssociationText}}<br><small class="hint">{{.PddAssociationText}}</small>{{end}}</td>
<td> <td>
{{if eq .Stage "purchase_ready"}} {{if eq .Stage "purchase_ready"}}
<button type="button" data-modal-open="purchase-modal" data-purchase-open <button type="button" data-modal-open="purchase-modal" data-purchase-open
+3
View File
@@ -227,6 +227,9 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
- 找到蝦皮商品后,可在同一弹窗填写带 `goods_id` 的完整 PDD 链接,或直接输入 - 找到蝦皮商品后,可在同一弹窗填写带 `goods_id` 的完整 PDD 链接,或直接输入
6~24 位纯数字 PDD 商品 ID;纯 ID 先转换为标准商品链接,再复用现有 PDD 商品档案和 6~24 位纯数字 PDD 商品 ID;纯 ID 先转换为标准商品链接,再复用现有 PDD 商品档案和
采集结果。关联完成后仍由采购员另行创建采集任务,换成其他商品仍须明确确认。 采集结果。关联完成后仍由采购员另行创建采集任务,换成其他商品仍须明确确认。
- 顺运宝同步出的蝦皮商品 ID 如果已经在 `shopee_products` 关联有效、未删除的 PDD
商品,新货运单明细必须直接复用该关系并进入后续采集或规格匹配阶段,不要求采购员
重复输入 PDD URL。关系仍只保存在蝦皮商品上,顺运宝明细不复制 PDD 字段。
- 显示蝦皮规格原文和当前 PDD 商品 `skus_json` 里的动态规格组合与人民币价格; - 显示蝦皮规格原文和当前 PDD 商品 `skus_json` 里的动态规格组合与人民币价格;
- PDD 维度来自采集结果的 `dimensions`,不得写死成颜色、尺码两个维度; - PDD 维度来自采集结果的 `dimensions`,不得写死成颜色、尺码两个维度;
+4
View File
@@ -820,6 +820,10 @@ users(采购员)──1:N 当前归属──────────┘
- `shopee_products.pdd_goods_id`:PDD 商品下架换代时改 - `shopee_products.pdd_goods_id`:PDD 商品下架换代时改
- `spec_mappings` 按 `(蝦皮商品, 顺运宝规格键, PDD商品)` 存:换了商品自然查不到旧映射 - `spec_mappings` 按 `(蝦皮商品, 顺运宝规格键, PDD商品)` 存:换了商品自然查不到旧映射
`syb_orders` 不保存 PDD 商品 ID。新货运单通过 `shopee_goods_id` 动态读取
`shopee_products.pdd_goods_id`;只要目标 `pdd_products` 未软删除,就自动复用当前
关联。这样换绑后所有货运单立即看到新关系,也不会留下需要同步更新的第二份 PDD 字段。
## 11. 与 Client 数据模型的关系 ## 11. 与 Client 数据模型的关系
Admin 使用集中 MySQL 8.4,Client 仍使用各自的本地 SQLite;两者只通过接口交换数据。 Admin 使用集中 MySQL 8.4,Client 仍使用各自的本地 SQLite;两者只通过接口交换数据。
+4
View File
@@ -564,6 +564,10 @@ PDD 采集中(超时)行可用于
标准商品链接并按 `goods_id` 复用已有档案;换成其他商品需要明确确认。待采集或采集失败时显示 标准商品链接并按 `goods_id` 复用已有档案;换成其他商品需要明确确认。待采集或采集失败时显示
“创建采集任务”或“重新创建采集任务”,采集中和已采集状态只显示说明。 “创建采集任务”或“重新创建采集任务”,采集中和已采集状态只显示说明。
顺运宝明细通过蝦皮商品 ID 命中有效 PDD 关联时,列表阶段区域和详情弹窗显示
“使用蝦皮商品的 PDD 关联:<商品 ID>,无需重复输入链接”。关联表单仍保留给主动换品,
并说明只有需要更换采购商品时才修改;PDD 商品缺失或已软删除时不显示复用提示。
单条创建和重新创建采集任务时,按钮上方显示有可见标签的“执行客户端” 单条创建和重新创建采集任务时,按钮上方显示有可见标签的“执行客户端”
下拉框,只列出当前账号可见的 Client,并显示名称、Client ID 和在线状态。 下拉框,只列出当前账号可见的 Client,并显示名称、Client ID 和在线状态。
默认为“不指定(任意客户端可领取)”;指定后只等待该 Client 领取。持续 默认为“不指定(任意客户端可领取)”;指定后只等待该 Client 领取。持续