feat: 养护模板/社区运营/体重趋势/计划日历 + 雪花ID重构 + 文件表与图片上传

后端
- 主键改雪花字符串 ID(pkg/idgen + Base.BeforeCreate),全表外键/JWT/中间件随之调整
- 新增 files 表:/api/upload 按 MD5 去重,返回 {id,url,md5},服务端只收图片
- 头像/记录附图/帖子图改 file_id 关联,读取解析为 URL
- 养护模板(物种×阶段)后台可配 + AI 生成草稿;建档按模板生成任务/计划/提醒
- 社区 AI 运营:虚拟账号池 + 每日定时/手动生成,帖子带 AI 标
- 计划路线图节点带真实日期;首页周历与计划日历同源;新增 day-plan 当日安排
- 体重趋势接口带备注;记录列表分页

小程序
- 公共图片上传 utils/upload.js(仅图片);记录拍照/我的头像/社区发图三处接入
- 首页日历点选查当日任务;记录页体重趋势可点看备注 + 时间轴分页折叠
- 计划页日历点选查当日计划;自定义 tabBar 高度调整

后台(React)
- 新增「养护模板」「社区运营」页;用户管理加机器人筛选

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-06 08:48:15 +08:00
parent 609f7d06cf
commit 2f5dd6eefc
83 changed files with 2253 additions and 355 deletions
+63 -35
View File
@@ -20,9 +20,10 @@ type PetInput struct {
Weight string
Stage string
Age string
Color string
Breed string
Goals datatypes.JSON
Color string
Breed string
AvatarFileID string
Goals datatypes.JSON
}
func normalizeWeight(w string) string {
@@ -37,19 +38,31 @@ func normalizeWeight(w string) string {
}
// ListPets 用户的全部宠物
func (s *Service) ListPets(userID uint) ([]model.Pet, error) {
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 uint) (*model.Pet, error) {
return s.ownedPet(userID, petID)
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 uint, in PetInput) (*model.Pet, error) {
func (s *Service) CreatePet(userID string, in PetInput) (*model.Pet, error) {
pet := model.Pet{
UserID: userID,
Name: in.Name,
@@ -62,6 +75,7 @@ func (s *Service) CreatePet(userID uint, in PetInput) (*model.Pet, error) {
Age: in.Age,
Color: in.Color,
Breed: in.Breed,
AvatarFileID: in.AvatarFileID,
HealthStatus: "正常",
Goals: in.Goals,
}
@@ -77,7 +91,7 @@ func (s *Service) CreatePet(userID uint, in PetInput) (*model.Pet, error) {
}
// UpdatePet 更新宠物字段
func (s *Service) UpdatePet(userID, petID uint, fields map[string]any) (*model.Pet, error) {
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
}
@@ -91,13 +105,13 @@ func (s *Service) UpdatePet(userID, petID uint, fields map[string]any) (*model.P
}
// DeletePet 删除宠物并级联清除其记录/任务/计划/提醒/每日建议(不留孤儿数据)
func (s *Service) DeletePet(userID, petID uint) error {
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 []uint
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 {
@@ -117,7 +131,7 @@ func (s *Service) DeletePet(userID, petID uint) error {
}
// Onboarding 建首宠 + 标记用户已引导
func (s *Service) Onboarding(userID uint, in PetInput) (*model.Pet, error) {
func (s *Service) Onboarding(userID string, in PetInput) (*model.Pet, error) {
pet, err := s.CreatePet(userID, in)
if err != nil {
return nil, err
@@ -128,41 +142,55 @@ func (s *Service) Onboarding(userID uint, in PetInput) (*model.Pet, error) {
return pet, nil
}
// seedPetDefaults 为新宠生成默认今日任务、提醒、30 天计划
// 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
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)
}
}
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 len(tasks) > 0 {
if err := tx.Create(&tasks).Error; err != nil {
return err
}
}
if err := tx.Create(&reminders).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: 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"},
},
StartDate: &start, EndDate: &end, CompletionPct: 0, Status: "active",
Tasks: planTasks,
}
return tx.Create(&plan).Error
}