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:
@@ -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/articles(id>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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
Reference in New Issue
Block a user