Files
MailUI4Agents/server/internal
JianFeeeee dc7bf57ceb fix(calendar): 农历提醒按本地公历日推进,修正凌晨跨 UTC 日期错一天
# 现象

全量服务端测试稳定失败:

    --- FAIL: TestStaleLunarRecurringDoesNotFlood
        calendar_test.go:869: 农历日从 21 变成 20

不是随机失败,也不是测试写错 —— 是产品逻辑的真实缺陷。

# 根因

SQLite 的 DSN 带 `_timezone=UTC`(为了让 `expires_at > NOW()` 这类字符串比较
同一时间轴,见 db.sqliteDSN 的注释)。因此从库里 Scan 出来的 `event_time` 是
UTC 时刻的表示。

对公历重复规则,这无关紧要 —— `AddDate` 操作的是同一时刻的另一种表示。
但**农历换算直接读取 Year/Month/Day**:

    本地 2025-09-12 07:00 (+0800) → 存库 → 读出 UTC 2025-09-11 23:00
    农历(本地) = 七月廿一        → 农历(UTC 字段) = 七月二十   ← 少一天

后果:在本地时间 0:00–8:00(+0800)创建的农历提醒,之后每次推进都按前一天
计算,日期永久偏一天;而且只有等到下一次该提醒时才暴露,没有任何报错。

# 修法

在 `AdvanceRecurrence` 里,仅对两条农历规则把 event_time 转回 `time.Local`
再交给 `NextOccurrence`。

只转农历规则而不是无条件转:公历规则不需要,且 UTC 与 Local 表示同一时刻,
`AddDate` 在两者上结果相同 —— 无条件转会掩盖「DSN 时间是 UTC」这个事实,
让后来者更难判断该在哪一层做时区处理。

# 测试

新增 `TestAdvanceRecurrenceLunarUsesLocalCalendarDay`,用**固定日期**
(2025-09-12 07:00 本地)而不是 `time.Now()`,因此任何时刻跑都稳定;
并且它先断言测试前提成立:

  - 库里读回的时刻确实与输入跨了不同公历日
  - 直接按 UTC 字段做农历换算确实会得到不同的农历日

前提不成立就直接 Fatal —— 否则这个用例可能在某个时区/时段下变成永远通过的
空壳(那正是它要防的那类假绿)。

# 验证

- 新用例与原有的两条农历用例 ×10 连跑全绿(`-count=10`)
- `go vet ./...` 干净;`go test ./... -count=1` 全量通过
- 修复前该用例 5/5 失败,修复后 10/10 通过
2026-09-12 08:01:59 +08:00
..