feat: 增加管理员 AI 模型配置 (#200)
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 19
|
||||
const mysqlSchemaVersion = 20
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -652,10 +652,75 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 19, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v19 失败: %w", err)
|
||||
}
|
||||
current = 19
|
||||
}
|
||||
if current < 20 {
|
||||
if err := migrateMySQLV20(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v20 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV20Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v20 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 20, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v20 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV20 保存 AI 服务商的非敏感配置和配置审计。
|
||||
// API Key 只存在独立密钥文件中,不在这两张表里留列。
|
||||
func migrateMySQLV20(db *sql.DB) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS ai_provider_configs (
|
||||
provider_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
name VARCHAR(191) NOT NULL,
|
||||
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,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
active_slot TINYINT GENERATED ALWAYS AS (IF(enabled=1,1,NULL)) STORED,
|
||||
last_test_status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
last_test_message VARCHAR(500),
|
||||
last_tested_at VARCHAR(35),
|
||||
last_test_fingerprint CHAR(64) COLLATE utf8mb4_bin,
|
||||
created_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
updated_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
UNIQUE KEY uq_ai_provider_name (name),
|
||||
UNIQUE KEY uq_ai_provider_active (active_slot),
|
||||
KEY idx_ai_provider_updated (updated_at DESC,provider_id),
|
||||
CONSTRAINT fk_ai_provider_created_by FOREIGN KEY (created_by_user_id) REFERENCES users(user_id),
|
||||
CONSTRAINT fk_ai_provider_updated_by FOREIGN KEY (updated_by_user_id) REFERENCES users(user_id),
|
||||
CONSTRAINT chk_ai_provider_enabled CHECK (enabled IN (0,1)),
|
||||
CONSTRAINT chk_ai_provider_timeout CHECK (timeout_seconds BETWEEN 1 AND 120),
|
||||
CONSTRAINT chk_ai_provider_concurrency CHECK (max_concurrency BETWEEN 1 AND 16),
|
||||
CONSTRAINT chk_ai_provider_confidence CHECK (confidence_threshold_bps BETWEEN 0 AND 10000),
|
||||
CONSTRAINT chk_ai_provider_test_status CHECK (last_test_status IN ('pending','succeeded','failed'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS ai_provider_audits (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
provider_id VARCHAR(191) COLLATE utf8mb4_bin,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
details_json LONGTEXT NOT NULL,
|
||||
actor_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
KEY idx_ai_provider_audit (provider_id,created_at DESC,id DESC),
|
||||
CONSTRAINT fk_ai_audit_provider FOREIGN KEY (provider_id) REFERENCES ai_provider_configs(provider_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_ai_audit_actor FOREIGN KEY (actor_user_id) REFERENCES users(user_id)
|
||||
) 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
|
||||
}
|
||||
|
||||
// migrateMySQLV15 以低优先级补全历史 SYB 商品的店铺和图片。
|
||||
func migrateMySQLV15(db *sql.DB) error {
|
||||
return backfillSybProductMetadata(db)
|
||||
@@ -1798,6 +1863,7 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
"task_syb_sources",
|
||||
"task_sequences",
|
||||
"syb_allowed_shops",
|
||||
"ai_provider_configs", "ai_provider_audits",
|
||||
}
|
||||
if err := checkMySQLSchema(db, mysqlRequiredTables); err != nil {
|
||||
return err
|
||||
@@ -1850,7 +1916,42 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV18Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV19Shape(db)
|
||||
if err := checkMySQLV19Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV20Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV20Shape(db *sql.DB) error {
|
||||
if err := checkMySQLSchema(db, []string{"ai_provider_configs", "ai_provider_audits"}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range []string{"provider_id", "base_url", "model", "timeout_seconds", "max_concurrency",
|
||||
"confidence_threshold_bps", "enabled", "active_slot", "last_test_status", "last_test_fingerprint"} {
|
||||
exists, err := mysqlColumnExists(db, "ai_provider_configs", name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("AI 服务商配置字段 %s 缺失: %v", name, err)
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ kind, table, name string }{
|
||||
{"index", "ai_provider_configs", "uq_ai_provider_name"},
|
||||
{"index", "ai_provider_configs", "uq_ai_provider_active"},
|
||||
{"constraint", "ai_provider_configs", "chk_ai_provider_test_status"},
|
||||
{"constraint", "ai_provider_configs", "fk_ai_provider_created_by"},
|
||||
{"constraint", "ai_provider_audits", "fk_ai_audit_actor"},
|
||||
} {
|
||||
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 checkMySQLV15Shape(db *sql.DB) error {
|
||||
|
||||
Reference in New Issue
Block a user