feat: 档口入库码后台批量回写 (#246)

This commit is contained in:
chengma
2026-08-15 17:08:17 +08:00
parent 6b30594f4c
commit f8f22a15c7
21 changed files with 708 additions and 134 deletions
+93 -2
View File
@@ -20,7 +20,7 @@ import (
"cmautobuy/admin/spec"
)
const mysqlSchemaVersion = 24
const mysqlSchemaVersion = 25
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
@@ -712,10 +712,64 @@ func MigrateMySQL(db *sql.DB) error {
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 24, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v24 失败: %w", err)
}
current = 24
}
if current < 25 {
if err := migrateMySQLV25(db); err != nil {
return fmt.Errorf("执行 MySQL schema v25 失败: %w", err)
}
if err := checkMySQLV25Shape(db); err != nil {
return fmt.Errorf("MySQL schema v25 自检失败,未记录版本: %w", err)
}
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)
}
}
return CheckMySQLSchema(db)
}
// migrateMySQLV25 在现有档口入库码业务表上增加后台批次状态,不另建批次表。
func migrateMySQLV25(db *sql.DB) error {
for _, column := range []struct{ name, ddl string }{
{"apply_batch_id", `ALTER TABLE syb_inner_code_records ADD COLUMN apply_batch_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER source_duplicate_count`},
{"apply_queued_at", `ALTER TABLE syb_inner_code_records ADD COLUMN apply_queued_at VARCHAR(35) NULL AFTER apply_batch_id`},
} {
exists, err := mysqlColumnExists(db, "syb_inner_code_records", column.name)
if err != nil {
return err
}
if !exists {
if _, err := db.Exec(column.ddl); err != nil {
return fmt.Errorf("增加 syb_inner_code_records.%s 失败: %w", column.name, err)
}
}
}
clause, exists, err := mysqlCheckConstraintClause(db, "syb_inner_code_records", "chk_inner_code_status")
if err != nil {
return err
}
if exists && !strings.Contains(strings.ToLower(clause), "queued") {
if _, err := db.Exec(`ALTER TABLE syb_inner_code_records DROP CHECK chk_inner_code_status`); err != nil {
return fmt.Errorf("更新档口入库码状态约束前删除旧约束失败: %w", err)
}
exists = false
}
if !exists {
if _, err := db.Exec(`ALTER TABLE syb_inner_code_records ADD CONSTRAINT chk_inner_code_status
CHECK (status IN ('pending','ready','queued','applying','updated','already_filled','skipped','failed','needs_check'))`); err != nil {
return fmt.Errorf("增加档口入库码后台状态约束失败: %w", err)
}
}
if exists, err := mysqlIndexExists(db, "syb_inner_code_records", "idx_inner_code_apply_batch"); err != nil {
return err
} else if !exists {
if _, err := db.Exec(`ALTER TABLE syb_inner_code_records ADD INDEX idx_inner_code_apply_batch (apply_batch_id,status,id)`); err != nil {
return fmt.Errorf("增加档口入库码后台批次索引失败: %w", err)
}
}
return nil
}
// migrateMySQLV24 为档口入库码增加可恢复软删除审计。
func migrateMySQLV24(db *sql.DB) error {
for _, column := range []struct{ name, ddl string }{
@@ -1939,6 +1993,23 @@ func mysqlConstraintExists(db *sql.DB, table, constraint string) (bool, error) {
return count == 1, nil
}
func mysqlCheckConstraintClause(db *sql.DB, table, constraint string) (string, bool, error) {
var clause string
err := db.QueryRow(`SELECT cc.check_clause
FROM information_schema.table_constraints tc
JOIN information_schema.check_constraints cc
ON cc.constraint_schema=tc.constraint_schema AND cc.constraint_name=tc.constraint_name
WHERE tc.constraint_schema=DATABASE() AND tc.table_name=? AND tc.constraint_name=?
AND tc.constraint_type='CHECK'`, table, constraint).Scan(&clause)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
if err != nil {
return "", false, fmt.Errorf("读取 MySQL 约束 %s.%s 表达式失败: %w", table, constraint, err)
}
return clause, true, nil
}
func mysqlIndexExists(db *sql.DB, table, index string) (bool, error) {
var count int
if err := db.QueryRow(`SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics
@@ -2181,7 +2252,27 @@ func CheckMySQLSchema(db *sql.DB) error {
if err := checkMySQLV23Shape(db); err != nil {
return err
}
return checkMySQLV24Shape(db)
if err := checkMySQLV24Shape(db); err != nil {
return err
}
return checkMySQLV25Shape(db)
}
func checkMySQLV25Shape(db *sql.DB) error {
for _, name := range []string{"apply_batch_id", "apply_queued_at"} {
exists, err := mysqlColumnExists(db, "syb_inner_code_records", name)
if err != nil || !exists {
return fmt.Errorf("档口入库码后台回写字段 %s 缺失: %v", name, err)
}
}
if exists, err := mysqlIndexExists(db, "syb_inner_code_records", "idx_inner_code_apply_batch"); err != nil || !exists {
return fmt.Errorf("档口入库码后台回写索引 idx_inner_code_apply_batch 缺失: %v", err)
}
clause, exists, err := mysqlCheckConstraintClause(db, "syb_inner_code_records", "chk_inner_code_status")
if err != nil || !exists || !strings.Contains(strings.ToLower(clause), "queued") {
return fmt.Errorf("档口入库码状态约束未包含 queued: %v", err)
}
return nil
}
func checkMySQLV24Shape(db *sql.DB) error {