| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182 |
- // 教龄列精度口径锁(P0 回归门禁)——锁住 `teacher_exp_min/max = DECIMAL(6,3)` 这个**业务决定**。
- //
- // ---------------------------------------------------------------------------
- // 为什么需要一把专门的"口径锁"(而不是靠 `TestModelsColumnByColumn`)
- // ---------------------------------------------------------------------------
- //
- // `TestModelsColumnByColumn` 断言的是「struct tag ↔ DDL fixture **一致**」。
- // 它抓得住"只改了一边"(改 DDL 忘改 tag),但抓不住"**两边一起改回 (6,2)**"——
- // 那时两边仍然一致,门禁**依然是绿的**,而精度已经悄悄收敛了。
- //
- // 而精度在这里**不是格式偏好,是语义**(P0,架构师-2 AC-10 审计):
- // - 源列 `teacher_exp_min/max` = `float(10,3)`,dump 首行实测 `0.000 / 0.999`;
- // - 收敛到 2 位后 `0.999 → 1.00` ⇒ 区间 `[0.000, 1.000]` **开始包含教龄 1.0**;
- // - 下一档 `[1.000, 1.999]` 也含 1.0 ⇒ **两档并列命中** ⇒ 按 `ORDER BY grade_id DESC,
- // subject_id DESC, id ASC` 取到**低档** ⇒ 课酬按错档算(D7 双跑必红,代码却"看起来没错")。
- //
- // 所以本文件把"3 位"这一决定**钉死在两个独立位置**(DDL 文本 + struct tag),
- // 任何一边被改回 2 位都会红;两边一起改也**照样红**(这正是 `TestModelsColumnByColumn` 的盲区)。
- //
- // 自证失败(变异实测,见交付报告):
- // - 只改 tag → 本文件红 + TestModelsColumnByColumn 红;
- // - 只改 DDL fixture → 本文件红 + TestModelsColumnByColumn 红 + 指纹门禁红;
- // - **tag 与 DDL 一起改成 (6,2)** → TestModelsColumnByColumn **绿**(盲区),而**本文件红**。
- package model
- import (
- "os"
- "regexp"
- "strings"
- "testing"
- )
- // teacherExpPrecisionPattern DDL 里两列声明的精确形态(列名 + DECIMAL 精度)。
- //
- // 用"声明行"而不是全局搜 `DECIMAL(6,3)`:避免把注释里提到 `decimal(6,3)` 的说明文字也算命中
- // (否则把列改回 (6,2)、只在注释里留一句 "6,3" 就能骗过门禁)。
- var teacherExpPrecisionPattern = map[string]*regexp.Regexp{
- "teacher_exp_min": regexp.MustCompile("(?i)^\\s*`teacher_exp_min`\\s+DECIMAL\\(6,3\\)"),
- "teacher_exp_max": regexp.MustCompile("(?i)^\\s*`teacher_exp_max`\\s+DECIMAL\\(6,3\\)"),
- }
- // TestPayrollTeacherExpPrecisionLocked 教龄两列必须是 `DECIMAL(6,3)`(DDL 文本 + struct tag 双锁)。
- func TestPayrollTeacherExpPrecisionLocked(t *testing.T) {
- raw, err := os.ReadFile(ddlFixturePath)
- if err != nil {
- t.Fatalf("读 DDL fixture 失败: %v", err)
- }
- lines := strings.Split(string(lfNormalize(raw)), "\n")
- for column, pattern := range teacherExpPrecisionPattern {
- found := false
- for _, line := range lines {
- if pattern.MatchString(line) {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("DDL fixture 中 `%s` 不是 DECIMAL(6,3):精度收敛会把 0.999 舍成 1.00,"+
- "使相邻教龄档并列命中、课酬按错档算(P0,见本文件头)", column)
- }
- }
- // struct tag 侧同样锁死(DDL 与 tag 必须**都**是 3 位;只锁 DDL 会漏掉"tag 单独被改回"的路径)
- tag := string(mustReadFile(t, "models_payroll.go"))
- for _, col := range []string{"teacher_exp_min", "teacher_exp_max"} {
- want := "column:" + col + ";type:decimal(6,3)"
- if !strings.Contains(tag, want) {
- t.Errorf("models_payroll.go 缺少 `%s`(教龄列 tag 必须是 decimal(6,3))", want)
- }
- }
- }
- // mustReadFile 读包目录下的源码文件(仅本文件用)。
- func mustReadFile(t *testing.T, name string) []byte {
- t.Helper()
- raw, err := os.ReadFile(name)
- if err != nil {
- t.Fatalf("读 %s 失败: %v", name, err)
- }
- return raw
- }
|