package repository import ( "database/sql" "errors" "fmt" "github.com/go-sql-driver/mysql" "cmautobuy/admin/model" ) var ( // ErrPurchaseSpecResolutionExists 表示解析编号或业务身份已经存在,调用方应读取旧记录复查。 ErrPurchaseSpecResolutionExists = errors.New("采购运行时规格解析记录已存在") // ErrPurchaseSpecResolutionConflict 表示相同业务身份对应了不同请求内容。 ErrPurchaseSpecResolutionConflict = errors.New("采购运行时规格解析请求冲突") // ErrPurchaseSpecResolutionNotFound 表示要完成的解析记录不存在。 ErrPurchaseSpecResolutionNotFound = errors.New("采购运行时规格解析记录不存在") // ErrPurchaseSpecResolutionCompleted 表示解析记录已经完成,不能覆盖第一次决策。 ErrPurchaseSpecResolutionCompleted = errors.New("采购运行时规格解析记录已经完成") ) // InsertPurchaseSpecResolution 保存一次候选观察。匹配逻辑不在 Repository 中执行。 func InsertPurchaseSpecResolution(q Execer, resolution model.PurchaseSpecResolution) error { if resolution.Outcome == "" { resolution.Outcome = model.PurchaseSpecResolutionPending } _, err := q.Exec(`INSERT INTO purchase_spec_resolutions (resolution_id,task_id,attempt_id,client_id,task_version,pdd_goods_id, original_options_json,selected_color,target_size,candidates_json,candidate_snapshot_hash, request_hash,observed_at,outcome,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, resolution.ResolutionID, resolution.TaskID, resolution.AttemptID, resolution.ClientID, resolution.TaskVersion, resolution.PddGoodsID, resolution.OriginalOptionsJSON, resolution.SelectedColor, resolution.TargetSize, resolution.CandidatesJSON, resolution.CandidateSnapshotHash, resolution.RequestHash, resolution.ObservedAt, resolution.Outcome, resolution.CreatedAt) if err == nil { return nil } var mysqlErr *mysql.MySQLError if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { return ErrPurchaseSpecResolutionExists } return fmt.Errorf("写入采购运行时规格解析观察失败: %w", err) } // GetPurchaseSpecResolutionByID 按公开解析编号读取候选观察和最终决策。 func GetPurchaseSpecResolutionByID(q Execer, resolutionID string) (*model.PurchaseSpecResolution, error) { return scanPurchaseSpecResolution(q.QueryRow(purchaseSpecResolutionSelect+` WHERE resolution_id=?`, resolutionID)) } // GetPurchaseSpecResolutionByIdentity 按任务、执行尝试和候选快照读取唯一解析记录。 func GetPurchaseSpecResolutionByIdentity(q Execer, taskID, attemptID, candidateSnapshotHash string) (*model.PurchaseSpecResolution, error) { return scanPurchaseSpecResolution(q.QueryRow(purchaseSpecResolutionSelect+ ` WHERE task_id=? AND attempt_id=? AND candidate_snapshot_hash=?`, taskID, attemptID, candidateSnapshotHash)) } // GetPurchaseSpecResolutionForReplay 复查幂等重放。相同业务身份但请求哈希不同必须冲突。 func GetPurchaseSpecResolutionForReplay(q Execer, taskID, attemptID, candidateSnapshotHash, requestHash string) (*model.PurchaseSpecResolution, error) { resolution, err := GetPurchaseSpecResolutionByIdentity(q, taskID, attemptID, candidateSnapshotHash) if err != nil || resolution == nil { return resolution, err } if resolution.RequestHash != requestHash { return nil, ErrPurchaseSpecResolutionConflict } return resolution, nil } // CompletePurchaseSpecResolution 只允许把 pending 记录完成一次,保留第一次决策审计。 func CompletePurchaseSpecResolution(q Execer, decision model.PurchaseSpecResolutionDecision) error { result, err := q.Exec(`UPDATE purchase_spec_resolutions SET outcome=?,decision_source=?,chosen_candidate_id=?,resolved_options_json=?,confidence_bps=?, reason=?,provider_id=?,source_model=?,config_fingerprint=?,rules_version=?,prompt_version=?,decided_at=? WHERE resolution_id=? AND outcome='pending'`, decision.Outcome, nullableText(string(decision.DecisionSource)), nullableText(decision.ChosenCandidateID), nullableText(decision.ResolvedOptionsJSON), nullableInt(decision.ConfidenceBPS, decision.ConfidenceSet), nullableText(decision.Reason), nullableText(decision.ProviderID), nullableText(decision.SourceModel), nullableText(decision.ConfigFingerprint), nullableText(decision.RulesVersion), nullableText(decision.PromptVersion), decision.DecidedAt, decision.ResolutionID) if err != nil { return fmt.Errorf("完成采购运行时规格解析失败: %w", err) } affected, err := result.RowsAffected() if err != nil { return fmt.Errorf("读取采购运行时规格解析更新结果失败: %w", err) } if affected == 1 { return nil } existing, err := GetPurchaseSpecResolutionByID(q, decision.ResolutionID) if err != nil { return err } if existing == nil { return ErrPurchaseSpecResolutionNotFound } return ErrPurchaseSpecResolutionCompleted } const purchaseSpecResolutionSelect = `SELECT resolution_id,task_id,attempt_id,client_id,task_version,pdd_goods_id, original_options_json,selected_color,target_size,candidates_json,candidate_snapshot_hash, request_hash,observed_at,outcome,decision_source,chosen_candidate_id,resolved_options_json, confidence_bps,reason,provider_id,source_model,config_fingerprint,rules_version,prompt_version, created_at,decided_at FROM purchase_spec_resolutions` func scanPurchaseSpecResolution(row *sql.Row) (*model.PurchaseSpecResolution, error) { var resolution model.PurchaseSpecResolution var source, candidateID, resolvedOptions, reason, providerID sql.NullString var sourceModel, configFingerprint, rulesVersion, promptVersion, decidedAt sql.NullString var confidence sql.NullInt64 err := row.Scan( &resolution.ResolutionID, &resolution.TaskID, &resolution.AttemptID, &resolution.ClientID, &resolution.TaskVersion, &resolution.PddGoodsID, &resolution.OriginalOptionsJSON, &resolution.SelectedColor, &resolution.TargetSize, &resolution.CandidatesJSON, &resolution.CandidateSnapshotHash, &resolution.RequestHash, &resolution.ObservedAt, &resolution.Outcome, &source, &candidateID, &resolvedOptions, &confidence, &reason, &providerID, &sourceModel, &configFingerprint, &rulesVersion, &promptVersion, &resolution.CreatedAt, &decidedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("读取采购运行时规格解析失败: %w", err) } resolution.DecisionSource = model.PurchaseSpecResolutionSource(source.String) resolution.ChosenCandidateID = candidateID.String resolution.ResolvedOptionsJSON = resolvedOptions.String resolution.ConfidenceBPS = int(confidence.Int64) resolution.ConfidenceSet = confidence.Valid resolution.Reason = reason.String resolution.ProviderID = providerID.String resolution.SourceModel = sourceModel.String resolution.ConfigFingerprint = configFingerprint.String resolution.RulesVersion = rulesVersion.String resolution.PromptVersion = promptVersion.String resolution.DecidedAt = decidedAt.String return &resolution, nil }