174 lines
8.5 KiB
Go
174 lines
8.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"cmautobuy/admin/model"
|
|
)
|
|
|
|
// GetSpecMapping 读取某个蝦皮商品的顺运宝规格在指定 PDD 商品下的映射。
|
|
func GetSpecMapping(q Execer, shopeeGoodsID, specKey, pddGoodsID string) (*model.SpecMapping, error) {
|
|
var m model.SpecMapping
|
|
var mappedBy, providerID, sourceModel, reason, sourceVersion, contextVersion sql.NullString
|
|
var confidence sql.NullInt64
|
|
query := `
|
|
SELECT shopee_goods_id, spec_key, pdd_goods_id, pdd_option_key,
|
|
pdd_options, spec_raw, mapped_at, mapped_by,source,source_provider_id,
|
|
source_model,confidence_bps,source_reason,source_version,context_version
|
|
FROM spec_mappings
|
|
WHERE shopee_goods_id = ? AND spec_key = ? AND pdd_goods_id = ?`
|
|
err := q.QueryRow(query, shopeeGoodsID, specKey, pddGoodsID).Scan(
|
|
&m.ShopeeGoodsID, &m.SpecKey, &m.PddGoodsID, &m.PddOptionKey,
|
|
&m.PddOptions, &m.SpecRaw, &m.MappedAt, &mappedBy, &m.Source, &providerID,
|
|
&sourceModel, &confidence, &reason, &sourceVersion, &contextVersion)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("查询规格映射失败: %w", err)
|
|
}
|
|
m.MappedBy = mappedBy.String
|
|
m.SourceProviderID = providerID.String
|
|
m.SourceModel = sourceModel.String
|
|
m.ConfidenceBPS = int(confidence.Int64)
|
|
m.ConfidenceSet = confidence.Valid
|
|
m.SourceReason = reason.String
|
|
m.SourceVersion = sourceVersion.String
|
|
m.ContextVersion = contextVersion.String
|
|
return &m, nil
|
|
}
|
|
|
|
// UpsertSpecMapping 保存可复用规格映射,唯一键包含当前 PDD 商品。
|
|
func UpsertSpecMapping(q Execer, m model.SpecMapping) error {
|
|
if m.Source == "" {
|
|
m.Source = "manual"
|
|
}
|
|
_, err := q.Exec(`
|
|
INSERT INTO spec_mappings
|
|
(shopee_goods_id, spec_key, pdd_goods_id, pdd_option_key,
|
|
pdd_options, spec_raw, mapped_at, mapped_by,source,source_provider_id,
|
|
source_model,confidence_bps,source_reason,source_version,context_version)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
pdd_option_key = VALUES(pdd_option_key),
|
|
pdd_options = VALUES(pdd_options),
|
|
spec_raw = VALUES(spec_raw),
|
|
mapped_at = VALUES(mapped_at),
|
|
mapped_by = VALUES(mapped_by),
|
|
source = VALUES(source),
|
|
source_provider_id = VALUES(source_provider_id),
|
|
source_model = VALUES(source_model),
|
|
confidence_bps = VALUES(confidence_bps),
|
|
source_reason = VALUES(source_reason),
|
|
source_version = VALUES(source_version),
|
|
context_version = VALUES(context_version)`,
|
|
m.ShopeeGoodsID, m.SpecKey, m.PddGoodsID, m.PddOptionKey,
|
|
m.PddOptions, m.SpecRaw, m.MappedAt, nullableText(m.MappedBy), m.Source,
|
|
nullableText(m.SourceProviderID), nullableText(m.SourceModel), nullableInt(m.ConfidenceBPS, m.ConfidenceSet),
|
|
nullableText(m.SourceReason), nullableText(m.SourceVersion), nullableText(m.ContextVersion))
|
|
if err != nil {
|
|
return fmt.Errorf("保存规格映射失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpsertAutomaticSpecMapping 只在没有人工映射且上下文版本变化时写入自动结果。
|
|
// 同一上下文的并发 AI 请求由先写入者获胜;人工保存无论先后都不会被自动结果覆盖。
|
|
func UpsertAutomaticSpecMapping(q Execer, m model.SpecMapping) error {
|
|
_, err := UpsertAutomaticSpecMappingChanged(q, m)
|
|
return err
|
|
}
|
|
|
|
// UpsertAutomaticSpecMappingChanged 与 UpsertAutomaticSpecMapping 使用同一条并发安全 SQL,
|
|
// 并返回本次是否真正插入或更新。调用方据此避免为幂等重放重复追加决策审计。
|
|
func UpsertAutomaticSpecMappingChanged(q Execer, m model.SpecMapping) (bool, error) {
|
|
result, err := q.Exec(`
|
|
INSERT INTO spec_mappings
|
|
(shopee_goods_id,spec_key,pdd_goods_id,pdd_option_key,pdd_options,spec_raw,
|
|
mapped_at,mapped_by,source,source_provider_id,source_model,confidence_bps,
|
|
source_reason,source_version,context_version)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
ON DUPLICATE KEY UPDATE
|
|
pdd_option_key=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(pdd_option_key),pdd_option_key),
|
|
pdd_options=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(pdd_options),pdd_options),
|
|
spec_raw=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(spec_raw),spec_raw),
|
|
mapped_at=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(mapped_at),mapped_at),
|
|
mapped_by=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(mapped_by),mapped_by),
|
|
source_provider_id=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(source_provider_id),source_provider_id),
|
|
source_model=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(source_model),source_model),
|
|
confidence_bps=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(confidence_bps),confidence_bps),
|
|
source_reason=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(source_reason),source_reason),
|
|
source_version=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(source_version),source_version),
|
|
source=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(source),source),
|
|
context_version=IF(source<>'manual' AND COALESCE(context_version,'')<>VALUES(context_version),VALUES(context_version),context_version)`,
|
|
m.ShopeeGoodsID, m.SpecKey, m.PddGoodsID, m.PddOptionKey, m.PddOptions, m.SpecRaw,
|
|
m.MappedAt, nullableText(m.MappedBy), m.Source, nullableText(m.SourceProviderID),
|
|
nullableText(m.SourceModel), nullableInt(m.ConfidenceBPS, m.ConfidenceSet), nullableText(m.SourceReason),
|
|
nullableText(m.SourceVersion), nullableText(m.ContextVersion))
|
|
if err != nil {
|
|
return false, fmt.Errorf("保存自动规格映射失败: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, fmt.Errorf("读取自动规格映射保存结果失败: %w", err)
|
|
}
|
|
return rows > 0, nil
|
|
}
|
|
|
|
// DeleteAutomaticSpecMappingByVersion 只删除指定规则自己生成的映射。
|
|
// 人工和 AI 结果以及其他规则版本永远不受影响。
|
|
func DeleteAutomaticSpecMappingByVersion(q Execer, shopeeGoodsID, specKey, pddGoodsID, sourceVersion string) (bool, error) {
|
|
result, err := q.Exec(`DELETE FROM spec_mappings
|
|
WHERE shopee_goods_id=? AND spec_key=? AND pdd_goods_id=?
|
|
AND source='rule' AND source_version=?`,
|
|
shopeeGoodsID, specKey, pddGoodsID, sourceVersion)
|
|
if err != nil {
|
|
return false, fmt.Errorf("清除失效的颜色辅助规格映射失败: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, fmt.Errorf("读取颜色辅助规格映射清除结果失败: %w", err)
|
|
}
|
|
return rows > 0, nil
|
|
}
|
|
|
|
func InsertAISpecMatchDecision(q Execer, d model.AISpecMatchDecision) error {
|
|
_, err := q.Exec(`INSERT INTO ai_spec_match_decisions
|
|
(shopee_goods_id,spec_key,pdd_goods_id,context_version,rules_version,prompt_version,
|
|
provider_id,provider_name,model,config_fingerprint,candidates_json,chosen_candidate_id,
|
|
chosen_option_key,confidence_bps,outcome,reason,conflict_dimensions_json,
|
|
missing_dimensions_json,decided_by,decided_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, d.ShopeeGoodsID, d.SpecKey,
|
|
d.PddGoodsID, d.ContextVersion, d.RulesVersion, d.PromptVersion,
|
|
nullableText(d.ProviderID), nullableText(d.ProviderName), nullableText(d.Model),
|
|
nullableText(d.ConfigFingerprint), d.CandidatesJSON, nullableText(d.ChosenCandidateID),
|
|
nullableText(d.ChosenOptionKey), nullableInt(d.ConfidenceBPS, d.ConfidenceSet), d.Outcome,
|
|
nullableText(d.Reason), d.ConflictDimensionsJSON, d.MissingDimensionsJSON,
|
|
nullableText(d.DecidedBy), d.DecidedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("写入 AI 规格匹配审计失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nullableInt(value int, valid bool) any {
|
|
if !valid {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func InsertSpecMappingDecision(q Execer, d model.SpecMappingDecision) error {
|
|
_, err := q.Exec(`INSERT INTO spec_mapping_decisions
|
|
(shopee_goods_id,spec_key,pdd_goods_id,rules_version,suggested_option_key,
|
|
chosen_option_key,accepted,decided_by,decided_at) VALUES (?,?,?,?,?,?,?,?,?)`,
|
|
d.ShopeeGoodsID, d.SpecKey, d.PddGoodsID, d.RulesVersion, nullableText(d.SuggestedOptionKey),
|
|
d.ChosenOptionKey, d.Accepted, nullableText(d.DecidedBy), d.DecidedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("写入规格匹配决策审计失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|