feat: 定义运行时规格解析契约与审计模型 (#254)
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 25
|
||||
const mysqlSchemaVersion = 26
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -724,10 +724,71 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 25, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v25 失败: %w", err)
|
||||
}
|
||||
current = 25
|
||||
}
|
||||
if current < 26 {
|
||||
if err := migrateMySQLV26(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v26 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV26Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v26 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 26, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v26 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV26 建立采购运行时规格解析审计。一个记录同时保存候选观察和最终决策,
|
||||
// 不覆盖 PDD 商品主数据,也不通过外键阻止任务的既有硬删除流程。
|
||||
func migrateMySQLV26(db *sql.DB) error {
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS purchase_spec_resolutions (
|
||||
resolution_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
task_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
attempt_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
client_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
task_version BIGINT NOT NULL,
|
||||
pdd_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
original_options_json JSON NOT NULL,
|
||||
selected_color VARCHAR(191) NOT NULL,
|
||||
target_size VARCHAR(191) NOT NULL,
|
||||
candidates_json JSON NOT NULL,
|
||||
candidate_snapshot_hash CHAR(64) COLLATE utf8mb4_bin NOT NULL,
|
||||
request_hash CHAR(64) COLLATE utf8mb4_bin NOT NULL,
|
||||
observed_at VARCHAR(35) NOT NULL,
|
||||
outcome VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
decision_source VARCHAR(16) NULL,
|
||||
chosen_candidate_id VARCHAR(16) COLLATE utf8mb4_bin NULL,
|
||||
resolved_options_json JSON NULL,
|
||||
confidence_bps INT NULL,
|
||||
reason VARCHAR(500) NULL,
|
||||
provider_id VARCHAR(191) COLLATE utf8mb4_bin NULL,
|
||||
source_model VARCHAR(191) NULL,
|
||||
config_fingerprint CHAR(64) COLLATE utf8mb4_bin NULL,
|
||||
rules_version VARCHAR(32) NULL,
|
||||
prompt_version VARCHAR(32) NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
decided_at VARCHAR(35) NULL,
|
||||
UNIQUE KEY uq_purchase_spec_resolution_identity (task_id,attempt_id,candidate_snapshot_hash),
|
||||
KEY idx_purchase_spec_resolution_task (task_id,created_at DESC,resolution_id),
|
||||
KEY idx_purchase_spec_resolution_outcome (outcome,created_at,resolution_id),
|
||||
CONSTRAINT chk_purchase_spec_resolution_task_version CHECK (task_version > 0),
|
||||
CONSTRAINT chk_purchase_spec_resolution_options CHECK (
|
||||
JSON_TYPE(original_options_json)='OBJECT' AND JSON_LENGTH(original_options_json) BETWEEN 1 AND 16 AND
|
||||
JSON_TYPE(candidates_json)='ARRAY' AND JSON_LENGTH(candidates_json) BETWEEN 1 AND 100 AND
|
||||
(resolved_options_json IS NULL OR JSON_TYPE(resolved_options_json)='OBJECT')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_outcome CHECK (outcome IN ('pending','matched','uncertain','rejected','failed')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_source CHECK (decision_source IS NULL OR decision_source IN ('rule','ai','reused')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_confidence CHECK (confidence_bps IS NULL OR confidence_bps BETWEEN 0 AND 10000),
|
||||
CONSTRAINT chk_purchase_spec_resolution_decision CHECK (
|
||||
(outcome='pending' AND decided_at IS NULL AND decision_source IS NULL AND chosen_candidate_id IS NULL AND resolved_options_json IS NULL) OR
|
||||
(outcome='matched' AND decided_at IS NOT NULL AND decision_source IS NOT NULL AND chosen_candidate_id IS NOT NULL AND resolved_options_json IS NOT NULL AND reason IS NOT NULL) OR
|
||||
(outcome IN ('uncertain','rejected','failed') AND decided_at IS NOT NULL AND chosen_candidate_id IS NULL AND resolved_options_json IS NULL AND reason IS NOT NULL))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`)
|
||||
return err
|
||||
}
|
||||
|
||||
// migrateMySQLV25 在现有档口入库码业务表上增加后台批次状态,不另建批次表。
|
||||
func migrateMySQLV25(db *sql.DB) error {
|
||||
for _, column := range []struct{ name, ddl string }{
|
||||
@@ -2185,6 +2246,7 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
"ai_provider_configs", "ai_provider_audits",
|
||||
"ai_spec_match_decisions",
|
||||
"ai_match_batches", "ai_match_batch_items",
|
||||
"purchase_spec_resolutions",
|
||||
}
|
||||
if err := checkMySQLSchema(db, mysqlRequiredTables); err != nil {
|
||||
return err
|
||||
@@ -2255,7 +2317,51 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV24Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV25Shape(db)
|
||||
if err := checkMySQLV25Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV26Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV26Shape(db *sql.DB) error {
|
||||
if err := checkMySQLSchema(db, []string{"purchase_spec_resolutions"}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range []string{
|
||||
"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",
|
||||
} {
|
||||
exists, err := mysqlColumnExists(db, "purchase_spec_resolutions", name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("采购运行时规格解析字段 %s 缺失: %v", name, err)
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ kind, name string }{
|
||||
{"index", "uq_purchase_spec_resolution_identity"},
|
||||
{"index", "idx_purchase_spec_resolution_task"},
|
||||
{"index", "idx_purchase_spec_resolution_outcome"},
|
||||
{"constraint", "chk_purchase_spec_resolution_task_version"},
|
||||
{"constraint", "chk_purchase_spec_resolution_options"},
|
||||
{"constraint", "chk_purchase_spec_resolution_outcome"},
|
||||
{"constraint", "chk_purchase_spec_resolution_source"},
|
||||
{"constraint", "chk_purchase_spec_resolution_confidence"},
|
||||
{"constraint", "chk_purchase_spec_resolution_decision"},
|
||||
} {
|
||||
var exists bool
|
||||
var err error
|
||||
if item.kind == "index" {
|
||||
exists, err = mysqlIndexExists(db, "purchase_spec_resolutions", item.name)
|
||||
} else {
|
||||
exists, err = mysqlConstraintExists(db, "purchase_spec_resolutions", item.name)
|
||||
}
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("采购运行时规格解析%s %s 缺失: %v", item.kind, item.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV25Shape(db *sql.DB) error {
|
||||
|
||||
@@ -1166,6 +1166,100 @@ func TestMySQLMigrate_V24升级V25且后台队列状态有效(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLMigrate_V25升级V26且规格解析审计幂等(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
cleanMySQLTestSchema(t, db)
|
||||
defer cleanMySQLTestSchema(t, db)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustExec(t, db, `DROP TABLE purchase_spec_resolutions`)
|
||||
mustExec(t, db, `DELETE FROM schema_migrations WHERE version>=26`)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v25 升级 v26 失败: %v", err)
|
||||
}
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v26 重复迁移失败: %v", err)
|
||||
}
|
||||
if err := checkMySQLV26Shape(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createdAt := "2026-08-17T08:00:00Z"
|
||||
resolution := model.PurchaseSpecResolution{
|
||||
ResolutionID: "PSR-1",
|
||||
TaskID: "cg254",
|
||||
AttemptID: "attempt-254",
|
||||
ClientID: "client-254",
|
||||
TaskVersion: 3,
|
||||
PddGoodsID: "937122477375",
|
||||
OriginalOptionsJSON: `{"color":"黑色","size":"60公斤"}`,
|
||||
SelectedColor: "黑色",
|
||||
TargetSize: "60公斤",
|
||||
CandidatesJSON: `[{"candidate_id":"c1","raw_text":"120斤","options":{"color":"黑色","size":"120斤"}}]`,
|
||||
CandidateSnapshotHash: strings.Repeat("a", 64),
|
||||
RequestHash: strings.Repeat("b", 64),
|
||||
ObservedAt: createdAt,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
if err := InsertPurchaseSpecResolution(db, resolution); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
replayed, err := GetPurchaseSpecResolutionForReplay(db, resolution.TaskID, resolution.AttemptID,
|
||||
resolution.CandidateSnapshotHash, resolution.RequestHash)
|
||||
if err != nil || replayed == nil || replayed.ResolutionID != resolution.ResolutionID {
|
||||
t.Fatalf("相同请求未命中解析记录: resolution=%+v err=%v", replayed, err)
|
||||
}
|
||||
if _, err := GetPurchaseSpecResolutionForReplay(db, resolution.TaskID, resolution.AttemptID,
|
||||
resolution.CandidateSnapshotHash, strings.Repeat("c", 64)); !errors.Is(err, ErrPurchaseSpecResolutionConflict) {
|
||||
t.Fatalf("相同业务身份的不同请求应冲突,实际 %v", err)
|
||||
}
|
||||
duplicate := resolution
|
||||
duplicate.ResolutionID = "PSR-2"
|
||||
if err := InsertPurchaseSpecResolution(db, duplicate); !errors.Is(err, ErrPurchaseSpecResolutionExists) {
|
||||
t.Fatalf("业务身份唯一键未生效,实际 %v", err)
|
||||
}
|
||||
|
||||
decision := model.PurchaseSpecResolutionDecision{
|
||||
ResolutionID: resolution.ResolutionID,
|
||||
Outcome: model.PurchaseSpecResolutionMatched,
|
||||
DecisionSource: model.PurchaseSpecResolutionAI,
|
||||
ChosenCandidateID: "c1",
|
||||
ResolvedOptionsJSON: `{"color":"黑色","size":"120斤"}`,
|
||||
ConfidenceBPS: 9300,
|
||||
ConfidenceSet: true,
|
||||
Reason: "候选唯一且通过门禁",
|
||||
ProviderID: "AI-1",
|
||||
SourceModel: "model-1",
|
||||
ConfigFingerprint: strings.Repeat("d", 64),
|
||||
RulesVersion: "runtime-v1",
|
||||
PromptVersion: "runtime-v1",
|
||||
DecidedAt: "2026-08-17T08:00:01Z",
|
||||
}
|
||||
if err := CompletePurchaseSpecResolution(db, decision); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
completed, err := GetPurchaseSpecResolutionByID(db, resolution.ResolutionID)
|
||||
if err != nil || completed == nil || completed.Outcome != model.PurchaseSpecResolutionMatched ||
|
||||
completed.DecisionSource != model.PurchaseSpecResolutionAI || !completed.ConfidenceSet || completed.ConfidenceBPS != 9300 {
|
||||
t.Fatalf("解析决策读取不完整: resolution=%+v err=%v", completed, err)
|
||||
}
|
||||
if err := CompletePurchaseSpecResolution(db, decision); !errors.Is(err, ErrPurchaseSpecResolutionCompleted) {
|
||||
t.Fatalf("已经完成的决策不应被覆盖,实际 %v", err)
|
||||
}
|
||||
|
||||
tooManyCandidates := resolution
|
||||
tooManyCandidates.ResolutionID = "PSR-TOO-MANY"
|
||||
tooManyCandidates.AttemptID = "attempt-too-many"
|
||||
tooManyCandidates.CandidateSnapshotHash = strings.Repeat("e", 64)
|
||||
tooManyCandidates.RequestHash = strings.Repeat("f", 64)
|
||||
tooManyCandidates.CandidatesJSON = `[` + strings.TrimSuffix(strings.Repeat(`{"candidate_id":"c1"},`, 101), ",") + `]`
|
||||
if err := InsertPurchaseSpecResolution(db, tooManyCandidates); err == nil {
|
||||
t.Fatal("数据库必须拒绝超过 100 个候选的解析观察")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user