|
@@ -0,0 +1,82 @@
|
|
|
|
|
+// 教龄列精度口径锁(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
|
|
|
|
|
+}
|