diff --git a/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/CreateAdminUserRequest.cs b/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/CreateAdminUserRequest.cs index c546a5c..9da5769 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/CreateAdminUserRequest.cs +++ b/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/CreateAdminUserRequest.cs @@ -24,10 +24,15 @@ public class CreateAdminUserRequest public string Password { get; set; } = null!; /// - /// 真实姓名 + /// 真实姓名(为空时自动转 null) /// [MaxLength(50, ErrorMessage = "真实姓名最多50个字符")] - public string? RealName { get; set; } + public string? RealName + { + get => _realName; + set => _realName = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _realName; /// /// 头像URL @@ -36,17 +41,27 @@ public class CreateAdminUserRequest public string? Avatar { get; set; } /// - /// 邮箱 + /// 邮箱(为空时跳过格式验证) /// [MaxLength(100, ErrorMessage = "邮箱最多100个字符")] [EmailAddress(ErrorMessage = "邮箱格式不正确")] - public string? Email { get; set; } + public string? Email + { + get => _email; + set => _email = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _email; /// - /// 手机号 + /// 手机号(为空时自动转 null) /// [MaxLength(20, ErrorMessage = "手机号最多20个字符")] - public string? Phone { get; set; } + public string? Phone + { + get => _phone; + set => _phone = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _phone; /// /// 部门ID diff --git a/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/UpdateAdminUserRequest.cs b/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/UpdateAdminUserRequest.cs index 04895bc..5b56211 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/UpdateAdminUserRequest.cs +++ b/server/MiAssessment/src/MiAssessment.Admin/Models/AdminUser/UpdateAdminUserRequest.cs @@ -8,10 +8,15 @@ namespace MiAssessment.Admin.Models.AdminUser; public class UpdateAdminUserRequest { /// - /// 真实姓名 + /// 真实姓名(为空时自动转 null) /// [MaxLength(50, ErrorMessage = "真实姓名最多50个字符")] - public string? RealName { get; set; } + public string? RealName + { + get => _realName; + set => _realName = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _realName; /// /// 头像URL @@ -20,17 +25,27 @@ public class UpdateAdminUserRequest public string? Avatar { get; set; } /// - /// 邮箱 + /// 邮箱(为空时跳过格式验证) /// [MaxLength(100, ErrorMessage = "邮箱最多100个字符")] [EmailAddress(ErrorMessage = "邮箱格式不正确")] - public string? Email { get; set; } + public string? Email + { + get => _email; + set => _email = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _email; /// - /// 手机号 + /// 手机号(为空时自动转 null) /// [MaxLength(20, ErrorMessage = "手机号最多20个字符")] - public string? Phone { get; set; } + public string? Phone + { + get => _phone; + set => _phone = string.IsNullOrWhiteSpace(value) ? null : value; + } + private string? _phone; /// /// 部门ID diff --git a/server/MiAssessment/src/MiAssessment.Admin/Services/AdminUserService.cs b/server/MiAssessment/src/MiAssessment.Admin/Services/AdminUserService.cs index 6c88de6..0a5a54a 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/Services/AdminUserService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin/Services/AdminUserService.cs @@ -15,15 +15,17 @@ public class AdminUserService : IAdminUserService private readonly AdminDbContext _dbContext; private readonly ILogger _logger; private readonly IAuthService _authService; + private readonly IPermissionService _permissionService; // 超级管理员角色编码 private const string SuperAdminRoleCode = "super_admin"; - public AdminUserService(AdminDbContext dbContext, ILogger logger, IAuthService authService) + public AdminUserService(AdminDbContext dbContext, ILogger logger, IAuthService authService, IPermissionService permissionService) { _dbContext = dbContext; _logger = logger; _authService = authService; + _permissionService = permissionService; } /// @@ -310,6 +312,7 @@ public class AdminUserService : IAdminUserService } await _dbContext.SaveChangesAsync(); + _permissionService.InvalidateCache(userId); _logger.LogInformation("管理员 {UserId} 分配角色成功,角色数量: {Count}", userId, roleIds.Count); } @@ -353,6 +356,10 @@ public class AdminUserService : IAdminUserService } await _dbContext.SaveChangesAsync(); + + // 清除用户权限缓存,使新菜单权限立即生效 + _permissionService.InvalidateCache(userId); + _logger.LogInformation("管理员 {UserId} 分配用户专属菜单成功,菜单数量: {Count}", userId, menuIds.Count); } diff --git a/server/MiAssessment/src/MiAssessment.Admin/Services/PermissionService.cs b/server/MiAssessment/src/MiAssessment.Admin/Services/PermissionService.cs index 9dd2a03..e5d941e 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/Services/PermissionService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin/Services/PermissionService.cs @@ -212,14 +212,71 @@ public class PermissionService : IPermissionService var roleIds = userRoles.Select(ur => ur.RoleId).ToList(); - // 获取角色关联的权限 - var permissions = await _dbContext.RolePermissions + // 1. 获取角色关联的权限(permissions 表) + var rolePermissions = await _dbContext.RolePermissions .Where(rp => roleIds.Contains(rp.RoleId)) .Include(rp => rp.Permission) .Select(rp => rp.Permission.Code) .Distinct() .ToListAsync(); - return permissions; + // 2. 收集角色关联的菜单 ID + var roleMenuIds = await _dbContext.RoleMenus + .Where(rm => roleIds.Contains(rm.RoleId)) + .Select(rm => rm.MenuId) + .Distinct() + .ToListAsync(); + + // 3. 收集用户专属菜单 ID + var userMenuIds = await _dbContext.AdminUserMenus + .Where(um => um.AdminUserId == adminUserId) + .Select(um => um.MenuId) + .Distinct() + .ToListAsync(); + + // 4. 合并所有菜单 ID,并递归查找所有子菜单 + var allMenuIds = roleMenuIds.Concat(userMenuIds).Distinct().ToHashSet(); + var allChildMenuIds = await GetAllChildMenuIdsAsync(allMenuIds); + allMenuIds.UnionWith(allChildMenuIds); + + // 5. 从所有菜单中提取权限标识 + var menuPermissions = await _dbContext.Set() + .Where(m => allMenuIds.Contains(m.Id) && m.Permission != null && m.Permission != "") + .Select(m => m.Permission!) + .Distinct() + .ToListAsync(); + + // 合并所有权限 + var allPermissions = rolePermissions + .Concat(menuPermissions) + .Distinct() + .ToList(); + + return allPermissions; + } + + /// + /// 递归获取所有子菜单 ID + /// + private async Task> GetAllChildMenuIdsAsync(HashSet parentIds) + { + var result = new HashSet(); + var currentParentIds = parentIds; + + while (currentParentIds.Count > 0) + { + var childIds = await _dbContext.Set() + .Where(m => currentParentIds.Contains(m.ParentId)) + .Select(m => m.Id) + .ToListAsync(); + + var newIds = childIds.Where(id => !result.Contains(id) && !parentIds.Contains(id)).ToHashSet(); + if (newIds.Count == 0) break; + + result.UnionWith(newIds); + currentParentIds = newIds; + } + + return result; } } diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/operationLog.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/operationLog.ts index 729b33a..c63cd05 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/operationLog.ts +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/operationLog.ts @@ -2,28 +2,29 @@ import { request, type ApiResponse, type PagedResult } from '@/utils/request' export interface OperationLog { id: number - userId: number - username: string - module: string - action: string - method: string - url: string - params: string | null + adminUserId: number | null + username: string | null + module: string | null + action: string | null + method: string | null + url: string | null ip: string | null - userAgent: string | null - duration: number + requestData: string | null + responseData: string | null status: number - errorMessage: string | null + errorMsg: string | null + duration: number createdAt: string } export interface OperationLogQuery { - keyword?: string - module?: string - startDate?: string - endDate?: string page: number pageSize: number + username?: string + module?: string + status?: number + startDate?: string + endDate?: string } // 获取操作日志列表 @@ -35,7 +36,7 @@ export function getOperationLogList(params: OperationLogQuery): Promise> { return request({ url: `/admin/logs/${id}`, diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/router/index.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/router/index.ts index 78078a7..ab7f8ec 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/router/index.ts +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/router/index.ts @@ -10,7 +10,7 @@ export { businessRoutes, type RouteMeta, getBusinessPermissions, filterRoutesByP const constantRoutes: RouteRecordRaw[] = [ { path: '/', - redirect: '/dashboard' + redirect: '/dashboard' // 默认值,登录后会被动态路由覆盖 }, { path: '/login', @@ -60,6 +60,23 @@ const router = createRouter({ // 白名单路由 const whiteList = ['/login', '/404'] +/** + * 递归获取用户有权限的第一个可访问页面路径 + */ +function getFirstAccessiblePath(routes: RouteRecordRaw[], parentPath = ''): string | null { + for (const route of routes) { + const fullPath = parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : route.path + // 有 component 且不是 Layout 的就是叶子页面 + if (route.children && route.children.length > 0) { + const childPath = getFirstAccessiblePath(route.children, fullPath) + if (childPath) return childPath + } else if (route.component) { + return fullPath + } + } + return null +} + // 标记动态路由是否已加载 let dynamicRoutesLoaded = false @@ -94,6 +111,11 @@ router.beforeEach(async (to, _from, next) => { accessRoutes.forEach(route => { router.addRoute(route) }) + // 动态设置首页重定向到用户有权限的第一个页面 + const firstRoute = getFirstAccessiblePath(accessRoutes) + if (firstRoute) { + router.addRoute({ path: '/', redirect: firstRoute }) + } // 添加 404 兜底路由 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }) // 标记动态路由已加载 diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts index 70fba6b..82b609d 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts @@ -123,6 +123,11 @@ async function handle401Error(error: any): Promise { // Don't retry if already retried or if it's a refresh request if (originalConfig._retry || isWhiteListUrl(originalConfig.url)) { + // 白名单接口(如登录)的 401 错误,直接显示后端返回的错误信息 + const msg = error.response?.data?.message + if (msg) { + ElMessage.error(msg) + } return Promise.reject(error) } diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/system/user/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/system/user/index.vue index 7ecb73b..2ddc4c0 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/system/user/index.vue +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/system/user/index.vue @@ -161,7 +161,6 @@ show-checkbox node-key="id" :default-checked-keys="checkedMenuIds" - :check-strictly="true" />