权限分配
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-04-19 20:38:08 +08:00
parent 36b08997bd
commit b82c43e513
8 changed files with 126 additions and 8 deletions

View File

@ -15,15 +15,17 @@ public class AdminUserService : IAdminUserService
private readonly AdminDbContext _dbContext;
private readonly ILogger<AdminUserService> _logger;
private readonly IAuthService _authService;
private readonly IPermissionService _permissionService;
// 超级管理员角色编码
private const string SuperAdminRoleCode = "super_admin";
public AdminUserService(AdminDbContext dbContext, ILogger<AdminUserService> logger, IAuthService authService)
public AdminUserService(AdminDbContext dbContext, ILogger<AdminUserService> logger, IAuthService authService, IPermissionService permissionService)
{
_dbContext = dbContext;
_logger = logger;
_authService = authService;
_permissionService = permissionService;
}
/// <inheritdoc />
@ -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);
}

View File

@ -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<Entities.Menu>()
.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;
}
/// <summary>
/// 递归获取所有子菜单 ID
/// </summary>
private async Task<HashSet<long>> GetAllChildMenuIdsAsync(HashSet<long> parentIds)
{
var result = new HashSet<long>();
var currentParentIds = parentIds;
while (currentParentIds.Count > 0)
{
var childIds = await _dbContext.Set<Entities.Menu>()
.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;
}
}

View File

@ -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' })
// 标记动态路由已加载

View File

@ -123,6 +123,11 @@ async function handle401Error(error: any): Promise<any> {
// 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)
}

View File

@ -161,7 +161,6 @@
show-checkbox
node-key="id"
:default-checked-keys="checkedMenuIds"
:check-strictly="true"
/>
<template #footer>
<el-button @click="menuDialogVisible = false">取消</el-button>
@ -304,6 +303,25 @@ const passwordRules: FormRules = {
]
}
/**
* 过滤出叶子节点 ID用于树组件回显 check-strictly 模式
*/
function filterLeafMenuIds(tree: MenuTree[], checkedIds: number[]): number[] {
const result: number[] = []
const idSet = new Set(checkedIds)
function walk(nodes: MenuTree[]) {
for (const node of nodes) {
if (node.children && node.children.length > 0) {
walk(node.children)
} else if (idSet.has(node.id)) {
result.push(node.id)
}
}
}
walk(tree)
return result
}
const fetchData = async () => {
loading.value = true
try {
@ -453,14 +471,20 @@ const handleSubmitDepartment = async () => {
const handleAssignMenu = async (row: AdminUser) => {
currentUserId.value = row.id
const res = await getUserMenus(row.id)
checkedMenuIds.value = res.data
// check-strictly ID
const allIds: number[] = res.data
const leafIds = filterLeafMenuIds(menuTree.value, allIds)
checkedMenuIds.value = leafIds
menuDialogVisible.value = true
}
const handleSubmitMenus = async () => {
menuSubmitLoading.value = true
try {
const menuIds = menuTreeRef.value?.getCheckedKeys(false) as number[]
const menuIds = [
...(menuTreeRef.value?.getCheckedKeys(false) as number[]),
...(menuTreeRef.value?.getHalfCheckedKeys() as number[])
]
await assignUserMenus({ userId: currentUserId.value, menuIds })
ElMessage.success('分配成功')
menuDialogVisible.value = false

1
temp_menu29.txt Normal file
View File

@ -0,0 +1 @@
{"code":0,"message":"success","data":{"id":29,"parentId":28,"name":"用户列表","path":"/user/list","component":"business/user/index","icon":"peoples","menuType":2,"permission":"user:view","sortOrder":1,"status":1,"isExternal":false,"isCache":true,"createdAt":"2026-02-03T17:32:15.877","updatedAt":"2026-04-19T20:22:50.107"}}

1
temp_menus.json Normal file

File diff suppressed because one or more lines are too long

1
temp_test_menus.txt Normal file
View File

@ -0,0 +1 @@
{"code":0,"message":"success","data":[28,29,30,31,32,34,35,36,37]}