feat: 档口入库码后台批量回写 (#246)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user