| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616 |
- // 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 打印第二批涉及表当前行数
- //
- // 结构对齐(created_at / updated_at 补 DEFAULT):
- // align 逐表比对 DDL 与实库,打印 ALTER 清单并做 dry-run(不执行)
- // align --apply 清单经人工确认后执行
- // align --verify 只复核两列的 COLUMN_DEFAULT 是否全部非 NULL
- //
- // 环境变量(口令不落盘、不打印):
- //
- // 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"
- "strconv"
- "strings"
- "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 "align":
- os.Exit(runAlign(args))
- case "map3":
- os.Exit(runMap3(args))
- case "verify3":
- os.Exit(runVerify3(args))
- case "idem3":
- os.Exit(runIdem3(args))
- case "run3":
- os.Exit(runRun3(args))
- case "clean3":
- os.Exit(runClean3(args))
- case "count3":
- os.Exit(runCount3(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 打印第二批涉及表当前行数
- 第三批(org 域:t_institution / t_campus,D1 + 引用完整性):
- map3 迁移指定机构与其校区(默认 1,63317,63323,63324,63325,63332)
- verify3 逐列核对 + 机构集合 + 引用完整性(孤儿=0)+ R17 + bit(1)
- idem3 幂等实测(跑两次比行数与 id)
- run3 map3 + verify3 + idem3
- clean3 清理并复核两表回 0 行
- count3 打印 org 两表行数
- 参数:--ids 1,63317,... --all-campuses=true
- 结构对齐(created_at / updated_at 补 DEFAULT,只 MODIFY 不重建):
- align 逐表比对 DDL 与实库,打印 ALTER 清单 + dry-run(不执行)
- align --apply 清单确认后执行
- align --verify 只复核 COLUMN_DEFAULT 是否全部非 NULL
- 第一批参数:--user <legacy userid>(默认 53691), --ddl <path>
- 第二批参数:--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 runAlign(args []string) int {
- fs := flag.NewFlagSet("align", flag.ContinueOnError)
- fs.SetOutput(os.Stderr)
- apply := fs.Bool("apply", false, "执行清单里的 ALTER(默认只打印 + dry-run)")
- verifyOnly := fs.Bool("verify", false, "只复核列默认值")
- modelDir := fs.String("model", "", "DDL 目录(默认 AC15_MODEL_DIR 或内置路径)")
- columns := fs.String("columns", "created_at,updated_at",
- "参与对齐的列:created_at,updated_at | create_time | all")
- algorithm := fs.String("algorithm", "instant",
- "ALGORITHM 子句:instant|inplace|copy|none(MySQL 8.0.23 不支持 instant,需显式 none)")
- fixNullable := fs.Bool("fix-nullable", false, "按 DDL 修正 NULL 性(默认只报告不改)")
- if err := fs.Parse(args); err != nil {
- return 2
- }
- cfg, code := loadConfig()
- if code != 0 {
- return code
- }
- ctx := context.Background()
- cols, err := parseColumns(*columns)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 2
- }
- if *verifyOnly {
- total, missing, err := fixture.VerifyAlign(ctx, cfg, cols)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fmt.Printf("== 复核 %v:共 %d 列 ==\n", cols, total)
- if len(missing) == 0 {
- fmt.Printf("COLUMN_DEFAULT 全部非 NULL:PASS\n")
- return 0
- }
- fmt.Printf("仍有 %d 列为 NULL:\n", len(missing))
- for _, m := range missing {
- fmt.Printf(" - %s\n", m)
- }
- return 1
- }
- opts := fixture.AlignOptions{
- ModelDir: *modelDir, Columns: cols,
- Algorithm: *algorithm, FixNullable: *fixNullable,
- }
- rep, err := fixture.BuildAlignReport(ctx, cfg, opts)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- if err := fixture.DryRunAlign(ctx, cfg, rep); err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: dry-run 失败:%v\n", err)
- return 3
- }
- if *apply {
- if err := fixture.ApplyAlign(ctx, cfg, rep); err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- }
- fixture.PrintAlignReport(os.Stdout, rep)
- if *apply {
- total, missing, err := fixture.VerifyAlign(ctx, cfg, cols)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fmt.Printf("\n== 执行后复核 %v:共 %d 列 ==\n", cols, total)
- if len(missing) == 0 {
- fmt.Printf("COLUMN_DEFAULT 全部非 NULL:PASS\n")
- } else {
- fmt.Printf("仍有 %d 列为 NULL:\n", len(missing))
- for _, m := range missing {
- fmt.Printf(" - %s\n", m)
- }
- }
- if rep.Failed > 0 {
- return 3
- }
- }
- return 0
- }
- // parseColumns 解析 --columns:all = 审计列 + 业务列。
- func parseColumns(s string) ([]string, error) {
- s = strings.TrimSpace(s)
- switch s {
- case "", "audit":
- return fixture.ColumnsAudit, nil
- case "business":
- return fixture.ColumnsBusiness, nil
- case "all":
- return append(append([]string{}, fixture.ColumnsAudit...), fixture.ColumnsBusiness...), nil
- }
- var out []string
- for _, p := range strings.Split(s, ",") {
- p = strings.TrimSpace(p)
- if p == "" {
- continue
- }
- out = append(out, p)
- }
- if len(out) == 0 {
- return nil, fmt.Errorf("--columns 为空")
- }
- return out, nil
- }
- // ---------- 第三批 ----------
- func bootstrap3(args []string) (fixture.Config, fixture.OrgOptions, int) {
- def := fixture.DefaultOrgOptions()
- fs := flag.NewFlagSet("ac15fixture-b3", flag.ContinueOnError)
- fs.SetOutput(os.Stderr)
- ids := fs.String("ids", "", "逗号分隔的机构 id(留空=默认 6 个)")
- allCampuses := fs.Bool("all-campuses", def.WithAllCampuses, "迁移所选机构的全部校区(否则只迁数据里出现过的)")
- orgDB := fs.String("org-db", def.OrgSourceDB, "org 源库(默认 yuansiwei_i)")
- if err := fs.Parse(args); err != nil {
- return fixture.Config{}, fixture.OrgOptions{}, 2
- }
- opts := def
- if strings.TrimSpace(*ids) != "" {
- opts.InstitutionIDs = nil
- for _, p := range strings.Split(*ids, ",") {
- p = strings.TrimSpace(p)
- if p == "" {
- continue
- }
- v, err := strconv.ParseInt(p, 10, 64)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: 非法机构 id %q\n", p)
- return fixture.Config{}, fixture.OrgOptions{}, 2
- }
- opts.InstitutionIDs = append(opts.InstitutionIDs, v)
- }
- }
- opts.WithAllCampuses = *allCampuses
- opts.OrgSourceDB = *orgDB
- cfg, code := loadConfig()
- if code != 0 {
- return fixture.Config{}, fixture.OrgOptions{}, code
- }
- fmt.Printf("org 源=%s\n", cfg.OrgMasked())
- return cfg, opts, 0
- }
- func runMap3(args []string) int {
- cfg, opts, code := bootstrap3(args)
- if code != 0 {
- return code
- }
- rep, err := fixture.PrepareOrg(context.Background(), cfg, opts)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fixture.PrintOrgReport(os.Stdout, rep)
- return 0
- }
- func runVerify3(args []string) int {
- cfg, opts, code := bootstrap3(args)
- if code != 0 {
- return code
- }
- rep, err := fixture.VerifyOrg(context.Background(), cfg, opts)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fixture.PrintOrgVerify(os.Stdout, rep)
- if !rep.OK() {
- return 1
- }
- return 0
- }
- func runIdem3(args []string) int {
- cfg, opts, code := bootstrap3(args)
- if code != 0 {
- return code
- }
- rep, err := fixture.IdempotencyCheckOrg(context.Background(), cfg, opts)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fixture.PrintOrgIdem(os.Stdout, rep)
- if !rep.AllOK() {
- return 1
- }
- return 0
- }
- func runRun3(args []string) int {
- if code := runMap3(args); code != 0 {
- return code
- }
- fmt.Println()
- if code := runVerify3(args); code != 0 {
- return code
- }
- fmt.Println()
- return runIdem3(args)
- }
- func runClean3(args []string) int {
- cfg, opts, code := bootstrap3(args)
- if code != 0 {
- return code
- }
- rep, err := fixture.CleanupOrg(context.Background(), cfg, opts)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fixture.PrintOrgClean(os.Stdout, rep)
- return 0
- }
- func runCount3(args []string) int {
- cfg, _, code := bootstrap3(args)
- if code != 0 {
- return code
- }
- counts, err := fixture.CountOrgTables(context.Background(), cfg)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fixture.PrintCounts(os.Stdout, "第三批 org 表行数", counts, []string{"t_institution", "t_campus"})
- 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
- }
|