package memory import ( "sort" "time" "yunyan/comm" "yunyan/pb" ) /* 提醒时刻的计算与展开。 设计文档 v0.1 写的是「remind_at 服务端算好、客户端直接用」。那对一次性项成立, 对重复项**不成立**:一条 daily 闹钟只有一行记录、一个时刻字段,谁在它过去之后 把它推到明天?所以这里的口径是: - next_remind_at 只存「下一次」,供服务端侧索引用; - memory_upcoming 返回时按 repeat_rule **就地展开**成未来 N 天的绝对时刻列表, 不落库。客户端拿到一串绝对时刻,直接排通知。 时区口径:单条记忆项按它自己的 tz 算(录入时记下的),**不跟着人走**。 「今天有哪些事」是视图问题可以跟着人走,「明早 8 点响」是承诺问题不能。 */ // itemLocation 取这条记忆项该用的时区 func itemLocation(item *pb.DBMemoryItem, fallbackTz string) *time.Location { if item.Tz != "" { return comm.LoadMemoryLocation(item.Tz) } return comm.LoadMemoryLocation(fallbackTz) } // happenAt 算出这条项在某一天的发生时刻。没有 happen_time 的按当天 9:00 计 // (全天事项也得有个播报时间点,否则 0:00 提醒等于半夜吵人)。 func happenAt(item *pb.DBMemoryItem, day time.Time, loc *time.Location) time.Time { h, m, ok := comm.ParseMemoryClock(item.HappenTime) if !ok { h, m = 9, 0 } return time.Date(day.Year(), day.Month(), day.Day(), h, m, 0, 0, loc) } // recalcRemind 重算 next_remind_at(新增与修改后都要调)。 // 算不出或已过期的一律置 0——0 表示「没有下一次」,不是「1970 年」。 func (this *Memory) recalcRemind(item *pb.DBMemoryItem) { item.NextRemindAt = 0 if item.RemindAhead <= 0 || item.State != pb.MemoryState_MemoryState_Pending { return } // 灵感与花销是「记下来」不是「提醒我」,不排提醒 if item.Category == comm.MemoryCatIdea || item.Category == comm.MemoryCatExpense { return } loc := itemLocation(item, "") slots := expandItem(item, time.Now().In(loc), 366, loc) if len(slots) > 0 { item.NextRemindAt = slots[0].RemindAt } } // expandItem 把一条记忆项在 [from, from+days) 内展开成提醒时刻。 // 只返回还没过去的(严格晚于 from)。 func expandItem(item *pb.DBMemoryItem, from time.Time, days int, loc *time.Location) []*pb.MemoryRemindSlot { out := make([]*pb.MemoryRemindSlot, 0, 4) if item.RemindAhead <= 0 || item.State != pb.MemoryState_MemoryState_Pending { return out } if item.Category == comm.MemoryCatIdea || item.Category == comm.MemoryCatExpense { return out } anchor, ok := comm.ParseMemoryDate(item.HappenDate) if !ok { return out } anchor = time.Date(anchor.Year(), anchor.Month(), anchor.Day(), 0, 0, 0, 0, loc) ahead := time.Duration(item.RemindAhead) * time.Minute rule := item.RepeatRule if rule == "" { rule = comm.MemoryRepeatOnce } appendSlot := func(day time.Time) { hap := happenAt(item, day, loc) rem := hap.Add(-ahead) if !rem.After(from) { return } out = append(out, &pb.MemoryRemindSlot{ ItemId: item.Id, RemindAt: rem.Unix(), HappenAt: hap.Unix(), Title: item.Title, Detail: item.Detail, Category: item.Category, }) } if rule == comm.MemoryRepeatOnce { appendSlot(anchor) return out } // 重复项:从今天与锚点日的较晚者开始逐日试 start := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, loc) if anchor.After(start) { start = anchor } for i := 0; i < days; i++ { day := start.AddDate(0, 0, i) if comm.MemoryRepeatHits(rule, item.Weekday, day, anchor) { appendSlot(day) } } return out } // expandAll 把一批记忆项展开并按时间排序。 // // ⚠️ 重复项要**按天合并**:一条 daily 闹钟展开 7 天就吃掉 7 个通知名额, // 而 iOS 只允许 64 条待处理通知,三条日常闹钟就占掉一半。这里不做合并, // 而是交给客户端按 remind_at 取前 N 条——但顺序必须是升序,否则客户端截断 // 会把明天的排掉、留下一周后的。 func expandAll(items []*pb.DBMemoryItem, days int, fallbackTz string) []*pb.MemoryRemindSlot { all := make([]*pb.MemoryRemindSlot, 0, len(items)*2) now := time.Now() for _, it := range items { loc := itemLocation(it, fallbackTz) all = append(all, expandItem(it, now.In(loc), days, loc)...) } sort.Slice(all, func(i, j int) bool { return all[i].RemindAt < all[j].RemindAt }) return all }