Files
012-kaopeilian/frontend/src/router/guard.ts
yuliang_guo 64f5d567fa
Some checks failed
continuous-integration/drone/push Build is failing
feat: 实现 KPL 系统功能改进计划
1. 课程学习进度追踪
   - 新增 UserCourseProgress 和 UserMaterialProgress 模型
   - 新增 /api/v1/progress/* 进度追踪 API
   - 更新 admin.py 使用真实课程完成率数据

2. 路由权限检查完善
   - 新增前端 permissionChecker.ts 权限检查工具
   - 更新 router/guard.ts 实现团队和课程权限验证
   - 新增后端 permission_service.py

3. AI 陪练音频转文本
   - 新增 speech_recognition.py 语音识别服务
   - 新增 /api/v1/speech/* API
   - 更新 ai-practice-coze.vue 支持语音输入

4. 双人对练报告生成
   - 更新 practice_room_service.py 添加报告生成功能
   - 新增 /rooms/{room_code}/report API
   - 更新 duo-practice-report.vue 调用真实 API

5. 学习提醒推送
   - 新增 notification_service.py 通知服务
   - 新增 scheduler_service.py 定时任务服务
   - 支持钉钉、企微、站内消息推送

6. 智能学习推荐
   - 新增 recommendation_service.py 推荐服务
   - 新增 /api/v1/recommendations/* API
   - 支持错题、能力、进度、热门多维度推荐

7. 安全问题修复
   - DEBUG 默认值改为 False
   - 添加 SECRET_KEY 安全警告
   - 新增 check_security_settings() 检查函数

8. 证书 PDF 生成
   - 更新 certificate_service.py 添加 PDF 生成
   - 添加 weasyprint、Pillow、qrcode 依赖
   - 更新下载 API 支持 PDF 和 PNG 格式
2026-01-30 14:22:35 +08:00

307 lines
7.3 KiB
TypeScript
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.
/**
* 路由守卫
* 处理路由权限验证、登录状态检查等
*/
import { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
import { ElMessage } from 'element-plus'
import { authManager } from '@/utils/auth'
import { loadingManager } from '@/utils/loadingManager'
import { checkTeamMembership, checkCourseAccess, clearPermissionCache } from '@/utils/permissionChecker'
// 白名单路由(不需要登录)
const WHITE_LIST = ['/login', '/register', '/404']
// 需要特殊权限的路由映射
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/admin': ['admin'],
'/manager': ['manager', 'admin'],
'/analysis': ['manager', 'admin']
}
/**
* 设置路由守卫
*/
export function setupRouterGuard(router: Router) {
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 显示页面加载状态
loadingManager.start('page-loading', {
message: '页面加载中...',
background: 'rgba(255, 255, 255, 0.8)'
})
try {
await handleRouteGuard(to, from, next)
} catch (error) {
console.error('Route guard error:', error)
ElMessage.error('页面加载失败,请重试')
next('/404')
}
})
// 全局后置守卫
router.afterEach((to, from) => {
// 隐藏页面加载状态
loadingManager.stop('page-loading')
// 设置页面标题
document.title = getPageTitle(to.meta?.title as string)
// 记录页面访问日志
if (process.env.NODE_ENV === 'development') {
console.log(`[Router] Navigate from ${from.path} to ${to.path}`)
}
})
// 路由错误处理
router.onError((error) => {
console.error('Router error:', error)
ElMessage.error('路由加载失败')
loadingManager.stop('page-loading')
})
}
/**
* 处理路由守卫逻辑
*/
async function handleRouteGuard(
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const { path } = to
// 白名单路由直接通过
if (WHITE_LIST.includes(path)) {
// 如果已登录用户访问登录页,重定向到首页
if (path === '/login' && authManager.isAuthenticated()) {
next(authManager.getDefaultRoute())
return
}
next()
return
}
// 检查登录状态
if (!authManager.isAuthenticated()) {
ElMessage.warning('请先登录')
next(`/login?redirect=${encodeURIComponent(path)}`)
return
}
// 检查token是否过期
if (authManager.isTokenExpired()) {
try {
// 尝试刷新token
await authManager.refreshToken()
} catch (error) {
ElMessage.error('登录已过期,请重新登录')
next(`/login?redirect=${encodeURIComponent(path)}`)
return
}
}
// 检查路由权限
if (!checkRoutePermission(path)) {
ElMessage.error('您没有访问此页面的权限')
// 根据用户角色重定向到合适的页面
next(authManager.getDefaultRoute())
return
}
// 检查特殊路由规则(先进行同步检查)
if (!checkSpecialRouteRules(to)) {
ElMessage.error('访问被拒绝')
next(authManager.getDefaultRoute())
return
}
// 异步权限检查(团队和课程权限)
const hasSpecialAccess = await checkSpecialRouteRulesAsync(to)
if (!hasSpecialAccess) {
ElMessage.error('您没有访问此资源的权限')
next(authManager.getDefaultRoute())
return
}
next()
}
/**
* 检查路由权限
*/
function checkRoutePermission(path: string): boolean {
// 检查是否可以访问路由
if (!authManager.canAccessRoute(path)) {
return false
}
// 检查特定路由的权限要求
for (const [routePrefix, roles] of Object.entries(ROUTE_PERMISSIONS)) {
if (path.startsWith(routePrefix)) {
const userRole = authManager.getUserRole()
if (!userRole || !roles.includes(userRole)) {
return false
}
}
}
return true
}
/**
* 检查特殊路由规则(异步版本)
*/
async function checkSpecialRouteRulesAsync(to: RouteLocationNormalized): Promise<boolean> {
const { path, params } = to
// 检查用户ID参数权限只能访问自己的数据管理员除外
if (params.userId && !authManager.isAdmin()) {
const currentUser = authManager.getCurrentUser()
if (currentUser && String(params.userId) !== String(currentUser.id)) {
return false
}
}
// 检查团队ID参数权限
if (params.teamId && !authManager.isAdmin()) {
const teamId = Number(params.teamId)
if (!isNaN(teamId)) {
const isMember = await checkTeamMembership(teamId)
if (!isMember) {
return false
}
}
}
// 检查课程访问权限
if (path.includes('/course/') && params.courseId) {
const courseId = Number(params.courseId)
if (!isNaN(courseId)) {
const hasAccess = await checkCourseAccess(courseId)
if (!hasAccess) {
return false
}
}
}
return true
}
/**
* 检查特殊路由规则(同步版本,用于简单检查)
*/
function checkSpecialRouteRules(to: RouteLocationNormalized): boolean {
const { params } = to
// 检查用户ID参数权限只能访问自己的数据管理员除外
if (params.userId && !authManager.isAdmin()) {
const currentUser = authManager.getCurrentUser()
if (currentUser && String(params.userId) !== String(currentUser.id)) {
return false
}
}
return true
}
/**
* 获取页面标题
*/
function getPageTitle(title?: string): string {
const appTitle = '考培练系统'
return title ? `${title} - ${appTitle}` : appTitle
}
/**
* 动态添加路由(用于角色权限路由)
*/
export function addDynamicRoutes(router: Router) {
const userRole = authManager.getUserRole()
if (!userRole) return
// 根据角色动态添加路由
const dynamicRoutes = getDynamicRoutesByRole(userRole)
dynamicRoutes.forEach(route => {
router.addRoute(route)
})
}
/**
* 根据角色获取动态路由
*/
function getDynamicRoutesByRole(role: string) {
const routes: any[] = []
// 根据角色添加不同的路由
switch (role) {
case 'admin':
// 管理员可以访问所有路由
break
case 'manager':
// 管理者路由
break
case 'trainee':
// 学员路由
break
}
return routes
}
/**
* 检查页面权限(组件内使用)
*/
export function checkPagePermission(permissions: string[]): boolean {
if (!permissions || permissions.length === 0) {
return true
}
const userRole = authManager.getUserRole()
if (!userRole) return false
return permissions.includes(userRole) || authManager.isAdmin()
}
/**
* 权限指令v-permission
*/
export const permissionDirective = {
mounted(el: HTMLElement, binding: any) {
const { value } = binding
if (value && !checkPagePermission(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
}
},
updated(el: HTMLElement, binding: any) {
const { value } = binding
if (value && !checkPagePermission(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
/**
* 路由元信息接口扩展
*/
declare module 'vue-router' {
interface RouteMeta {
title?: string
icon?: string
hidden?: boolean
roles?: string[]
permissions?: string[]
keepAlive?: boolean
affix?: boolean
breadcrumb?: boolean
activeMenu?: string
}
}