package handler import ( "time" "github.com/gin-gonic/gin" "gorm.io/datatypes" "github.com/sundynix/pets-be/internal/middleware" "github.com/sundynix/pets-be/internal/model" "github.com/sundynix/pets-be/internal/service" "github.com/sundynix/pets-be/pkg/response" ) type petReq struct { Name string `json:"name"` Emoji string `json:"emoji"` Type string `json:"type"` Gender string `json:"gender"` Birthday string `json:"birthday"` // YYYY-MM-DD ArrivedAt string `json:"arrived_at"` // YYYY-MM-DD,到家日期 Weight string `json:"weight"` Stage string `json:"stage"` Age string `json:"age"` Color string `json:"color"` Breed string `json:"breed"` AvatarFileID string `json:"avatar_file_id"` Goals []string `json:"goals"` } func (r petReq) toInput() service.PetInput { in := service.PetInput{ Name: r.Name, Emoji: r.Emoji, Type: r.Type, Gender: r.Gender, Weight: r.Weight, Stage: r.Stage, Age: r.Age, Color: r.Color, Breed: r.Breed, AvatarFileID: r.AvatarFileID, } if r.Birthday != "" { if t, err := time.ParseInLocation("2006-01-02", r.Birthday, time.Local); err == nil { // 生日不接受未来日期:前端限制过一道,服务端不能只信前端 if !t.After(time.Now()) { in.Birthday = &t } } } if r.ArrivedAt != "" { if t, err := time.ParseInLocation("2006-01-02", r.ArrivedAt, time.Local); err == nil && !t.After(time.Now()) { in.ArrivedAt = &t } } // 阶段归一化不在这里做:狗要按品种查体型才能定阈值,需要访问 DB。 // 交给 handler 里 h.svc.NormalizeStage 处理。 if r.Goals != nil { if b, err := jsonMarshal(r.Goals); err == nil { in.Goals = datatypes.JSON(b) } } return in } // ListPets GET /api/pets func (h *Handler) ListPets(c *gin.Context) { pets, err := h.svc.ListPets(middleware.UserID(c)) if err != nil { response.FailErr(c, err) return } response.OK(c, pets) } // CreatePet POST /api/pets func (h *Handler) CreatePet(c *gin.Context) { var req petReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } in := req.toInput() in.Stage = h.svc.NormalizeStage(service.SpeciesOf(req.Type), req.Breed, in.Stage, in.Birthday) pet, err := h.svc.CreatePet(middleware.UserID(c), in) if err != nil { response.FailErr(c, err) return } response.OK(c, pet) } // GetPet GET /api/pets/:id func (h *Handler) GetPet(c *gin.Context) { pet, err := h.svc.GetPet(middleware.UserID(c), idParam(c, "id")) if err != nil { respondErr(c, err) return } response.OK(c, pet) } // UpdatePet PUT /api/pets/:id func (h *Handler) UpdatePet(c *gin.Context) { var req petReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } fields := map[string]any{} if req.Name != "" { fields["name"] = req.Name } if req.Emoji != "" { fields["emoji"] = req.Emoji } if req.Weight != "" { fields["weight"] = req.Weight } if req.Stage != "" { fields["stage"] = req.Stage } if req.Gender != "" { fields["gender"] = req.Gender } if req.Age != "" { fields["age"] = req.Age } if req.Color != "" { fields["color"] = req.Color } if req.Breed != "" { fields["breed"] = req.Breed } if req.AvatarFileID != "" { fields["avatar_file_id"] = req.AvatarFileID } pet, err := h.svc.UpdatePet(middleware.UserID(c), idParam(c, "id"), fields) if err != nil { respondErr(c, err) return } response.OK(c, pet) } // DeletePet DELETE /api/pets/:id func (h *Handler) DeletePet(c *gin.Context) { if err := h.svc.DeletePet(middleware.UserID(c), idParam(c, "id")); err != nil { respondErr(c, err) return } response.OK(c, gin.H{"deleted": true}) } // Onboarding POST /api/onboarding func (h *Handler) Onboarding(c *gin.Context) { var req petReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } in := req.toInput() in.Stage = h.svc.NormalizeStage(service.SpeciesOf(req.Type), req.Breed, in.Stage, in.Birthday) pet, err := h.svc.Onboarding(middleware.UserID(c), in) if err != nil { response.FailErr(c, err) return } response.OK(c, pet) } // Breeds GET /api/breeds?species=cat 品种,按首字母分组。 // 建档第二步的选择器直接照这个渲染,前端不做分组也不做排序 func (h *Handler) Breeds(c *gin.Context) { response.OK(c, h.svc.ListBreeds(c.Query("species"))) } // LifeStages GET /api/life-stages?species=dog&size=large // 生命阶段的划分标准。建档时选阶段那里、和「该换方案了」的提示里都要展示依据, // 所以直接把驱动判定的那张表吐出来 —— 不另写一份说明文案,免得两边漂 func (h *Handler) LifeStages(c *gin.Context) { species := c.Query("species") if species == "" { species = model.SpeciesCat } response.OK(c, gin.H{ "species": species, "size": c.Query("size"), "stages": service.StageRules(species, c.Query("size")), "sizes": service.SizeLabels, }) } // PetStageDrift GET /api/pets/:id/stage-drift // 存的阶段和按生日算的对不上时,告诉前端可以提示用户换一套方案 func (h *Handler) PetStageDrift(c *gin.Context) { pet, err := h.svc.GetPet(middleware.UserID(c), idParam(c, "id")) if err != nil { respondErr(c, err) return } response.OK(c, h.svc.CheckStageDrift(pet)) } // PetIDCard GET /api/pets/:id/id-card 宠物身份卡内容(含小程序码,未发布时无码) func (h *Handler) PetIDCard(c *gin.Context) { card, err := h.svc.GetIDCard(middleware.UserID(c), idParam(c, "id")) if err != nil { respondErr(c, err) return } response.OK(c, card) }