From 6757644756915e25ec855620fe5b8c708a6201fe Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 2 Oct 2026 10:57:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=88=A4=E6=8D=AE):=20=E5=86=9C=E5=8E=86?= =?UTF-8?q?=E6=8E=A8=E8=BF=9B=E7=9A=84=E8=B5=B7=E7=82=B9=E5=BF=85=E9=A1=BB?= =?UTF-8?q?=E5=9C=A8=E6=9C=AA=E6=9D=A5=20=E2=80=94=E2=80=94=20=E5=90=A6?= =?UTF-8?q?=E5=88=99=20advanceToFuture=20=E8=B7=B3=E8=BF=87=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E6=9C=88=E4=BB=BD=E5=BF=85=E5=81=87=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象 部署被测试闸拦下(这正是纪律该做的事):`TestAdvanceRecurrenceLunar` 报 「间隔 58 天不像一个农历月」。已确认与本次 SSE 改动无关 (把我的改动全部 stash 后它同样红)。 ## 根因(实测复算,不是推测) `AdvanceRecurrence` 走 `advanceToFuture`,职责是「推进到**未来**」: 已经过去的农历月会被跳过(`if cur.After(now) { return }` 那个循环)。 原起点写死 `2026-09-03`,而今天已是 10-02 ⇒ 那个农历月(10-02)已过去 ⇒ 循环再推一个月。探针实测: 起点 2026-09-03 ⇒ 落点 2026-10-31 间隔 58 天 ← 旧起点,今天跑必红 起点 2026-10-03 ⇒ 落点 2026-11-01 间隔 29 天 ← 明天,正确 农历库本身是对的:直接调 `AddMonths(1).ToSolar()` 得到的落点恰好 29 天。 所以**不是代码缺陷,是判据的期望依赖了「今天离起点不到一个月」**这个 随日期漂移的前提。 ## 修法 起点改为「明天」起算(不写死具体日期 ⇒ 明年跑也成立), 农历日的期望也跟着起点走。 ★ 不用 `time.Now().AddDate(0,1,0)` 那种相对写法:农历月 29/30 天不定, 起点落在月末时下一个同农历日可能被夹(commit 3459605 记的同族假红)。 明天起算同时满足「确保在未来」与「落点就是下一个农历月」。 ## ★★ 修判据时差点削弱了它(变异验证抓出来的) 把起点改成明天后判据绿了,但我立刻做变异验证: 变异:advanceToFuture 不跳过已过期月份 ⇒ TestAdvanceRecurrenceLunar **全绿** 因为起点在未来,第一个落点本来就在未来,循环与单步没有区别 —— **我修好了假红,却顺手删掉了「跳过过期」这个真行为的判别力。** 补了一格:用**已过期 70 天**的起点单独钉它,落点必须在未来, 否则「每次扫描都重复触发同一封提醒」。变异重测 ⇒ 红 1 格。 这是同一天内第二次判据自身缺陷(第一次是 SSE 那格只查文本不查控制流)。 **改判据后必须变异验证判别力还在**,否则就是用改测试掩盖问题。 --- server/internal/repo/calendar_test.go | 56 +++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/server/internal/repo/calendar_test.go b/server/internal/repo/calendar_test.go index ce117c8..502242b 100644 --- a/server/internal/repo/calendar_test.go +++ b/server/internal/repo/calendar_test.go @@ -623,7 +623,24 @@ func TestAdvanceRecurrenceLunar(t *testing.T) { setupTestDB(t) ctx := context.Background() - start := time.Date(2026, 9, 3, 9, 0, 0, 0, time.Local) + // ★ 2026-10-02:起点必须是**未来**的,否则这条判据必红(同族第三次)。 + // + // `AdvanceRecurrence` 走的是 `advanceToFuture`,它的职责是「推进到**未来**」: + // 已经过去的农历月会被跳过(`if cur.After(now) { return }` 那个循环)。 + // + // 原起点写死 `2026-09-03`,而今天已是 10-02 ⇒ 那个农历月(10-02)已过去 + // ⇒ 循环再推一个月 ⇒ 落点 10-31,间隔 **58 天**,与「一个农历月」的断言矛盾。 + // + // 实测(本次修复时跑的探针): + // 起点 2026-09-03 ⇒ 落点 2026-10-31 间隔 58 天 ← 旧起点,今天跑必红 + // 起点 2026-10-03 ⇒ 落点 2026-11-01 间隔 29 天 ← 明天,正确 + // + // 为什么不能用 `time.Now().AddDate(0,1,0)` 那种「相对现在」写法: + // 农历月 29/30 天不定,起点落在月末时下一个同农历日可能被夹 + // (见 commit 3459605 记的同族问题:那是**夹到短月**导致的另一种假红)。 + // 这里要的是「起点确保在未来,且落点就是下一个农历月」—— + // 明天起算就同时满足两者,且不依赖具体日期。 + start := time.Now().AddDate(0, 0, 1).Truncate(time.Hour).Add(9 * time.Hour) e := seedEvent(t, &models.CalendarEvent{ Title: "农历每月十五(这里用廿二)", EventTime: start, @@ -645,15 +662,46 @@ func TestAdvanceRecurrenceLunar(t *testing.T) { if !after.EventTime.After(start) { t.Errorf("event_time 未前进:%v", after.EventTime) } - // 农历日保持 - if d := lunar.FromSolar(after.EventTime); d.Day != 22 { - t.Errorf("农历日变成 %d,期望 22(%s)", d.Day, d.String()) + // 农历日保持(起点是明天,那个日就是起点的农历日) + wantLunarDay := lunar.FromSolar(start).Day + if d := lunar.FromSolar(after.EventTime); d.Day != wantLunarDay { + t.Errorf("农历日变成 %d,期望 %d(%s)", d.Day, wantLunarDay, d.String()) } // 公历间隔应在一个农历月内 gap := int(after.EventTime.Sub(start).Hours() / 24) if gap < 28 || gap > 31 { t.Errorf("间隔 %d 天不像一个农历月", gap) } + + /* + ★ 补这一格(2026-10-02):起点改成「明天」之后,判据的**判别力**下降了。 + + 实测:把 `advanceToFuture` 改成「不跳过已过期的农历月」, + `TestAdvanceRecurrenceLunar` 依然**全绿** —— 因为起点在未来, + 第一个落点本来就在未来,循环与单步没有区别。 + + 而「跳过过期」是**真行为**:`AdvanceRecurrence` 存在的意义就是 + 把过期事件推到将来(否则每次扫描都会重复触发同一封信)。 + 下面这格用一个**已过期**的起点单独钉它。 + */ + past := time.Now().AddDate(0, 0, -70) + e2 := seedEvent(t, &models.CalendarEvent{ + Title: "过期起点:必须被推到未来", + EventTime: past, + Recurrence: models.RecurLunarMonthly, + }) + if _, err := AdvanceRecurrence(ctx, e2.EventID); err != nil { + t.Fatalf("过期起点推进: %v", err) + } + after2, err := GetCalendarEvent(ctx, e2.EventID) + if err != nil { + t.Fatalf("读回: %v", err) + } + if !after2.EventTime.After(time.Now()) { + t.Errorf("★ 起点已过期时,落点 %s 仍不在未来 —— "+ + "advanceToFuture 必须跳过已过去的农历月,否则每次扫描都重复触发同一封提醒", + after2.EventTime.Format(time.RFC3339)) + } } // ─── 多收件人 ───