feat: 增加规格匹配建议与决策审计 (#90)

This commit is contained in:
chengma
2026-08-10 12:41:17 +08:00
parent 5abe81e128
commit 5a77bde534
15 changed files with 680 additions and 46 deletions
+42 -14
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"math"
"strings"
"unicode/utf8"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
@@ -13,19 +14,23 @@ import (
// PddOptionChoice 是采集结果中的一个可购买规格组合。
type PddOptionChoice struct {
Key string
OptionsJSON string
Label string
PriceText string
PriceCent int64
HasPrice bool
Selected bool
Key string
OptionsJSON string
Label string
PriceText string
PriceCent int64
HasPrice bool
Selected bool
Options map[string]string
Recommended bool
RecommendationReason string
MatchLevel string
}
func pddOptionChoices(raw string) ([]PddOptionChoice, []string, error) {
func pddOptionChoices(raw string) ([]PddOptionChoice, []string, []string, error) {
collected, err := parseCollected(raw)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
keys, names := dimensionOrder(collected)
choices := make([]PddOptionChoice, 0, len(collected.SKUs))
@@ -36,7 +41,7 @@ func pddOptionChoices(raw string) ([]PddOptionChoice, []string, error) {
}
key, err := OptionKey(sku.Options)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if seen[key] {
continue
@@ -52,18 +57,18 @@ func pddOptionChoices(raw string) ([]PddOptionChoice, []string, error) {
}
choice := PddOptionChoice{
Key: key, OptionsJSON: key, Label: strings.Join(parts, " / "),
PriceText: formatPriceCent(sku.PriceCent),
PriceText: formatPriceCent(sku.PriceCent), Options: sku.Options,
}
if sku.PriceCent != nil && *sku.PriceCent > 0 {
choice.PriceCent, choice.HasPrice = *sku.PriceCent, true
}
choices = append(choices, choice)
}
return choices, names, nil
return choices, keys, names, nil
}
func findPddChoice(raw, key string) (*PddOptionChoice, error) {
choices, _, err := pddOptionChoices(raw)
choices, _, _, err := pddOptionChoices(raw)
if err != nil {
return nil, err
}
@@ -77,7 +82,7 @@ func findPddChoice(raw, key string) (*PddOptionChoice, error) {
}
// SaveSybMapping 保存顺运宝商品规格到当前 PDD 商品的可复用映射。
func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string) error {
func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string, expectedVersions ...string) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开始保存规格映射事务失败: %w", err)
@@ -90,6 +95,13 @@ func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string) error {
if context == nil {
return fmt.Errorf("顺运宝明细不存在")
}
expectedContextVersion := mappingContextVersion(*context)
if len(expectedVersions) > 0 {
expectedContextVersion = expectedVersions[0]
}
if expectedContextVersion == "" || mappingContextVersion(*context) != expectedContextVersion {
return fmt.Errorf("数据或匹配规则已变化,请刷新后重新核对")
}
key, err := spec.SpecKey(context.Order.ProductSpec)
if err != nil || context.Order.SpecKey == "" {
return fmt.Errorf("顺运宝未提供有效规格,不能保存映射")
@@ -108,6 +120,14 @@ func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string) error {
if choice == nil {
return fmt.Errorf("所选 PDD 规格已不存在或不可购买,请刷新后重试")
}
choices, dimensionKeys, dimensionNames, err := pddOptionChoices(context.PddSkusJSON)
if err != nil {
return fmt.Errorf("读取 PDD 规格失败: %w", err)
}
match := rankSpecChoices(context.Order.ProductSpec, choices, dimensionKeys, dimensionNames)
if utf8.RuneCountInString(choice.Key) > 191 || utf8.RuneCountInString(match.SuggestedOptionKey) > 191 || utf8.RuneCountInString(SpecMatchRulesVersion) > 32 {
return fmt.Errorf("规格选项键或规则版本超过数据库列宽,未保存")
}
if err := repository.UpsertSpecMapping(tx, model.SpecMapping{
ShopeeGoodsID: context.Order.ShopeeGoodsID, SpecKey: key,
SpecRaw: context.Order.ProductSpec, PddGoodsID: context.PddGoodsID,
@@ -116,6 +136,14 @@ func SaveSybMapping(db *sql.DB, sybID, optionKey, operator string) error {
}); err != nil {
return err
}
if err := repository.InsertSpecMappingDecision(tx, model.SpecMappingDecision{
ShopeeGoodsID: context.Order.ShopeeGoodsID, SpecKey: key, PddGoodsID: context.PddGoodsID,
RulesVersion: SpecMatchRulesVersion, SuggestedOptionKey: match.SuggestedOptionKey,
ChosenOptionKey: choice.Key, Accepted: match.SuggestedOptionKey != "" && match.SuggestedOptionKey == choice.Key,
DecidedBy: strings.TrimSpace(operator), DecidedAt: model.NowISO(),
}); err != nil {
return err
}
return tx.Commit()
}