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:
Blizzard
2026-07-03 15:33:31 +08:00
commit 609f7d06cf
180 changed files with 15259 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
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
}