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 := 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 fmt.Errorf("保存自动规格映射失败: %w", err) } return 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 }