package service import ( "database/sql" "fmt" "math" "strings" "cmautobuy/admin/model" "cmautobuy/admin/repository" ) // PddOptionChoice 是采集结果中的一个可购买规格组合。 type PddOptionChoice struct { Key string OptionsJSON string Label string PriceText string PriceCent int64 HasPrice bool Selected bool } func pddOptionChoices(raw string) ([]PddOptionChoice, []string, error) { collected, err := parseCollected(raw) if err != nil { return nil, nil, err } keys, names := dimensionOrder(collected) choices := make([]PddOptionChoice, 0, len(collected.SKUs)) seen := map[string]bool{} for _, sku := range collected.SKUs { if !sku.Available || len(sku.Options) == 0 { continue } key, err := OptionKey(sku.Options) if err != nil { return nil, nil, err } if seen[key] { continue } seen[key] = true parts := make([]string, 0, len(keys)) for i, dimensionKey := range keys { name := dimensionKey if i < len(names) && names[i] != "" { name = names[i] } parts = append(parts, name+":"+sku.Options[dimensionKey]) } choice := PddOptionChoice{ Key: key, OptionsJSON: key, Label: strings.Join(parts, " / "), PriceText: formatPriceCent(sku.PriceCent), } if sku.PriceCent != nil && *sku.PriceCent > 0 { choice.PriceCent, choice.HasPrice = *sku.PriceCent, true } choices = append(choices, choice) } return choices, names, nil } func findPddChoice(raw, key string) (*PddOptionChoice, error) { choices, _, err := pddOptionChoices(raw) if err != nil { return nil, err } for _, choice := range choices { if choice.Key == key { copy := choice return ©, nil } } return nil, nil } // SaveSybMapping 保存顺运宝明细对应蝦皮 SKU 到当前 PDD 商品的可复用映射。 func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("开始保存规格映射事务失败: %w", err) } defer tx.Rollback() context, err := repository.GetSybOrderContext(tx, strings.TrimSpace(sybID)) if err != nil { return err } if context == nil { return fmt.Errorf("顺运宝明细不存在") } if context.Order.ShopeeSKUID == "" { return fmt.Errorf("请先确认蝦皮规格") } if context.PddGoodsID == "" || context.PddCollectStatus != string(model.CollectCollected) || strings.TrimSpace(context.PddSkusJSON) == "" { return fmt.Errorf("当前 PDD 商品尚未完成采集") } choice, err := findPddChoice(context.PddSkusJSON, strings.TrimSpace(optionKey)) if err != nil { return fmt.Errorf("读取 PDD 规格失败: %w", err) } if choice == nil { return fmt.Errorf("所选 PDD 规格已不存在或不可购买,请刷新后重试") } if err := repository.UpsertSKUMapping(tx, model.SKUMapping{ ShopeeSKUID: context.Order.ShopeeSKUID, PddGoodsID: context.PddGoodsID, PddOptionKey: choice.Key, PddOptions: choice.OptionsJSON, GoodsID: context.Order.ShopeeGoodsID, MappedAt: model.NowISO(), MappedBy: strings.TrimSpace(operator), }); err != nil { return err } return tx.Commit() } func mappingIsValid(context repository.SybOrderContext) bool { if context.MappingOptionKey == "" || context.PddSkusJSON == "" { return false } choice, err := findPddChoice(context.PddSkusJSON, context.MappingOptionKey) return err == nil && choice != nil } // PurchaseTaskRequest 是一条采购任务的人工确认输入。价格单位固定为人民币分。 type PurchaseTaskRequest struct { SybID string MaxPriceCent int64 } // PurchaseTaskResult 同时返回已创建数量和每条无法创建的原因。 type PurchaseTaskResult struct { Created int Failures []TaskCreateError } // CreatePurchaseTasks 校验并创建采购任务。业务校验失败按明细返回,数据库错误整体回滚。 func CreatePurchaseTasks(db *sql.DB, actor *model.User, requests []PurchaseTaskRequest, clientID string) (PurchaseTaskResult, error) { var result PurchaseTaskResult visibleUserID, err := visibleClientUserID(actor) if err != nil { return result, err } clientID = strings.TrimSpace(clientID) if clientID == "" { return result, fmt.Errorf("请选择执行采购任务的客户端") } tx, err := db.Begin() if err != nil { return result, fmt.Errorf("开始创建采购任务事务失败: %w", err) } defer tx.Rollback() visible, err := repository.ClientVisibleToUser(tx, clientID, visibleUserID) if err != nil { return result, err } if !visible { return result, fmt.Errorf("所选客户端不存在或不在当前账号可见范围") } seen := map[string]bool{} for _, request := range requests { request.SybID = strings.TrimSpace(request.SybID) if request.SybID == "" || seen[request.SybID] { if request.SybID != "" { result.Failures = append(result.Failures, TaskCreateError{SybID: request.SybID, Reason: "重复选择"}) } continue } seen[request.SybID] = true reason, context, optionsJSON, err := validatePurchaseRequest(tx, request) if err != nil { return PurchaseTaskResult{}, err } if reason != "" { result.Failures = append(result.Failures, TaskCreateError{SybID: request.SybID, Reason: reason}) continue } if err := repository.InsertPurchaseTask(tx, model.Task{ TaskID: newPurchaseTaskID(), AssignedClient: clientID, SybID: context.Order.SybID, OrderNo: context.Order.OrderNo, GoodsID: context.Order.ShopeeGoodsID, ShopeeSKUID: context.Order.ShopeeSKUID, PddGoodsURL: context.PddGoodsURL, PddGoodsID: context.PddGoodsID, PddOptions: optionsJSON, Quantity: context.Order.Quantity, MaxPriceCent: request.MaxPriceCent, }); err != nil { return PurchaseTaskResult{}, err } result.Created++ } if err := tx.Commit(); err != nil { return PurchaseTaskResult{}, fmt.Errorf("提交采购任务失败: %w", err) } return result, nil } func validatePurchaseRequest(q repository.Execer, request PurchaseTaskRequest) (string, *repository.SybOrderContext, string, error) { context, err := repository.GetSybOrderContext(q, request.SybID) if err != nil { return "", nil, "", err } if context == nil { return "顺运宝明细不存在", nil, "", nil } if context.Order.ShopeeSKUID == "" { return "蝦皮规格未确认", context, "", nil } if context.PddGoodsID == "" || context.PddGoodsURL == "" { return "未关联 PDD 商品", context, "", nil } if context.PddCollectStatus != string(model.CollectCollected) || context.PddSkusJSON == "" { return "PDD 商品尚未采集完成", context, "", nil } if context.MappingOptionKey == "" { return "PDD 规格尚未匹配", context, "", nil } choice, err := findPddChoice(context.PddSkusJSON, context.MappingOptionKey) if err != nil { return "", nil, "", err } if choice == nil { return "已保存的 PDD 规格已失效,请重新匹配", context, "", nil } if context.Order.Quantity <= 0 { return "采购数量必须大于 0", context, "", nil } if request.MaxPriceCent <= 0 { return "人民币价格上限必须大于 0", context, "", nil } active, err := repository.HasActivePurchaseTask(q, request.SybID) if err != nil { return "", nil, "", err } if active { return "已有未结束的采购任务", context, "", nil } return "", context, choice.OptionsJSON, nil } func newPurchaseTaskID() string { id := newID() if len(id) > 16 { id = id[:16] } return "PUR-" + id } // parsePriceYuanToCent 把网页输入的人民币元精确转成分,不使用浮点数。 func ParsePriceYuanToCent(raw string) (int64, error) { raw = strings.TrimSpace(raw) if raw == "" { return 0, fmt.Errorf("价格上限不能为空") } var whole, fraction string parts := strings.Split(raw, ".") if len(parts) > 2 { return 0, fmt.Errorf("价格格式不正确") } whole = parts[0] if len(parts) == 2 { fraction = parts[1] } if whole == "" || len(fraction) > 2 { return 0, fmt.Errorf("价格最多保留两位小数") } for _, value := range []string{whole, fraction} { for _, r := range value { if r < '0' || r > '9' { return 0, fmt.Errorf("价格只能填写数字") } } } if len(fraction) == 0 { fraction = "00" } else if len(fraction) == 1 { fraction += "0" } var yuan, cent int64 if _, err := fmt.Sscan(whole, &yuan); err != nil { return 0, fmt.Errorf("价格格式不正确") } if _, err := fmt.Sscan(fraction, ¢); err != nil { return 0, fmt.Errorf("价格格式不正确") } if yuan > (math.MaxInt64-cent)/100 { return 0, fmt.Errorf("价格上限过大") } total := yuan*100 + cent if total <= 0 { return 0, fmt.Errorf("价格上限必须大于 0") } return total, nil }