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