Files
cmautobuy/admin/service/shopee_pdd.go
T

191 lines
7.1 KiB
Go
Raw Normal View History

package service
import (
"database/sql"
"errors"
"fmt"
"strings"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
// ErrPddAssociationReplaceRequired 表示当前蝦皮商品已经关联了另一个 PDD 商品。
var ErrPddAssociationReplaceRequired = errors.New("更换 PDD 商品需要明确确认")
const canonicalPddGoodsURL = "https://mobile.yangkeduo.com/goods.html?goods_id="
// normalizeSybPddReference 只在顺运宝详情入口兼容纯商品 ID。
// 其他入口仍沿用完整 PDD 链接校验,避免无意扩大已经确认的输入范围。
func normalizeSybPddReference(raw string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
return "", fmt.Errorf("%w: PDD 商品链接或商品 ID 不能为空", ErrBadPddURL)
}
if isGoodsID(value) {
return canonicalPddGoodsURL + value, nil
}
if isASCIIDigits(value) {
return "", fmt.Errorf("%w: PDD 商品 ID 必须是 6~24 位纯数字", ErrBadPddURL)
}
if !strings.Contains(value, "://") {
return "", fmt.Errorf("%w: 请输入带 goods_id 的完整拼多多链接,或 6~24 位纯数字商品 ID", ErrBadPddURL)
}
return value, nil
}
func isASCIIDigits(value string) bool {
for _, char := range value {
if char < '0' || char > '9' {
return false
}
}
return true
}
// 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})
}
// CreateShopeePddCollectTaskForUser 为网页登录入口记录任务创建人。
func CreateShopeePddCollectTaskForUser(db *sql.DB, actor *model.User, 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 CreatePddCollectTasksForUser(db, actor, []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("顺运宝明细不存在")
}
2026-08-10 12:24:23 +08:00
if context.Order.ShopeeGoodsID == "" {
return "", fmt.Errorf("顺运宝未提供蝦皮商品 ID")
}
if context.Order.SpecKey == "" {
return "", fmt.Errorf("顺运宝未提供规格,不能进入采购流程")
}
normalizedURL, err := normalizeSybPddReference(rawURL)
if err != nil {
return "", err
}
return AssociateShopeePdd(db, context.Order.ShopeeGoodsID, normalizedURL, 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("顺运宝明细不存在")
}
if context.PddGoodsID == "" {
return CollectTaskResult{}, fmt.Errorf("请先关联 PDD 商品")
}
return createPddCollectTasks(db, []string{context.PddGoodsID}, "", "", "",
map[string][]string{context.PddGoodsID: {context.Order.SybID}})
}
// CreateSybPddCollectTaskForUser 为网页登录入口记录任务创建人。
func CreateSybPddCollectTaskForUser(db *sql.DB, actor *model.User, sybID string) (CollectTaskResult, error) {
return CreateSybPddCollectTasksForUser(db, actor, []string{sybID}, "")
}
// CreateSybPddCollectTasksForUser 把勾选的顺运宝明细转换成 PDD 商品后批量建采集任务。
// 同一 PDD 商品可能对应多条货运单明细,因此先按 goods_id 去重;任何明细不存在或
// 已不处于可采集阶段时整批拒绝,避免浏览器过期或参数篡改造成部分创建。
func CreateSybPddCollectTasksForUser(db *sql.DB, actor *model.User, sybIDs []string, clientID string) (CollectTaskResult, error) {
sybIDs = dedupe(sybIDs)
if len(sybIDs) == 0 {
return CollectTaskResult{}, invalidInput("没有选择可采集的顺运宝明细")
}
goodsIDs := make([]string, 0, len(sybIDs))
sourcesByGoodsID := make(map[string][]string)
for _, sybID := range sybIDs {
context, err := repository.GetSybOrderContext(db, sybID)
if err != nil {
return CollectTaskResult{}, err
}
if context == nil {
return CollectTaskResult{}, invalidInput("顺运宝明细 %s 不存在,请刷新页面后重新选择", sybID)
}
stage, _, _, _ := sybStageFor(*context)
if stage != SybStagePddPending && stage != SybStagePddFailed && stage != SybStagePddCollectingStale {
return CollectTaskResult{}, invalidInput("顺运宝明细 %s 已不处于 PDD 待采集、采集失败或采集超时状态,请刷新页面后重新选择", sybID)
}
goodsIDs = append(goodsIDs, context.PddGoodsID)
sourcesByGoodsID[context.PddGoodsID] = append(sourcesByGoodsID[context.PddGoodsID], sybID)
}
visibleUserID, err := taskActorScope(actor)
if err != nil {
return CollectTaskResult{}, err
}
return createPddCollectTasks(db, dedupe(goodsIDs), strings.TrimSpace(clientID),
visibleUserID, actor.UserID, sourcesByGoodsID)
}