Files
EchoChat/backend/go-service/app/admin/service/user_manage_service.go
bujinyuan fa6f1e5c58 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
2026-03-02 15:40:02 +08:00

374 lines
11 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 service 提供 admin 模块的核心业务逻辑
package service
import (
"context"
"errors"
"math"
authDAO "github.com/echochat/backend/app/auth/dao"
"github.com/echochat/backend/app/auth/model"
adminDAO "github.com/echochat/backend/app/admin/dao"
"github.com/echochat/backend/app/constants"
"github.com/echochat/backend/app/dto"
"github.com/echochat/backend/pkg/logs"
"github.com/echochat/backend/pkg/utils"
"go.uber.org/zap"
"gorm.io/gorm"
)
var (
ErrUserNotFound = errors.New("用户不存在")
ErrUserExists = errors.New("用户名或邮箱已被注册")
ErrInvalidStatus = errors.New("无效的用户状态")
ErrInvalidRole = errors.New("无效的角色代码")
ErrCannotDisableSelf = errors.New("不能禁用自己的账号")
ErrInsufficientPermission = errors.New("权限不足,无法操作更高等级的用户")
ErrCannotAssignHigherRole = errors.New("不能分配高于自身等级的角色")
)
// UserManageService 管理端用户管理服务
type UserManageService struct {
userManageDAO *adminDAO.UserManageDAO
userDAO *authDAO.UserDAO
roleDAO *authDAO.RoleDAO
}
// NewUserManageService 创建用户管理服务实例
func NewUserManageService(
userManageDAO *adminDAO.UserManageDAO,
userDAO *authDAO.UserDAO,
roleDAO *authDAO.RoleDAO,
) *UserManageService {
return &UserManageService{
userManageDAO: userManageDAO,
userDAO: userDAO,
roleDAO: roleDAO,
}
}
// GetUserList 获取用户列表(分页 + 搜索 + 筛选)
func (s *UserManageService) GetUserList(ctx context.Context, req *dto.UserListRequest) (*dto.UserListResponse, error) {
funcName := "service.user_manage_service.GetUserList"
logs.Info(ctx, funcName, "获取用户列表")
users, total, err := s.userManageDAO.ListUsers(ctx, req)
if err != nil {
return nil, err
}
list := make([]dto.AdminUserInfo, 0, len(users))
for _, user := range users {
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if roleErr != nil {
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
zap.Int64("user_id", user.ID), zap.Error(roleErr))
roles = []model.Role{}
}
list = append(list, *s.buildAdminUserInfo(&user, roles))
}
return &dto.UserListResponse{
Total: total,
List: list,
}, nil
}
// GetUserDetail 获取单个用户详情(含角色信息)
func (s *UserManageService) GetUserDetail(ctx context.Context, userID int64) (*dto.AdminUserInfo, error) {
funcName := "service.user_manage_service.GetUserDetail"
logs.Info(ctx, funcName, "获取用户详情", zap.Int64("user_id", userID))
user, err := s.userDAO.FindByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if roleErr != nil {
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
zap.Int64("user_id", user.ID), zap.Error(roleErr))
roles = []model.Role{}
}
return s.buildAdminUserInfo(user, roles), nil
}
// UpdateUserStatus 启用/禁用用户
// adminUserID 用于防止管理员禁用自己;同时校验操作者权限等级必须高于目标用户
func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error {
funcName := "service.user_manage_service.UpdateUserStatus"
logs.Info(ctx, funcName, "更新用户状态",
zap.Int64("target_user_id", userID),
zap.Int("status", status),
zap.Int64("admin_user_id", adminUserID),
)
if userID == adminUserID {
return ErrCannotDisableSelf
}
if status != constants.UserStatusActive && status != constants.UserStatusDisabled {
return ErrInvalidStatus
}
_, err := s.userDAO.FindByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrUserNotFound
}
return err
}
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
return err
}
if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil {
return err
}
logs.Info(ctx, funcName, "用户状态更新成功",
zap.Int64("user_id", userID),
zap.Int("new_status", status),
)
return nil
}
// SetUserRoles 批量设置用户角色(事务内先清后设)
// adminUserID 用于校验操作者等级,防止越权分配高等级角色
func (s *UserManageService) SetUserRoles(ctx context.Context, userID int64, roleCodes []string, adminUserID int64) error {
funcName := "service.user_manage_service.SetUserRoles"
logs.Info(ctx, funcName, "设置用户角色",
zap.Int64("target_user_id", userID),
zap.Strings("role_codes", roleCodes),
zap.Int64("admin_user_id", adminUserID),
)
_, err := s.userDAO.FindByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrUserNotFound
}
return err
}
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
return err
}
roles, err := s.roleDAO.FindByCodeList(ctx, roleCodes)
if err != nil {
return err
}
if len(roles) != len(roleCodes) {
return ErrInvalidRole
}
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
return err
}
for _, r := range roles {
if r.Level <= adminLevel {
logs.Warn(ctx, funcName, "尝试分配高于自身等级的角色",
zap.String("role_code", r.Code),
zap.Int("role_level", r.Level),
zap.Int("admin_level", adminLevel),
)
return ErrCannotAssignHigherRole
}
}
roleIDs := make([]int, 0, len(roles))
for _, r := range roles {
roleIDs = append(roleIDs, r.ID)
}
if err := s.roleDAO.SetUserRoles(ctx, userID, roleIDs); err != nil {
return err
}
logs.Info(ctx, funcName, "用户角色设置成功",
zap.Int64("user_id", userID),
zap.Strings("role_codes", roleCodes),
)
return nil
}
// GetAllRoles 获取所有角色列表
func (s *UserManageService) GetAllRoles(ctx context.Context) ([]dto.RoleInfo, error) {
funcName := "service.user_manage_service.GetAllRoles"
logs.Info(ctx, funcName, "获取所有角色列表")
roles, err := s.roleDAO.GetAllRoles(ctx)
if err != nil {
return nil, err
}
result := make([]dto.RoleInfo, 0, len(roles))
for _, r := range roles {
result = append(result, dto.RoleInfo{
Code: r.Code,
Name: r.Name,
Level: r.Level,
})
}
return result, nil
}
// checkPermissionLevel 校验操作者的权限等级是否高于目标用户
// 操作者 level 必须 < 目标用户 level数值更小 = 权限更高)
func (s *UserManageService) checkPermissionLevel(ctx context.Context, adminUserID, targetUserID int64) error {
funcName := "service.user_manage_service.checkPermissionLevel"
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
logs.Error(ctx, funcName, "获取操作者权限等级失败", zap.Error(err))
return err
}
targetLevel, err := s.roleDAO.GetUserMaxLevel(ctx, targetUserID)
if err != nil {
logs.Error(ctx, funcName, "获取目标用户权限等级失败", zap.Error(err))
return err
}
if adminLevel >= targetLevel {
logs.Warn(ctx, funcName, "权限不足",
zap.Int64("admin_user_id", adminUserID),
zap.Int("admin_level", adminLevel),
zap.Int64("target_user_id", targetUserID),
zap.Int("target_level", targetLevel),
)
return ErrInsufficientPermission
}
return nil
}
// CreateUser 管理员手动创建用户
// adminUserID 用于校验操作者权限等级,防止越权分配高等级角色
func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest, adminUserID int64) (*dto.AdminUserInfo, error) {
funcName := "service.user_manage_service.CreateUser"
logs.Info(ctx, funcName, "管理员创建用户",
zap.String("username", req.Username),
zap.String("email", logs.MaskEmail(req.Email)),
zap.Int64("admin_user_id", adminUserID),
)
existing, findErr := s.userDAO.FindByUsername(ctx, req.Username)
if findErr != nil && !errors.Is(findErr, gorm.ErrRecordNotFound) {
return nil, findErr
}
if existing != nil {
return nil, ErrUserExists
}
existing, findErr = s.userDAO.FindByEmail(ctx, req.Email)
if findErr != nil && !errors.Is(findErr, gorm.ErrRecordNotFound) {
return nil, findErr
}
if existing != nil {
return nil, ErrUserExists
}
roleCode := req.RoleCode
if roleCode == "" {
roleCode = constants.RoleUser
}
role, roleErr := s.roleDAO.FindByCode(ctx, roleCode)
if roleErr != nil {
return nil, ErrInvalidRole
}
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
return nil, err
}
if role.Level <= adminLevel {
logs.Warn(ctx, funcName, "尝试为新用户分配高于自身等级的角色",
zap.String("role_code", roleCode),
zap.Int("role_level", role.Level),
zap.Int("admin_level", adminLevel),
)
return nil, ErrCannotAssignHigherRole
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
return nil, err
}
nickname := req.Nickname
if nickname == "" {
nickname = req.Username
}
user := &model.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Nickname: nickname,
Status: constants.UserStatusActive,
}
if err := s.userDAO.Create(ctx, user); err != nil {
return nil, err
}
if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil {
logs.Warn(ctx, funcName, "角色分配失败",
zap.Int64("user_id", user.ID), zap.Error(assignErr))
}
userRoles, rolesErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if rolesErr != nil {
logs.Warn(ctx, funcName, "获取新建用户角色失败",
zap.Int64("user_id", user.ID), zap.Error(rolesErr))
userRoles = []model.Role{}
}
logs.Info(ctx, funcName, "管理员创建用户成功", zap.Int64("user_id", user.ID))
return s.buildAdminUserInfo(user, userRoles), nil
}
// buildAdminUserInfo 从 model.User + []model.Role 构建 dto.AdminUserInfo
func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []model.Role) *dto.AdminUserInfo {
roleInfos := make([]dto.RoleInfo, 0, len(roles))
minLevel := math.MaxInt32
for _, r := range roles {
roleInfos = append(roleInfos, dto.RoleInfo{
Code: r.Code,
Name: r.Name,
Level: r.Level,
})
if r.Level < minLevel {
minLevel = r.Level
}
}
info := &dto.AdminUserInfo{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Nickname: user.Nickname,
Avatar: user.Avatar,
Gender: user.Gender,
Status: user.Status,
StatusText: constants.UserStatusMap[user.Status],
Roles: roleInfos,
MaxLevel: minLevel,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}
if user.Phone != nil {
info.Phone = *user.Phone
}
if user.LastLoginAt != nil {
info.LastLoginAt = user.LastLoginAt.Format("2006-01-02 15:04:05")
}
if user.LastLoginIP != nil {
info.LastLoginIP = *user.LastLoginIP
}
return info
}