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("顺运宝明细不存在") } 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) }