package fixture import ( "fmt" "io" "strings" ) // 本文件只做**终端报告排版**,不含任何业务判断。 // 排版目标是「能直接贴进报告文档」:定宽列 + 明确的 ✔/✘ 标记。 const ( markOK = "✔" markFail = "✘" ) // PrintDDLReport 打印结构补齐结果。 func PrintDDLReport(w io.Writer, r *DDLReport) { fmt.Fprintf(w, "== core 结构补齐 ==\n") fmt.Fprintf(w, "DDL 文件:%s\n", r.Path) fmt.Fprintf(w, "表数:%d → %d\n", r.TablesBefore, r.TablesAfter) fmt.Fprintf(w, "新增(%d):%s\n", len(r.Created), strings.Join(r.Created, ", ")) fmt.Fprintf(w, "跳过已存在(%d):%s\n", len(r.Skipped), strings.Join(r.Skipped, ", ")) dropped := 0 seeds := 0 for _, a := range r.Actions { switch a.Kind { case "skipped-drop": dropped++ case "seed": seeds++ } } fmt.Fprintf(w, "丢弃的 DROP TABLE 语句:%d 条(不执行)\n", dropped) fmt.Fprintf(w, "执行的 INSERT IGNORE 种子:%d 条(幂等)\n", seeds) } // PrintMapReport 打印搬数据结果。 func PrintMapReport(w io.Writer, r *MapReport) { fmt.Fprintf(w, "== 映射与装载 ==\n") fmt.Fprintf(w, "legacy userid:%d\n", r.LegacyUserID) fmt.Fprintf(w, "canonical_user_id:%s\n", r.CanonicalUserID) fmt.Fprintf(w, "legacy 行数:t_user_expense=%d, t_user_expense_school=%d, t_user_expense_log=%d(合计 %d)\n", r.LegacyCounts["t_user_expense"], r.LegacyCounts["t_user_expense_school"], r.LegacyCounts["t_user_expense_log"], r.LegacyRows) fmt.Fprintf(w, "写入:t_user=%d 行, t_legacy_mapping=%d 行(含创建人 %d 个)\n", r.UsersWritten, r.MappingsWritten, len(r.CreatorIDs)) fmt.Fprintf(w, "流水 id(D1 原样保留):%v\n", r.LogIDs) if len(r.Samples) > 0 { fmt.Fprintf(w, "流水样本(remark 已脱敏):\n") for _, s := range r.Samples { fmt.Fprintf(w, " id=%-8d type=%-4d money=%-12s create_time=%s remark=%s\n", s.ID, s.Type, s.Money, s.CreateTime, s.RemarkMask) } } if len(r.Warnings) > 0 { fmt.Fprintf(w, "映射告警(%d):\n", len(r.Warnings)) for _, x := range r.Warnings { fmt.Fprintf(w, " - %s\n", x) } } else { fmt.Fprintf(w, "映射告警:无\n") } } // PrintVerifyReport 打印逐列核对表 + 断言 + 两组证据。 func PrintVerifyReport(w io.Writer, r *VerifyReport) { fmt.Fprintf(w, "== 逐列核对 ==\n") for _, table := range r.SortedTableNames() { fmt.Fprintf(w, "\n-- %s --\n", table) fmt.Fprintf(w, " %s %-26s %-26s %-26s %-26s %s\n", "st", "rowKey", "column", "legacy", "target", "note") for _, c := range r.Checks { if c.Table != table { continue } st := markOK if !c.Equal { st = markFail } fmt.Fprintf(w, " %s %-26s %-26s %-26s %-26s %s\n", st, c.RowKey, c.Column, truncate(c.Legacy, 26), truncate(c.Target, 26), c.Note) } } fmt.Fprintf(w, "\n== 语义断言 ==\n") for _, a := range r.Assertions { st := markOK if !a.Pass { st = markFail } fmt.Fprintf(w, " %s %s\n %s\n", st, a.Name, a.Detail) } fmt.Fprintf(w, "\n== float 陷阱证据(legacy 逐条 3 位小数)==\n") f := r.FloatTrap for i := range f.LegacyValues { fmt.Fprintf(w, " [%d] legacy=%-12s ROUND(x,2)=%-10s\n", i, f.LegacyValues[i], f.PerRowRounded[i]) } fmt.Fprintf(w, " Σ ROUND(x,2) = %s(迁移口径,逐行取整后相加)\n", f.SumRoundedStr) fmt.Fprintf(w, " ROUND(Σ x, 2) = %s(旧口径,先加后取整)\n", f.RoundOfSumStr) fmt.Fprintf(w, " 两者分差 = %d 分\n", f.DeltaVsPerRow) fmt.Fprintf(w, " float64 直接累加 = %s(精确和 = %s)\n", FormatFloat(f.FloatSumRaw), CentsToDecimal(f.ExactSumThousandths/10)) fmt.Fprintf(w, " float64 累加误差 = %s\n", FormatFloat(f.FloatAccumErr)) fmt.Fprintf(w, "\n== AUTO_INCREMENT 不倒退 ==\n") a := r.AutoIncTest st := markOK if !a.Pass { st = markFail } fmt.Fprintf(w, " %s 真实表 t_user_expense_log:max(id)=%d, AUTO_INCREMENT=%d(= max+1? %v)\n", st, a.MaxIDBefore, a.AutoIncrementVal, a.EqualsMaxPlusOne) fmt.Fprintf(w, " 不带 id 插入 → id=%d(= 计数器 %d)⇒ 计数器没有被显式插 id 重置\n", a.ProbeID, a.ExpectedProbeID) stm := markOK if !a.MirrorPass { stm = markFail } fmt.Fprintf(w, " %s 临时表镜像(CREATE TEMPORARY TABLE ... LIKE):显式插 id=%d → 不带 id 插入得到 id=%d(期望 %d)\n", stm, a.MirrorExplicitID, a.MirrorProbeID, a.MirrorExpected) fmt.Fprintf(w, "\n核对结论:%s(不等列数 + 失败断言数 = %d)\n", verdict(r.OK()), r.FailCount()) } // PrintCleanReport 打印清理结果。 func PrintCleanReport(w io.Writer, r *CleanReport) { fmt.Fprintf(w, "== 清理 ==\n") fmt.Fprintf(w, "删除的 canonical_user_id(%d 个):%s\n", len(r.CanonicalIDs), strings.Join(r.CanonicalIDs, ", ")) for _, t := range []string{"t_user_expense", "t_user_expense_school", "t_user_expense_log", "t_legacy_mapping", "t_user"} { fmt.Fprintf(w, " %-24s 删除 %-4d 行,剩余全表 %d 行\n", t, r.Deleted[t], r.Remaining[t]) } zero := true for _, t := range []string{"t_user_expense", "t_user_expense_school", "t_user_expense_log"} { if r.Remaining[t] != 0 { zero = false } } fmt.Fprintf(w, "4 张表回 0 行:%s\n", verdict(zero)) } func verdict(ok bool) string { if ok { return "PASS" } return "FAIL" } // truncate 按字符截断展示宽度(中文按 2 列宽估算,够用即可)。 func truncate(s string, max int) string { r := []rune(s) if len(r) <= max { return s } return string(r[:max-1]) + "…" }