修复权限:隔离教师工作台与学员中心菜单自服务可见性

This commit is contained in:
wangzhibiao committed 2026-09-22 11:59:38 +08:00
1 parent 2b1fcdb260
commit ecfd38be17
15 files changed
+929 -17

No files matched your search

+14 -7
View File
@@ -4,6 +4,7 @@ import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
import { isSelfServiceRouteAllowed } from '@/utils/selfServiceRoute'
const permission = {
state: {
@@ -30,16 +31,15 @@ const permission = {
},
actions: {
// 生成路由
GenerateRoutes({ commit }) {
GenerateRoutes({ commit, rootGetters }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
//console.log("getRouters_res==============");
//console.log(res);
const roles = (rootGetters && rootGetters.roles) || []
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const sidebarRoutes = filterAsyncRouter(sdata, false, false, roles)
const rewriteRoutes = filterAsyncRouter(rdata, false, true, roles)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
@@ -55,8 +55,11 @@ const permission = {
}
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false, roles = []) {
return asyncRouterMap.filter(route => {
if (!isSelfServiceRouteAllowed(route, roles)) {
return false
}
if (type && route.children) {
route.children = filterChildren(route.children)
}
@@ -73,7 +76,11 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
}
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type)
route.children = filterAsyncRouter(route.children, route, type, roles)
// 如果作为wrapper的父路由在过滤后子路由为空,则一并移除
if (route.children.length === 0) {
return false
}
} else {
delete route['children']
delete route['redirect']
+30 -2
View File
@@ -65,12 +65,15 @@ service.interceptors.request.use(config => {
Promise.reject(error)
})
let lastForbiddenNotificationTime = 0
const FORBIDDEN_DEBOUNCE_MS = 2500 // 2.5秒内对403权限拒绝只提醒一次
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 获取错误信息,优先使用服务端返回的明确业务说明
const msg = res.data.msg || errorCode[code] || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
@@ -88,6 +91,18 @@ service.interceptors.response.use(res => {
})
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 403) {
const forbiddenMsg = res.data.msg || errorCode['403'] || '当前操作没有权限'
const now = Date.now()
if (now - lastForbiddenNotificationTime > FORBIDDEN_DEBOUNCE_MS) {
lastForbiddenNotificationTime = now
Notification.error({
title: '访问受限',
message: forbiddenMsg,
duration: 3500
})
}
return Promise.reject(new Error(forbiddenMsg))
} else if (code === 500) {
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
@@ -104,6 +119,19 @@ service.interceptors.response.use(res => {
error => {
console.log('err' + error)
let { message } = error
if (error.response && (error.response.status === 403 || (error.response.data && error.response.data.code === 403))) {
const forbiddenMsg = (error.response.data && error.response.data.msg) || '当前操作没有权限'
const now = Date.now()
if (now - lastForbiddenNotificationTime > FORBIDDEN_DEBOUNCE_MS) {
lastForbiddenNotificationTime = now
Notification.error({
title: '访问受限',
message: forbiddenMsg,
duration: 3500
})
}
return Promise.reject(error)
}
if (message === 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
+45
View File
@@ -0,0 +1,45 @@
/**
* 角色专属自服务路由与菜单权限过滤工具
*
* 业务规则:
* 1. 教师工作台 (train/me/teacher/index):仅对具备 train_teacher 角色的用户可见。
* 2. 学员中心 (train/me/student/index):仅对具备 train_student 角色的用户可见。
* 3. 即使拥有超级管理员 (admin) 或培训管理员 (train_admin) 角色,无真实身份/角色时自服务菜单一律隐藏。
* 4. 普通管理与业务菜单(如人员管理下的教师信息、学员信息等)不受本规则过滤。
*/
export function isSelfServiceRouteAllowed(route, roles = []) {
if (!route) return false
const comp = route.component
// 教师工作台专属自服务组件
if (comp === 'train/me/teacher/index') {
return Array.isArray(roles) && roles.includes('train_teacher')
}
// 学员中心专属自服务组件
if (comp === 'train/me/student/index') {
return Array.isArray(roles) && roles.includes('train_student')
}
return true
}
/**
* 深度过滤路由树中的自服务专属路由
*/
export function filterSelfServiceRoutes(routes, roles = []) {
if (!Array.isArray(routes)) return []
return routes.reduce((acc, route) => {
if (!isSelfServiceRouteAllowed(route, roles)) {
return acc
}
const item = { ...route }
if (item.children && Array.isArray(item.children)) {
item.children = filterSelfServiceRoutes(item.children, roles)
// 如果作为父容器wrapper在子路由被完全过滤后变为空,则移除该父容器
if (item.children.length === 0 && (item.component === 'Layout' || !item.component || item.path === '/')) {
return acc
}
}
acc.push(item)
return acc
}, [])
}
@@ -374,12 +374,6 @@ export default {
methods: {
initStudentData() {
this.loadProfile()
this.loadEnrollments()
this.loadTimetable()
this.loadReport()
this.loadAttendance()
this.loadDuration()
this.loadPortrait()
},
loadProfile() {
this.profileLoading = true
@@ -388,6 +382,13 @@ export default {
if (res.code === 200) {
this.studentProfile = res.data
this.identityErrorMsg = ''
// 学员身份校验通过后,再并发加载学业与选课各Tab数据
this.loadEnrollments()
this.loadTimetable()
this.loadReport()
this.loadAttendance()
this.loadDuration()
this.loadPortrait()
} else if (res.code === 403) {
this.identityErrorMsg = res.msg || '当前登录账号未绑定任何学员档案,请联系管理员核对分配。'
}
@@ -309,8 +309,6 @@ export default {
methods: {
initTeacherData() {
this.loadProfile()
this.loadMyPlans()
this.loadMyTimetable()
},
loadProfile() {
this.profileLoading = true
@@ -319,6 +317,9 @@ export default {
if (res.code === 200) {
this.teacherProfile = res.data
this.identityErrorMsg = ''
// 教师身份校验通过后,再加载任教班级与课表日程
this.loadMyPlans()
this.loadMyTimetable()
} else if (res.code === 403) {
this.identityErrorMsg = res.msg || '当前登录账号未绑定任何教师档案,请联系管理员分配绑定。'
}