- 新增 /api/auth/register、/api/auth/login,JWT 签发与 Bearer 鉴权中间件 - notes 需登录,users/user-groups 仅管理员;auth 配置项随版本 1→2 自动补全 - internal/api 仅保留路由装配,拆分为 auth/user/usergroup/note/httpx/testutil - 同步更新 Swagger 文档与前端注册接口路径
251 lines
8.4 KiB
Go
251 lines
8.4 KiB
Go
// Package usergroup 提供用户组接口。
|
||
package usergroup
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
|
||
"rill/internal/httpx"
|
||
"rill/internal/model"
|
||
)
|
||
|
||
// Request 创建/更新用户组请求。
|
||
type Request struct {
|
||
Name string `json:"name" binding:"required,max=50" example:"运营组"`
|
||
Description string `json:"description" binding:"max=255" example:"负责日常运营"`
|
||
}
|
||
|
||
// ListResponse 用户组分页列表响应。
|
||
type ListResponse struct {
|
||
Items []model.UserGroup `json:"items"`
|
||
Total int64 `json:"total" example:"42"`
|
||
Page int `json:"page" example:"1"`
|
||
PageSize int `json:"page_size" example:"20"`
|
||
}
|
||
|
||
// @Summary List user groups
|
||
// @Description 分页查询用户组列表,按 id 升序返回。page 从 1 开始;page_size 取值 1-100,默认 20。
|
||
// @Tags user-groups
|
||
// @Produce json
|
||
// @Param page query int false "页码,默认 1" example(1)
|
||
// @Param page_size query int false "每页数量,默认 20,最大 100" example(20)
|
||
// @Success 200 {object} usergroup.ListResponse
|
||
// @Failure 500 {object} httpx.ErrorResponse
|
||
// @Security BearerAuth
|
||
// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期"
|
||
// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用"
|
||
// @Router /user-groups [get]
|
||
func List(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
page, pageSize := httpx.ParsePagination(c)
|
||
ctx := c.Request.Context()
|
||
|
||
var total int64
|
||
if err := db.WithContext(ctx).Model(&model.UserGroup{}).Count(&total).Error; err != nil {
|
||
httpx.RespondDBError(c, err)
|
||
return
|
||
}
|
||
|
||
var groups []model.UserGroup
|
||
if err := db.WithContext(ctx).
|
||
Order("id ASC").
|
||
Offset((page - 1) * pageSize).
|
||
Limit(pageSize).
|
||
Find(&groups).Error; err != nil {
|
||
httpx.RespondDBError(c, err)
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, ListResponse{
|
||
Items: groups,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
})
|
||
}
|
||
}
|
||
|
||
// @Summary Create a user group
|
||
// @Description 创建用户组。name 必填且唯一(最长 50 字符),description 可选(最长 255 字符);id 由服务端分配。
|
||
// @Tags user-groups
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param group body usergroup.Request true "用户组信息"
|
||
// @Success 201 {object} model.UserGroup
|
||
// @Failure 400 {object} httpx.ErrorResponse "参数无效"
|
||
// @Failure 409 {object} httpx.ErrorResponse "用户组名称已存在"
|
||
// @Failure 500 {object} httpx.ErrorResponse
|
||
// @Security BearerAuth
|
||
// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期"
|
||
// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用"
|
||
// @Router /user-groups [post]
|
||
func Create(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
var req Request
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
ctx := c.Request.Context()
|
||
var group model.UserGroup
|
||
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var maxID uint
|
||
if err := tx.Model(&model.UserGroup{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||
return err
|
||
}
|
||
group = model.UserGroup{
|
||
ID: maxID + 1,
|
||
Name: req.Name,
|
||
Description: req.Description,
|
||
}
|
||
return tx.Create(&group).Error
|
||
})
|
||
if err != nil {
|
||
httpx.RespondDuplicateOrDBError(c, err, "用户组名称已存在")
|
||
return
|
||
}
|
||
c.JSON(http.StatusCreated, group)
|
||
}
|
||
}
|
||
|
||
// @Summary Get a user group
|
||
// @Description 按 id 查询单个用户组,id 0 为内置 admin 组。
|
||
// @Tags user-groups
|
||
// @Produce json
|
||
// @Param id path int true "用户组 ID" example(1)
|
||
// @Success 200 {object} model.UserGroup
|
||
// @Failure 400 {object} httpx.ErrorResponse "id 无效"
|
||
// @Failure 404 {object} httpx.ErrorResponse "记录不存在"
|
||
// @Failure 500 {object} httpx.ErrorResponse
|
||
// @Security BearerAuth
|
||
// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期"
|
||
// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用"
|
||
// @Router /user-groups/{id} [get]
|
||
func Get(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
id, ok := parseGroupID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var group model.UserGroup
|
||
if err := db.WithContext(c.Request.Context()).First(&group, id).Error; err != nil {
|
||
httpx.RespondGetError(c, err)
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, group)
|
||
}
|
||
}
|
||
|
||
// @Summary Update a user group
|
||
// @Description 更新用户组的 name 与 description,name 必填且唯一。
|
||
// @Tags user-groups
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path int true "用户组 ID" example(1)
|
||
// @Param group body usergroup.Request true "用户组信息"
|
||
// @Success 200 {object} model.UserGroup
|
||
// @Failure 400 {object} httpx.ErrorResponse "参数无效或 id 无效"
|
||
// @Failure 404 {object} httpx.ErrorResponse "记录不存在"
|
||
// @Failure 409 {object} httpx.ErrorResponse "用户组名称已存在"
|
||
// @Failure 500 {object} httpx.ErrorResponse
|
||
// @Security BearerAuth
|
||
// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期"
|
||
// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用"
|
||
// @Router /user-groups/{id} [put]
|
||
func Update(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
id, ok := parseGroupID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var req Request
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
ctx := c.Request.Context()
|
||
var group model.UserGroup
|
||
if err := db.WithContext(ctx).First(&group, id).Error; err != nil {
|
||
httpx.RespondGetError(c, err)
|
||
return
|
||
}
|
||
|
||
group.Name = req.Name
|
||
group.Description = req.Description
|
||
if err := db.WithContext(ctx).Save(&group).Error; err != nil {
|
||
httpx.RespondDuplicateOrDBError(c, err, "用户组名称已存在")
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, group)
|
||
}
|
||
}
|
||
|
||
// @Summary Delete a user group
|
||
// @Description 按 id 删除用户组,成功时返回 204 且无响应体。系统内置组或组内仍有用户时返回 409。
|
||
// @Tags user-groups
|
||
// @Produce json
|
||
// @Param id path int true "用户组 ID" example(2)
|
||
// @Success 204 "删除成功"
|
||
// @Failure 400 {object} httpx.ErrorResponse "id 无效"
|
||
// @Failure 404 {object} httpx.ErrorResponse "记录不存在"
|
||
// @Failure 409 {object} httpx.ErrorResponse "系统内置组不可删除或用户组内仍有用户"
|
||
// @Failure 500 {object} httpx.ErrorResponse
|
||
// @Security BearerAuth
|
||
// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期"
|
||
// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用"
|
||
// @Router /user-groups/{id} [delete]
|
||
func Delete(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
id, ok := parseGroupID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
ctx := c.Request.Context()
|
||
var group model.UserGroup
|
||
if err := db.WithContext(ctx).First(&group, id).Error; err != nil {
|
||
httpx.RespondGetError(c, err)
|
||
return
|
||
}
|
||
if group.IsSystem {
|
||
c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "系统内置组不可删除"})
|
||
return
|
||
}
|
||
|
||
var members int64
|
||
if err := db.WithContext(ctx).Model(&model.UserGroupMember{}).
|
||
Where("user_group_id = ?", id).
|
||
Count(&members).Error; err != nil {
|
||
httpx.RespondDBError(c, err)
|
||
return
|
||
}
|
||
if members > 0 {
|
||
c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "用户组内仍有用户,无法删除"})
|
||
return
|
||
}
|
||
|
||
if err := db.WithContext(ctx).Delete(&group).Error; err != nil {
|
||
httpx.RespondDBError(c, err)
|
||
return
|
||
}
|
||
c.Status(http.StatusNoContent)
|
||
}
|
||
}
|
||
|
||
// parseGroupID 解析用户组 id,允许内置组 id 0。
|
||
func parseGroupID(c *gin.Context) (uint, bool) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "id 无效"})
|
||
return 0, false
|
||
}
|
||
return uint(id), true
|
||
}
|