package memory import ( "sort" "yunyan/comm" "yunyan/lego/sys/mysql" "yunyan/pb" ) /* 周期统计。 ⚠️ 金额与完成数**全部走 SQL**,绝不让 LLM 算。LLM 只负责把算好的 stat_json 组织成一段适合朗读的话——数字算错比话说得干巴严重得多。 */ // 明细条数上限。一个重度用户一周能攒两百条未完成,全塞进 prompt 会顶爆上下文、 // 拖慢生成,而且**播报出来没人听得完**。总数用 TodoUndone 表达,明细只举几条。 const statItemLimit = 20 type statBrief struct { Id uint64 `json:"id"` Title string `json:"title"` HappenDate string `json:"happen_date"` } type statExpense struct { Currency string `json:"currency"` TotalCents int64 `json:"total_cents"` Count int32 `json:"count"` } type memoryStat struct { PeriodStart string `json:"period_start"` PeriodEnd string `json:"period_end"` TodoTotal int32 `json:"todo_total"` TodoDone int32 `json:"todo_done"` TodoUndone int32 `json:"todo_undone"` AlarmTotal int32 `json:"alarm_total"` IdeaCount int32 `json:"idea_count"` UndoneItems []statBrief `json:"undone_items"` UndoneTruncated bool `json:"undone_truncated"` IdeaItems []statBrief `json:"idea_items"` IdeaTruncated bool `json:"idea_truncated"` Expenses []statExpense `json:"expenses"` } // hasContent 报告有没有值得播报的内容。全空就别生成了。 func (s *memoryStat) hasContent() bool { return s.TodoTotal > 0 || s.AlarmTotal > 0 || s.IdeaCount > 0 || len(s.Expenses) > 0 } // computeStats 按日期范围算统计(含 start 与 end 当天) func (this *Memory) computeStats(uid, start, end string) (*memoryStat, error) { st := &memoryStat{PeriodStart: start, PeriodEnd: end, UndoneItems: []statBrief{}, IdeaItems: []statBrief{}, Expenses: []statExpense{}} base := func() interface { Count(*int64) error } { return nil } _ = base type catCount struct { Category string State int32 N int64 } rows := make([]catCount, 0) err := mysql.Table(comm.TableMemoryItem). Select("category, state, count(*) as n"). Where("uid = ? and happen_date >= ? and happen_date <= ?", uid, start, end). Group("category, state").Find(&rows).Error if err != nil { return nil, err } for _, r := range rows { switch r.Category { case comm.MemoryCatTodo: st.TodoTotal += int32(r.N) if r.State == int32(pb.MemoryState_MemoryState_Done) { st.TodoDone += int32(r.N) } case comm.MemoryCatAlarm: st.AlarmTotal += int32(r.N) case comm.MemoryCatIdea: st.IdeaCount += int32(r.N) } } st.TodoUndone = st.TodoTotal - st.TodoDone // 花销按币种聚合。金额是 int64 分,SUM 不会有浮点误差。 expRows := make([]statExpense, 0) err = mysql.Table(comm.TableMemoryItem). Select("currency, sum(amount_cents) as total_cents, count(*) as count"). Where("uid = ? and happen_date >= ? and happen_date <= ? and category = ?", uid, start, end, comm.MemoryCatExpense). Group("currency").Find(&expRows).Error if err != nil { return nil, err } sort.Slice(expRows, func(i, j int) bool { return expRows[i].TotalCents > expRows[j].TotalCents }) st.Expenses = expRows // 明细:未完成待办 undone := make([]*pb.DBMemoryItem, 0) err = mysql.Table(comm.TableMemoryItem). Where("uid = ? and happen_date >= ? and happen_date <= ? and category = ? and state = ?", uid, start, end, comm.MemoryCatTodo, int32(pb.MemoryState_MemoryState_Pending)). Order("happen_date asc").Limit(statItemLimit + 1).Find(&undone).Error if err != nil { return nil, err } if len(undone) > statItemLimit { st.UndoneTruncated = true undone = undone[:statItemLimit] } for _, it := range undone { st.UndoneItems = append(st.UndoneItems, statBrief{Id: it.Id, Title: it.Title, HappenDate: it.HappenDate}) } // 明细:灵感 ideas := make([]*pb.DBMemoryItem, 0) err = mysql.Table(comm.TableMemoryItem). Where("uid = ? and happen_date >= ? and happen_date <= ? and category = ?", uid, start, end, comm.MemoryCatIdea). Order("happen_date asc").Limit(statItemLimit + 1).Find(&ideas).Error if err != nil { return nil, err } if len(ideas) > statItemLimit { st.IdeaTruncated = true ideas = ideas[:statItemLimit] } for _, it := range ideas { st.IdeaItems = append(st.IdeaItems, statBrief{Id: it.Id, Title: it.Title, HappenDate: it.HappenDate}) } return st, nil }