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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user