| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- package fixture
- import (
- "database/sql"
- "time"
- )
- // 本文件只放**数据形状**:legacy 读出来的行 + 目标态要写的行。
- // 转换规则一律放在 mapping.go(纯函数,可单测),落库一律放 load.go。
- // ---------- legacy 侧(读出来即定型,金额统一用十进制字符串) ----------
- // LegacyExpense 对应 legacy `t_user_expense`(按 userid 1 行)。
- type LegacyExpense struct {
- UserID int64
- Balance sql.NullString // float(10,3)
- Spend sql.NullString // float(10,3)
- TotalIncome sql.NullString // float(10,3)
- TotalExpenditure sql.NullString // float(10,3)
- TotalRecommendedAmount sql.NullString // decimal(10,3)
- SpecialExpenses sql.NullString // decimal(10,3)
- }
- // LegacyExpenseSchool 对应 legacy `t_user_expense_school`(userid + schoolid + campusid)。
- type LegacyExpenseSchool struct {
- ID int64
- UserID int64
- SchoolID int64 // → institution_id
- CampusID int64 // → campus_id(legacy 可空,读时 IFNULL 0)
- Balance sql.NullString
- Spend sql.NullString
- TotalIncome sql.NullString
- TotalExpenditure sql.NullString
- TotalRecommendedAmount sql.NullString
- SpecialExpenses sql.NullString
- }
- // LegacyExpenseLog 对应 legacy `t_user_expense_log`(int 自增 id)。
- type LegacyExpenseLog struct {
- ID int64
- UserID int64
- Type int32
- LogID sql.NullString // varchar(32) 旧 UUID → target log_id varchar(64)
- Money sql.NullString // decimal(10,3)
- CreateUserID sql.NullInt64 // int:0 或系统 → target NULL
- CreateTime sql.NullTime // datetime(0) → target datetime(3)(补 .000)
- Remark sql.NullString // text → varchar(1000)
- }
- // ---------- 目标侧(金额一律整数分,写入前才格式化成十进制字符串) ----------
- // TargetExpense 对应目标 `t_user_expense`(主键 user_id,无自增 id)。
- type TargetExpense struct {
- UserID string
- BalanceCents int64
- SpendCents int64
- TotalIncomeCents int64
- TotalExpenditureCents int64
- TotalRecommendedCents int64
- SpecialExpensesCents int64
- }
- // TargetExpenseSchool 对应目标 `t_user_expense_school`。
- //
- // id 策略:**不保留** legacy id——该表在目标态的对齐键是复合业务键
- // (user_id, institution_id, campus_id),与流水表的 D1 口径不同(见 README §3)。
- type TargetExpenseSchool struct {
- UserID string
- InstitutionID int64
- CampusID int64
- BalanceCents int64
- SpendCents int64
- TotalIncomeCents int64
- TotalExpenditureCents int64
- TotalRecommendedCents int64
- SpecialExpensesCents int64
- }
- // TargetExpenseLog 对应目标 `t_user_expense_log`(D1:显式插 legacy id)。
- type TargetExpenseLog struct {
- ID int64 // = legacy id(D1)
- UserID string
- Type int32
- LogID sql.NullString // legacy logid
- RequestID sql.NullString // legacy 无此列 → 恒 NULL
- MoneyCents int64
- BalanceAfter sql.NullString // legacy 无此列 → 恒 NULL(decimal(12,2) 用字符串承载)
- CreateUserID sql.NullString // legacy int create_userid → ULID;0/系统 → NULL
- CreateTime time.Time // 秒级(DATETIME(3) 落 .000)
- Remark sql.NullString
- }
- // MappingEntry 是一条 t_legacy_mapping 记录(fixture 自己插)。
- type MappingEntry struct {
- CanonicalUserID string
- LegacyAcademicUserID int64
- InstitutionID int64
- CampusID int64
- Role string
- MatchSource string
- MatchConfidence int
- }
- // ---------- 报告用结构 ----------
- // ColumnCheck 是核对表的一行:一列的 legacy 值 / target 值 / 是否相等。
- type ColumnCheck struct {
- Table string
- RowKey string
- Column string
- Legacy string
- Target string
- Equal bool
- Note string
- }
- // VerifyReport 是一轮核对的完整结果。
- type VerifyReport struct {
- Checks []ColumnCheck
- Assertions []Assertion
- LoadedRows map[string]int
- TargetRows map[string]int
- FloatTrap FloatTrapDemo
- AutoIncTest AutoIncrementDemo
- }
- // Assertion 是一条语义断言(如「balance_after 必须为 NULL」)。
- type Assertion struct {
- Name string
- Pass bool
- Detail string
- }
- // FloatTrapDemo 记录「float 直接相加」与「逐行取整到分再相加」的差异证据。
- type FloatTrapDemo struct {
- LegacyValues []string // 逐条 legacy money
- PerRowRounded []string // 逐条 ROUND(x,2)
- SumOfRoundedCents int64 // Σ ROUND(x,2)
- SumRoundedStr string // 上面那个值的十进制表示
- FloatSumRaw float64 // 直接用 float64 相加
- RoundOfSumStr string // ROUND(Σx, 2)(先加后取整)
- DeltaVsPerRow int64 // ROUND(Σ) 与 ΣROUND 的分差
- FloatAccumErr float64 // float64 累加误差(FloatSumRaw - 精确和)
- ExactSumThousandths int64 // 精确和(千分位整数,用于展示 float 累加误差量级)
- }
- // AutoIncrementDemo 记录「显式插 id 后 AUTO_INCREMENT 不倒退」的证据。
- //
- // 两段证据:
- //
- // A. 真实表 t_user_expense_log:显式插了 legacy id 之后,**不带 id** 插入的新行 id
- // 必须等于即时读到的 AUTO_INCREMENT,且 >= max(id)+1 —— 只要它不是 1 附近的小数,
- // 就说明计数器没有被显式插 id 弄回起点。
- // B. 临时表镜像(CREATE TEMPORARY TABLE ... LIKE,会话结束自动消失):
- // 显式插 id=100,再不带 id 插,必须得到 101,把「max+1」这条语义**可重复地**演示出来。
- //
- // 为什么要 B:MySQL 的 AUTO_INCREMENT 计数器**DELETE 之后不回退**,
- // 所以 fixture 反复跑时真实表的计数器会单调增长,真实表上「恰好 max+1」只有首次能观测到;
- // 临时表镜像每次都从空表起步,因此每次都能给出一模一样的 max+1 结论。
- type AutoIncrementDemo struct {
- MaxIDBefore int64
- AutoIncrementVal int64
- ProbeID int64
- ExpectedProbeID int64
- EqualsMaxPlusOne bool
- Pass bool
- MirrorExplicitID int64
- MirrorProbeID int64
- MirrorExpected int64
- MirrorPass bool
- }
- // FailCount 统计核对表中不相等的列数。
- func (r *VerifyReport) FailCount() int {
- n := 0
- for _, c := range r.Checks {
- if !c.Equal {
- n++
- }
- }
- for _, a := range r.Assertions {
- if !a.Pass {
- n++
- }
- }
- return n
- }
- // OK 表示整轮核对通过。
- func (r *VerifyReport) OK() bool { return r.FailCount() == 0 }
|