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)) + } } // ─── 多收件人 ───