2f2757267e
- TogglePlanTask:完成无 sheet_type 的任务时补一条 note 记录, 首页/计划页两条路径都统一留痕(ToggleTask 委托它);去掉前端重复建记录 - 报告页「完成任务」原来统计空的 DailyTask 恒为 0,改成经 plan 关联 统计 PlanTask 本周完成数;用户汇总的 tasksDone 同改 - 首页今日完成度 todayCompletionPct 同样从 DailyTask 改成今日 PlanTask Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
248 lines
8.3 KiB
Go
248 lines
8.3 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"time"
|
||
|
||
"github.com/sundynix/pets-be/internal/ai"
|
||
"github.com/sundynix/pets-be/internal/model"
|
||
)
|
||
|
||
// HomeInsight 首页洞察条一项:Bold 为大号加粗值,Text 为说明
|
||
type HomeInsight struct {
|
||
Bold string `json:"bold"`
|
||
Text string `json:"text"`
|
||
}
|
||
|
||
// WeekDay 首页日期条一格
|
||
type WeekDay struct {
|
||
Weekday string `json:"weekday"` // 一二三四五六日
|
||
Day int `json:"day"`
|
||
Date string `json:"date"` // YYYY-MM-DD,供首页点击查当日任务
|
||
Active bool `json:"active"` // 是否今天
|
||
HasDot bool `json:"has_dot"` // 当天有任务/记录/提醒
|
||
}
|
||
|
||
// HomeSummary 首页所需的全部动态数据
|
||
type HomeSummary struct {
|
||
Greeting string `json:"greeting"`
|
||
Insights []HomeInsight `json:"insights"`
|
||
Week []WeekDay `json:"week"`
|
||
Advice string `json:"advice"`
|
||
HealthPct int `json:"health_pct"`
|
||
HealthStatus string `json:"health_status"`
|
||
}
|
||
|
||
var weekdayCN = []string{"日", "一", "二", "三", "四", "五", "六"}
|
||
|
||
// GetHomeSummary 计算首页汇总
|
||
func (s *Service) GetHomeSummary(userID, petID string) (*HomeSummary, error) {
|
||
pet, err := s.ownedPet(userID, petID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
now := time.Now()
|
||
|
||
res := &HomeSummary{
|
||
Greeting: greeting(now, s.userNickname(userID)),
|
||
HealthStatus: pet.HealthStatus,
|
||
Insights: s.homeInsights(petID, now),
|
||
Week: s.homeWeek(petID, now),
|
||
Advice: s.dailyAdvice(pet),
|
||
HealthPct: s.todayCompletionPct(petID, now),
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
func (s *Service) userNickname(userID string) string {
|
||
var u model.User
|
||
if err := s.db.Select("nickname").First(&u, userID).Error; err == nil && u.Nickname != "" {
|
||
return u.Nickname
|
||
}
|
||
return "铲屎官"
|
||
}
|
||
|
||
func greeting(t time.Time, name string) string {
|
||
h := t.Hour()
|
||
switch {
|
||
case h < 11:
|
||
return "早上好," + name
|
||
case h < 14:
|
||
return "中午好," + name
|
||
case h < 18:
|
||
return "下午好," + name
|
||
default:
|
||
return "晚上好," + name
|
||
}
|
||
}
|
||
|
||
func (s *Service) homeInsights(petID string, now time.Time) []HomeInsight {
|
||
insights := make([]HomeInsight, 0, 3)
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
|
||
// 1) 最近一条未到期的提醒
|
||
var rem model.Reminder
|
||
if err := s.db.Where("pet_id = ? AND next_due_date >= ?", petID, today).
|
||
Order("next_due_date asc").First(&rem).Error; err == nil && rem.NextDueDate != nil {
|
||
days := int(math.Ceil(rem.NextDueDate.Sub(today).Hours() / 24))
|
||
bold := fmt.Sprintf("%d天后", days)
|
||
if days <= 0 {
|
||
bold = "今天"
|
||
}
|
||
insights = append(insights, HomeInsight{Bold: bold, Text: rem.Title})
|
||
}
|
||
|
||
// 2) 本周体重变化
|
||
weekAgo := today.AddDate(0, 0, -7)
|
||
var latest, earliest model.HealthRecord
|
||
if s.db.Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).
|
||
Order("occurred_at desc").First(&latest).Error == nil {
|
||
if s.db.Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).
|
||
Order("occurred_at asc").First(&earliest).Error == nil {
|
||
d := latest.NumValue - earliest.NumValue
|
||
insights = append(insights, HomeInsight{Bold: fmt.Sprintf("%+.1fkg", d), Text: "本周体重"})
|
||
}
|
||
}
|
||
|
||
// 3) 连续记录天数
|
||
if n := s.streakDays(petID, now); n > 0 {
|
||
insights = append(insights, HomeInsight{Bold: fmt.Sprintf("%d天", n), Text: "连续记录"})
|
||
}
|
||
return insights
|
||
}
|
||
|
||
// streakDays 从今天往前,连续有健康记录的天数
|
||
func (s *Service) streakDays(petID string, now time.Time) int {
|
||
streak := 0
|
||
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
for i := 0; i < 60; i++ {
|
||
start := day.AddDate(0, 0, -i)
|
||
end := start.AddDate(0, 0, 1)
|
||
var cnt int64
|
||
s.db.Model(&model.HealthRecord{}).
|
||
Where("pet_id = ? AND occurred_at >= ? AND occurred_at < ?", petID, start, end).Count(&cnt)
|
||
if cnt == 0 {
|
||
break
|
||
}
|
||
streak++
|
||
}
|
||
return streak
|
||
}
|
||
|
||
func (s *Service) homeWeek(petID string, now time.Time) []WeekDay {
|
||
// 本周一为起点
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
offset := (int(today.Weekday()) + 6) % 7 // 周一=0
|
||
monday := today.AddDate(0, 0, -offset)
|
||
|
||
dotSet := s.datesWithActivity(petID, monday, monday.AddDate(0, 0, 7))
|
||
|
||
week := make([]WeekDay, 7)
|
||
for i := 0; i < 7; i++ {
|
||
d := monday.AddDate(0, 0, i)
|
||
week[i] = WeekDay{
|
||
Weekday: weekdayCN[int(d.Weekday())],
|
||
Day: d.Day(),
|
||
Date: d.Format("2006-01-02"),
|
||
Active: d.Equal(today),
|
||
HasDot: dotSet[d.Format("2006-01-02")],
|
||
}
|
||
}
|
||
return week
|
||
}
|
||
|
||
// datesWithActivity 区间内有任务/记录/提醒的日期集合
|
||
func (s *Service) datesWithActivity(petID string, start, end time.Time) map[string]bool {
|
||
set := map[string]bool{}
|
||
var ts []time.Time
|
||
s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end).Pluck("task_date", &ts)
|
||
for _, t := range ts {
|
||
set[t.Format("2006-01-02")] = true
|
||
}
|
||
var rs []time.Time
|
||
s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND occurred_at >= ? AND occurred_at < ?", petID, start, end).Pluck("occurred_at", &rs)
|
||
for _, t := range rs {
|
||
set[t.Format("2006-01-02")] = true
|
||
}
|
||
var reminders []model.Reminder
|
||
s.db.Where("pet_id = ? AND next_due_date >= ? AND next_due_date < ?", petID, start, end).Find(&reminders)
|
||
for _, r := range reminders {
|
||
if r.NextDueDate != nil {
|
||
set[r.NextDueDate.Format("2006-01-02")] = true
|
||
}
|
||
}
|
||
// 30 天计划路线图节点也算「有活动」,让首页周历与计划日历一致
|
||
for _, d := range s.planNodeDates(petID, start, end) {
|
||
set[d.Format("2006-01-02")] = true
|
||
}
|
||
return set
|
||
}
|
||
|
||
func (s *Service) todayCompletionPct(petID string, now time.Time) int {
|
||
// 任务是 PlanTask(DailyTask 已空)。取宠物的 active 30 天计划,算今天那天的完成占比
|
||
var plan model.Plan
|
||
if s.db.Where("pet_id = ? AND kind = ? AND status = ?", petID, model.PlanThirtyDay, "active").
|
||
Order("id desc").First(&plan).Error != nil {
|
||
return 0
|
||
}
|
||
day := dayIndexOf(&plan, now)
|
||
if day < 0 {
|
||
return 0
|
||
}
|
||
var total, done int64
|
||
s.db.Model(&model.PlanTask{}).Where("plan_id = ? AND day = ?", plan.ID, day).Count(&total)
|
||
if total == 0 {
|
||
return 0
|
||
}
|
||
s.db.Model(&model.PlanTask{}).Where("plan_id = ? AND day = ? AND done = ?", plan.ID, day, true).Count(&done)
|
||
return int(math.Round(float64(done) / float64(total) * 100))
|
||
}
|
||
|
||
// dailyAdvice 今日建议:每宠每天缓存一条;启用模型则真调,否则规则化
|
||
func (s *Service) dailyAdvice(pet *model.Pet) string {
|
||
day := time.Now().Format("2006-01-02")
|
||
var da model.DailyAdvice
|
||
if err := s.db.Where("pet_id = ? AND day = ?", pet.ID, day).First(&da).Error; err == nil {
|
||
return da.Text
|
||
}
|
||
|
||
text := fmt.Sprintf("%s处于%s,建议今天重点观察体重、饮食和排便。若出现连续呕吐、精神明显变差或便血,应尽快就医。", pet.Name, pet.Stage)
|
||
if s.ai != nil && s.ai.Enabled() {
|
||
system := aiSafetyPrompt + "\n\n" + s.petBrief(pet.ID) +
|
||
"\n请用2-3句话给出今天的养宠重点提醒,亲切简洁,直接给建议不要寒暄。"
|
||
if r, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: "今天的养宠建议"}}, ai.Options{Temperature: -1}); err == nil && r != "" {
|
||
text = r
|
||
}
|
||
}
|
||
_ = s.db.Create(&model.DailyAdvice{PetID: pet.ID, Day: day, Text: text}).Error
|
||
return text
|
||
}
|
||
|
||
// UserSummary 我的页所需计数
|
||
type UserSummary struct {
|
||
Pets int64 `json:"pets"`
|
||
Records int64 `json:"records"`
|
||
Reminders int64 `json:"reminders"`
|
||
Following int64 `json:"following"` // 我关注了多少人
|
||
Followers int64 `json:"followers"` // 多少人关注我
|
||
ProStatus string `json:"pro_status"`
|
||
}
|
||
|
||
// GetUserSummary 汇总当前用户名下计数
|
||
func (s *Service) GetUserSummary(userID string) (*UserSummary, error) {
|
||
var sum UserSummary
|
||
s.db.Model(&model.Pet{}).Where("user_id = ?", userID).Count(&sum.Pets)
|
||
s.db.Model(&model.HealthRecord{}).Where("user_id = ?", userID).Count(&sum.Records)
|
||
s.db.Model(&model.Reminder{}).Where("user_id = ?", userID).Count(&sum.Reminders)
|
||
s.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&sum.Following)
|
||
s.db.Model(&model.Follow{}).Where("followee_id = ?", userID).Count(&sum.Followers)
|
||
|
||
sum.ProStatus = model.ProNone
|
||
var m model.ProMembership
|
||
if err := s.db.Where("user_id = ?", userID).First(&m).Error; err == nil {
|
||
sum.ProStatus = m.Status
|
||
}
|
||
return &sum, nil
|
||
}
|