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
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
|
|
}
|
|
|