// Command ac15fixture 是 AC-15 阶段 2 前置件的 CLI 入口。 // // 两批能力: // // 第一批(资金域 4 表:t_user_expense / _school / _log + t_user / t_legacy_mapping) // ddl 用 core-model-ddl.sql 补齐目标库结构(只 CREATE,不 DROP) // map --user 53691 抽样搬运 // verify --user 53691 逐列核对(退出码 0=PASS,1=FAIL) // run --user 53691 map + verify // clean --user 53691 删除 fixture 数据并复核回 0 行 // count 打印相关表当前行数 // // 第二批(t_user_divide_into / _log / t_user_balance_request / t_expenditure_log // + 字典 t_expenditure,biz_no 口径首次落地) // map2 抽样搬运 4 表 // verify2 逐列核对 + biz_no 四项 + D1 id 一致性 // idem2 幂等实测(同一批跑两次,比对行数与 id) // run2 map2 + verify2 + idem2 // clean2 清理并复核 7 张表回 0 行 // count2 打印第二批涉及表当前行数 // // 环境变量(口令不落盘、不打印): // // AC15_DB_HOST / AC15_DB_PORT / AC15_DB_USER / AC15_DB_PASS / AC15_TARGET_DB // AC15_LEGACY_USER / AC15_LEGACY_PASS / AC15_LEGACY_DB // AC15_CORE_DDL core DDL 路径(默认取仓库外部的 02-data-model/core-model-ddl.sql) // // 退出码:0 = 成功/PASS;1 = 核对 FAIL;2 = 参数错误;3 = 环境或守卫失败。 package main import ( "context" "flag" "fmt" "os" "yuansiwei/unified-backend/internal/academic/dualrun/fixture" ) func main() { if len(os.Args) < 2 { usage() os.Exit(2) } cmd := os.Args[1] args := os.Args[2:] switch cmd { case "ddl": os.Exit(runDDL(args)) case "map": os.Exit(runMap(args)) case "verify": os.Exit(runVerify(args)) case "run": os.Exit(runAll(args)) case "clean": os.Exit(runClean(args)) case "count": os.Exit(runCount(args)) case "map2": os.Exit(runMap2(args)) case "verify2": os.Exit(runVerify2(args)) case "idem2": os.Exit(runIdem2(args)) case "run2": os.Exit(runRun2(args)) case "clean2": os.Exit(runClean2(args)) case "count2": os.Exit(runCount2(args)) case "-h", "--help", "help": usage() os.Exit(0) default: fmt.Fprintf(os.Stderr, "未知子命令:%s\n\n", cmd) usage() os.Exit(2) } } func usage() { fmt.Fprint(os.Stderr, `ac15fixture —— AC-15 阶段 2 前置件(资金域映射 + 逐列核对) 第一批(D1 口径为主): ddl 补齐 core-model-ddl.sql 的 13 张表(只 CREATE TABLE IF NOT EXISTS,禁止 DROP) map 抽样搬运一个 legacy 用户(账户 + 校区账户 + 流水) verify 逐列核对 4 张表 + AUTO_INCREMENT + 空值语义 + float 陷阱 run map + verify clean 删除 fixture 数据并复核 4 张表回 0 行 count 打印相关表当前行数 第二批(biz_no 口径首次落地): map2 搬运 t_user_divide_into / _log / t_user_balance_request / t_expenditure_log(+字典) verify2 逐列核对 + biz_no 四项 + D1 id 一致性 idem2 幂等实测(同一批跑两次,比行数与 id) run2 map2 + verify2 + idem2 clean2 清理并复核 7 张表回 0 行 count2 打印第二批涉及表当前行数 第一批参数:--user (默认 53691), --ddl 第二批参数:--root <分账树根 userid,默认 395>, --buser <充值工单用户,默认 31348>, --euser <收支记录创建人,默认 395>, --dict <是否迁移字典,默认 true> `) } // bootstrap 解析第一批公共参数、加载配置并跑守卫。 func bootstrap(args []string, withUser bool) (fixture.Config, int64, string, int) { fs := flag.NewFlagSet("ac15fixture", flag.ContinueOnError) fs.SetOutput(os.Stderr) user := fs.Int64("user", fixture.DefaultLegacyUserID, "legacy userid") ddl := fs.String("ddl", "", "core DDL 路径(默认 AC15_CORE_DDL 或仓库外部默认路径)") if err := fs.Parse(args); err != nil { return fixture.Config{}, 0, "", 2 } if !withUser { *user = fixture.DefaultLegacyUserID } cfg, code := loadConfig() if code != 0 { return fixture.Config{}, 0, "", code } return cfg, *user, *ddl, 0 } // loadConfig 取工作目录 → 读配置 → 守卫 → 打印脱敏后的连接串。 func loadConfig() (fixture.Config, int) { repoRoot, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "FATAL: 取工作目录失败:%v\n", err) return fixture.Config{}, 3 } cfg, err := fixture.LoadConfig(repoRoot) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return fixture.Config{}, 3 } if err := cfg.Guard(); err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return fixture.Config{}, 3 } fmt.Printf("target=%s legacy=%s\n\n", cfg.TargetMasked(), cfg.LegacyMasked()) return cfg, 0 } // bootstrap2 解析第二批参数。 func bootstrap2(args []string) (fixture.Config, fixture.Batch2Options, int) { def := fixture.DefaultBatch2Options() fs := flag.NewFlagSet("ac15fixture-b2", flag.ContinueOnError) fs.SetOutput(os.Stderr) root := fs.Int64("root", def.DivideRootUserID, "分账树根 userid(取整棵子树)") buser := fs.Int64("buser", def.BalanceRequestUserID, "充值工单所属 userid") euser := fs.Int64("euser", def.ExpenditureLogUserID, "收支记录创建人 userid") dict := fs.Bool("dict", def.WithExpenditureDict, "是否同时 D1 迁移 t_expenditure 字典") if err := fs.Parse(args); err != nil { return fixture.Config{}, fixture.Batch2Options{}, 2 } cfg, code := loadConfig() if code != 0 { return fixture.Config{}, fixture.Batch2Options{}, code } return cfg, fixture.Batch2Options{ DivideRootUserID: *root, BalanceRequestUserID: *buser, ExpenditureLogUserID: *euser, WithExpenditureDict: *dict, }, 0 } func runDDL(args []string) int { cfg, _, ddlPath, code := bootstrap(args, false) if code != 0 { return code } rep, err := fixture.ApplyCoreDDL(context.Background(), cfg, ddlPath) if rep != nil { fixture.PrintDDLReport(os.Stdout, rep) } if err != nil { fmt.Fprintf(os.Stderr, "\nFATAL: %v\n", err) return 3 } return 0 } func runMap(args []string) int { cfg, user, _, code := bootstrap(args, true) if code != 0 { return code } rep, err := fixture.PrepareAndLoad(context.Background(), cfg, user) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintMapReport(os.Stdout, rep) return 0 } func runVerify(args []string) int { cfg, user, _, code := bootstrap(args, true) if code != 0 { return code } rep, err := fixture.Verify(context.Background(), cfg, user) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintVerifyReport(os.Stdout, rep) if !rep.OK() { return 1 } return 0 } func runAll(args []string) int { if code := runMap(args); code != 0 { return code } fmt.Println() return runVerify(args) } func runClean(args []string) int { cfg, user, _, code := bootstrap(args, true) if code != 0 { return code } rep, err := fixture.Cleanup(context.Background(), cfg, user) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintCleanReport(os.Stdout, rep) return 0 } func runCount(args []string) int { cfg, _, _, code := bootstrap(args, false) if code != 0 { return code } counts, err := fixture.CountTargetTables(context.Background(), cfg) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintCounts(os.Stdout, "第一批目标表行数", counts, []string{"t_user", "t_legacy_mapping", "t_user_expense", "t_user_expense_school", "t_user_expense_log"}) return 0 } // ---------- 第二批 ---------- func runMap2(args []string) int { cfg, opts, code := bootstrap2(args) if code != 0 { return code } rep, err := fixture.PrepareBatch2(context.Background(), cfg, opts) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintBatch2Report(os.Stdout, rep) return 0 } func runVerify2(args []string) int { cfg, opts, code := bootstrap2(args) if code != 0 { return code } rep, err := fixture.VerifyBatch2(context.Background(), cfg, opts) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintBatch2Verify(os.Stdout, rep) if !rep.OK() { return 1 } return 0 } func runIdem2(args []string) int { cfg, opts, code := bootstrap2(args) if code != 0 { return code } rep, err := fixture.IdempotencyCheck(context.Background(), cfg, opts) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintIdempotency(os.Stdout, rep) if !rep.AllOK() { return 1 } return 0 } func runRun2(args []string) int { if code := runMap2(args); code != 0 { return code } fmt.Println() if code := runVerify2(args); code != 0 { return code } fmt.Println() return runIdem2(args) } func runClean2(args []string) int { cfg, opts, code := bootstrap2(args) if code != 0 { return code } rep, err := fixture.CleanupBatch2(context.Background(), cfg, opts) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintBatch2Clean(os.Stdout, rep) return 0 } func runCount2(args []string) int { cfg, _, code := bootstrap2(args) if code != 0 { return code } counts, err := fixture.CountBatch2Tables(context.Background(), cfg) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) return 3 } fixture.PrintCounts(os.Stdout, "第二批目标表行数", counts, fixture.Batch2Tables()) return 0 }