package service import ( "math" "strings" "time" "gorm.io/datatypes" "gorm.io/gorm" "github.com/sundynix/pets-be/internal/model" ) // TogglePlanTask 切换计划明细完成状态(校验归属) func (s *Service) TogglePlanTask(userID, taskID uint) error { var pt model.PlanTask if err := s.db.First(&pt, taskID).Error; err != nil { return ErrNotFound } var plan model.Plan if err := s.db.Where("id = ? AND user_id = ?", pt.PlanID, userID).First(&plan).Error; err != nil { return ErrNotFound } return s.db.Model(&model.PlanTask{}).Where("id = ?", taskID).Update("done", !pt.Done).Error } // GetPlan 取宠物的 30 天计划(含明细) func (s *Service) GetPlan(userID, petID uint) (*model.Plan, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, err } var plan model.Plan err := s.db.Preload("Tasks"). Where("pet_id = ? AND kind = ?", petID, model.PlanThirtyDay). Order("id desc").First(&plan).Error if err == gorm.ErrRecordNotFound { return nil, ErrNotFound } if err != nil { return nil, err } // 真实完成度:已完成明细 / 总明细 if n := len(plan.Tasks); n > 0 { done := 0 for _, t := range plan.Tasks { if t.Done { done++ } } plan.CompletionPct = int(math.Round(float64(done) / float64(n) * 100)) } else { plan.CompletionPct = 0 } return &plan, nil } // CalendarResult 日历视图 type CalendarResult struct { Year int `json:"year"` Month int `json:"month"` TaskedDays []int `json:"tasked_days"` Today int `json:"today"` } // Calendar 某月有任务/提醒的日期 func (s *Service) Calendar(userID, petID uint, year, month int) (*CalendarResult, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, err } loc := time.Now().Location() start := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc) end := start.AddDate(0, 1, 0) daySet := map[int]struct{}{} var taskDates []time.Time s.db.Model(&model.DailyTask{}). Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end). Pluck("task_date", &taskDates) for _, t := range taskDates { daySet[t.Day()] = struct{}{} } 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 { daySet[r.NextDueDate.Day()] = struct{}{} } } days := make([]int, 0, len(daySet)) for d := range daySet { days = append(days, d) } today := 0 now := time.Now() if now.Year() == year && int(now.Month()) == month { today = now.Day() } return &CalendarResult{Year: year, Month: month, TaskedDays: days, Today: today}, nil } // CreateAIPlan 基于用户描述做规则化提取,生成待确认的 AI 计划 func (s *Service) CreateAIPlan(userID, petID uint, input string) (*model.Plan, error) { pet, err := s.ownedPet(userID, petID) if err != nil { return nil, err } // 默认(规则化)提取与任务 var extracted any = extractAIInfo(input, pet.Stage) tasks := []model.PlanTask{ {DayLabel: "Day 1-2", Title: "观察食欲和便便", Description: "每日记录饮食、便便、精神状态。"}, {DayLabel: "Day 3-5", Title: "记录换粮比例", Description: "逐步提高新粮比例,留意软便。"}, {DayLabel: "Day 6-7", Title: "生成复盘建议", Description: "汇总一周状态,输出复盘。"}, } // 启用模型则用真实提取覆盖 if ex, ok := s.aiPlanExtract(pet, input); ok { extracted = map[string]string{ "stage": ex.Stage, "risk": ex.Risk, "priority": ex.Priority, "reminder": ex.Reminder, } tasks = tasks[:0] for _, t := range ex.Tasks { tasks = append(tasks, model.PlanTask{DayLabel: t.DayLabel, Title: t.Title, Description: t.Description}) } } b, _ := jsonMarshalAny(extracted) plan := model.Plan{ PetID: petID, UserID: userID, Kind: model.PlanAI, Stage: pet.Stage, UserInput: input, Extracted: datatypes.JSON(b), Status: "draft", Tasks: tasks, } if err := s.db.Create(&plan).Error; err != nil { return nil, err } return &plan, nil } // ApplyAIPlan 确认并应用 AI 计划 func (s *Service) ApplyAIPlan(userID, planID uint) (*model.Plan, error) { var plan model.Plan if err := s.db.Where("id = ? AND user_id = ?", planID, userID).First(&plan).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrNotFound } return nil, err } now := time.Now() end := now.AddDate(0, 0, 7) if err := s.db.Model(&plan).Updates(map[string]any{ "status": "active", "start_date": now, "end_date": end, }).Error; err != nil { return nil, err } if err := s.db.Preload("Tasks").First(&plan, plan.ID).Error; err != nil { return nil, err } return &plan, nil } // extractAIInfo 规则化提取(后续可替换为大模型) func extractAIInfo(input, stage string) map[string]string { risk := "暂无明显风险" priority := "日常观察" reminder := "常规提醒" if strings.Contains(input, "换粮") || strings.Contains(input, "软便") { risk = "换粮软便" priority = "排便观察" } if strings.Contains(input, "疫苗") { reminder = "第二针疫苗" } if stage == "" { stage = "幼年期" } return map[string]string{ "stage": stage + " / 疫苗期", "risk": risk, "priority": priority, "reminder": reminder, } }