// 教龄换算单测(PHP `utils.php:924-935` `calculatingAge()` 口径)。 // // 三条口径各有一组断言,**任一条写错都会让课酬"稳定地跨错档且不报错"**: // // ① 除数固定 365(不是 365.25、不是年份相减) // ② 四舍五入到 1 位小数(不是 3 位、不是截断) // ③ 空 / 非法 / 未来日期 → 0 // // 为什么用固定 `now`:真实 `time.Now()` 会让断言依赖运行时刻(跨零点跑两次结果不同), // 而教龄恰好要在"整数年边界"上稳定 —— 边界值必须可复现。 package service import ( "testing" "time" ) // dateUTC 构造 UTC 日期(只取年月日,与 `t_teacher_information.teacher_exp` 的 DATE 语义一致)。 func dateUTC(year int, month time.Month, day int) time.Time { return time.Date(year, month, day, 0, 0, 0, 0, time.UTC) } // ptrTime 时间指针(可空列用)。 func ptrTime(t time.Time) *time.Time { return &t } // TestCalculatingAgeYearsMatchesPHP 教龄换算逐值断言(固定 now,边界可复现)。 func TestCalculatingAgeYearsMatchesPHP(t *testing.T) { now := dateUTC(2026, time.November, 9) cases := []struct { name string exp *time.Time want float64 }{ {"nil 起算日 → 0", nil, 0}, {"零值起算日 → 0", ptrTime(time.Time{}), 0}, {"恰好 365 天 → 1.0", ptrTime(now.AddDate(0, 0, -365)), 1.0}, {"恰好 730 天 → 2.0", ptrTime(now.AddDate(0, 0, -730)), 2.0}, // ① 除数口径:313 天 = 0.8575 年 → 0.9。若按"年份相减"会得 **1.0**(2025-12-31 → 2026-11-09) {"313 天(跨年但不足一年)→ 0.9 而非 1.0", ptrTime(dateUTC(2025, time.December, 31)), 0.9}, // ② 小数位口径:10 天 = 0.0274 年 → **0.0**(按 3 位会是 0.027,按截断也是 0.0 但 100 天会分叉) {"10 天 → 0.0(1 位小数舍入)", ptrTime(now.AddDate(0, 0, -10)), 0}, {"100 天 → 0.3(0.274 → 0.3)", ptrTime(now.AddDate(0, 0, -100)), 0.3}, {"400 天 → 1.1(1.0959 → 1.1)", ptrTime(now.AddDate(0, 0, -400)), 1.1}, // ③ 未来日期 → 0(PHP 的负时间戳分支) {"未来日期 → 0", ptrTime(now.AddDate(0, 0, 30)), 0}, } for _, tc := range cases { got := calculatingAgeYears(tc.exp, now) if got != tc.want { t.Errorf("%s:calculatingAgeYears = %v,期望 %v", tc.name, got, tc.want) } } } // ⚠ **本测试覆盖不到的一处,写在最前面免得被误解为"已覆盖"**: // 除数取 `365` 还是 `365.25`,在 `round(x,1)` 之下**对现实教龄区间不可观测**—— // 两者每天只差 1.88e-6 年,要拉出 0.05 年的差异需 26000+ 天(70 年以上)才可能跨舍入边界。 // 我们仍写 `365` 的唯一理由是**逐字照抄 PHP**(`Utils.php:930`),不是因为有断言能抓到; // 若有人改成 365.25,现有用例**不会红**——记在这里,不假装有防线。 // 真正可观测、且已被用例钉住的是:① 不得用年份相减(见下一条)、② 必须舍入到 1 位、③ 空/未来 → 0。 // TestCalculatingAgeYearsUses365NotCalendar 专门钉住"不得用年份相减"。 // // 同一个起算日,两种算法给出**不同档位**(0.9 vs 1.0),而 `teacher_exp_min=1.000` 的规则 // 只在后者命中 ⇒ 写错口径会**稳定地跨错档**(且课酬算错、无任何报错)。 func TestCalculatingAgeYearsUses365NotCalendar(t *testing.T) { now := dateUTC(2026, time.November, 9) exp := dateUTC(2025, time.December, 31) // 日历相减:2026-2025 = 1 calendarYears := float64(now.Year() - exp.Year()) got := calculatingAgeYears(ptrTime(exp), now) if got == calendarYears { t.Fatalf("教龄 = %v,与「年份相减」结果相同:说明没按 365 天口径算(本例应为 0.9,年份相减是 1.0)", got) } if got != 0.9 { t.Fatalf("教龄 = %v,期望 0.9(313 天 / 365,四舍五入 1 位)", got) } } // TestRoundToDecimal 舍入辅助的边界(0.5 进位按 PHP round 的四舍五入)。 func TestRoundToDecimal(t *testing.T) { cases := []struct{ in, want float64 }{ {0.5479, 0.5}, {0.55, 0.6}, {1.0959, 1.1}, {2.04, 2.0}, {9.96, 10.0}, } for _, tc := range cases { if got := roundToDecimal(tc.in, 1); got != tc.want { t.Errorf("roundToDecimal(%v,1) = %v,期望 %v", tc.in, got, tc.want) } } }