feat: 统一蝦皮与顺运宝 PDD 关联入口 (#68)
This commit is contained in:
@@ -192,18 +192,19 @@ type ShopeeSpecView struct {
|
||||
}
|
||||
|
||||
// ShopeeProductDetail 是双击弹窗要显示的全部内容。
|
||||
//
|
||||
// `[必须]` 这个弹窗只读,不含可提交的表单——本工单不实现保存,
|
||||
// ShopeeSave 仍是 501,见 #41。
|
||||
type ShopeeProductDetail struct {
|
||||
GoodsID string
|
||||
Title string
|
||||
ShopeeStatus string
|
||||
MainSKUCode string
|
||||
|
||||
PddURL string
|
||||
PddMissing bool
|
||||
StatusText string
|
||||
PddURL string
|
||||
PddGoodsID string
|
||||
PddMissing bool
|
||||
StatusText string
|
||||
CollectStatus string
|
||||
CollectMsg string
|
||||
CanCollect bool
|
||||
|
||||
SKUs []ShopeeSpecView
|
||||
PendingCount int
|
||||
@@ -222,6 +223,7 @@ func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, e
|
||||
Title: p.Title,
|
||||
ShopeeStatus: p.ShopeeStatus,
|
||||
MainSKUCode: p.MainSKUCode,
|
||||
PddGoodsID: p.PddGoodsID,
|
||||
}
|
||||
if d.Title == "" {
|
||||
d.Title = placeholder
|
||||
@@ -239,12 +241,16 @@ func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, e
|
||||
d.StatusText = "未填链接"
|
||||
} else {
|
||||
d.PddURL = p.PddGoodsURL
|
||||
status, _, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID)
|
||||
status, msg, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status.Valid {
|
||||
d.CollectStatus = status.String
|
||||
d.CollectMsg = msg.String
|
||||
d.StatusText = collectStatusText(model.CollectStatus(status.String))
|
||||
d.CanCollect = status.String == string(model.CollectPending) ||
|
||||
status.String == string(model.CollectFailed)
|
||||
} else {
|
||||
d.StatusText = "PDD 商品缺失"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/repository"
|
||||
)
|
||||
|
||||
// ErrPddAssociationReplaceRequired 表示当前蝦皮商品已经关联了另一个 PDD 商品。
|
||||
var ErrPddAssociationReplaceRequired = errors.New("更换 PDD 商品需要明确确认")
|
||||
|
||||
// AssociateShopeePdd 把蝦皮商品关联到 PDD 商品档案。
|
||||
// 解析、建档/复活和关联更新在同一事务内完成,任一步失败都不留下半成品。
|
||||
func AssociateShopeePdd(db *sql.DB, shopeeGoodsID, rawURL string, confirmReplace bool) (string, error) {
|
||||
shopeeGoodsID = strings.TrimSpace(shopeeGoodsID)
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
pddGoodsID, err := ParsePddGoodsID(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("开始关联 PDD 商品事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
product, err := repository.GetShopeeProductByGoodsID(tx, shopeeGoodsID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if product == nil {
|
||||
return "", fmt.Errorf("蝦皮商品 %s 不存在", shopeeGoodsID)
|
||||
}
|
||||
if product.PddGoodsID != "" && product.PddGoodsID != pddGoodsID && !confirmReplace {
|
||||
return "", fmt.Errorf("%w:当前是 %s,新链接解析为 %s",
|
||||
ErrPddAssociationReplaceRequired, product.PddGoodsID, pddGoodsID)
|
||||
}
|
||||
if _, err := repository.EnsurePddProduct(tx, pddGoodsID, rawURL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := repository.UpdateShopeePddLink(tx, shopeeGoodsID, pddGoodsID, rawURL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", fmt.Errorf("提交 PDD 商品关联失败: %w", err)
|
||||
}
|
||||
return pddGoodsID, nil
|
||||
}
|
||||
|
||||
// CreateShopeePddCollectTask 按蝦皮商品当前关联创建采集任务,避免相信表单里的 PDD ID。
|
||||
func CreateShopeePddCollectTask(db *sql.DB, shopeeGoodsID string) (CollectTaskResult, error) {
|
||||
product, err := repository.GetShopeeProductByGoodsID(db, strings.TrimSpace(shopeeGoodsID))
|
||||
if err != nil {
|
||||
return CollectTaskResult{}, err
|
||||
}
|
||||
if product == nil {
|
||||
return CollectTaskResult{}, fmt.Errorf("蝦皮商品不存在")
|
||||
}
|
||||
if product.PddGoodsID == "" {
|
||||
return CollectTaskResult{}, fmt.Errorf("请先关联 PDD 商品")
|
||||
}
|
||||
return CreatePddCollectTasks(db, []string{product.PddGoodsID})
|
||||
}
|
||||
|
||||
// AssociateSybPdd 从顺运宝明细解析出可信的蝦皮商品,再复用统一关联逻辑。
|
||||
func AssociateSybPdd(db *sql.DB, sybID, rawURL string, confirmReplace bool) (string, error) {
|
||||
context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if context == nil {
|
||||
return "", fmt.Errorf("顺运宝明细不存在")
|
||||
}
|
||||
if context.Order.ShopeeSKUID == "" {
|
||||
return "", fmt.Errorf("请先确认蝦皮规格")
|
||||
}
|
||||
return AssociateShopeePdd(db, context.Order.ShopeeGoodsID, rawURL, confirmReplace)
|
||||
}
|
||||
|
||||
// CreateSybPddCollectTask 按顺运宝明细当前关联创建采集任务。
|
||||
func CreateSybPddCollectTask(db *sql.DB, sybID string) (CollectTaskResult, error) {
|
||||
context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID))
|
||||
if err != nil {
|
||||
return CollectTaskResult{}, err
|
||||
}
|
||||
if context == nil {
|
||||
return CollectTaskResult{}, fmt.Errorf("顺运宝明细不存在")
|
||||
}
|
||||
return CreateShopeePddCollectTask(db, context.Order.ShopeeGoodsID)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"cmautobuy/admin/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
pddURLA = "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267"
|
||||
pddURLB = "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375"
|
||||
)
|
||||
|
||||
func TestAssociateShopeePdd_新建并重复复用(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "1001", "商品一")
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
got, err := AssociateShopeePdd(db, "1001", pddURLA, false)
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 次关联失败: %v", i+1, err)
|
||||
}
|
||||
if got != "737116531267" {
|
||||
t.Fatalf("goods_id = %q", got)
|
||||
}
|
||||
}
|
||||
var count int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM pdd_products WHERE goods_id = ?`, "737116531267").Scan(&count)
|
||||
if count != 1 {
|
||||
t.Fatalf("同一 PDD 商品建了 %d 条,想要 1", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssociateShopeePdd_无效链接不写任何数据(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "1001", "商品一")
|
||||
if _, err := AssociateShopeePdd(db, "1001", "https://p.pinduoduo.com/short", false); err == nil {
|
||||
t.Fatal("短链接应被拒绝")
|
||||
}
|
||||
var pddCount int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM pdd_products`).Scan(&pddCount)
|
||||
product, _ := repository.GetShopeeProductByGoodsID(db, "1001")
|
||||
if pddCount != 0 || product.PddGoodsID != "" {
|
||||
t.Fatalf("失败后仍写入:pdd=%d, link=%q", pddCount, product.PddGoodsID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssociateShopeePdd_换商品必须确认且旧档案保留(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "1001", "商品一")
|
||||
if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := AssociateShopeePdd(db, "1001", pddURLB, false); !errors.Is(err, ErrPddAssociationReplaceRequired) {
|
||||
t.Fatalf("未确认换品错误 = %v", err)
|
||||
}
|
||||
product, _ := repository.GetShopeeProductByGoodsID(db, "1001")
|
||||
if product.PddGoodsID != "737116531267" {
|
||||
t.Fatalf("未确认时关联被改成 %q", product.PddGoodsID)
|
||||
}
|
||||
if _, err := AssociateShopeePdd(db, "1001", pddURLB, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
product, _ = repository.GetShopeeProductByGoodsID(db, "1001")
|
||||
if product.PddGoodsID != "937122477375" {
|
||||
t.Fatalf("确认后关联 = %q", product.PddGoodsID)
|
||||
}
|
||||
old, _ := repository.GetPddProductByGoodsID(db, "737116531267")
|
||||
if old == nil {
|
||||
t.Fatal("换品不应删除旧 PDD 档案")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssociateShopeePdd_软删除商品可复活(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "1001", "商品一")
|
||||
repository.EnsurePddProduct(db, "737116531267", pddURLA)
|
||||
if _, err := repository.SoftDeletePddProducts(db, []string{"737116531267"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
|
||||
if p == nil || p.IsDeleted() {
|
||||
t.Fatal("关联应复活软删除的 PDD 商品")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateShopeePddCollectTask_按当前关联创建(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "1001", "商品一")
|
||||
AssociateShopeePdd(db, "1001", pddURLA, false)
|
||||
result, err := CreateShopeePddCollectTask(db, "1001")
|
||||
if err != nil || result.Created != 1 {
|
||||
t.Fatalf("创建结果 = %+v, err=%v", result, err)
|
||||
}
|
||||
_, _, _, ok := collectTaskOf(t, db, "737116531267")
|
||||
if !ok {
|
||||
t.Fatal("没有创建采集任务")
|
||||
}
|
||||
}
|
||||
@@ -1177,6 +1177,11 @@ type SybProcessingDetail struct {
|
||||
StageHelp string
|
||||
Candidates []SybSKUCandidateView
|
||||
ShopeeExists bool
|
||||
PddGoodsID string
|
||||
PddURL string
|
||||
CollectStatus string
|
||||
CollectMsg string
|
||||
CanCollect bool
|
||||
}
|
||||
|
||||
func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, error) {
|
||||
@@ -1189,7 +1194,12 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err
|
||||
SybID: o.SybID, OrderNo: o.OrderNo, Title: o.Title, ProductSpec: o.ProductSpec,
|
||||
ShopeeGoodsID: o.ShopeeGoodsID, ShopeeSKUID: o.ShopeeSKUID,
|
||||
Quantity: o.Quantity, ShopeeExists: context.ShopeeExists,
|
||||
PddGoodsID: context.PddGoodsID, PddURL: context.PddGoodsURL,
|
||||
CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg,
|
||||
}
|
||||
d.CanCollect = context.PddGoodsID != "" &&
|
||||
(context.PddCollectStatus == string(model.CollectPending) ||
|
||||
context.PddCollectStatus == string(model.CollectFailed))
|
||||
d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context)
|
||||
if context.ShopeeExists {
|
||||
skus, err := repository.ListShopeeSKUsByGoodsID(db, o.ShopeeGoodsID)
|
||||
|
||||
Reference in New Issue
Block a user