72c5308214
GetPoster 用 ownedPet 只填了 Age,头像 URL 没解析,导致 pet_avatar_url 恒为空、海报总是退回 emoji。改成显式 s.fileURL(pet.AvatarFileID)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
241 lines
8.8 KiB
Go
241 lines
8.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)
|
||
|
||
// 任务已并成 PlanTask(DailyTask 表已空),要经 plan 关联到宠物来统计本周完成数
|
||
var tasksCompleted int64
|
||
s.db.Model(&model.PlanTask{}).
|
||
Where("plan_id IN (SELECT id FROM sundynix_plans 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"`
|
||
PetAvatarURL string `json:"pet_avatar_url"` // 上传过照片就有,海报优先用它,没有才用 emoji
|
||
Age string `json:"age"`
|
||
Weight string `json:"weight"`
|
||
Stage string `json:"stage"`
|
||
Period string `json:"period"` // 报告周期,如 "07.25 - 07.31"
|
||
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
|
||
}
|
||
// 这是周报告,所有统计按近 7 天算(和报告页「本周概览」口径一致)
|
||
now := time.Now()
|
||
weekAgo := now.AddDate(0, 0, -7)
|
||
|
||
var tasksDone, weightRecs, vaccineRecs, highRisk int64
|
||
s.db.Model(&model.PlanTask{}).
|
||
Where("plan_id IN (SELECT id FROM sundynix_plans WHERE pet_id = ?) AND done = ? AND updated_at >= ?", petID, true, weekAgo).
|
||
Count(&tasksDone)
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).Count(&weightRecs)
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordVaccine, weekAgo).Count(&vaccineRecs)
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND category = ? AND occurred_at >= ?", petID, model.RecordSymptom, "高", weekAgo).Count(&highRisk)
|
||
|
||
// ownedPet 只填了 Age,头像 URL 得在这里从 file id 解析(否则是空的)
|
||
return &Poster{
|
||
PetName: pet.Name, PetEmoji: pet.Emoji, PetAvatarURL: s.fileURL(pet.AvatarFileID),
|
||
Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage,
|
||
Period: weekAgo.Format("01.02") + " - " + now.Format("01.02"),
|
||
TasksCompleted: tasksDone, WeightRecords: weightRecs, VaccineRecords: vaccineRecs,
|
||
HighRiskCount: highRisk, Headline: "稳定成长",
|
||
}, nil
|
||
}
|