feat: 增加顺运宝批量 AI 规格匹配 (#202)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user