package handler import ( "github.com/gin-gonic/gin" "github.com/sundynix/pets-be/internal/middleware" "github.com/sundynix/pets-be/internal/model" "github.com/sundynix/pets-be/internal/service" 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 } data, err := h.adminTokens(admin) if err != nil { response.FailErr(c, err) return } response.OK(c, data) } // adminTokens 签发后台的「access + refresh」令牌对。access token 2 小时。 func (h *Handler) adminTokens(admin *model.Admin) (gin.H, error) { token, err := h.jwt.Generate(admin.ID, appjwt.KindAdmin, admin.Username, admin.Role) if err != nil { return nil, err } refresh, refreshExp, err := h.svc.IssueRefreshToken(string(appjwt.KindAdmin), admin.ID) if err != nil { return nil, err } return gin.H{ "token": token, "refresh_token": refresh, "expires_in": h.jwt.ExpireSeconds(), "refresh_expires_at": refreshExp, "admin": admin, }, nil } // AdminRefresh POST /api/admin/refresh func (h *Handler) AdminRefresh(c *gin.Context) { var req refreshReq _ = c.ShouldBindJSON(&req) adminID, err := h.svc.ConsumeRefreshToken(req.RefreshToken, string(appjwt.KindAdmin)) if err != nil { response.Fail(c, 40100, err.Error()) return } admin, err := h.svc.GetAdmin(adminID) if err != nil { response.Fail(c, 40100, "登录已失效,请重新登录") return } if admin.Disabled { response.Fail(c, 40300, "账号已被禁用") return } data, err := h.adminTokens(admin) if err != nil { response.FailErr(c, err) return } response.OK(c, data) } // AdminLogout POST /api/admin/logout 作废当前刷新令牌 func (h *Handler) AdminLogout(c *gin.Context) { var req refreshReq _ = c.ShouldBindJSON(&req) h.svc.RevokeRefreshToken(req.RefreshToken, string(appjwt.KindAdmin)) response.OK(c, gin.H{"ok": true}) } // 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"` Type string `form:"type"` } // 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.Type, 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(idParam(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(idParam(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(idParam(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 string `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(idParam(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)) } // AdminCareTemplateMeta GET /api/admin/care-templates/meta // 给后台下拉用:物种、阶段、可选关联记录/弹层类型、提醒类型 // ── 品种 ── type breedReq struct { ID string `json:"id"` Species string `json:"species"` Name string `json:"name"` Initial string `json:"initial"` Sort int `json:"sort"` Enabled bool `json:"enabled"` } // AdminListBreeds GET /api/admin/breeds?species=cat 连停用的一起返回 func (h *Handler) AdminListBreeds(c *gin.Context) { response.OK(c, h.svc.AdminListBreeds(c.Query("species"))) } // AdminSaveBreed POST /api/admin/breeds func (h *Handler) AdminSaveBreed(c *gin.Context) { var req breedReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } b := &model.Breed{Species: req.Species, Name: req.Name, Initial: req.Initial, Sort: req.Sort, Enabled: req.Enabled} b.ID = req.ID if err := h.svc.SaveBreed(b); err != nil { respondErr(c, err) return } response.OK(c, b) } // AdminDeleteBreed DELETE /api/admin/breeds/:id func (h *Handler) AdminDeleteBreed(c *gin.Context) { if err := h.svc.DeleteBreed(idParam(c, "id")); err != nil { respondErr(c, err) return } response.OK(c, nil) } // ── 记录类型 ── type recordTypeReq struct { ID string `json:"id"` Code string `json:"code"` Label string `json:"label"` Icon string `json:"icon"` Group string `json:"group"` Sort int `json:"sort"` Enabled bool `json:"enabled"` Form string `json:"form"` Fields string `json:"fields_raw"` } // AdminListRecordTypes GET /api/admin/record-types 连停用的一起返回 func (h *Handler) AdminListRecordTypes(c *gin.Context) { response.OK(c, h.svc.AdminListRecordTypes()) } // AdminSaveRecordType POST /api/admin/record-types 新增或更新 func (h *Handler) AdminSaveRecordType(c *gin.Context) { var req recordTypeReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } t := &model.RecordType{ Code: req.Code, Label: req.Label, Icon: req.Icon, GroupKey: req.Group, Sort: req.Sort, Enabled: req.Enabled, Form: req.Form, Fields: req.Fields, } t.ID = req.ID if err := h.svc.SaveRecordType(t); err != nil { respondErr(c, err) return } response.OK(c, t) } // AdminDeleteRecordType DELETE /api/admin/record-types/:id // locked 的和已有记录的删不了,service 层硬拦 func (h *Handler) AdminDeleteRecordType(c *gin.Context) { if err := h.svc.DeleteRecordType(idParam(c, "id")); err != nil { respondErr(c, err) return } response.OK(c, nil) } func (h *Handler) AdminCareTemplateMeta(c *gin.Context) { response.OK(c, gin.H{ "species": []gin.H{ {"key": model.SpeciesCat, "label": "猫猫"}, {"key": model.SpeciesDog, "label": "狗狗"}, }, "stages": service.CareStages, // 「关联记录事项」的可选值读 record_types 表,不再写死。 // 原来是两个硬编码数组(task_sheets / plan_sheets),里头还有 // report / taskDetail 这种早就不存在的值 —— 选了等于点了没反应; // 而后台新加的 24 种事项一个都选不到 "record_types": h.svc.AdminListRecordTypes(), "repeats": []string{"once", "daily", "weekly", "monthly"}, "reminder_types": []string{"vaccine", "deworm", "weight", "monthlyReport"}, "priorities": []string{"", "重要"}, }) } // AdminListCareTemplates GET /api/admin/care-templates 全部方案 func (h *Handler) AdminListCareTemplates(c *gin.Context) { response.OK(c, h.svc.AdminListTemplates()) } // AdminGetCareTemplate GET /api/admin/care-templates/:id 一套方案 func (h *Handler) AdminGetCareTemplate(c *gin.Context) { tpl, err := h.svc.AdminGetTemplate(idParam(c, "id")) if err != nil { respondErr(c, err) return } response.OK(c, tpl) } type careTemplateReq struct { ID string `json:"id"` Species string `json:"species"` Stage string `json:"stage"` Name string `json:"name"` Summary string `json:"summary"` Sort int `json:"sort"` Enabled bool `json:"enabled"` Items []model.CareTemplateItem `json:"items"` } // AdminSaveCareTemplate POST /api/admin/care-templates 新增或更新一套 func (h *Handler) AdminSaveCareTemplate(c *gin.Context) { var req careTemplateReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } tpl := &model.CareTemplate{ Species: req.Species, Stage: req.Stage, Name: req.Name, Summary: req.Summary, Sort: req.Sort, Enabled: req.Enabled, } tpl.ID = req.ID if err := h.svc.SaveTemplate(tpl, req.Items); err != nil { respondErr(c, err) return } saved, _ := h.svc.AdminGetTemplate(tpl.ID) response.OK(c, saved) } // AdminDeleteCareTemplate DELETE /api/admin/care-templates/:id func (h *Handler) AdminDeleteCareTemplate(c *gin.Context) { if err := h.svc.DeleteTemplate(idParam(c, "id")); err != nil { respondErr(c, err) return } response.OK(c, nil) } type careGenReq struct { Species string `json:"species"` Stage string `json:"stage"` } // AdminGenerateCareTemplate POST /api/admin/care-templates/generate // AI 生成草稿(不落库),未开启 AI 时回退内置默认 func (h *Handler) AdminGenerateCareTemplate(c *gin.Context) { var req careGenReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } items, err := h.svc.GenerateCareTemplate(req.Species, req.Stage) if err != nil { response.FailParams(c, err.Error()) return } response.OK(c, gin.H{"species": req.Species, "stage": req.Stage, "items": items}) } // AdminGetCommunityBot GET /api/admin/community-bot func (h *Handler) AdminGetCommunityBot(c *gin.Context) { response.OK(c, h.svc.GetCommunityBotConfig()) } // AdminSaveCommunityBot PUT /api/admin/community-bot func (h *Handler) AdminSaveCommunityBot(c *gin.Context) { var req model.CommunityBotConfig if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } saved, err := h.svc.SaveCommunityBotConfig(req) if err != nil { response.FailErr(c, err) return } response.OK(c, saved) } type communityGenReq struct { Count int `json:"count"` } // AdminGenerateCommunityPosts POST /api/admin/community-bot/generate func (h *Handler) AdminGenerateCommunityPosts(c *gin.Context) { var req communityGenReq if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } if req.Count <= 0 { req.Count = 1 } if req.Count > 30 { req.Count = 30 } made, err := h.svc.GenerateCommunityPosts(req.Count) if err != nil { response.FailErr(c, err) return } response.OK(c, gin.H{"made": made}) } // AdminGetAIQuota GET /api/admin/ai-quota func (h *Handler) AdminGetAIQuota(c *gin.Context) { response.OK(c, h.svc.GetAIQuotaConfig()) } // AdminSaveAIQuota PUT /api/admin/ai-quota func (h *Handler) AdminSaveAIQuota(c *gin.Context) { var req model.AIQuotaConfig if err := c.ShouldBindJSON(&req); err != nil { response.FailParams(c, err.Error()) return } cfg, err := h.svc.SaveAIQuotaConfig(req) if err != nil { response.FailErr(c, err) return } response.OK(c, cfg) }