payroll_precision_test.go 3.7 KB

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