model.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package fixture
  2. import (
  3. "database/sql"
  4. "time"
  5. )
  6. // 本文件只放**数据形状**:legacy 读出来的行 + 目标态要写的行。
  7. // 转换规则一律放在 mapping.go(纯函数,可单测),落库一律放 load.go。
  8. // ---------- legacy 侧(读出来即定型,金额统一用十进制字符串) ----------
  9. // LegacyExpense 对应 legacy `t_user_expense`(按 userid 1 行)。
  10. type LegacyExpense struct {
  11. UserID int64
  12. Balance sql.NullString // float(10,3)
  13. Spend sql.NullString // float(10,3)
  14. TotalIncome sql.NullString // float(10,3)
  15. TotalExpenditure sql.NullString // float(10,3)
  16. TotalRecommendedAmount sql.NullString // decimal(10,3)
  17. SpecialExpenses sql.NullString // decimal(10,3)
  18. }
  19. // LegacyExpenseSchool 对应 legacy `t_user_expense_school`(userid + schoolid + campusid)。
  20. type LegacyExpenseSchool struct {
  21. ID int64
  22. UserID int64
  23. SchoolID int64 // → institution_id
  24. CampusID int64 // → campus_id(legacy 可空,读时 IFNULL 0)
  25. Balance sql.NullString
  26. Spend sql.NullString
  27. TotalIncome sql.NullString
  28. TotalExpenditure sql.NullString
  29. TotalRecommendedAmount sql.NullString
  30. SpecialExpenses sql.NullString
  31. }
  32. // LegacyExpenseLog 对应 legacy `t_user_expense_log`(int 自增 id)。
  33. type LegacyExpenseLog struct {
  34. ID int64
  35. UserID int64
  36. Type int32
  37. LogID sql.NullString // varchar(32) 旧 UUID → target log_id varchar(64)
  38. Money sql.NullString // decimal(10,3)
  39. CreateUserID sql.NullInt64 // int:0 或系统 → target NULL
  40. CreateTime sql.NullTime // datetime(0) → target datetime(3)(补 .000)
  41. Remark sql.NullString // text → varchar(1000)
  42. }
  43. // ---------- 目标侧(金额一律整数分,写入前才格式化成十进制字符串) ----------
  44. // TargetExpense 对应目标 `t_user_expense`(主键 user_id,无自增 id)。
  45. type TargetExpense struct {
  46. UserID string
  47. BalanceCents int64
  48. SpendCents int64
  49. TotalIncomeCents int64
  50. TotalExpenditureCents int64
  51. TotalRecommendedCents int64
  52. SpecialExpensesCents int64
  53. }
  54. // TargetExpenseSchool 对应目标 `t_user_expense_school`。
  55. //
  56. // id 策略:**不保留** legacy id——该表在目标态的对齐键是复合业务键
  57. // (user_id, institution_id, campus_id),与流水表的 D1 口径不同(见 README §3)。
  58. type TargetExpenseSchool struct {
  59. UserID string
  60. InstitutionID int64
  61. CampusID int64
  62. BalanceCents int64
  63. SpendCents int64
  64. TotalIncomeCents int64
  65. TotalExpenditureCents int64
  66. TotalRecommendedCents int64
  67. SpecialExpensesCents int64
  68. }
  69. // TargetExpenseLog 对应目标 `t_user_expense_log`(D1:显式插 legacy id)。
  70. type TargetExpenseLog struct {
  71. ID int64 // = legacy id(D1)
  72. UserID string
  73. Type int32
  74. LogID sql.NullString // legacy logid
  75. RequestID sql.NullString // legacy 无此列 → 恒 NULL
  76. MoneyCents int64
  77. BalanceAfter sql.NullString // legacy 无此列 → 恒 NULL(decimal(12,2) 用字符串承载)
  78. CreateUserID sql.NullString // legacy int create_userid → ULID;0/系统 → NULL
  79. CreateTime time.Time // 秒级(DATETIME(3) 落 .000)
  80. Remark sql.NullString
  81. }
  82. // MappingEntry 是一条 t_legacy_mapping 记录(fixture 自己插)。
  83. type MappingEntry struct {
  84. CanonicalUserID string
  85. LegacyAcademicUserID int64
  86. InstitutionID int64
  87. CampusID int64
  88. Role string
  89. MatchSource string
  90. MatchConfidence int
  91. }
  92. // ---------- 报告用结构 ----------
  93. // ColumnCheck 是核对表的一行:一列的 legacy 值 / target 值 / 是否相等。
  94. type ColumnCheck struct {
  95. Table string
  96. RowKey string
  97. Column string
  98. Legacy string
  99. Target string
  100. Equal bool
  101. Note string
  102. }
  103. // VerifyReport 是一轮核对的完整结果。
  104. type VerifyReport struct {
  105. Checks []ColumnCheck
  106. Assertions []Assertion
  107. LoadedRows map[string]int
  108. TargetRows map[string]int
  109. FloatTrap FloatTrapDemo
  110. AutoIncTest AutoIncrementDemo
  111. }
  112. // Assertion 是一条语义断言(如「balance_after 必须为 NULL」)。
  113. type Assertion struct {
  114. Name string
  115. Pass bool
  116. Detail string
  117. }
  118. // FloatTrapDemo 记录「float 直接相加」与「逐行取整到分再相加」的差异证据。
  119. type FloatTrapDemo struct {
  120. LegacyValues []string // 逐条 legacy money
  121. PerRowRounded []string // 逐条 ROUND(x,2)
  122. SumOfRoundedCents int64 // Σ ROUND(x,2)
  123. SumRoundedStr string // 上面那个值的十进制表示
  124. FloatSumRaw float64 // 直接用 float64 相加
  125. RoundOfSumStr string // ROUND(Σx, 2)(先加后取整)
  126. DeltaVsPerRow int64 // ROUND(Σ) 与 ΣROUND 的分差
  127. FloatAccumErr float64 // float64 累加误差(FloatSumRaw - 精确和)
  128. ExactSumThousandths int64 // 精确和(千分位整数,用于展示 float 累加误差量级)
  129. }
  130. // AutoIncrementDemo 记录「显式插 id 后 AUTO_INCREMENT 不倒退」的证据。
  131. //
  132. // 两段证据:
  133. //
  134. // A. 真实表 t_user_expense_log:显式插了 legacy id 之后,**不带 id** 插入的新行 id
  135. // 必须等于即时读到的 AUTO_INCREMENT,且 >= max(id)+1 —— 只要它不是 1 附近的小数,
  136. // 就说明计数器没有被显式插 id 弄回起点。
  137. // B. 临时表镜像(CREATE TEMPORARY TABLE ... LIKE,会话结束自动消失):
  138. // 显式插 id=100,再不带 id 插,必须得到 101,把「max+1」这条语义**可重复地**演示出来。
  139. //
  140. // 为什么要 B:MySQL 的 AUTO_INCREMENT 计数器**DELETE 之后不回退**,
  141. // 所以 fixture 反复跑时真实表的计数器会单调增长,真实表上「恰好 max+1」只有首次能观测到;
  142. // 临时表镜像每次都从空表起步,因此每次都能给出一模一样的 max+1 结论。
  143. type AutoIncrementDemo struct {
  144. MaxIDBefore int64
  145. AutoIncrementVal int64
  146. ProbeID int64
  147. ExpectedProbeID int64
  148. EqualsMaxPlusOne bool
  149. Pass bool
  150. MirrorExplicitID int64
  151. MirrorProbeID int64
  152. MirrorExpected int64
  153. MirrorPass bool
  154. }
  155. // FailCount 统计核对表中不相等的列数。
  156. func (r *VerifyReport) FailCount() int {
  157. n := 0
  158. for _, c := range r.Checks {
  159. if !c.Equal {
  160. n++
  161. }
  162. }
  163. for _, a := range r.Assertions {
  164. if !a.Pass {
  165. n++
  166. }
  167. }
  168. return n
  169. }
  170. // OK 表示整轮核对通过。
  171. func (r *VerifyReport) OK() bool { return r.FailCount() == 0 }