Files
cmautobuy/admin/service/purchase_workflow.go
T

294 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 &copy, 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, &cent); 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
}