609f7d06cf
- pets-fe: 微信原生小程序(首页/计划/记录/报告/社区/引导), 服务端驱动、无假数据;弹层改用 scroll-view,打开时隐藏自定义 tabBar - pets-be: Gin + GORM(MySQL, sundynix_ 前缀) + MinIO,统一响应/分页, 微信 code2session 登录,provider-neutral AI(DeepSeek),go:embed React 后台 - 修复:分段选择类型不匹配(字符串 vs 数字)导致选不中 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
230 lines
7.5 KiB
Go
230 lines
7.5 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"`
|
|
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 uint) (*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 uint) 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 uint, 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 uint, 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 uint, 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(),
|
|
Active: d.Equal(today),
|
|
HasDot: dotSet[d.Format("2006-01-02")],
|
|
}
|
|
}
|
|
return week
|
|
}
|
|
|
|
// datesWithActivity 区间内有任务/记录/提醒的日期集合
|
|
func (s *Service) datesWithActivity(petID uint, 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
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func (s *Service) todayCompletionPct(petID uint, now time.Time) int {
|
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
end := start.AddDate(0, 0, 1)
|
|
var total, done int64
|
|
s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end).Count(&total)
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ? AND done = ?", petID, start, end, 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"`
|
|
ProStatus string `json:"pro_status"`
|
|
}
|
|
|
|
// GetUserSummary 汇总当前用户名下计数
|
|
func (s *Service) GetUserSummary(userID uint) (*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)
|
|
|
|
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
|
|
}
|