Files
cmautobuy/admin/repository/purchase_spec_resolution.go
T

144 lines
6.9 KiB
Go

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
}