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) {
list, err := os.ReadFile("templates/catalog/list.html")
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) {
t.Run("纯商品ID", func(t *testing.T) {
db := newTestDB(t)
+37 -24
View File
@@ -925,29 +925,30 @@ func EnsureSybSession(db *sql.DB, client *syb.Client, username string, now time.
// SybOrderView 是列表页一行要显示的全部内容,已经格式化成字符串,
// 模板里不做判断和格式化,和其余四个模块的做法一致。
type SybOrderView struct {
SybID string
OrderNo string
ShopName string
Title string
ProductSpec string
ShopeeGoodsID string
Quantity int
PriceText string // "NT$239.00",和人民币价格一眼分得清
ImageURL string
Stage string
StageText string
StageHelp string
ActionText string
NeedsAttention bool
CanCollect bool
CanPurchase bool
SelectionHint string
DefaultUnitPrice string
DefaultTotalPrice string
MappedPddChoice string
MappingOptionKey string
ContextVersion string
UpdatedAt string
SybID string
OrderNo string
ShopName string
Title string
ProductSpec string
ShopeeGoodsID string
Quantity int
PriceText string // "NT$239.00",和人民币价格一眼分得清
ImageURL string
Stage string
StageText string
StageHelp string
PddAssociationText string
ActionText string
NeedsAttention bool
CanCollect bool
CanPurchase bool
SelectionHint string
DefaultUnitPrice string
DefaultTotalPrice string
MappedPddChoice string
MappingOptionKey string
ContextVersion string
UpdatedAt string
}
const (
@@ -1154,6 +1155,7 @@ func sybOrderViewFor(context repository.SybOrderContext) SybOrderView {
Quantity: o.Quantity, ImageURL: o.ImageURL,
UpdatedAt: formatLocalTime(o.UpdatedAt)}
v.Stage, v.StageText, v.StageHelp, v.ActionText = sybStageFor(context)
v.PddAssociationText = sybPddAssociationText(context)
v.CanCollect = v.Stage == SybStagePddPending || v.Stage == SybStagePddFailed ||
v.Stage == SybStagePddCollectingStale
v.CanPurchase = v.Stage == SybStagePurchaseReady
@@ -1209,6 +1211,7 @@ type SybProcessingDetail struct {
ShopeeExists bool
PddGoodsID string
PddURL string
PddAssociationText string
CollectStatus string
CollectMsg string
CanCollect bool
@@ -1231,7 +1234,8 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
ShopeeGoodsID: o.ShopeeGoodsID,
Quantity: o.Quantity, ShopeeExists: context.ShopeeExists,
PddGoodsID: context.PddGoodsID, PddURL: context.PddGoodsURL,
CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg,
PddAssociationText: sybPddAssociationText(*context),
CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg,
}
d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context)
d.CanCollect = context.PddGoodsID != "" &&
@@ -1258,6 +1262,15 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
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 登录成功后把会话缓存进数据库。
//
// `[必须]` 缓存写失败不能让已经登录的会话失效——本函数把错误原样
+1
View File
@@ -20,6 +20,7 @@
<p class="missing" role="alert">顺运宝没有提供规格原文,系统无法安全判断要采购的颜色和尺码。请先核对上游货运单数据。</p>
{{else}}
<h3>关联采购商品</h3>
{{if .PddAssociationText}}<p class="hint" role="status">{{.PddAssociationText}};仅在需要更换采购商品时修改下面的关联。</p>{{end}}
<form method="post" action="/syb/associate-pdd" class="form-stack">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<input type="hidden" name="syb_id" value="{{.SybID}}">
+1 -1
View File
@@ -149,7 +149,7 @@
</button>
{{else}}—{{end}}
</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>
{{if eq .Stage "purchase_ready"}}
<button type="button" data-modal-open="purchase-modal" data-purchase-open
+3
View File
@@ -227,6 +227,9 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
- 找到蝦皮商品后,可在同一弹窗填写带 `goods_id` 的完整 PDD 链接,或直接输入
6~24 位纯数字 PDD 商品 ID;纯 ID 先转换为标准商品链接,再复用现有 PDD 商品档案和
采集结果。关联完成后仍由采购员另行创建采集任务,换成其他商品仍须明确确认。
- 顺运宝同步出的蝦皮商品 ID 如果已经在 `shopee_products` 关联有效、未删除的 PDD
商品,新货运单明细必须直接复用该关系并进入后续采集或规格匹配阶段,不要求采购员
重复输入 PDD URL。关系仍只保存在蝦皮商品上,顺运宝明细不复制 PDD 字段。
- 显示蝦皮规格原文和当前 PDD 商品 `skus_json` 里的动态规格组合与人民币价格;
- PDD 维度来自采集结果的 `dimensions`,不得写死成颜色、尺码两个维度;
+4
View File
@@ -820,6 +820,10 @@ users(采购员)──1:N 当前归属──────────┘
- `shopee_products.pdd_goods_id`:PDD 商品下架换代时改
- `spec_mappings` 按 `(蝦皮商品, 顺运宝规格键, PDD商品)` 存:换了商品自然查不到旧映射
`syb_orders` 不保存 PDD 商品 ID。新货运单通过 `shopee_goods_id` 动态读取
`shopee_products.pdd_goods_id`;只要目标 `pdd_products` 未软删除,就自动复用当前
关联。这样换绑后所有货运单立即看到新关系,也不会留下需要同步更新的第二份 PDD 字段。
## 11. 与 Client 数据模型的关系
Admin 使用集中 MySQL 8.4,Client 仍使用各自的本地 SQLite;两者只通过接口交换数据。
+4
View File
@@ -564,6 +564,10 @@ PDD 采集中(超时)行可用于
标准商品链接并按 `goods_id` 复用已有档案;换成其他商品需要明确确认。待采集或采集失败时显示
“创建采集任务”或“重新创建采集任务”,采集中和已采集状态只显示说明。
顺运宝明细通过蝦皮商品 ID 命中有效 PDD 关联时,列表阶段区域和详情弹窗显示
“使用蝦皮商品的 PDD 关联:<商品 ID>,无需重复输入链接”。关联表单仍保留给主动换品,
并说明只有需要更换采购商品时才修改;PDD 商品缺失或已软删除时不显示复用提示。
单条创建和重新创建采集任务时,按钮上方显示有可见标签的“执行客户端”
下拉框,只列出当前账号可见的 Client,并显示名称、Client ID 和在线状态。
默认为“不指定(任意客户端可领取)”;指定后只等待该 Client 领取。持续