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
+223
View File
@@ -0,0 +1,223 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/internal/model"
appjwt "github.com/sundynix/pets-be/pkg/jwt"
"github.com/sundynix/pets-be/pkg/response"
)
type adminLoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
}
// AdminLogin POST /api/admin/login
func (h *Handler) AdminLogin(c *gin.Context) {
var req adminLoginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
admin, err := h.svc.AdminLogin(req.Username, req.Password)
if err != nil {
response.Fail(c, 40100, "账号或密码错误")
return
}
token, err := h.jwt.Generate(admin.ID, appjwt.KindAdmin, admin.Username, admin.Role)
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"token": token, "admin": admin})
}
// AdminMe GET /api/admin/me
func (h *Handler) AdminMe(c *gin.Context) {
admin, err := h.svc.GetAdmin(middleware.AdminID(c))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, admin)
}
// AdminStats GET /api/admin/stats
func (h *Handler) AdminStats(c *gin.Context) {
stats, err := h.svc.Stats()
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, stats)
}
type adminListReq struct {
response.PageQuery
Keyword string `form:"keyword"`
Status string `form:"status"`
}
// AdminListUsers GET /api/admin/users
func (h *Handler) AdminListUsers(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
users, total, err := h.svc.ListUsers(req.Keyword, req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(users, total, req.PageQuery))
}
type disableReq struct {
Disabled bool `json:"disabled"`
}
// AdminSetUserDisabled PUT /api/admin/users/:id/disabled
func (h *Handler) AdminSetUserDisabled(c *gin.Context) {
var req disableReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
if err := h.svc.SetUserDisabled(uintParam(c, "id"), req.Disabled); err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"ok": true})
}
// AdminListPets GET /api/admin/pets
func (h *Handler) AdminListPets(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
pets, total, err := h.svc.ListPetsAdmin(req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(pets, total, req.PageQuery))
}
// AdminListPosts GET /api/admin/posts
func (h *Handler) AdminListPosts(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
posts, total, err := h.svc.ListPostsAdmin(req.Status, req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(posts, total, req.PageQuery))
}
type postStatusReq struct {
Status string `json:"status"`
}
// AdminSetPostStatus PUT /api/admin/posts/:id/status
func (h *Handler) AdminSetPostStatus(c *gin.Context) {
var req postStatusReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
if err := h.svc.SetPostStatus(uintParam(c, "id"), req.Status); err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"ok": true})
}
// AdminListComments GET /api/admin/comments
func (h *Handler) AdminListComments(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
comments, total, err := h.svc.ListCommentsAdmin(req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(comments, total, req.PageQuery))
}
// AdminDeleteComment DELETE /api/admin/comments/:id
func (h *Handler) AdminDeleteComment(c *gin.Context) {
if err := h.svc.DeleteCommentAdmin(uintParam(c, "id")); err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"ok": true})
}
// AdminListArticles GET /api/admin/articles
func (h *Handler) AdminListArticles(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
articles, total, err := h.svc.ListArticlesAdmin(req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(articles, total, req.PageQuery))
}
type articleReq struct {
ID uint `json:"id"`
Icon string `json:"icon"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
Category string `json:"category"`
RelatedSheetType string `json:"related_sheet_type"`
Published bool `json:"published"`
}
// AdminSaveArticle POST /api/admin/articlesid>0 为更新)
func (h *Handler) AdminSaveArticle(c *gin.Context) {
var req articleReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
article := &model.Article{
Icon: req.Icon, Title: req.Title, Description: req.Description, Content: req.Content,
Category: req.Category, RelatedSheetType: req.RelatedSheetType, Published: req.Published,
}
article.ID = req.ID
if err := h.svc.SaveArticle(article); err != nil {
response.FailErr(c, err)
return
}
response.OK(c, article)
}
// AdminDeleteArticle DELETE /api/admin/articles/:id
func (h *Handler) AdminDeleteArticle(c *gin.Context) {
if err := h.svc.DeleteArticle(uintParam(c, "id")); err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"ok": true})
}
// AdminListPro GET /api/admin/memberships
func (h *Handler) AdminListPro(c *gin.Context) {
var req adminListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
list, total, err := h.svc.ListProAdmin(req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(list, total, req.PageQuery))
}
+120
View File
@@ -0,0 +1,120 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
appjwt "github.com/sundynix/pets-be/pkg/jwt"
"github.com/sundynix/pets-be/pkg/response"
)
type loginReq struct {
Nickname string `json:"nickname"`
}
// Login 开发态 Mock 登录
func (h *Handler) Login(c *gin.Context) {
if !h.cfg.Auth.DevLogin {
response.Fail(c, 40300, "开发登录未开启,请使用微信登录")
return
}
var req loginReq
_ = c.ShouldBindJSON(&req)
user, err := h.svc.MockLogin(req.Nickname)
if err != nil {
response.FailErr(c, err)
return
}
token, err := h.jwt.Generate(user.ID, appjwt.KindUser, user.Nickname, "")
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"token": token, "user": user})
}
type wechatLoginReq struct {
Code string `json:"code"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}
// WechatLogin 微信小程序登录:前端 wx.login 拿 code 传入
func (h *Handler) WechatLogin(c *gin.Context) {
var req wechatLoginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
if req.Code == "" {
response.FailParams(c, "缺少 code")
return
}
user, err := h.svc.WechatLogin(req.Code)
if err != nil {
response.Fail(c, 40100, err.Error())
return
}
// 可选:首次登录写入用户授权的昵称/头像
if fields := map[string]any{}; true {
if req.Nickname != "" && user.Nickname == "微信用户" {
fields["nickname"] = req.Nickname
}
if req.Avatar != "" && user.Avatar == "" {
fields["avatar"] = req.Avatar
}
if len(fields) > 0 {
if u, e := h.svc.UpdateUser(user.ID, fields); e == nil {
user = u
}
}
}
token, err := h.jwt.Generate(user.ID, appjwt.KindUser, user.Nickname, "")
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"token": token, "user": user})
}
// Profile 当前用户资料
func (h *Handler) Profile(c *gin.Context) {
user, err := h.svc.GetUser(middleware.UserID(c))
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, user)
}
type updateProfileReq struct {
Nickname *string `json:"nickname"`
Avatar *string `json:"avatar"`
Phone *string `json:"phone"`
}
// UpdateProfile 更新资料
func (h *Handler) UpdateProfile(c *gin.Context) {
var req updateProfileReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
fields := map[string]any{}
if req.Nickname != nil {
fields["nickname"] = *req.Nickname
}
if req.Avatar != nil {
fields["avatar"] = *req.Avatar
}
if req.Phone != nil {
fields["phone"] = *req.Phone
}
user, err := h.svc.UpdateUser(middleware.UserID(c), fields)
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, user)
}
+130
View File
@@ -0,0 +1,130 @@
package handler
import (
"github.com/gin-gonic/gin"
"gorm.io/datatypes"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/internal/service"
"github.com/sundynix/pets-be/pkg/response"
)
type postListReq struct {
response.PageQuery
Tab string `form:"tab"`
}
// ListPosts GET /api/posts?tab=&page=&page_size=
func (h *Handler) ListPosts(c *gin.Context) {
var req postListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
posts, total, err := h.svc.ListPosts(req.Tab, req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(posts, total, req.PageQuery))
}
// GetPost GET /api/posts/:id
func (h *Handler) GetPost(c *gin.Context) {
post, err := h.svc.GetPost(uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, post)
}
type postReq struct {
PetID *uint `json:"pet_id"`
Identity string `json:"identity"`
Content string `json:"content"`
Tags []string `json:"tags"`
Images []string `json:"images"`
}
// CreatePost POST /api/posts
func (h *Handler) CreatePost(c *gin.Context) {
var req postReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
in := service.PostInput{PetID: req.PetID, Identity: req.Identity, Content: req.Content}
if b, err := jsonMarshal(req.Tags); err == nil {
in.Tags = datatypes.JSON(b)
}
if b, err := jsonMarshal(req.Images); err == nil {
in.Images = datatypes.JSON(b)
}
post, err := h.svc.CreatePost(middleware.UserID(c), in)
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, post)
}
// LikePost POST /api/posts/:id/like
func (h *Handler) LikePost(c *gin.Context) {
count, err := h.svc.LikePost(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"like_count": count})
}
// UnlikePost DELETE /api/posts/:id/like
func (h *Handler) UnlikePost(c *gin.Context) {
count, err := h.svc.UnlikePost(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"like_count": count})
}
type commentListReq struct {
response.PageQuery
}
// ListComments GET /api/posts/:id/comments
func (h *Handler) ListComments(c *gin.Context) {
var req commentListReq
_ = c.ShouldBindQuery(&req)
req.Normalize()
comments, total, err := h.svc.ListComments(uintParam(c, "id"), req.Offset(), req.Limit())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, response.NewPage(comments, total, req.PageQuery))
}
type commentReq struct {
Content string `json:"content"`
}
// CreateComment POST /api/posts/:id/comments
func (h *Handler) CreateComment(c *gin.Context) {
var req commentReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
if req.Content == "" {
response.FailParams(c, "评论内容不能为空")
return
}
comment, err := h.svc.CreateComment(middleware.UserID(c), uintParam(c, "id"), req.Content)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, comment)
}
+18
View File
@@ -0,0 +1,18 @@
package handler
import (
"github.com/sundynix/pets-be/internal/config"
"github.com/sundynix/pets-be/internal/service"
appjwt "github.com/sundynix/pets-be/pkg/jwt"
)
// Handler 所有 HTTP 处理器聚合,方法按领域分散在各文件
type Handler struct {
svc *service.Service
jwt *appjwt.Manager
cfg *config.Config
}
func New(svc *service.Service, jm *appjwt.Manager, cfg *config.Config) *Handler {
return &Handler{svc: svc, jwt: jm, cfg: cfg}
}
+33
View File
@@ -0,0 +1,33 @@
package handler
import (
"encoding/json"
"errors"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/service"
"github.com/sundynix/pets-be/pkg/errcode"
"github.com/sundynix/pets-be/pkg/response"
)
// uintParam 解析路径参数为 uint
func uintParam(c *gin.Context, key string) uint {
v, _ := strconv.ParseUint(c.Param(key), 10, 64)
return uint(v)
}
// jsonMarshal 便捷序列化
func jsonMarshal(v any) ([]byte, error) {
return json.Marshal(v)
}
// respondErr 统一错误响应:区分 not found 与内部错误
func respondErr(c *gin.Context, err error) {
if errors.Is(err, service.ErrNotFound) {
response.Fail(c, errcode.ErrNotFound, "")
return
}
response.FailErr(c, err)
}
+117
View File
@@ -0,0 +1,117 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/internal/service"
"github.com/sundynix/pets-be/pkg/response"
)
// ListArticles GET /api/articles
func (h *Handler) ListArticles(c *gin.Context) {
articles, err := h.svc.ListArticles()
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, articles)
}
// GetArticle GET /api/articles/:id
func (h *Handler) GetArticle(c *gin.Context) {
article, err := h.svc.GetArticle(uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, article)
}
// GetPro GET /api/pro
func (h *Handler) GetPro(c *gin.Context) {
info, err := h.svc.GetPro(middleware.UserID(c))
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, info)
}
// ActivatePro POST /api/pro/activate
func (h *Handler) ActivatePro(c *gin.Context) {
info, err := h.svc.ActivatePro(middleware.UserID(c))
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, info)
}
type aiChatReq struct {
PetID *uint `json:"pet_id"`
Session string `json:"session"`
Text string `json:"text"`
}
// AIChat POST /api/ai/chat
func (h *Handler) AIChat(c *gin.Context) {
var req aiChatReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
if req.Text == "" {
response.FailParams(c, "问题不能为空")
return
}
reply, err := h.svc.AIChat(middleware.UserID(c), req.PetID, req.Session, req.Text)
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"reply": reply})
}
// HomeSummary GET /api/pets/:id/home-summary
func (h *Handler) HomeSummary(c *gin.Context) {
res, err := h.svc.GetHomeSummary(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, res)
}
// UserSummary GET /api/user/summary
func (h *Handler) UserSummary(c *gin.Context) {
res, err := h.svc.GetUserSummary(middleware.UserID(c))
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, res)
}
type assessSymptomReq struct {
Symptoms []string `json:"symptoms"`
Duration string `json:"duration"`
Spirit string `json:"spirit"`
}
// AssessSymptom POST /api/pets/:id/ai/assess-symptom
func (h *Handler) AssessSymptom(c *gin.Context) {
var req assessSymptomReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
res, err := h.svc.AssessSymptom(middleware.UserID(c), uintParam(c, "id"), service.SymptomInput{
Symptoms: req.Symptoms, Duration: req.Duration, Spirit: req.Spirit,
})
if err != nil {
respondErr(c, err)
return
}
response.OK(c, res)
}
+143
View File
@@ -0,0 +1,143 @@
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/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
Weight string `json:"weight"`
Stage string `json:"stage"`
Age string `json:"age"`
Color string `json:"color"`
Breed string `json:"breed"`
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,
}
if r.Birthday != "" {
if t, err := time.Parse("2006-01-02", r.Birthday); err == nil {
in.Birthday = &t
}
}
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
}
pet, err := h.svc.CreatePet(middleware.UserID(c), req.toInput())
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), uintParam(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
}
pet, err := h.svc.UpdatePet(middleware.UserID(c), uintParam(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), uintParam(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
}
pet, err := h.svc.Onboarding(middleware.UserID(c), req.toInput())
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, pet)
}
+111
View File
@@ -0,0 +1,111 @@
package handler
import (
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/pkg/response"
)
// ListTasks GET /api/pets/:id/tasks?date=YYYY-MM-DD
func (h *Handler) ListTasks(c *gin.Context) {
var date *time.Time
if q := c.Query("date"); q != "" {
if t, err := time.Parse("2006-01-02", q); err == nil {
date = &t
}
}
tasks, err := h.svc.ListTasks(middleware.UserID(c), uintParam(c, "id"), date)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, tasks)
}
// ToggleTask POST /api/tasks/:id/toggle
func (h *Handler) ToggleTask(c *gin.Context) {
task, err := h.svc.ToggleTask(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, task)
}
// CompleteAllTasks POST /api/pets/:id/tasks/complete-all
func (h *Handler) CompleteAllTasks(c *gin.Context) {
tasks, err := h.svc.CompleteAllTasks(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, tasks)
}
// GetPlan GET /api/pets/:id/plan
func (h *Handler) GetPlan(c *gin.Context) {
plan, err := h.svc.GetPlan(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, plan)
}
// PlanCalendar GET /api/pets/:id/plan/calendar?month=YYYY-MM
func (h *Handler) PlanCalendar(c *gin.Context) {
now := time.Now()
year, month := now.Year(), int(now.Month())
if q := c.Query("month"); q != "" {
if t, err := time.Parse("2006-01", q); err == nil {
year, month = t.Year(), int(t.Month())
}
}
res, err := h.svc.Calendar(middleware.UserID(c), uintParam(c, "id"), year, month)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, res)
}
type aiPlanReq struct {
Input string `json:"input"`
}
// CreateAIPlan POST /api/pets/:id/ai-plan
func (h *Handler) CreateAIPlan(c *gin.Context) {
var req aiPlanReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
plan, err := h.svc.CreateAIPlan(middleware.UserID(c), uintParam(c, "id"), req.Input)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, plan)
}
// ApplyAIPlan POST /api/ai-plan/:id/apply
func (h *Handler) ApplyAIPlan(c *gin.Context) {
plan, err := h.svc.ApplyAIPlan(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, plan)
}
// TogglePlanTask POST /api/plan-tasks/:id/toggle
func (h *Handler) TogglePlanTask(c *gin.Context) {
if err := h.svc.TogglePlanTask(middleware.UserID(c), uintParam(c, "id")); err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"ok": true})
}
+77
View File
@@ -0,0 +1,77 @@
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/service"
"github.com/sundynix/pets-be/pkg/response"
)
type recordReq struct {
Type string `json:"type"`
Icon string `json:"icon"`
Title string `json:"title"`
Description string `json:"description"`
NumValue float64 `json:"num_value"`
Category string `json:"category"`
ImageURL string `json:"image_url"`
Extra datatypes.JSON `json:"extra"`
OccurredAt string `json:"occurred_at"`
}
// ListRecords GET /api/pets/:id/records?type=
func (h *Handler) ListRecords(c *gin.Context) {
records, err := h.svc.ListRecords(middleware.UserID(c), uintParam(c, "id"), c.Query("type"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, records)
}
// CreateRecord POST /api/pets/:id/records
func (h *Handler) CreateRecord(c *gin.Context) {
var req recordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
in := service.RecordInput{
Type: req.Type, Icon: req.Icon, Title: req.Title, Description: req.Description,
NumValue: req.NumValue, Category: req.Category, ImageURL: req.ImageURL, Extra: req.Extra,
}
if req.OccurredAt != "" {
if t, err := time.Parse(time.RFC3339, req.OccurredAt); err == nil {
in.OccurredAt = &t
}
}
rec, err := h.svc.CreateRecord(middleware.UserID(c), uintParam(c, "id"), in)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, rec)
}
// DeleteRecord DELETE /api/records/:id
func (h *Handler) DeleteRecord(c *gin.Context) {
if err := h.svc.DeleteRecord(middleware.UserID(c), uintParam(c, "id")); err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"deleted": true})
}
// WeightTrend GET /api/pets/:id/records/weight-trend
func (h *Handler) WeightTrend(c *gin.Context) {
points, err := h.svc.WeightTrend(middleware.UserID(c), uintParam(c, "id"), 7)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, points)
}
+85
View File
@@ -0,0 +1,85 @@
package handler
import (
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/internal/service"
"github.com/sundynix/pets-be/pkg/response"
)
type reminderReq struct {
Type string `json:"type"`
Title string `json:"title"`
NextDueDate string `json:"next_due_date"` // YYYY-MM-DD
Frequency string `json:"frequency"`
}
// ListReminders GET /api/pets/:id/reminders
func (h *Handler) ListReminders(c *gin.Context) {
reminders, err := h.svc.ListReminders(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, reminders)
}
// CreateReminder POST /api/pets/:id/reminders
func (h *Handler) CreateReminder(c *gin.Context) {
var req reminderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
in := service.ReminderInput{Type: req.Type, Title: req.Title, Frequency: req.Frequency}
if req.NextDueDate != "" {
if t, err := time.Parse("2006-01-02", req.NextDueDate); err == nil {
in.NextDueDate = &t
}
}
r, err := h.svc.CreateReminder(middleware.UserID(c), uintParam(c, "id"), in)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, r)
}
// UpdateReminder PUT /api/reminders/:id
func (h *Handler) UpdateReminder(c *gin.Context) {
var req reminderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailParams(c, err.Error())
return
}
fields := map[string]any{}
if req.Title != "" {
fields["title"] = req.Title
}
if req.Frequency != "" {
fields["frequency"] = req.Frequency
}
if req.NextDueDate != "" {
if t, err := time.Parse("2006-01-02", req.NextDueDate); err == nil {
fields["next_due_date"] = t
}
}
r, err := h.svc.UpdateReminder(middleware.UserID(c), uintParam(c, "id"), fields)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, r)
}
// DeleteReminder DELETE /api/reminders/:id
func (h *Handler) DeleteReminder(c *gin.Context) {
if err := h.svc.DeleteReminder(middleware.UserID(c), uintParam(c, "id")); err != nil {
respondErr(c, err)
return
}
response.OK(c, gin.H{"deleted": true})
}
+49
View File
@@ -0,0 +1,49 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/pkg/response"
)
// WeeklyReport GET /api/pets/:id/report/weekly
func (h *Handler) WeeklyReport(c *gin.Context) {
rep, err := h.svc.GetWeeklyReport(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, rep)
}
// Bill GET /api/pets/:id/bill?period=month
func (h *Handler) Bill(c *gin.Context) {
period := c.DefaultQuery("period", "month")
bill, err := h.svc.GetBill(middleware.UserID(c), uintParam(c, "id"), period)
if err != nil {
respondErr(c, err)
return
}
response.OK(c, bill)
}
// HealthSummary GET /api/pets/:id/health-summary
func (h *Handler) HealthSummary(c *gin.Context) {
sum, err := h.svc.GetHealthSummary(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, sum)
}
// Poster GET /api/pets/:id/poster
func (h *Handler) Poster(c *gin.Context) {
poster, err := h.svc.GetPoster(middleware.UserID(c), uintParam(c, "id"))
if err != nil {
respondErr(c, err)
return
}
response.OK(c, poster)
}
+42
View File
@@ -0,0 +1,42 @@
package handler
import (
"fmt"
"path"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/pets-be/internal/middleware"
"github.com/sundynix/pets-be/pkg/response"
)
// Upload POST /api/upload multipart/form-data,字段名 file
func (h *Handler) Upload(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
response.FailParams(c, "缺少上传文件 file")
return
}
src, err := fileHeader.Open()
if err != nil {
response.FailErr(c, err)
return
}
defer src.Close()
ext := strings.ToLower(path.Ext(fileHeader.Filename))
objectName := fmt.Sprintf("uploads/%d/%d%s", middleware.UserID(c), time.Now().UnixNano(), ext)
contentType := fileHeader.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
url, err := h.svc.Upload(objectName, src, fileHeader.Size, contentType)
if err != nil {
response.FailErr(c, err)
return
}
response.OK(c, gin.H{"url": url})
}