feat: 角色等级体系与权限管控实施

- 数据库:auth_roles 表新增 level 字段(1=超管, 10=管理员, 100=普通用户)
- 后端:新增 GetAllRoles/GetUserMaxLevel/SetUserRoles/FindByCodeList DAO 方法
- 后端:SetRolesRequest 替换 AssignRoleRequest,AdminUserInfo.Roles 改为 []RoleInfo
- 后端:所有管理操作(禁用/启用/角色分配/创建用户)强制层级权限校验
- 后端:PUT /users/:id/roles(批量设置角色)+ GET /roles(角色列表)
- 前端:角色管理改为 Checkbox Group 多选,高等级角色禁用
- 前端:禁用/启用按钮受角色层级约束,列表页操作按钮权限管控
- 前端:创建用户对话框角色选项根据操作者等级动态过滤
- 文档:API 文档、设计方案、集成规范、进度文档同步更新

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-03-02 15:40:02 +08:00
parent dd84e50429
commit fa6f1e5c58
18 changed files with 771 additions and 279 deletions

View File

@@ -2,6 +2,7 @@ package dao
import (
"context"
"math"
"github.com/echochat/backend/app/auth/model"
"github.com/echochat/backend/pkg/logs"
@@ -117,3 +118,87 @@ func (d *RoleDAO) RemoveUserRoles(ctx context.Context, userID int64) error {
Where("user_id = ?", userID).
Delete(&model.UserRole{}).Error
}
// GetAllRoles 获取所有角色列表(按 level 升序排列)
func (d *RoleDAO) GetAllRoles(ctx context.Context) ([]model.Role, error) {
funcName := "dao.role_dao.GetAllRoles"
logs.Debug(ctx, funcName, "获取所有角色列表")
var roles []model.Role
err := d.db.WithContext(ctx).Order("level ASC").Find(&roles).Error
if err != nil {
logs.Error(ctx, funcName, "获取所有角色失败", zap.Error(err))
return nil, err
}
return roles, nil
}
// GetUserMaxLevel 获取用户的最高权限等级(最小 level 值)
// 如果用户无角色,返回 math.MaxInt32 表示无权限
func (d *RoleDAO) GetUserMaxLevel(ctx context.Context, userID int64) (int, error) {
funcName := "dao.role_dao.GetUserMaxLevel"
logs.Debug(ctx, funcName, "获取用户最高权限等级", zap.Int64("user_id", userID))
var minLevel *int
err := d.db.WithContext(ctx).
Model(&model.UserRole{}).
Joins("JOIN auth_roles ON auth_roles.id = auth_user_roles.role_id").
Where("auth_user_roles.user_id = ?", userID).
Select("MIN(auth_roles.level)").
Scan(&minLevel).Error
if err != nil {
logs.Error(ctx, funcName, "获取用户权限等级失败", zap.Error(err))
return 0, err
}
if minLevel == nil {
return math.MaxInt32, nil
}
return *minLevel, nil
}
// SetUserRoles 事务内先清除再批量设置用户角色
func (d *RoleDAO) SetUserRoles(ctx context.Context, userID int64, roleIDs []int) error {
funcName := "dao.role_dao.SetUserRoles"
logs.Info(ctx, funcName, "设置用户角色",
zap.Int64("user_id", userID),
zap.Ints("role_ids", roleIDs),
)
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.UserRole{}).Error; err != nil {
logs.Error(ctx, funcName, "清除用户角色失败", zap.Error(err))
return err
}
if len(roleIDs) == 0 {
return nil
}
userRoles := make([]model.UserRole, 0, len(roleIDs))
for _, rid := range roleIDs {
userRoles = append(userRoles, model.UserRole{
UserID: userID,
RoleID: rid,
})
}
if err := tx.Create(&userRoles).Error; err != nil {
logs.Error(ctx, funcName, "批量设置用户角色失败", zap.Error(err))
return err
}
return nil
})
}
// FindByCodeList 按角色代码列表批量查询角色
func (d *RoleDAO) FindByCodeList(ctx context.Context, codes []string) ([]model.Role, error) {
funcName := "dao.role_dao.FindByCodeList"
logs.Debug(ctx, funcName, "批量查询角色", zap.Strings("codes", codes))
var roles []model.Role
err := d.db.WithContext(ctx).Where("code IN ?", codes).Find(&roles).Error
if err != nil {
logs.Error(ctx, funcName, "批量查询角色失败", zap.Error(err))
return nil, err
}
return roles, nil
}

View File

@@ -8,6 +8,7 @@ type Role struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin对应 constants.RoleCode*
Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」
Level int `json:"level" gorm:"not null;default:100"` // 角色等级值越小权限越高1=超管, 10=管理员, 100=普通用户
Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒
}