package service import ( "fmt" "time" "gorm.io/datatypes" "gorm.io/gorm" "github.com/sundynix/pets-be/internal/model" ) // RecordInput 健康记录入参 type RecordInput struct { Type string Icon string Title string Description string NumValue float64 Category string ImageURL string ImageFileID string Extra datatypes.JSON OccurredAt *time.Time } // ListRecords 列出宠物的健康记录(可按 type 过滤) func (s *Service) ListRecords(userID, petID string, recordType string, offset, limit int) ([]model.HealthRecord, int64, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, 0, err } q := s.db.Model(&model.HealthRecord{}).Where("pet_id = ?", petID) if recordType != "" { q = q.Where("type = ?", recordType) } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var records []model.HealthRecord err := q.Order("occurred_at desc, id desc").Offset(offset).Limit(limit).Find(&records).Error // 用 file_id 回填图片 URL ids := make([]string, 0) for i := range records { if records[i].ImageFileID != "" && records[i].ImageURL == "" { ids = append(ids, records[i].ImageFileID) } } if len(ids) > 0 { urls := s.fileURLs(ids) for i := range records { if records[i].ImageURL == "" { records[i].ImageURL = urls[records[i].ImageFileID] } } } return records, total, err } // CreateRecord 新增健康记录;weight 类型同步更新宠物体重 func (s *Service) CreateRecord(userID, petID string, in RecordInput) (*model.HealthRecord, error) { pet, err := s.ownedPet(userID, petID) if err != nil { return nil, err } occurred := time.Now() if in.OccurredAt != nil { occurred = *in.OccurredAt } rec := model.HealthRecord{ PetID: petID, UserID: userID, Type: in.Type, Icon: in.Icon, Title: in.Title, Description: in.Description, NumValue: in.NumValue, Category: in.Category, ImageURL: in.ImageURL, ImageFileID: in.ImageFileID, Extra: in.Extra, OccurredAt: occurred, } // 传了 file_id 未传 url 时,用 file 表的 url 回填 ImageURL 便于直接展示 if in.ImageFileID != "" && rec.ImageURL == "" { rec.ImageURL = s.fileURL(in.ImageFileID) } if err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&rec).Error; err != nil { return err } if in.Type == model.RecordWeight && in.NumValue > 0 { weight := fmt.Sprintf("%gkg", in.NumValue) if err := tx.Model(&model.Pet{}).Where("id = ?", pet.ID).Update("weight", weight).Error; err != nil { return err } } return nil }); err != nil { return nil, err } return &rec, nil } // DeleteRecord 删除记录 func (s *Service) DeleteRecord(userID, recordID string) error { res := s.db.Where("id = ? AND user_id = ?", recordID, userID).Delete(&model.HealthRecord{}) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrNotFound } return nil } // WeightPoint 体重趋势点 type WeightPoint struct { Value float64 `json:"value"` OccurredAt time.Time `json:"occurred_at"` Note string `json:"note"` // 记录时填的备注 } // WeightTrend 最近 N 次体重(升序) func (s *Service) WeightTrend(userID, petID string, limit int) ([]WeightPoint, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, err } if limit <= 0 { limit = 7 } var records []model.HealthRecord if err := s.db.Where("pet_id = ? AND type = ?", petID, model.RecordWeight). Order("occurred_at desc").Limit(limit).Find(&records).Error; err != nil { return nil, err } points := make([]WeightPoint, 0, len(records)) for i := len(records) - 1; i >= 0; i-- { points = append(points, WeightPoint{Value: records[i].NumValue, OccurredAt: records[i].OccurredAt, Note: records[i].Description}) } return points, nil }