main.go 9.4 KB

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