feat(role): 新增平台角色管理功能增强

- 权限表增加 available_for_role_types 字段,支持标记权限可用角色类型
- 权限列表和权限树接口支持按 available_for_role_type 过滤
- 新增角色状态切换接口 PUT /api/admin/roles/:id/status
- 角色分配权限时验证权限的可用角色类型
- 完善数据库迁移脚本和单元测试
- 补充数据库迁移相关开发规范文档
This commit is contained in:
2026-01-14 12:15:57 +08:00
parent 9c399df6bc
commit 5556b1028c
22 changed files with 1474 additions and 87 deletions

View File

@@ -109,7 +109,15 @@ func (h *PermissionHandler) List(c *fiber.Ctx) error {
// GetTree 获取权限树
// GET /api/v1/permissions/tree
func (h *PermissionHandler) GetTree(c *fiber.Ctx) error {
tree, err := h.service.GetTree(c.UserContext())
var availableForRoleType *int
if roleTypeStr := c.Query("available_for_role_type"); roleTypeStr != "" {
roleType, err := strconv.Atoi(roleTypeStr)
if err == nil && (roleType == 1 || roleType == 2) {
availableForRoleType = &roleType
}
}
tree, err := h.service.GetTree(c.UserContext(), availableForRoleType)
if err != nil {
return err
}

View File

@@ -162,3 +162,23 @@ func (h *RoleHandler) RemovePermission(c *fiber.Ctx) error {
return response.Success(c, nil)
}
// UpdateStatus 更新角色状态
// PUT /api/v1/roles/:id/status
func (h *RoleHandler) UpdateStatus(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的角色 ID")
}
var req model.UpdateRoleStatusRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
return err
}
return response.Success(c, nil)
}

View File

@@ -9,14 +9,15 @@ type Permission struct {
gorm.Model
BaseModel `gorm:"embedded"`
PermName string `gorm:"column:perm_name;not null;size:50;comment:权限名称" json:"perm_name"`
PermCode string `gorm:"column:perm_code;uniqueIndex:idx_permission_code,where:deleted_at IS NULL;not null;size:100;comment:权限编码" json:"perm_code"`
PermType int `gorm:"column:perm_type;not null;index;comment:权限类型 1=菜单 2=按钮" json:"perm_type"`
Platform string `gorm:"column:platform;type:varchar(20);default:'all';comment:适用端口 all=全部 web=Web后台 h5=H5端" json:"platform"`
URL string `gorm:"column:url;size:255;comment:URL路径" json:"url,omitempty"`
ParentID *uint `gorm:"column:parent_id;index;comment:上级权限ID" json:"parent_id,omitempty"`
Sort int `gorm:"column:sort;not null;default:0;comment:排序" json:"sort"`
Status int `gorm:"column:status;not null;default:1;comment:状态 0=禁用 1=启用" json:"status"`
PermName string `gorm:"column:perm_name;not null;size:50;comment:权限名称" json:"perm_name"`
PermCode string `gorm:"column:perm_code;uniqueIndex:idx_permission_code,where:deleted_at IS NULL;not null;size:100;comment:权限编码" json:"perm_code"`
PermType int `gorm:"column:perm_type;not null;index;comment:权限类型 1=菜单 2=按钮" json:"perm_type"`
Platform string `gorm:"column:platform;type:varchar(20);default:'all';comment:适用端口 all=全部 web=Web后台 h5=H5端" json:"platform"`
AvailableForRoleTypes string `gorm:"column:available_for_role_types;type:varchar(20);default:'1,2';not null;comment:可用角色类型 1=平台 2=客户" json:"available_for_role_types"`
URL string `gorm:"column:url;size:255;comment:URL路径" json:"url,omitempty"`
ParentID *uint `gorm:"column:parent_id;index;comment:上级权限ID" json:"parent_id,omitempty"`
Sort int `gorm:"column:sort;not null;default:0;comment:排序" json:"sort"`
Status int `gorm:"column:status;not null;default:1;comment:状态 0=禁用 1=启用" json:"status"`
}
// TableName 指定表名

View File

@@ -30,31 +30,33 @@ type UpdatePermissionParams struct {
// PermissionListRequest 权限列表查询请求
type PermissionListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
PermName string `json:"perm_name" query:"perm_name" validate:"omitempty,max=50" maxLength:"50" description:"权限名称模糊查询"`
PermCode string `json:"perm_code" query:"perm_code" validate:"omitempty,max=100" maxLength:"100" description:"权限编码模糊查询"`
PermType *int `json:"perm_type" query:"perm_type" validate:"omitempty,min=1,max=2" minimum:"1" maximum:"2" description:"权限类型"`
Platform string `json:"platform" query:"platform" validate:"omitempty,oneof=all web h5" description:"适用端口"`
ParentID *uint `json:"parent_id" query:"parent_id" description:"父权限ID"`
Status *int `json:"status" query:"status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"状态"`
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
PermName string `json:"perm_name" query:"perm_name" validate:"omitempty,max=50" maxLength:"50" description:"权限名称模糊查询"`
PermCode string `json:"perm_code" query:"perm_code" validate:"omitempty,max=100" maxLength:"100" description:"权限编码模糊查询"`
PermType *int `json:"perm_type" query:"perm_type" validate:"omitempty,min=1,max=2" minimum:"1" maximum:"2" description:"权限类型"`
Platform string `json:"platform" query:"platform" validate:"omitempty,oneof=all web h5" description:"适用端口"`
AvailableForRoleType *int `json:"available_for_role_type" query:"available_for_role_type" validate:"omitempty,min=1,max=2" minimum:"1" maximum:"2" description:"可用角色类型 (1:平台角色, 2:客户角色)"`
ParentID *uint `json:"parent_id" query:"parent_id" description:"父权限ID"`
Status *int `json:"status" query:"status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"状态"`
}
// PermissionResponse 权限响应
type PermissionResponse struct {
ID uint `json:"id" description:"权限ID"`
PermName string `json:"perm_name" description:"权限名称"`
PermCode string `json:"perm_code" description:"权限编码"`
PermType int `json:"perm_type" description:"权限类型"`
Platform string `json:"platform" description:"适用端口"`
URL string `json:"url,omitempty" description:"请求路径"`
ParentID *uint `json:"parent_id,omitempty" description:"父权限ID"`
Sort int `json:"sort" description:"排序值"`
Status int `json:"status" description:"状态"`
Creator uint `json:"creator" description:"创建人ID"`
Updater uint `json:"updater" description:"更新人ID"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
ID uint `json:"id" description:"权限ID"`
PermName string `json:"perm_name" description:"权限名称"`
PermCode string `json:"perm_code" description:"权限编码"`
PermType int `json:"perm_type" description:"权限类型"`
Platform string `json:"platform" description:"适用端口"`
AvailableForRoleTypes string `json:"available_for_role_types" description:"可用角色类型"`
URL string `json:"url,omitempty" description:"请求路径"`
ParentID *uint `json:"parent_id,omitempty" description:"父权限ID"`
Sort int `json:"sort" description:"排序值"`
Status int `json:"status" description:"状态"`
Creator uint `json:"creator" description:"创建人ID"`
Updater uint `json:"updater" description:"更新人ID"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
// PermissionPageResult 权限分页响应
@@ -67,12 +69,13 @@ type PermissionPageResult struct {
// PermissionTreeNode 权限树节点(用于层级展示)
type PermissionTreeNode struct {
ID uint `json:"id" description:"权限ID"`
PermName string `json:"perm_name" description:"权限名称"`
PermCode string `json:"perm_code" description:"权限编码"`
PermType int `json:"perm_type" description:"权限类型"`
Platform string `json:"platform" description:"适用端口"`
URL string `json:"url,omitempty" description:"请求路径"`
Sort int `json:"sort" description:"排序值"`
Children []*PermissionTreeNode `json:"children,omitempty" description:"子权限列表"`
ID uint `json:"id" description:"权限ID"`
PermName string `json:"perm_name" description:"权限名称"`
PermCode string `json:"perm_code" description:"权限编码"`
PermType int `json:"perm_type" description:"权限类型"`
Platform string `json:"platform" description:"适用端口"`
AvailableForRoleTypes string `json:"available_for_role_types" description:"可用角色类型"`
URL string `json:"url,omitempty" description:"请求路径"`
Sort int `json:"sort" description:"排序值"`
Children []*PermissionTreeNode `json:"children,omitempty" description:"子权限列表"`
}

View File

@@ -66,3 +66,14 @@ type RemovePermissionParams struct {
RoleID uint `path:"role_id" required:"true" description:"角色ID"`
PermID uint `path:"perm_id" required:"true" description:"权限ID"`
}
// UpdateRoleStatusRequest 更新角色状态请求
type UpdateRoleStatusRequest struct {
Status int `json:"status" validate:"required,min=0,max=1" required:"true" minimum:"0" maximum:"1" description:"状态 (0:禁用, 1:启用)"`
}
// UpdateRoleStatusParams 更新角色状态参数聚合
type UpdateRoleStatusParams struct {
IDReq
UpdateRoleStatusRequest
}

View File

@@ -42,6 +42,13 @@ func registerRoleRoutes(api fiber.Router, h *admin.RoleHandler, doc *openapi.Gen
Output: new(model.RoleResponse),
})
Register(roles, doc, groupPath, "PUT", "/:id/status", h.UpdateStatus, RouteSpec{
Summary: "更新角色状态",
Tags: []string{"Role"},
Input: new(model.UpdateRoleStatusParams),
Output: nil,
})
Register(roles, doc, groupPath, "DELETE", "/:id", h.Delete, RouteSpec{
Summary: "删除角色",
Tags: []string{"Role"},

View File

@@ -202,6 +202,9 @@ func (s *Service) List(ctx context.Context, req *model.PermissionListRequest) ([
if req.Platform != "" {
filters["platform"] = req.Platform
}
if req.AvailableForRoleType != nil {
filters["available_for_role_type"] = *req.AvailableForRoleType
}
if req.ParentID != nil {
filters["parent_id"] = *req.ParentID
}
@@ -213,35 +216,32 @@ func (s *Service) List(ctx context.Context, req *model.PermissionListRequest) ([
}
// GetTree 获取权限树
func (s *Service) GetTree(ctx context.Context) ([]*model.PermissionTreeNode, error) {
// 获取所有权限
permissions, err := s.permissionStore.GetAll(ctx)
func (s *Service) GetTree(ctx context.Context, availableForRoleType *int) ([]*model.PermissionTreeNode, error) {
permissions, err := s.permissionStore.GetAll(ctx, availableForRoleType)
if err != nil {
return nil, fmt.Errorf("获取权限列表失败: %w", err)
}
// 构建树结构
return buildPermissionTree(permissions), nil
}
// buildPermissionTree 构建权限树
func buildPermissionTree(permissions []*model.Permission) []*model.PermissionTreeNode {
// 转换为节点映射
nodeMap := make(map[uint]*model.PermissionTreeNode)
for _, p := range permissions {
nodeMap[p.ID] = &model.PermissionTreeNode{
ID: p.ID,
PermName: p.PermName,
PermCode: p.PermCode,
PermType: p.PermType,
Platform: p.Platform,
URL: p.URL,
Sort: p.Sort,
Children: make([]*model.PermissionTreeNode, 0),
ID: p.ID,
PermName: p.PermName,
PermCode: p.PermCode,
PermType: p.PermType,
Platform: p.Platform,
AvailableForRoleTypes: p.AvailableForRoleTypes,
URL: p.URL,
Sort: p.Sort,
Children: make([]*model.PermissionTreeNode, 0),
}
}
// 构建树
var roots []*model.PermissionTreeNode
for _, p := range permissions {
node := nodeMap[p.ID]
@@ -250,7 +250,6 @@ func buildPermissionTree(permissions []*model.Permission) []*model.PermissionTre
} else if parent, ok := nodeMap[*p.ParentID]; ok {
parent.Children = append(parent.Children, node)
} else {
// 如果找不到父节点,作为根节点处理
roots = append(roots, node)
}
}

View File

@@ -5,6 +5,7 @@ package role
import (
"context"
"fmt"
"strings"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store"
@@ -151,14 +152,12 @@ func (s *Service) List(ctx context.Context, req *model.RoleListRequest) ([]*mode
// AssignPermissions 为角色分配权限
func (s *Service) AssignPermissions(ctx context.Context, roleID uint, permIDs []uint) ([]*model.RolePermission, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 检查角色存在
_, err := s.roleStore.GetByID(ctx, roleID)
role, err := s.roleStore.GetByID(ctx, roleID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeRoleNotFound, "角色不存在")
@@ -166,24 +165,32 @@ func (s *Service) AssignPermissions(ctx context.Context, roleID uint, permIDs []
return nil, fmt.Errorf("获取角色失败: %w", err)
}
// 验证所有权限存在
for _, permID := range permIDs {
_, err := s.permissionStore.GetByID(ctx, permID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodePermissionNotFound, fmt.Sprintf("权限 %d 不存在", permID))
}
return nil, fmt.Errorf("获取权限失败: %w", err)
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
if err != nil {
return nil, fmt.Errorf("获取权限失败: %w", err)
}
if len(permissions) != len(permIDs) {
return nil, errors.New(errors.CodePermissionNotFound, "部分权限不存在")
}
roleTypeStr := fmt.Sprintf("%d", role.RoleType)
var invalidPermIDs []uint
for _, perm := range permissions {
if !contains(perm.AvailableForRoleTypes, roleTypeStr) {
invalidPermIDs = append(invalidPermIDs, perm.ID)
}
}
// 创建关联
if len(invalidPermIDs) > 0 {
return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("权限 %v 不适用于此角色类型", invalidPermIDs))
}
var rps []*model.RolePermission
for _, permID := range permIDs {
// 检查是否已分配
exists, _ := s.rolePermissionStore.Exists(ctx, roleID, permID)
if exists {
continue // 跳过已存在的关联
continue
}
rp := &model.RolePermission{
@@ -227,7 +234,6 @@ func (s *Service) GetPermissions(ctx context.Context, roleID uint) ([]*model.Per
// RemovePermission 移除角色的权限
func (s *Service) RemovePermission(ctx context.Context, roleID, permID uint) error {
// 检查角色存在
_, err := s.roleStore.GetByID(ctx, roleID)
if err != nil {
if err == gorm.ErrRecordNotFound {
@@ -236,10 +242,44 @@ func (s *Service) RemovePermission(ctx context.Context, roleID, permID uint) err
return fmt.Errorf("获取角色失败: %w", err)
}
// 删除关联
if err := s.rolePermissionStore.Delete(ctx, roleID, permID); err != nil {
return fmt.Errorf("删除角色-权限关联失败: %w", err)
}
return nil
}
// UpdateStatus 更新角色状态
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
role, err := s.roleStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeRoleNotFound, "角色不存在")
}
return fmt.Errorf("获取角色失败: %w", err)
}
role.Status = status
role.Updater = currentUserID
if err := s.roleStore.Update(ctx, role); err != nil {
return fmt.Errorf("更新角色状态失败: %w", err)
}
return nil
}
func contains(availableForRoleTypes, roleTypeStr string) bool {
types := strings.Split(availableForRoleTypes, ",")
for _, t := range types {
if strings.TrimSpace(t) == roleTypeStr {
return true
}
}
return false
}

View File

@@ -2,6 +2,7 @@ package postgres
import (
"context"
"fmt"
"gorm.io/gorm"
@@ -72,6 +73,10 @@ func (s *PermissionStore) List(ctx context.Context, opts *store.QueryOptions, fi
if platform, ok := filters["platform"].(string); ok && platform != "" {
query = query.Where("platform = ?", platform)
}
if availableForRoleType, ok := filters["available_for_role_type"].(int); ok {
roleTypeStr := fmt.Sprintf("%d", availableForRoleType)
query = query.Where("available_for_role_types LIKE ?", "%"+roleTypeStr+"%")
}
if parentID, ok := filters["parent_id"].(uint); ok {
query = query.Where("parent_id = ?", parentID)
}
@@ -116,23 +121,33 @@ func (s *PermissionStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Pe
}
// GetAll 获取所有权限(用于构建权限树)
func (s *PermissionStore) GetAll(ctx context.Context) ([]*model.Permission, error) {
func (s *PermissionStore) GetAll(ctx context.Context, availableForRoleType *int) ([]*model.Permission, error) {
var permissions []*model.Permission
if err := s.db.WithContext(ctx).Order("sort ASC, id ASC").Find(&permissions).Error; err != nil {
return nil, err
}
return permissions, nil
}
query := s.db.WithContext(ctx)
// GetByPlatform 根据端口获取权限列表
// platform: 端口类型all/web/h5如果为空则返回所有权限
func (s *PermissionStore) GetByPlatform(ctx context.Context, platform string) ([]*model.Permission, error) {
var permissions []*model.Permission
query := s.db.WithContext(ctx).Where("status = ?", 1) // 只获取启用的权限
if platform != "" {
// 获取指定端口的权限或通用权限platform='all'
query = query.Where("platform = ? OR platform = ?", platform, "all")
if availableForRoleType != nil {
roleTypeStr := fmt.Sprintf("%d", *availableForRoleType)
query = query.Where("available_for_role_types LIKE ?", "%"+roleTypeStr+"%")
}
if err := query.Order("sort ASC, id ASC").Find(&permissions).Error; err != nil {
return nil, err
}
return permissions, nil
}
// GetByPlatform 根据端口获取权限列表
func (s *PermissionStore) GetByPlatform(ctx context.Context, platform string, availableForRoleType *int) ([]*model.Permission, error) {
var permissions []*model.Permission
query := s.db.WithContext(ctx).Where("status = ?", 1)
if platform != "" {
query = query.Where("platform = ? OR platform = ?", platform, "all")
}
if availableForRoleType != nil {
roleTypeStr := fmt.Sprintf("%d", *availableForRoleType)
query = query.Where("available_for_role_types LIKE ?", "%"+roleTypeStr+"%")
}
if err := query.Order("sort ASC, id ASC").Find(&permissions).Error; err != nil {