|
|
11 ore în urmă | |
|---|---|---|
| .. | ||
| README.md | 11 ore în urmă | |
| config.go | 11 ore în urmă | |
| ddl.go | 11 ore în urmă | |
| decimal.go | 11 ore în urmă | |
| extract.go | 11 ore în urmă | |
| load.go | 11 ore în urmă | |
| mapping.go | 11 ore în urmă | |
| mapping_test.go | 11 ore în urmă | |
| model.go | 11 ore în urmă | |
| report.go | 11 ore în urmă | |
| run.go | 11 ore în urmă | |
| verify.go | 11 ore în urmă | |
internal/academic/dualrun/fixture —— AC-15 阶段 2 前置件(资金域 4 表映射器)本包做一件事:把 legacy 的一个真实用户,按目标态口径搬到 yuansiwei_2026,再逐列核对、清理干净。
它是「阶段 2 双跑」的前置件——先证明「结构 + 映射 + 取整口径」这条链能跑通,再去比 8 个写 SP 的产出。
| 项 | 范围 |
|---|---|
| 表 | 资金域 4 表:t_user_expense / t_user_expense_school / t_user_expense_log + 解析用户所需的 t_user / t_legacy_mapping |
| 不做的 | 其余 8 张资金/活动表、课程链、8 个写 SP 的双跑比对(下一批) |
| 不碰的 | internal/academic/{service,repository,handler} 任何文件;e2e_test 库;finance_bk 只 SELECT |
# 1) 补齐 core 结构(只 CREATE TABLE IF NOT EXISTS,DDL 里的 DROP TABLE 全部丢弃)
go run ./cmd/ac15fixture ddl
# 2) 抽样搬运(默认 userid=53691)
go run ./cmd/ac15fixture map --user 53691
# 3) 逐列核对(0=PASS,1=FAIL)
go run ./cmd/ac15fixture verify --user 53691
# 4) 清理并复核 4 张表回 0 行
go run ./cmd/ac15fixture clean --user 53691
# 单测(纯映射规则,不连库)
go test ./internal/academic/dualrun/fixture/
环境变量(口令不落盘、不打印):AC15_DB_HOST/PORT/USER/PASS、AC15_TARGET_DB(默认 yuansiwei_2026)、
AC15_LEGACY_DB(默认 finance_bk)、AC15_LEGACY_USER/PASS、AC15_CORE_DDL。
未设 AC15_DB_PASS 时回退读仓库既有 config/dev.json 的 db.dsn(只读,不新增口令副本)。
守卫:host 必须本机;target 库必须在允许清单内且只允许写 yuansiwei_2026;
legacy 连接建立后立刻 SET SESSION TRANSACTION READ ONLY,写语句会被服务端拒绝。
| 文件 | 职责 |
|---|---|
config.go |
连接参数、DSN、白名单守卫、脱敏 |
decimal.go |
十进制串 ↔ 整数分(唯一金额换算通道,全程不经过 float64) |
model.go |
legacy 行 / 目标行 / 报告结构 |
mapping.go |
纯映射规则(取整、空值语义、时间、D1 id、确定性 ULID),可单测 |
extract.go |
legacy 只读查询(金额一律 CAST(... AS DECIMAL(20,3))) |
load.go |
幂等写入(ON DUPLICATE KEY UPDATE) |
verify.go |
逐列核对 + 语义断言 + float 陷阱 + AUTO_INCREMENT |
ddl.go |
把 core-model-ddl.sql 安全套到目标库(丢弃 DROP、改写 IF NOT EXISTS) |
run.go |
流程编排(PrepareAndLoad / Cleanup) |
report.go |
终端报告排版 |
mapping_test.go |
纯规则单测(含变异实验的红/绿基线) |
cmd/ac15fixture |
CLI 入口 |
t_legacy_mapping:legacy_academic_user_id(int) → canonical_user_id(VARCHAR(26) ULID)。
fixture 的 ULID 由 FixtureCanonicalUserID(legacyID) 确定性派生
(时间戳固定 2026-01-01 + 熵 = sha256("ac15-fixture:user:<id>")[:10]):
同一 legacy id 每次得到同一 ULID,于是 map/verify/clean 三步可独立执行、天然幂等。
生产迁移的 canonical_user_id 由归一 pipeline 的种子脚本发放,与此无关。
注意:流水里出现的 4 个「创建人」也各插 1 行映射 —— 否则 create_user_id 会全部退化成 NULL,
「解析成功」那一半就没被验证到。t_user_expense 用自然键 user_id(无自增 id);t_user_expense_school 用 uk_user_inst_campus
做幂等,不保留 legacy id(对齐键是业务复合键);t_user_expense_log 用 D1。INSERT ... (id, ...) VALUES (<legacy_id>, ...),同一 legacy 流水在目标侧 id 完全相同。
不从 legacy id 反算 target id。DECIMAL(12,2):逐行 ROUND(x,2),不是 ROUND(SUM,2);
读侧 CAST(x AS DECIMAL(20,3)) 拿十进制串,写侧把整数分格式化成 "-2160.49" 交给驱动,全程无 float。DATETIME(3):legacy datetime(0) 秒级原样保留,毫秒补 .000,不使用当前时间。create_user_id:legacy 0 / NULL / 无映射 → NULL(不是 0、不是空串)request_id:legacy 无此列 → NULLbalance_after:legacy 无快照 → NULL0.00,但必须产生告警(NULL ≠ 0,业务上是两件事)remark text → varchar(1000):按字符截断(不切半个汉字),超长产生告警。本 fixture 最长 271 字符,未触发。核对不用本包的映射代码产生期望值——否则映射写错时两边一起错,核对永远是绿的。
期望值一律由 MySQL 服务端算:CAST(x AS DECIMAL(12,2))、DATE_FORMAT(t,'%Y-%m-%d %H:%i:%s.%f')。
于是 Go 侧的取整/换算只要与 DECIMAL 语义不一致,核对立刻红;变异实验(故意改错映射)必然红。
information_schema.tables.AUTO_INCREMENT 是缓存值:MySQL 8.0 默认
information_schema_stats_expiry = 86400,读出来是 1。必须
SET SESSION information_schema_stats_expiry = 0 且 SET 与 SELECT 走同一条连接(连接池会换连接)。DELETE 不回退 AUTO_INCREMENT 计数器:fixture 反复跑时计数器单调增长,
真实表上「恰好 max+1」只有首次能观测到;因此另配 CREATE TEMPORARY TABLE ... LIKE 镜像,
每次都能可重复地演示「显式插 id=100 → 下一条 101」。SHOW TABLE STATUS LIKE ? 不支持占位符(Error 1064),表名只能内联(已加字符白名单校验)。t_user_expense_log.uk_request_type(request_id, type):若把 request_id 落成空串而不是 NULL,
同 type 的多条历史流水会撞唯一键(Error 1062)。NULL 不参与唯一键比较,这才是历史数据的正确落法。t_user_expense_log 里 create_userid = 0 或 NULL 的行数 = 0(90007 行全都有真实创建人)。
所以「0 → NULL」这条规则无法用真实数据证明,只能由 mapping_test.go 单测 + 变异实验 M3 证明。t_user_expense.balance/spend/total_income/total_expenditure 是 float(10,3),
t_user_expense_school.* 与 t_user_expense_log.money 是 decimal(10,3)——同一业务列两边类型不同,
所以读侧统一 CAST(... AS DECIMAL(20,3)),不按列假设类型。t_user_expense_school.schoolid 直迁为 institution_id(本 fixture 取到 63317),
org 域(机构/校区)映射属另一条链路,本批不涉及。