report.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package fixture
  2. import (
  3. "fmt"
  4. "io"
  5. "strings"
  6. )
  7. // 本文件只做**终端报告排版**,不含任何业务判断。
  8. // 排版目标是「能直接贴进报告文档」:定宽列 + 明确的 ✔/✘ 标记。
  9. const (
  10. markOK = "✔"
  11. markFail = "✘"
  12. )
  13. // PrintDDLReport 打印结构补齐结果。
  14. func PrintDDLReport(w io.Writer, r *DDLReport) {
  15. fmt.Fprintf(w, "== core 结构补齐 ==\n")
  16. fmt.Fprintf(w, "DDL 文件:%s\n", r.Path)
  17. fmt.Fprintf(w, "表数:%d → %d\n", r.TablesBefore, r.TablesAfter)
  18. fmt.Fprintf(w, "新增(%d):%s\n", len(r.Created), strings.Join(r.Created, ", "))
  19. fmt.Fprintf(w, "跳过已存在(%d):%s\n", len(r.Skipped), strings.Join(r.Skipped, ", "))
  20. dropped := 0
  21. seeds := 0
  22. for _, a := range r.Actions {
  23. switch a.Kind {
  24. case "skipped-drop":
  25. dropped++
  26. case "seed":
  27. seeds++
  28. }
  29. }
  30. fmt.Fprintf(w, "丢弃的 DROP TABLE 语句:%d 条(不执行)\n", dropped)
  31. fmt.Fprintf(w, "执行的 INSERT IGNORE 种子:%d 条(幂等)\n", seeds)
  32. }
  33. // PrintMapReport 打印搬数据结果。
  34. func PrintMapReport(w io.Writer, r *MapReport) {
  35. fmt.Fprintf(w, "== 映射与装载 ==\n")
  36. fmt.Fprintf(w, "legacy userid:%d\n", r.LegacyUserID)
  37. fmt.Fprintf(w, "canonical_user_id:%s\n", r.CanonicalUserID)
  38. fmt.Fprintf(w, "legacy 行数:t_user_expense=%d, t_user_expense_school=%d, t_user_expense_log=%d(合计 %d)\n",
  39. r.LegacyCounts["t_user_expense"], r.LegacyCounts["t_user_expense_school"],
  40. r.LegacyCounts["t_user_expense_log"], r.LegacyRows)
  41. fmt.Fprintf(w, "写入:t_user=%d 行, t_legacy_mapping=%d 行(含创建人 %d 个)\n",
  42. r.UsersWritten, r.MappingsWritten, len(r.CreatorIDs))
  43. fmt.Fprintf(w, "流水 id(D1 原样保留):%v\n", r.LogIDs)
  44. if len(r.Samples) > 0 {
  45. fmt.Fprintf(w, "流水样本(remark 已脱敏):\n")
  46. for _, s := range r.Samples {
  47. fmt.Fprintf(w, " id=%-8d type=%-4d money=%-12s create_time=%s remark=%s\n",
  48. s.ID, s.Type, s.Money, s.CreateTime, s.RemarkMask)
  49. }
  50. }
  51. if len(r.Warnings) > 0 {
  52. fmt.Fprintf(w, "映射告警(%d):\n", len(r.Warnings))
  53. for _, x := range r.Warnings {
  54. fmt.Fprintf(w, " - %s\n", x)
  55. }
  56. } else {
  57. fmt.Fprintf(w, "映射告警:无\n")
  58. }
  59. }
  60. // PrintVerifyReport 打印逐列核对表 + 断言 + 两组证据。
  61. func PrintVerifyReport(w io.Writer, r *VerifyReport) {
  62. fmt.Fprintf(w, "== 逐列核对 ==\n")
  63. for _, table := range r.SortedTableNames() {
  64. fmt.Fprintf(w, "\n-- %s --\n", table)
  65. fmt.Fprintf(w, " %s %-26s %-26s %-26s %-26s %s\n", "st", "rowKey", "column", "legacy", "target", "note")
  66. for _, c := range r.Checks {
  67. if c.Table != table {
  68. continue
  69. }
  70. st := markOK
  71. if !c.Equal {
  72. st = markFail
  73. }
  74. fmt.Fprintf(w, " %s %-26s %-26s %-26s %-26s %s\n",
  75. st, c.RowKey, c.Column, truncate(c.Legacy, 26), truncate(c.Target, 26), c.Note)
  76. }
  77. }
  78. fmt.Fprintf(w, "\n== 语义断言 ==\n")
  79. for _, a := range r.Assertions {
  80. st := markOK
  81. if !a.Pass {
  82. st = markFail
  83. }
  84. fmt.Fprintf(w, " %s %s\n %s\n", st, a.Name, a.Detail)
  85. }
  86. fmt.Fprintf(w, "\n== float 陷阱证据(legacy 逐条 3 位小数)==\n")
  87. f := r.FloatTrap
  88. for i := range f.LegacyValues {
  89. fmt.Fprintf(w, " [%d] legacy=%-12s ROUND(x,2)=%-10s\n", i, f.LegacyValues[i], f.PerRowRounded[i])
  90. }
  91. fmt.Fprintf(w, " Σ ROUND(x,2) = %s(迁移口径,逐行取整后相加)\n", f.SumRoundedStr)
  92. fmt.Fprintf(w, " ROUND(Σ x, 2) = %s(旧口径,先加后取整)\n", f.RoundOfSumStr)
  93. fmt.Fprintf(w, " 两者分差 = %d 分\n", f.DeltaVsPerRow)
  94. fmt.Fprintf(w, " float64 直接累加 = %s(精确和 = %s)\n",
  95. FormatFloat(f.FloatSumRaw), CentsToDecimal(f.ExactSumThousandths/10))
  96. fmt.Fprintf(w, " float64 累加误差 = %s\n", FormatFloat(f.FloatAccumErr))
  97. fmt.Fprintf(w, "\n== AUTO_INCREMENT 不倒退 ==\n")
  98. a := r.AutoIncTest
  99. st := markOK
  100. if !a.Pass {
  101. st = markFail
  102. }
  103. fmt.Fprintf(w, " %s 真实表 t_user_expense_log:max(id)=%d, AUTO_INCREMENT=%d(= max+1? %v)\n",
  104. st, a.MaxIDBefore, a.AutoIncrementVal, a.EqualsMaxPlusOne)
  105. fmt.Fprintf(w, " 不带 id 插入 → id=%d(= 计数器 %d)⇒ 计数器没有被显式插 id 重置\n",
  106. a.ProbeID, a.ExpectedProbeID)
  107. stm := markOK
  108. if !a.MirrorPass {
  109. stm = markFail
  110. }
  111. fmt.Fprintf(w, " %s 临时表镜像(CREATE TEMPORARY TABLE ... LIKE):显式插 id=%d → 不带 id 插入得到 id=%d(期望 %d)\n",
  112. stm, a.MirrorExplicitID, a.MirrorProbeID, a.MirrorExpected)
  113. fmt.Fprintf(w, "\n核对结论:%s(不等列数 + 失败断言数 = %d)\n", verdict(r.OK()), r.FailCount())
  114. }
  115. // PrintCleanReport 打印清理结果。
  116. func PrintCleanReport(w io.Writer, r *CleanReport) {
  117. fmt.Fprintf(w, "== 清理 ==\n")
  118. fmt.Fprintf(w, "删除的 canonical_user_id(%d 个):%s\n", len(r.CanonicalIDs), strings.Join(r.CanonicalIDs, ", "))
  119. for _, t := range []string{"t_user_expense", "t_user_expense_school", "t_user_expense_log", "t_legacy_mapping", "t_user"} {
  120. fmt.Fprintf(w, " %-24s 删除 %-4d 行,剩余全表 %d 行\n", t, r.Deleted[t], r.Remaining[t])
  121. }
  122. zero := true
  123. for _, t := range []string{"t_user_expense", "t_user_expense_school", "t_user_expense_log"} {
  124. if r.Remaining[t] != 0 {
  125. zero = false
  126. }
  127. }
  128. fmt.Fprintf(w, "4 张表回 0 行:%s\n", verdict(zero))
  129. }
  130. func verdict(ok bool) string {
  131. if ok {
  132. return "PASS"
  133. }
  134. return "FAIL"
  135. }
  136. // truncate 按字符截断展示宽度(中文按 2 列宽估算,够用即可)。
  137. func truncate(s string, max int) string {
  138. r := []rune(s)
  139. if len(r) <= max {
  140. return s
  141. }
  142. return string(r[:max-1]) + "…"
  143. }