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.
188 lines
6.4 KiB
188 lines
6.4 KiB
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
|
|
}
|
|
|
|
// countRepeatAlarmExtras 重复闹钟在周期内比「库里那一行」多出来的次数。
|
|
//
|
|
// 日期解析不了的项跳过(happen_date 是 date 列,正常不会出现,但别为一行坏数据
|
|
// 让整份报告失败)。周期最长一个月,逐日判定至多 31 次,成本可以忽略。
|
|
func (this *Memory) countRepeatAlarmExtras(uid, start, end string) (int, error) {
|
|
s, ok1 := comm.ParseMemoryDate(start)
|
|
e, ok2 := comm.ParseMemoryDate(end)
|
|
if !ok1 || !ok2 {
|
|
return 0, nil
|
|
}
|
|
items := make([]*pb.DBMemoryItem, 0)
|
|
if err := comm.ApplyRepeatCandidates(mysql.Table(comm.TableMemoryItem), uid, end,
|
|
[]string{comm.MemoryCatAlarm}).Find(&items).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
total := 0
|
|
for _, it := range items {
|
|
anchor, ok := comm.ParseMemoryDate(it.HappenDate)
|
|
if !ok {
|
|
continue
|
|
}
|
|
total += comm.MemoryExtraOccurrences(it.RepeatRule, it.Weekday, anchor, s, e)
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// computeStats 按日期范围算统计(含 start 与 end 当天)
|
|
func (this *Memory) computeStats(uid, start, end string) (*memoryStat, error) {
|
|
// ⚠️ 入参可能来自 memory_report 读回来的 period_start/period_end,那是 `type:date`
|
|
// 列 + parseTime=True,读成 string 是 `2026-09-07T00:00:00+08:00`。
|
|
// 下面的 SQL 靠 MySQL 转换还能对,但 Go 侧解析(重复项展开)会静默失败,
|
|
// 所以在入口先收敛一次;顺带让 stat_json 里的周期也是干净的日期,
|
|
// 那份 JSON 是要喂给 LLM 写播报文案的。
|
|
start, end = comm.NormalizeMemoryDate(start), comm.NormalizeMemoryDate(end)
|
|
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
|
|
|
|
// 重复闹钟按周期内实际响了几次算(见 comm.MemoryExtraOccurrences):
|
|
// 一条 daily 闹钟在库里只有一行、锚点可能在几个月前,上面那句 group by 数不到它,
|
|
// 于是「这周被闹钟叫了多少次」恒为 0。只补闹钟,别的分类不展开,理由在 comm 里写了。
|
|
if n, e := this.countRepeatAlarmExtras(uid, start, end); e != nil {
|
|
// 展开失败不算整体失败:宁可少算这一项,也不要让整份报告生成不出来
|
|
this.Warnf("周期统计 uid:%s 展开重复闹钟失败已忽略: %v", uid, e)
|
|
} else {
|
|
st.AlarmTotal += int32(n)
|
|
}
|
|
|
|
// 花销按币种聚合。金额是 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: comm.NormalizeMemoryDate(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: comm.NormalizeMemoryDate(it.HappenDate)})
|
|
}
|
|
return st, nil
|
|
}
|
|
|