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>
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
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
|
|
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 uint) ([]model.Pet, error) {
|
|
var pets []model.Pet
|
|
err := s.db.Where("user_id = ?", userID).Order("id asc").Find(&pets).Error
|
|
return pets, err
|
|
}
|
|
|
|
// GetPet 取单只宠物(校验归属)
|
|
func (s *Service) GetPet(userID, petID uint) (*model.Pet, error) {
|
|
return s.ownedPet(userID, petID)
|
|
}
|
|
|
|
// CreatePet 新增宠物并生成默认数据
|
|
func (s *Service) CreatePet(userID uint, 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,
|
|
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 uint, 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 uint) error {
|
|
if _, err := s.ownedPet(userID, petID); err != nil {
|
|
return err
|
|
}
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
// 先删计划明细(按 plan_id),再删计划
|
|
var planIDs []uint
|
|
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 uint, 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()
|
|
tasks := []model.DailyTask{
|
|
{PetID: pet.ID, UserID: pet.UserID, TaskDate: today, Title: "记录一次体重", Description: "幼年期建议每周至少记录 2 次", Priority: "重要"},
|
|
{PetID: pet.ID, UserID: pet.UserID, TaskDate: today, Title: "观察饮水和排便", Description: "换粮、应激都可能影响排便状态", SheetType: "poop"},
|
|
{PetID: pet.ID, UserID: pet.UserID, TaskDate: today, Title: "检查疫苗预约", Description: "第 2 针疫苗还有 5 天", SheetType: "vaccine"},
|
|
}
|
|
if err := tx.Create(&tasks).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
vaccineDue := today.AddDate(0, 0, 18)
|
|
dewormDue := today.AddDate(0, 0, 12)
|
|
reminders := []model.Reminder{
|
|
{PetID: pet.ID, UserID: pet.UserID, Type: model.ReminderVaccine, Title: "第 2 针疫苗", NextDueDate: &vaccineDue},
|
|
{PetID: pet.ID, UserID: pet.UserID, Type: model.ReminderDeworm, Title: "体内外驱虫", NextDueDate: &dewormDue},
|
|
{PetID: pet.ID, UserID: pet.UserID, Type: model.ReminderWeight, Title: "体重记录", Frequency: "每周二、周五"},
|
|
{PetID: pet.ID, UserID: pet.UserID, Type: model.ReminderMonthlyReport, Title: "月度报告", Frequency: "每月 1 日"},
|
|
}
|
|
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: 40, Status: "active",
|
|
Tasks: []model.PlanTask{
|
|
{Day: 0, DayLabel: "今天", Title: "观察排便状态", Description: "记录颜色、形态、次数,发现软便可连续观察。", SheetType: "taskDetail"},
|
|
{Day: 1, DayLabel: "明天", Title: "检查疫苗预约", Description: "距离下一针还有 5 天,提前确认医院和时间。", SheetType: "vaccine"},
|
|
{Day: 7, DayLabel: "第 7 天", Title: "体重趋势检查", Description: "幼年期每周称重,观察是否稳定增长。", SheetType: "weight"},
|
|
{Day: 14, DayLabel: "第 14 天", Title: "复盘饮食与便便", Description: "如果近期换粮,建议把换粮过程和异常记录合并查看。", SheetType: "taskDetail"},
|
|
},
|
|
}
|
|
return tx.Create(&plan).Error
|
|
}
|