init: 毛孩子计划 小程序 + Go 后端 + 内嵌后台
- 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>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 uint) (*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 := "稳定成长"
|
||||
if highRisk > 0 {
|
||||
status = "需要关注"
|
||||
}
|
||||
return &WeeklyReport{
|
||||
Summary: fmt.Sprintf("本周 %s 完成 %d 项任务,体重 %+.1fkg,无高风险异常记录。", pet.Name, tasksCompleted, gain),
|
||||
TasksCompleted: tasksCompleted,
|
||||
WeightGain: gain,
|
||||
HighRiskCount: highRisk,
|
||||
HealthStatus: status,
|
||||
NextWeekFocus: "第 2 针疫苗提醒、继续观察体重趋势、避免频繁更换食物。",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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 uint, 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 uint) (*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 uint) (*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
|
||||
}
|
||||
Reference in New Issue
Block a user