feat: 增加顺运宝批量 AI 规格匹配 (#202)

This commit is contained in:
chengma
2026-08-14 10:30:08 +08:00
parent 0201dab98c
commit d64579e9b3
26 changed files with 1289 additions and 26 deletions
+98 -2
View File
@@ -20,7 +20,7 @@ import (
"cmautobuy/admin/spec"
)
const mysqlSchemaVersion = 21
const mysqlSchemaVersion = 22
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
@@ -676,10 +676,75 @@ func MigrateMySQL(db *sql.DB) error {
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 21, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v21 失败: %w", err)
}
current = 21
}
if current < 22 {
if err := migrateMySQLV22(db); err != nil {
return fmt.Errorf("执行 MySQL schema v22 失败: %w", err)
}
if err := checkMySQLV22Shape(db); err != nil {
return fmt.Errorf("MySQL schema v22 自检失败,未记录版本: %w", err)
}
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 22, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v22 失败: %w", err)
}
}
return CheckMySQLSchema(db)
}
func migrateMySQLV22(db *sql.DB) error {
statements := []string{
`CREATE TABLE IF NOT EXISTS ai_match_batches (
batch_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
status VARCHAR(16) NOT NULL,
created_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
total_count INT NOT NULL,processed_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,reused_count INT NOT NULL DEFAULT 0,
manual_count INT NOT NULL DEFAULT 0,failed_count INT NOT NULL DEFAULT 0,
provider_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
provider_name VARCHAR(191) NOT NULL,provider_base_url VARCHAR(2048) NOT NULL,
model VARCHAR(191) NOT NULL,timeout_seconds INT NOT NULL,max_concurrency INT NOT NULL,
confidence_threshold_bps INT NOT NULL,config_fingerprint CHAR(64) COLLATE utf8mb4_bin NOT NULL,
rules_version VARCHAR(32) NOT NULL,prompt_version VARCHAR(32) NOT NULL,
error_message VARCHAR(500),created_at VARCHAR(35) NOT NULL,
started_at VARCHAR(35),finished_at VARCHAR(35),
KEY idx_ai_match_batch_user (created_by_user_id,created_at DESC,batch_id),
KEY idx_ai_match_batch_status (status,created_at,batch_id),
CONSTRAINT fk_ai_match_batch_user FOREIGN KEY (created_by_user_id) REFERENCES users(user_id),
CONSTRAINT chk_ai_match_batch_status CHECK (status IN ('queued','running','partial','succeeded','failed','interrupted')),
CONSTRAINT chk_ai_match_batch_total CHECK (total_count BETWEEN 1 AND 100),
CONSTRAINT chk_ai_match_batch_counts CHECK (processed_count BETWEEN 0 AND total_count AND success_count>=0 AND reused_count>=0 AND manual_count>=0 AND failed_count>=0),
CONSTRAINT chk_ai_match_batch_runtime CHECK (timeout_seconds BETWEEN 1 AND 120 AND max_concurrency BETWEEN 1 AND 16 AND confidence_threshold_bps BETWEEN 0 AND 10000)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`CREATE TABLE IF NOT EXISTS ai_match_batch_items (
item_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
batch_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
syb_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
identity_hash CHAR(64) COLLATE utf8mb4_bin NOT NULL,
leader_syb_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
context_version CHAR(64) COLLATE utf8mb4_bin NOT NULL,
position INT NOT NULL,status VARCHAR(16) NOT NULL,outcome VARCHAR(32),
message VARCHAR(500),mapping_source VARCHAR(16),option_key VARCHAR(191) COLLATE utf8mb4_bin,
confidence_bps INT,model_called TINYINT NOT NULL DEFAULT 0,
started_at VARCHAR(35),finished_at VARCHAR(35),
UNIQUE KEY uq_ai_match_batch_syb (batch_id,syb_id),
KEY idx_ai_match_batch_work (batch_id,status,leader_syb_id,position),
KEY idx_ai_match_batch_identity (batch_id,identity_hash,position),
CONSTRAINT fk_ai_match_item_batch FOREIGN KEY (batch_id) REFERENCES ai_match_batches(batch_id) ON DELETE CASCADE,
CONSTRAINT chk_ai_match_item_status CHECK (status IN ('queued','running','succeeded','reused','manual','failed','stale','interrupted')),
CONSTRAINT chk_ai_match_item_source CHECK (mapping_source IS NULL OR mapping_source IN ('manual','rule','ai')),
CONSTRAINT chk_ai_match_item_confidence CHECK (confidence_bps IS NULL OR confidence_bps BETWEEN 0 AND 10000),
CONSTRAINT chk_ai_match_item_called CHECK (model_called IN (0,1))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
}
for _, statement := range statements {
if _, err := db.Exec(statement); err != nil {
return err
}
}
return nil
}
// migrateMySQLV21 给当前有效规格映射增加来源,并建立只追加的 AI 决策审计。
func migrateMySQLV21(db *sql.DB) error {
columns := []struct{ name, ddl string }{
@@ -1944,6 +2009,7 @@ func CheckMySQLSchema(db *sql.DB) error {
"syb_allowed_shops",
"ai_provider_configs", "ai_provider_audits",
"ai_spec_match_decisions",
"ai_match_batches", "ai_match_batch_items",
}
if err := checkMySQLSchema(db, mysqlRequiredTables); err != nil {
return err
@@ -2002,7 +2068,37 @@ func CheckMySQLSchema(db *sql.DB) error {
if err := checkMySQLV20Shape(db); err != nil {
return err
}
return checkMySQLV21Shape(db)
if err := checkMySQLV21Shape(db); err != nil {
return err
}
return checkMySQLV22Shape(db)
}
func checkMySQLV22Shape(db *sql.DB) error {
if err := checkMySQLSchema(db, []string{"ai_match_batches", "ai_match_batch_items"}); err != nil {
return err
}
for _, item := range []struct{ kind, table, name string }{
{"constraint", "ai_match_batches", "chk_ai_match_batch_status"},
{"constraint", "ai_match_batches", "chk_ai_match_batch_counts"},
{"constraint", "ai_match_batches", "fk_ai_match_batch_user"},
{"constraint", "ai_match_batch_items", "chk_ai_match_item_status"},
{"constraint", "ai_match_batch_items", "fk_ai_match_item_batch"},
{"index", "ai_match_batch_items", "uq_ai_match_batch_syb"},
{"index", "ai_match_batch_items", "idx_ai_match_batch_identity"},
} {
var exists bool
var err error
if item.kind == "index" {
exists, err = mysqlIndexExists(db, item.table, item.name)
} else {
exists, err = mysqlConstraintExists(db, item.table, item.name)
}
if err != nil || !exists {
return fmt.Errorf("AI 匹配批次%s %s.%s 缺失: %v", item.kind, item.table, item.name, err)
}
}
return nil
}
func checkMySQLV21Shape(db *sql.DB) error {