7636454650
## 只砍入口,后端一行不动
10 个 AI 路由、service/ai_*.go、后台配额配置页、ai_usages / ai_messages
两张表全部保留。api.js 里那 5 个方法注释掉而不是删——想开回来解注释、
把入口接上就行,不用重写。
## 5 处露出
pages/ai 聊天页 删页面 + 从 app.json 移除
首页「AI 今日建议」卡 整张删(含「问问 AI 养宠助手」按钮)
计划页「AI 计划」tab 删 tab + wxml 分支 + ?tab 深链;只剩「路线图」一项后
整条 seg-tabs 也藏了——单选的 tab 条是个没用的控件
异常观察的 AI 风险评估 见下
引导页「AI 日历」文案 改成「养护日历」
## 异常观察这一处差点做错
原链路是:symptom 只收集表单 → AI 风险评估 → 用户在 risk 页再点保存 →
buildRecord 的 case 'risk' 才真正落库。**symptom 自己从来不落库。**
拆掉中间环节如果只删 risk,结果就是「记了异常但没存下来」,而且不报错。
补了 case 'symptom' 让它自己存。连带发现第二个问题:category 存的是 AI 给的
风险等级,而 report.go:46 按 category='高' 统计周报的高风险数——不写这个字段
周报会永远是 0。改成让用户自己选严重程度(轻微/需留意/严重 → 低/中/高):
谁看着它谁最清楚,比规则化猜一个准,还顺手保住了周报。
## FAB
首页 → 打开 24 项分组选择器(弹层新增 quickRecord 分支,
点某一项在同一个弹层内 setType 切过去,不关不跳)
社区 → 改成发帖(它本来就不该是记录入口)
记录/报告/我的/计划/学习 → 直接去掉,底部留白从 pad-b-fab 换成 pad-b-plain,
不然白留 330rpx
fab 组件原来图标写死成 ai,加了 icon 属性——按钮干什么事图标就得是什么。
顺手删了 settings.js 里一个死的 onFab(Phase A 拆页时漏的,页面上根本没有 fab)。
.tg 分组样式从 record.wxss 提到 app.wxss:记录页和快速记录弹层都在用,
页面级 wxss 跨不了页(这个项目已经栽过三次)。
## 顺手修了周报两个先前就有的 bug
验证时撞上的,和 AI 无关,但 AI 建议卡拆掉后周报权重变高了:
1. 摘要把「无高风险异常记录」写死,和它自己刚算出来的 highRisk 自相矛盾——
记了 2 条高风险,摘要还说没有
2. next_week_focus 是一整句静态文案「第 2 针疫苗提醒、继续观察体重趋势、
避免频繁更换食物」,不管谁的宠物多大年纪都是这句,而「第 2 针疫苗」
对成年猫狗根本不适用。改成按真实数据拼:未来 7 天到期的提醒 +
有高风险就提就医 + 一周没称体重就提醒补记
## 验证(预生产库实跑)
异常观察三档 低/中/高 各存一条,category 正确落库
周报 summary「有 2 条高风险异常记录」,不再自相矛盾
next_week_focus 有高风险的宠物 → 「继续观察上周记录的异常,必要时就医」
新建幼犬(提醒都在 7 天外、没称过体重)→ 「本周还没称体重,补记一次」
症状聚类洞察 仍然工作(insight.go 按 symptom 过滤,没受影响)
全站 grep 无 pages/ai / onFab / riskData / onGenRisk 残留
FAB 只剩首页(plus)和社区(edit)两处
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
7.8 KiB
Go
228 lines
7.8 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/sundynix/pets-be/internal/model"
|
||
)
|
||
|
||
// WeeklyReport 周报聚合
|
||
type WeeklyReport struct {
|
||
Summary string `json:"summary"`
|
||
TasksCompleted int64 `json:"tasks_completed"`
|
||
WeightGain float64 `json:"weight_gain"`
|
||
HighRiskCount int64 `json:"high_risk_count"`
|
||
HealthStatus string `json:"health_status"`
|
||
NextWeekFocus string `json:"next_week_focus"`
|
||
}
|
||
|
||
// GetWeeklyReport 计算最近 7 天周报
|
||
func (s *Service) GetWeeklyReport(userID, petID string) (*WeeklyReport, error) {
|
||
pet, err := s.ownedPet(userID, petID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
weekAgo := time.Now().AddDate(0, 0, -7)
|
||
|
||
var tasksCompleted int64
|
||
s.db.Model(&model.DailyTask{}).
|
||
Where("pet_id = ? AND done = ? AND updated_at >= ?", petID, true, weekAgo).
|
||
Count(&tasksCompleted)
|
||
|
||
// 体重增长:最近 7 天最新 - 最早
|
||
var latest, earliest model.HealthRecord
|
||
gain := 0.0
|
||
if err := s.db.Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).
|
||
Order("occurred_at desc").First(&latest).Error; err == nil {
|
||
if err := s.db.Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).
|
||
Order("occurred_at asc").First(&earliest).Error; err == nil {
|
||
gain = latest.NumValue - earliest.NumValue
|
||
}
|
||
}
|
||
|
||
var highRisk int64
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Where("pet_id = ? AND type = ? AND category = ? AND occurred_at >= ?", petID, model.RecordSymptom, "高", weekAgo).
|
||
Count(&highRisk)
|
||
|
||
status := "稳定成长"
|
||
risk := "无高风险异常记录"
|
||
if highRisk > 0 {
|
||
status = "需要关注"
|
||
// 原来这句写死成「无高风险异常记录」,和上面刚算出来的 highRisk 自相矛盾:
|
||
// 记了 3 条高风险,摘要还说没有
|
||
risk = fmt.Sprintf("有 %d 条高风险异常记录", highRisk)
|
||
}
|
||
|
||
return &WeeklyReport{
|
||
Summary: fmt.Sprintf("本周 %s 完成 %d 项任务,体重 %+.1fkg,%s。",
|
||
pet.Name, tasksCompleted, gain, risk),
|
||
TasksCompleted: tasksCompleted,
|
||
WeightGain: gain,
|
||
HighRiskCount: highRisk,
|
||
HealthStatus: status,
|
||
NextWeekFocus: s.nextWeekFocus(petID, highRisk, gain),
|
||
}, nil
|
||
}
|
||
|
||
// nextWeekFocus 下周重点。原来是一句写死的「第 2 针疫苗提醒、继续观察体重趋势、
|
||
// 避免频繁更换食物」——不管谁的宠物、多大年纪、有没有异常,看到的都是这句,
|
||
// 而且「第 2 针疫苗」对成年猫狗根本不适用。改成按真实数据拼。
|
||
func (s *Service) nextWeekFocus(petID string, highRisk int64, gain float64) string {
|
||
var parts []string
|
||
|
||
// 未来 7 天到期的提醒,是最具体的「下周要做什么」
|
||
now := time.Now()
|
||
var rems []model.Reminder
|
||
s.db.Where("pet_id = ? AND next_due_date >= ? AND next_due_date <= ?",
|
||
petID, now, now.AddDate(0, 0, 7)).Order("next_due_date asc").Limit(2).Find(&rems)
|
||
for _, r := range rems {
|
||
parts = append(parts, r.Title)
|
||
}
|
||
|
||
if highRisk > 0 {
|
||
parts = append(parts, "继续观察上周记录的异常,必要时就医")
|
||
}
|
||
|
||
// 一周没称过体重(gain 恰好为 0 也可能是真没变,所以查条数而不是看 gain)
|
||
var weighed int64
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, now.AddDate(0, 0, -7)).
|
||
Count(&weighed)
|
||
if weighed == 0 {
|
||
parts = append(parts, "本周还没称体重,补记一次")
|
||
}
|
||
|
||
if len(parts) == 0 {
|
||
return "各项都稳定,保持现在的记录节奏就好。"
|
||
}
|
||
return strings.Join(parts, ";") + "。"
|
||
}
|
||
|
||
// BillCategory 账单分类项
|
||
type BillCategory struct {
|
||
Category string `json:"category"`
|
||
Amount float64 `json:"amount"`
|
||
Percent int `json:"percent"`
|
||
}
|
||
|
||
// Bill 账单聚合
|
||
type Bill struct {
|
||
Period string `json:"period"`
|
||
Total float64 `json:"total"`
|
||
MaxSingle float64 `json:"max_single"`
|
||
Categories []BillCategory `json:"categories"`
|
||
}
|
||
|
||
// GetBill 账单(period=month 取本月)
|
||
func (s *Service) GetBill(userID, petID string, period string) (*Bill, error) {
|
||
if _, err := s.ownedPet(userID, petID); err != nil {
|
||
return nil, err
|
||
}
|
||
now := time.Now()
|
||
var start time.Time
|
||
if period == "year" {
|
||
start = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
|
||
} else {
|
||
period = "month"
|
||
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||
}
|
||
|
||
type row struct {
|
||
Category string
|
||
Amount float64
|
||
}
|
||
var rows []row
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Select("category, sum(num_value) as amount").
|
||
Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordCost, start).
|
||
Group("category").Scan(&rows)
|
||
|
||
var total, maxSingle float64
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Select("coalesce(sum(num_value),0)").
|
||
Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordCost, start).
|
||
Scan(&total)
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Select("coalesce(max(num_value),0)").
|
||
Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordCost, start).
|
||
Scan(&maxSingle)
|
||
|
||
cats := make([]BillCategory, 0, len(rows))
|
||
for _, r := range rows {
|
||
pct := 0
|
||
if total > 0 {
|
||
pct = int(r.Amount / total * 100)
|
||
}
|
||
cats = append(cats, BillCategory{Category: r.Category, Amount: r.Amount, Percent: pct})
|
||
}
|
||
return &Bill{Period: period, Total: total, MaxSingle: maxSingle, Categories: cats}, nil
|
||
}
|
||
|
||
// HealthSummary 健康摘要
|
||
type HealthSummary struct {
|
||
VaccineProgress string `json:"vaccine_progress"`
|
||
DewormStatus string `json:"deworm_status"`
|
||
WeightTrend string `json:"weight_trend"`
|
||
AnomalyCount int64 `json:"anomaly_count"`
|
||
}
|
||
|
||
// GetHealthSummary 健康摘要聚合
|
||
func (s *Service) GetHealthSummary(userID, petID string) (*HealthSummary, error) {
|
||
if _, err := s.ownedPet(userID, petID); err != nil {
|
||
return nil, err
|
||
}
|
||
var vaccineDone int64
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineDone)
|
||
|
||
dewormStatus := "暂无计划"
|
||
var dewormReminder model.Reminder
|
||
if err := s.db.Where("pet_id = ? AND type = ?", petID, model.ReminderDeworm).Order("next_due_date asc").First(&dewormReminder).Error; err == nil && dewormReminder.NextDueDate != nil {
|
||
dewormStatus = "下次 " + dewormReminder.NextDueDate.Format("1月2日")
|
||
}
|
||
|
||
var anomalies int64
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordSymptom).Count(&anomalies)
|
||
|
||
return &HealthSummary{
|
||
VaccineProgress: fmt.Sprintf("%d/3,即将到期", vaccineDone),
|
||
DewormStatus: dewormStatus,
|
||
WeightTrend: "稳定增长",
|
||
AnomalyCount: anomalies,
|
||
}, nil
|
||
}
|
||
|
||
// Poster 成长海报数据
|
||
type Poster struct {
|
||
PetName string `json:"pet_name"`
|
||
PetEmoji string `json:"pet_emoji"`
|
||
Age string `json:"age"`
|
||
Weight string `json:"weight"`
|
||
Stage string `json:"stage"`
|
||
TasksCompleted int64 `json:"tasks_completed"`
|
||
WeightRecords int64 `json:"weight_records"`
|
||
VaccineRecords int64 `json:"vaccine_records"`
|
||
HighRiskCount int64 `json:"high_risk_count"`
|
||
Headline string `json:"headline"`
|
||
}
|
||
|
||
// GetPoster 生成海报聚合数据
|
||
func (s *Service) GetPoster(userID, petID string) (*Poster, error) {
|
||
pet, err := s.ownedPet(userID, petID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var tasksDone, weightRecs, vaccineRecs int64
|
||
s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND done = ?", petID, true).Count(&tasksDone)
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordWeight).Count(&weightRecs)
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineRecs)
|
||
|
||
return &Poster{
|
||
PetName: pet.Name, PetEmoji: pet.Emoji, Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage,
|
||
TasksCompleted: tasksDone, WeightRecords: weightRecs, VaccineRecords: vaccineRecs,
|
||
HighRiskCount: 0, Headline: "稳定成长",
|
||
}, nil
|
||
}
|