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 }