Files
rill/internal/usergroup/user_group.go
T
kevin b683fb293e 增加注册登录与鉴权,并按功能拆分 internal 模块
- 新增 /api/auth/register、/api/auth/login,JWT 签发与 Bearer 鉴权中间件
- notes 需登录,users/user-groups 仅管理员;auth 配置项随版本 1→2 自动补全
- internal/api 仅保留路由装配,拆分为 auth/user/usergroup/note/httpx/testutil
- 同步更新 Swagger 文档与前端注册接口路径
2026-09-20 02:03:06 +08:00

251 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 与 descriptionname 必填且唯一。
// @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
}