package service import ( "strings" "time" "gorm.io/datatypes" "gorm.io/gorm" "github.com/sundynix/pets-be/internal/model" ) // PetInput 建/改宠物入参 type PetInput struct { Name string Emoji string Type string Gender string Birthday *time.Time Weight string Stage string Age string Color string Breed string AvatarFileID string Goals datatypes.JSON } func normalizeWeight(w string) string { w = strings.TrimSpace(w) if w == "" { return "" } if strings.Contains(w, "kg") { return w } return w + "kg" } // ListPets 用户的全部宠物 func (s *Service) ListPets(userID string) ([]model.Pet, error) { var pets []model.Pet err := s.db.Where("user_id = ?", userID).Order("id asc").Find(&pets).Error ids := make([]string, 0, len(pets)) for i := range pets { ids = append(ids, pets[i].AvatarFileID) } urls := s.fileURLs(ids) for i := range pets { pets[i].AvatarURL = urls[pets[i].AvatarFileID] } return pets, err } // GetPet 取单只宠物(校验归属) func (s *Service) GetPet(userID, petID string) (*model.Pet, error) { p, err := s.ownedPet(userID, petID) if err == nil && p != nil { p.AvatarURL = s.fileURL(p.AvatarFileID) } return p, err } // CreatePet 新增宠物并生成默认数据 func (s *Service) CreatePet(userID string, in PetInput) (*model.Pet, error) { pet := model.Pet{ UserID: userID, Name: in.Name, Emoji: in.Emoji, Type: in.Type, Gender: in.Gender, Birthday: in.Birthday, Weight: normalizeWeight(in.Weight), Stage: in.Stage, Age: in.Age, Color: in.Color, Breed: in.Breed, AvatarFileID: in.AvatarFileID, HealthStatus: "正常", Goals: in.Goals, } if err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&pet).Error; err != nil { return err } return s.seedPetDefaults(tx, &pet) }); err != nil { return nil, err } return &pet, nil } // UpdatePet 更新宠物字段 func (s *Service) UpdatePet(userID, petID string, fields map[string]any) (*model.Pet, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, err } if w, ok := fields["weight"].(string); ok { fields["weight"] = normalizeWeight(w) } if err := s.db.Model(&model.Pet{}).Where("id = ?", petID).Updates(fields).Error; err != nil { return nil, err } return s.ownedPet(userID, petID) } // DeletePet 删除宠物并级联清除其记录/任务/计划/提醒/每日建议(不留孤儿数据) func (s *Service) DeletePet(userID, petID string) error { if _, err := s.ownedPet(userID, petID); err != nil { return err } return s.db.Transaction(func(tx *gorm.DB) error { // 先删计划明细(按 plan_id),再删计划 var planIDs []string tx.Model(&model.Plan{}).Where("pet_id = ?", petID).Pluck("id", &planIDs) if len(planIDs) > 0 { if err := tx.Where("plan_id IN ?", planIDs).Delete(&model.PlanTask{}).Error; err != nil { return err } } for _, m := range []any{ &model.HealthRecord{}, &model.DailyTask{}, &model.Reminder{}, &model.Plan{}, &model.DailyAdvice{}, } { if err := tx.Where("pet_id = ?", petID).Delete(m).Error; err != nil { return err } } return tx.Where("id = ? AND user_id = ?", petID, userID).Delete(&model.Pet{}).Error }) } // Onboarding 建首宠 + 标记用户已引导 func (s *Service) Onboarding(userID string, in PetInput) (*model.Pet, error) { pet, err := s.CreatePet(userID, in) if err != nil { return nil, err } if err := s.db.Model(&model.User{}).Where("id = ?", userID).Update("onboarded", true).Error; err != nil { return nil, err } return pet, nil } // seedPetDefaults 为新宠按「物种 × 阶段」养护模板生成今日任务、提醒、30 天计划 func (s *Service) seedPetDefaults(tx *gorm.DB, pet *model.Pet) error { today := time.Now() items := s.careTemplateItems(SpeciesOf(pet.Type), pet.Stage) var tasks []model.DailyTask var reminders []model.Reminder var planTasks []model.PlanTask for _, it := range items { switch it.Kind { case model.CareKindTask: tasks = append(tasks, model.DailyTask{ PetID: pet.ID, UserID: pet.UserID, TaskDate: today, Title: it.Title, Description: it.Description, Priority: it.Priority, SheetType: it.SheetType, }) case model.CareKindPlan: planTasks = append(planTasks, model.PlanTask{ Day: it.Day, DayLabel: it.DayLabel, Title: it.Title, Description: it.Description, SheetType: it.SheetType, }) case model.CareKindReminder: r := model.Reminder{ PetID: pet.ID, UserID: pet.UserID, Type: it.ReminderType, Title: it.Title, Frequency: it.Frequency, } if it.OffsetDays > 0 { due := today.AddDate(0, 0, it.OffsetDays) r.NextDueDate = &due } reminders = append(reminders, r) } } if len(tasks) > 0 { if err := tx.Create(&tasks).Error; err != nil { return err } } if len(reminders) > 0 { if err := tx.Create(&reminders).Error; err != nil { return err } } start := today end := today.AddDate(0, 0, 30) plan := model.Plan{ PetID: pet.ID, UserID: pet.UserID, Kind: model.PlanThirtyDay, Stage: pet.Stage, StartDate: &start, EndDate: &end, CompletionPct: 0, Status: "active", Tasks: planTasks, } return tx.Create(&plan).Error }