main.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. // Command ac15fixture 是 AC-15 阶段 2 前置件的 CLI 入口。
  2. //
  3. // 两批能力:
  4. //
  5. // 第一批(资金域 4 表:t_user_expense / _school / _log + t_user / t_legacy_mapping)
  6. // ddl 用 core-model-ddl.sql 补齐目标库结构(只 CREATE,不 DROP)
  7. // map --user 53691 抽样搬运
  8. // verify --user 53691 逐列核对(退出码 0=PASS,1=FAIL)
  9. // run --user 53691 map + verify
  10. // clean --user 53691 删除 fixture 数据并复核回 0 行
  11. // count 打印相关表当前行数
  12. //
  13. // 第二批(t_user_divide_into / _log / t_user_balance_request / t_expenditure_log
  14. // + 字典 t_expenditure,biz_no 口径首次落地)
  15. // map2 抽样搬运 4 表
  16. // verify2 逐列核对 + biz_no 四项 + D1 id 一致性
  17. // idem2 幂等实测(同一批跑两次,比对行数与 id)
  18. // run2 map2 + verify2 + idem2
  19. // clean2 清理并复核 7 张表回 0 行
  20. // count2 打印第二批涉及表当前行数
  21. //
  22. // 结构对齐(created_at / updated_at 补 DEFAULT):
  23. // align 逐表比对 DDL 与实库,打印 ALTER 清单并做 dry-run(不执行)
  24. // align --apply 清单经人工确认后执行
  25. // align --verify 只复核两列的 COLUMN_DEFAULT 是否全部非 NULL
  26. //
  27. // 环境变量(口令不落盘、不打印):
  28. //
  29. // AC15_DB_HOST / AC15_DB_PORT / AC15_DB_USER / AC15_DB_PASS / AC15_TARGET_DB
  30. // AC15_LEGACY_USER / AC15_LEGACY_PASS / AC15_LEGACY_DB
  31. // AC15_CORE_DDL core DDL 路径(默认取仓库外部的 02-data-model/core-model-ddl.sql)
  32. //
  33. // 退出码:0 = 成功/PASS;1 = 核对 FAIL;2 = 参数错误;3 = 环境或守卫失败。
  34. package main
  35. import (
  36. "context"
  37. "flag"
  38. "fmt"
  39. "os"
  40. "yuansiwei/unified-backend/internal/academic/dualrun/fixture"
  41. )
  42. func main() {
  43. if len(os.Args) < 2 {
  44. usage()
  45. os.Exit(2)
  46. }
  47. cmd := os.Args[1]
  48. args := os.Args[2:]
  49. switch cmd {
  50. case "ddl":
  51. os.Exit(runDDL(args))
  52. case "map":
  53. os.Exit(runMap(args))
  54. case "verify":
  55. os.Exit(runVerify(args))
  56. case "run":
  57. os.Exit(runAll(args))
  58. case "clean":
  59. os.Exit(runClean(args))
  60. case "count":
  61. os.Exit(runCount(args))
  62. case "map2":
  63. os.Exit(runMap2(args))
  64. case "verify2":
  65. os.Exit(runVerify2(args))
  66. case "idem2":
  67. os.Exit(runIdem2(args))
  68. case "run2":
  69. os.Exit(runRun2(args))
  70. case "clean2":
  71. os.Exit(runClean2(args))
  72. case "count2":
  73. os.Exit(runCount2(args))
  74. case "align":
  75. os.Exit(runAlign(args))
  76. case "-h", "--help", "help":
  77. usage()
  78. os.Exit(0)
  79. default:
  80. fmt.Fprintf(os.Stderr, "未知子命令:%s\n\n", cmd)
  81. usage()
  82. os.Exit(2)
  83. }
  84. }
  85. func usage() {
  86. fmt.Fprint(os.Stderr, `ac15fixture —— AC-15 阶段 2 前置件(资金域映射 + 逐列核对)
  87. 第一批(D1 口径为主):
  88. ddl 补齐 core-model-ddl.sql 的 13 张表(只 CREATE TABLE IF NOT EXISTS,禁止 DROP)
  89. map 抽样搬运一个 legacy 用户(账户 + 校区账户 + 流水)
  90. verify 逐列核对 4 张表 + AUTO_INCREMENT + 空值语义 + float 陷阱
  91. run map + verify
  92. clean 删除 fixture 数据并复核 4 张表回 0 行
  93. count 打印相关表当前行数
  94. 第二批(biz_no 口径首次落地):
  95. map2 搬运 t_user_divide_into / _log / t_user_balance_request / t_expenditure_log(+字典)
  96. verify2 逐列核对 + biz_no 四项 + D1 id 一致性
  97. idem2 幂等实测(同一批跑两次,比行数与 id)
  98. run2 map2 + verify2 + idem2
  99. clean2 清理并复核 7 张表回 0 行
  100. count2 打印第二批涉及表当前行数
  101. 结构对齐(created_at / updated_at 补 DEFAULT,只 MODIFY 不重建):
  102. align 逐表比对 DDL 与实库,打印 ALTER 清单 + dry-run(不执行)
  103. align --apply 清单确认后执行
  104. align --verify 只复核 COLUMN_DEFAULT 是否全部非 NULL
  105. 第一批参数:--user <legacy userid>(默认 53691), --ddl <path>
  106. 第二批参数:--root <分账树根 userid,默认 395>, --buser <充值工单用户,默认 31348>,
  107. --euser <收支记录创建人,默认 395>, --dict <是否迁移字典,默认 true>
  108. `)
  109. }
  110. // bootstrap 解析第一批公共参数、加载配置并跑守卫。
  111. func bootstrap(args []string, withUser bool) (fixture.Config, int64, string, int) {
  112. fs := flag.NewFlagSet("ac15fixture", flag.ContinueOnError)
  113. fs.SetOutput(os.Stderr)
  114. user := fs.Int64("user", fixture.DefaultLegacyUserID, "legacy userid")
  115. ddl := fs.String("ddl", "", "core DDL 路径(默认 AC15_CORE_DDL 或仓库外部默认路径)")
  116. if err := fs.Parse(args); err != nil {
  117. return fixture.Config{}, 0, "", 2
  118. }
  119. if !withUser {
  120. *user = fixture.DefaultLegacyUserID
  121. }
  122. cfg, code := loadConfig()
  123. if code != 0 {
  124. return fixture.Config{}, 0, "", code
  125. }
  126. return cfg, *user, *ddl, 0
  127. }
  128. // loadConfig 取工作目录 → 读配置 → 守卫 → 打印脱敏后的连接串。
  129. func loadConfig() (fixture.Config, int) {
  130. repoRoot, err := os.Getwd()
  131. if err != nil {
  132. fmt.Fprintf(os.Stderr, "FATAL: 取工作目录失败:%v\n", err)
  133. return fixture.Config{}, 3
  134. }
  135. cfg, err := fixture.LoadConfig(repoRoot)
  136. if err != nil {
  137. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  138. return fixture.Config{}, 3
  139. }
  140. if err := cfg.Guard(); err != nil {
  141. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  142. return fixture.Config{}, 3
  143. }
  144. fmt.Printf("target=%s legacy=%s\n\n", cfg.TargetMasked(), cfg.LegacyMasked())
  145. return cfg, 0
  146. }
  147. // bootstrap2 解析第二批参数。
  148. func bootstrap2(args []string) (fixture.Config, fixture.Batch2Options, int) {
  149. def := fixture.DefaultBatch2Options()
  150. fs := flag.NewFlagSet("ac15fixture-b2", flag.ContinueOnError)
  151. fs.SetOutput(os.Stderr)
  152. root := fs.Int64("root", def.DivideRootUserID, "分账树根 userid(取整棵子树)")
  153. buser := fs.Int64("buser", def.BalanceRequestUserID, "充值工单所属 userid")
  154. euser := fs.Int64("euser", def.ExpenditureLogUserID, "收支记录创建人 userid")
  155. dict := fs.Bool("dict", def.WithExpenditureDict, "是否同时 D1 迁移 t_expenditure 字典")
  156. if err := fs.Parse(args); err != nil {
  157. return fixture.Config{}, fixture.Batch2Options{}, 2
  158. }
  159. cfg, code := loadConfig()
  160. if code != 0 {
  161. return fixture.Config{}, fixture.Batch2Options{}, code
  162. }
  163. return cfg, fixture.Batch2Options{
  164. DivideRootUserID: *root,
  165. BalanceRequestUserID: *buser,
  166. ExpenditureLogUserID: *euser,
  167. WithExpenditureDict: *dict,
  168. }, 0
  169. }
  170. func runDDL(args []string) int {
  171. cfg, _, ddlPath, code := bootstrap(args, false)
  172. if code != 0 {
  173. return code
  174. }
  175. rep, err := fixture.ApplyCoreDDL(context.Background(), cfg, ddlPath)
  176. if rep != nil {
  177. fixture.PrintDDLReport(os.Stdout, rep)
  178. }
  179. if err != nil {
  180. fmt.Fprintf(os.Stderr, "\nFATAL: %v\n", err)
  181. return 3
  182. }
  183. return 0
  184. }
  185. func runMap(args []string) int {
  186. cfg, user, _, code := bootstrap(args, true)
  187. if code != 0 {
  188. return code
  189. }
  190. rep, err := fixture.PrepareAndLoad(context.Background(), cfg, user)
  191. if err != nil {
  192. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  193. return 3
  194. }
  195. fixture.PrintMapReport(os.Stdout, rep)
  196. return 0
  197. }
  198. func runVerify(args []string) int {
  199. cfg, user, _, code := bootstrap(args, true)
  200. if code != 0 {
  201. return code
  202. }
  203. rep, err := fixture.Verify(context.Background(), cfg, user)
  204. if err != nil {
  205. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  206. return 3
  207. }
  208. fixture.PrintVerifyReport(os.Stdout, rep)
  209. if !rep.OK() {
  210. return 1
  211. }
  212. return 0
  213. }
  214. func runAll(args []string) int {
  215. if code := runMap(args); code != 0 {
  216. return code
  217. }
  218. fmt.Println()
  219. return runVerify(args)
  220. }
  221. func runClean(args []string) int {
  222. cfg, user, _, code := bootstrap(args, true)
  223. if code != 0 {
  224. return code
  225. }
  226. rep, err := fixture.Cleanup(context.Background(), cfg, user)
  227. if err != nil {
  228. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  229. return 3
  230. }
  231. fixture.PrintCleanReport(os.Stdout, rep)
  232. return 0
  233. }
  234. func runCount(args []string) int {
  235. cfg, _, _, code := bootstrap(args, false)
  236. if code != 0 {
  237. return code
  238. }
  239. counts, err := fixture.CountTargetTables(context.Background(), cfg)
  240. if err != nil {
  241. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  242. return 3
  243. }
  244. fixture.PrintCounts(os.Stdout, "第一批目标表行数", counts,
  245. []string{"t_user", "t_legacy_mapping", "t_user_expense", "t_user_expense_school", "t_user_expense_log"})
  246. return 0
  247. }
  248. // ---------- 第二批 ----------
  249. func runMap2(args []string) int {
  250. cfg, opts, code := bootstrap2(args)
  251. if code != 0 {
  252. return code
  253. }
  254. rep, err := fixture.PrepareBatch2(context.Background(), cfg, opts)
  255. if err != nil {
  256. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  257. return 3
  258. }
  259. fixture.PrintBatch2Report(os.Stdout, rep)
  260. return 0
  261. }
  262. func runVerify2(args []string) int {
  263. cfg, opts, code := bootstrap2(args)
  264. if code != 0 {
  265. return code
  266. }
  267. rep, err := fixture.VerifyBatch2(context.Background(), cfg, opts)
  268. if err != nil {
  269. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  270. return 3
  271. }
  272. fixture.PrintBatch2Verify(os.Stdout, rep)
  273. if !rep.OK() {
  274. return 1
  275. }
  276. return 0
  277. }
  278. func runIdem2(args []string) int {
  279. cfg, opts, code := bootstrap2(args)
  280. if code != 0 {
  281. return code
  282. }
  283. rep, err := fixture.IdempotencyCheck(context.Background(), cfg, opts)
  284. if err != nil {
  285. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  286. return 3
  287. }
  288. fixture.PrintIdempotency(os.Stdout, rep)
  289. if !rep.AllOK() {
  290. return 1
  291. }
  292. return 0
  293. }
  294. func runRun2(args []string) int {
  295. if code := runMap2(args); code != 0 {
  296. return code
  297. }
  298. fmt.Println()
  299. if code := runVerify2(args); code != 0 {
  300. return code
  301. }
  302. fmt.Println()
  303. return runIdem2(args)
  304. }
  305. func runClean2(args []string) int {
  306. cfg, opts, code := bootstrap2(args)
  307. if code != 0 {
  308. return code
  309. }
  310. rep, err := fixture.CleanupBatch2(context.Background(), cfg, opts)
  311. if err != nil {
  312. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  313. return 3
  314. }
  315. fixture.PrintBatch2Clean(os.Stdout, rep)
  316. return 0
  317. }
  318. func runAlign(args []string) int {
  319. fs := flag.NewFlagSet("align", flag.ContinueOnError)
  320. fs.SetOutput(os.Stderr)
  321. apply := fs.Bool("apply", false, "执行清单里的 ALTER(默认只打印 + dry-run)")
  322. verifyOnly := fs.Bool("verify", false, "只复核两列默认值")
  323. modelDir := fs.String("model", "", "DDL 目录(默认 AC15_MODEL_DIR 或内置路径)")
  324. if err := fs.Parse(args); err != nil {
  325. return 2
  326. }
  327. cfg, code := loadConfig()
  328. if code != 0 {
  329. return code
  330. }
  331. ctx := context.Background()
  332. if *verifyOnly {
  333. total, missing, err := fixture.VerifyAlign(ctx, cfg)
  334. if err != nil {
  335. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  336. return 3
  337. }
  338. fmt.Printf("== 复核:%d 个 created_at/updated_at 列 ==\n", total)
  339. if len(missing) == 0 {
  340. fmt.Printf("COLUMN_DEFAULT 全部非 NULL:PASS\n")
  341. return 0
  342. }
  343. fmt.Printf("仍有 %d 个列 DEFAULT 为 NULL:\n", len(missing))
  344. for _, m := range missing {
  345. fmt.Printf(" - %s\n", m)
  346. }
  347. return 1
  348. }
  349. rep, err := fixture.BuildAlignReport(ctx, cfg, *modelDir)
  350. if err != nil {
  351. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  352. return 3
  353. }
  354. if err := fixture.DryRunAlign(ctx, cfg, rep); err != nil {
  355. fmt.Fprintf(os.Stderr, "FATAL: dry-run 失败:%v\n", err)
  356. return 3
  357. }
  358. if *apply {
  359. if err := fixture.ApplyAlign(ctx, cfg, rep); err != nil {
  360. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  361. return 3
  362. }
  363. }
  364. fixture.PrintAlignReport(os.Stdout, rep)
  365. if *apply {
  366. total, missing, err := fixture.VerifyAlign(ctx, cfg)
  367. if err != nil {
  368. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  369. return 3
  370. }
  371. fmt.Printf("\n== 执行后复核:%d 列 ==\n", total)
  372. if len(missing) == 0 {
  373. fmt.Printf("COLUMN_DEFAULT 全部非 NULL:PASS\n")
  374. } else {
  375. fmt.Printf("仍有 %d 列为 NULL(含 drift-nullable 等未处理项):\n", len(missing))
  376. for _, m := range missing {
  377. fmt.Printf(" - %s\n", m)
  378. }
  379. }
  380. if rep.Failed > 0 {
  381. return 3
  382. }
  383. }
  384. return 0
  385. }
  386. func runCount2(args []string) int {
  387. cfg, _, code := bootstrap2(args)
  388. if code != 0 {
  389. return code
  390. }
  391. counts, err := fixture.CountBatch2Tables(context.Background(), cfg)
  392. if err != nil {
  393. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  394. return 3
  395. }
  396. fixture.PrintCounts(os.Stdout, "第二批目标表行数", counts, fixture.Batch2Tables())
  397. return 0
  398. }