From 1245d118ff86c366a2fd817cf8237433db8501d0 Mon Sep 17 00:00:00 2001 From: wangrui233 Date: Sun, 25 Oct 2026 21:03:49 +0800 Subject: [PATCH] api --- .codebuddy/rules/api-doc-sync.md | 53 ++++++++ internal/handler/admin/user/user_handler.go | 127 ++++++++++++++++++++ internal/service/admin/user/user_service.go | 78 ++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 .codebuddy/rules/api-doc-sync.md create mode 100644 internal/handler/admin/user/user_handler.go create mode 100644 internal/service/admin/user/user_service.go diff --git a/.codebuddy/rules/api-doc-sync.md b/.codebuddy/rules/api-doc-sync.md new file mode 100644 index 0000000..78ed3ce --- /dev/null +++ b/.codebuddy/rules/api-doc-sync.md @@ -0,0 +1,53 @@ +--- +description: 在 internal/handler 下新增或修改 HTTP 接口后,询问用户是否需要同步更新 .codebuddy/docs 中对应的接口文档(不强制,由用户决定)。适用于所有 gin 路由接口的开发与变更。 +globs: "internal/handler/**/*.go" +alwaysApply: false +--- + +# 接口文档同步提示(非强制) + +每次在 `internal/handler/` 下**新增或修改** HTTP 接口(gin 路由,如 `r.GET` / `r.POST` / `r.PUT` / `r.DELETE` 及对应的 handler 方法)后,**询问用户是否需要**同步更新 `.codebuddy/docs` 下对应的接口文档。是否补充由用户决定,不强制。 + +## 1. 文档归属 + +根据接口挂载的路由前缀判断归属,写入对应文件: + +| 接口归属 | 路由前缀特征 | 文档文件 | +| --- | --- | --- | +| 后台管理接口 | `/api/admin` | `.codebuddy/docs/admin-api.md` | +| 小程序端接口 | 小程序相关(如 `/api/miniapp`) | `.codebuddy/docs/miniapp-api.md` | +| 新增独立模块 | 既不在 admin 也不在 miniapp | 在 `.codebuddy/docs/` 下新建 `-api.md`,并在本规则 §1 登记 | + +> 归属以路由注册时挂载的 group 前缀为准,而非文件名。 + +## 2. 若用户同意补充,需包含的内容 + +每个接口在文档中至少包含: + +- **名称与简介**:一句话说明用途。 +- **请求**:方法 + 完整路径、是否需要鉴权、相关请求头(如 `Authorization`)。 +- **请求参数**: + - 路径参数 / 查询参数 / Body 字段,用表格列出:字段名、类型、是否必填、校验规则(`binding` tag,如 `required`、`max=32`)、说明。 + - 复杂结构给出 JSON 示例。 +- **响应**: + - 成功响应结构与字段说明(字段名、类型、含义)及示例 JSON。 + - 关键业务错误码(非 `0` 的 `code`)及其含义。 +- **示例**:至少一个成功调用示例(`curl` 或 JSON)。 + +## 3. 文档格式约定 + +- 对齐 `.codebuddy/docs` 已有文件的风格:参数/响应用 Markdown 表格描述,示例用 ```` ```json ```` / ```` ```bash ```` 代码块。 +- 文档应可独立阅读,前端/测试无需看代码即可对接。 +- 描述用中文;字段名、路径、代码保持英文原样。 + +## 4. 变更同步(仅当用户选择维护文档时) + +- 若用户选择维护文档,接口**删除 / 重命名 / 改路径 / 改参数 / 改响应结构**时,同步更新文档对应段落。 +- 不得遗留过期、错误的接口描述(仅在维护该文档的前提下)。 + +## 5. 提示时机与方式 + +- 完成接口代码后,主动询问用户:"是否需要更新接口文档?(admin-api.md / miniapp-api.md)" +- 用户确认后才执行文档更新;用户拒绝或忽略则跳过,不强制。 +- 若接口改动较大(删除 / 重命名 / 改路径 / 改参 / 改响应),仍只提示,不强制执行。 +- 用户同意补充时,参照 §2、§3 的约定撰写,并保证示例与代码行为一致。 diff --git a/internal/handler/admin/user/user_handler.go b/internal/handler/admin/user/user_handler.go new file mode 100644 index 0000000..650ecbe --- /dev/null +++ b/internal/handler/admin/user/user_handler.go @@ -0,0 +1,127 @@ +// Package userhdl 是后台管理小程序端用户的 HTTP 接入层。 +package userhdl + +import ( + "strconv" + + "github.com/gin-gonic/gin" + + "portal-mini-go/internal/pkg/errors" + "portal-mini-go/internal/pkg/response" + usersvc "portal-mini-go/internal/service/admin/user" +) + +// Handler 用户管理请求处理。 +type Handler struct { + svc *usersvc.Service +} + +func NewHandler(svc *usersvc.Service) *Handler { return &Handler{svc: svc} } + +// List 分页列表用户。支持 query:mini_id(0 表示全部)、keyword、page、size。 +func (h *Handler) List(c *gin.Context) { + miniID, _ := strconv.ParseUint(c.DefaultQuery("mini_id", "0"), 10, 64) + keyword := c.Query("keyword") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) + list, total, err := h.svc.List(c.Request.Context(), usersvc.ListReq{ + MiniID: miniID, + Keyword: keyword, + Page: page, + Size: size, + }) + if err != nil { + c.Error(err) + return + } + response.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size}) +} + +// Get 查询单个用户详情。 +func (h *Handler) Get(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + u, err := h.svc.GetByID(c.Request.Context(), id) + if err != nil { + c.Error(err) + return + } + response.OK(c, u) +} + +// UpdateReq 编辑用户资料请求;留空字段不更新。 +type UpdateReq struct { + Nickname *string `json:"nickname"` + Avatar *string `json:"avatar"` + Status *int8 `json:"status"` +} + +// Update 编辑用户资料(昵称 / 头像 / 状态)。 +func (h *Handler) Update(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req UpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.Error(errors.ErrBadRequest) + return + } + u, err := h.svc.Update(c.Request.Context(), id, usersvc.UpdateReq{ + Nickname: req.Nickname, + Avatar: req.Avatar, + Status: req.Status, + }) + if err != nil { + c.Error(err) + return + } + response.OK(c, u) +} + +// StatusReq 禁用 / 启用请求。 +type StatusReq struct { + Status int8 `json:"status" binding:"required"` +} + +// SetStatus 禁用 / 启用用户。 +func (h *Handler) SetStatus(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req StatusReq + if err := c.ShouldBindJSON(&req); err != nil { + c.Error(errors.ErrBadRequest) + return + } + u, err := h.svc.SetStatus(c.Request.Context(), id, req.Status) + if err != nil { + c.Error(err) + return + } + response.OK(c, u) +} + +// parseID 从路径参数解析并校验 uint64 主键。 +func parseID(c *gin.Context) (uint64, bool) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + c.Error(errors.ErrBadRequest) + return 0, false + } + return id, true +} + +// Register 注册后台用户管理路由。g 已带 /api/admin 前缀与 JWT 鉴权中间件。 +func Register(g *gin.RouterGroup, h *Handler) { + rg := g.Group("/users") + { + rg.GET("", h.List) + rg.GET("/:id", h.Get) + rg.PUT("/:id", h.Update) + rg.POST("/:id/status", h.SetStatus) + } +} diff --git a/internal/service/admin/user/user_service.go b/internal/service/admin/user/user_service.go new file mode 100644 index 0000000..98dfce4 --- /dev/null +++ b/internal/service/admin/user/user_service.go @@ -0,0 +1,78 @@ +// Package usersvc 是后台管理小程序端用户的业务逻辑。 +package usersvc + +import ( + "context" + + "portal-mini-go/internal/model" + "portal-mini-go/internal/pkg/errors" + "portal-mini-go/internal/repository/user" +) + +// Service 用户管理业务服务,依赖用户仓储。 +type Service struct { + repo *user.Repository +} + +func New(repo *user.Repository) *Service { return &Service{repo: repo} } + +// ListReq 列表查询条件。 +type ListReq struct { + MiniID uint64 + Keyword string + Page int + Size int +} + +// List 分页查询用户,缺省第 1 页、每页 20 条。 +func (s *Service) List(ctx context.Context, req ListReq) ([]model.User, int64, error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 20 + } + return s.repo.AdminList(ctx, user.AdminListFilter{ + MiniID: req.MiniID, + Keyword: req.Keyword, + Page: page, + Size: size, + }) +} + +// GetByID 按 ID 查询用户详情。 +func (s *Service) GetByID(ctx context.Context, id uint64) (*model.User, error) { + return s.repo.AdminGet(ctx, id) +} + +// UpdateReq 编辑用户资料请求;指针区分“未提供”与“置空”。 +type UpdateReq struct { + Nickname *string + Avatar *string + Status *int8 +} + +// Update 编辑用户资料与状态,返回更新后的用户。 +func (s *Service) Update(ctx context.Context, id uint64, req UpdateReq) (*model.User, error) { + if req.Status != nil { + if *req.Status != model.UserStatusEnabled && *req.Status != model.UserStatusDisabled { + return nil, errors.New(40000, "非法的用户状态") + } + } + if err := s.repo.AdminUpdate(ctx, id, req.Nickname, req.Avatar, req.Status); err != nil { + return nil, err + } + return s.repo.AdminGet(ctx, id) +} + +// SetStatus 禁用 / 启用用户,返回更新后的用户。 +func (s *Service) SetStatus(ctx context.Context, id uint64, status int8) (*model.User, error) { + if status != model.UserStatusEnabled && status != model.UserStatusDisabled { + return nil, errors.New(40000, "非法的用户状态") + } + if err := s.repo.AdminUpdate(ctx, id, nil, nil, &status); err != nil { + return nil, err + } + return s.repo.AdminGet(ctx, id) +}