You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

134 lines
4.4 KiB

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
}