feat(权限): 实现基于角色的路由权限控制

- 新增路由元信息类型定义 (requiresAuth, roles, title)
- 实现路由守卫中的角色权限校验逻辑
- 新增 403 禁止访问页面
- 提取权限校验函数 checkRoutePermission,提高可测试性
- 修复 JSON.parse 异常处理,增强健壮性
- 优化页面标题动态设置

测试优化:
- 重构 global-setup.ts,支持 JAR 文件启动后端服务
- 优化测试用例等待逻辑,减少硬编码延迟
- 简化 playwright 配置,移除多浏览器支持
- 新增路由权限守卫单元测试

关联需求:权限系统完善
This commit is contained in:
张翔
2026-04-08 15:29:03 +08:00
parent 9b2c8a47a4
commit 7420afa380
23 changed files with 933 additions and 349 deletions
+149 -120
View File
@@ -11,6 +11,18 @@ let backendProcess: ChildProcess | null = null;
let gatewayProcess: ChildProcess | null = null;
let healthCheckInterval: NodeJS.Timeout | null = null;
function renderProgressBar(label: string, current: number, total: number, width: number = 30): void {
const ratio = Math.min(current / total, 1);
const filled = Math.round(ratio * width);
const empty = width - filled;
const bar = '█'.repeat(filled) + '░'.repeat(empty);
const percent = (ratio * 100).toFixed(0);
process.stdout.write(`\r ${label} [${bar}] ${percent}% (${current}/${total}s)`);
if (ratio >= 1) {
process.stdout.write('\n');
}
}
async function checkBackendHealth(): Promise<boolean> {
try {
const response = await fetch('http://localhost:8084/actuator/health', {
@@ -87,137 +99,147 @@ async function globalSetup(config: FullConfig) {
process.env.NODE_ENV = 'test';
process.env.PLAYWRIGHT_HEADLESS = 'false';
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
let backendCommand: string;
let backendArgs: string[];
if (existsSync(jarFile)) {
console.log('📦 使用JAR文件启动后端服务...');
console.log(` JAR文件: ${jarFile}`);
backendCommand = 'java';
backendArgs = [
'-jar',
jarFile,
'--spring.profiles.active=test',
'-Xms256m',
'-Xmx512m'
];
const backendAlreadyRunning = await checkBackendHealth();
if (backendAlreadyRunning) {
console.log('✅ 后端服务已在运行,跳过启动');
} else {
console.log('📦 使用Maven启动后端服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
backendCommand = 'mvn';
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
}
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
console.log(` 目录: ${backendDir}`);
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
let backendCommand: string;
let backendArgs: string[];
backendProcess = spawn(backendCommand, backendArgs, {
cwd: backendDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
});
if (backendProcess.stdout) {
backendProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
console.log('✅ 后端服务启动成功');
}
});
}
if (backendProcess.stderr) {
backendProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 后端服务启动错误:', output);
}
});
}
backendProcess.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error);
});
backendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
if (existsSync(jarFile)) {
console.log('📦 使用JAR文件启动后端服务...');
console.log(` JAR文件: ${jarFile}`);
backendCommand = 'java';
backendArgs = [
'-jar',
jarFile,
'--spring.profiles.active=test',
'-Xms256m',
'-Xmx512m'
];
} else {
console.log('📦 使用Maven启动后端服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
backendCommand = 'mvn';
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
}
});
console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady();
console.log(` 目录: ${backendDir}`);
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
backendProcess = spawn(backendCommand, backendArgs, {
cwd: backendDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
});
let gatewayCommand: string;
let gatewayArgs: string[];
if (backendProcess.stdout) {
backendProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
console.log('✅ 后端服务启动成功');
}
});
}
if (existsSync(gatewayJarFile)) {
console.log('🚪 使用JAR文件启动网关服务...');
console.log(` JAR文件: ${gatewayJarFile}`);
gatewayCommand = 'java';
gatewayArgs = [
'-jar',
gatewayJarFile,
'--spring.profiles.active=dev',
'-Xms128m',
'-Xmx256m'
];
if (backendProcess.stderr) {
backendProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 后端服务启动错误:', output);
}
});
}
backendProcess.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error);
});
backendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
}
});
console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady();
}
const gatewayAlreadyRunning = await checkGatewayHealth();
if (gatewayAlreadyRunning) {
console.log('✅ 网关服务已在运行,跳过启动');
} else {
console.log('🚪 使用Maven启动网关服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
gatewayCommand = 'mvn';
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
}
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
console.log(` 目录: ${gatewayDir}`);
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
let gatewayCommand: string;
let gatewayArgs: string[];
gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
cwd: gatewayDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
});
if (gatewayProcess.stdout) {
gatewayProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started GatewayApplication') || output.includes('Netty started on port')) {
console.log('✅ 网关服务启动成功');
}
});
}
if (gatewayProcess.stderr) {
gatewayProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 网关服务启动错误:', output);
}
});
}
gatewayProcess.on('error', (error) => {
console.error('❌ 网关服务启动失败:', error);
});
gatewayProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
if (existsSync(gatewayJarFile)) {
console.log('🚪 使用JAR文件启动网关服务...');
console.log(` JAR文件: ${gatewayJarFile}`);
gatewayCommand = 'java';
gatewayArgs = [
'-jar',
gatewayJarFile,
'--spring.profiles.active=dev',
'-Xms128m',
'-Xmx256m'
];
} else {
console.log('🚪 使用Maven启动网关服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
gatewayCommand = 'mvn';
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
}
});
console.log('⏳ 等待网关服务就绪...');
await waitForGatewayReady();
console.log(` 目录: ${gatewayDir}`);
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
cwd: gatewayDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
});
if (gatewayProcess.stdout) {
gatewayProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started GatewayApplication') || output.includes('Netty started on port')) {
console.log('✅ 网关服务启动成功');
}
});
}
if (gatewayProcess.stderr) {
gatewayProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 网关服务启动错误:', output);
}
});
}
gatewayProcess.on('error', (error) => {
console.error('❌ 网关服务启动失败:', error);
});
gatewayProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
}
});
console.log('⏳ 等待网关服务就绪...');
await waitForGatewayReady();
}
console.log('🔍 验证所有服务连通性...');
await verifyAllServices();
@@ -276,6 +298,8 @@ async function waitForBackendReady(): Promise<void> {
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 后端服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:8084/actuator/health', {
signal: AbortSignal.timeout(5000) as any
@@ -283,9 +307,9 @@ async function waitForBackendReady(): Promise<void> {
if (response.ok) {
const data = await response.json();
if (data.status === 'UP') {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
// 验证服务连通性:测试登录API
try {
const loginTest = await fetch('http://localhost:8084/api/auth/login', {
method: 'POST',
@@ -322,6 +346,8 @@ async function waitForGatewayReady(): Promise<void> {
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 网关服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:8080/actuator/health', {
signal: AbortSignal.timeout(5000) as any
@@ -329,9 +355,9 @@ async function waitForGatewayReady(): Promise<void> {
if (response.ok) {
const data = await response.json();
if (data.status === 'UP') {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
// 验证网关连通性:通过网关测试登录API
try {
const loginTest = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
@@ -368,11 +394,14 @@ async function waitForFrontendReady(): Promise<void> {
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 前端服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:3002', {
signal: AbortSignal.timeout(5000) as any
});
if (response.ok) {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 前端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
return;
}