Some checks failed
continuous-integration/drone/push Build is failing
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 格式
307 lines
7.3 KiB
TypeScript
307 lines
7.3 KiB
TypeScript
/**
|
||
* 路由守卫
|
||
* 处理路由权限验证、登录状态检查等
|
||
*/
|
||
|
||
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
|
||
}
|
||
}
|