feat: 添加异常日志功能并优化UI样式

refactor: 重构后端查询逻辑和API响应处理

fix: 修复用户角色更新和文件上传问题

test: 添加前端性能测试脚本和E2E测试用例

chore: 更新依赖版本和配置文件

docs: 添加环境检查脚本和测试文档

style: 统一表格标签样式和路由命名

perf: 优化前端页面加载速度和响应时间
This commit is contained in:
张翔
2026-03-24 13:32:20 +08:00
parent a97d317e4a
commit be5d5ede90
184 changed files with 11231 additions and 1903 deletions
@@ -0,0 +1,146 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const E2E_DIR = path.join(__dirname, 'e2e');
const RESULTS_FILE = path.join(__dirname, 'e2e-performance-results.json');
function measureE2ETestPerformance() {
console.log('🚀 开始E2E性能测试...\n');
const startTime = Date.now();
try {
const output = execSync('npm run test:e2e', {
cwd: __dirname,
encoding: 'utf8',
stdio: 'pipe'
});
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
const results = {
timestamp: new Date().toISOString(),
duration: duration,
durationFormatted: formatDuration(duration),
success: true,
message: 'E2E测试执行成功'
};
saveResults(results);
console.log('\n✅ E2E测试执行成功!');
console.log(`⏱️ 总耗时: ${results.durationFormatted}`);
console.log(`📊 性能评估: ${evaluatePerformance(duration)}`);
return results;
} catch (error) {
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
const results = {
timestamp: new Date().toISOString(),
duration: duration,
durationFormatted: formatDuration(duration),
success: false,
message: error.message || 'E2E测试执行失败'
};
saveResults(results);
console.log('\n❌ E2E测试执行失败!');
console.log(`⏱️ 总耗时: ${results.durationFormatted}`);
console.log(`📊 性能评估: ${evaluatePerformance(duration)}`);
console.log(`💥 错误信息: ${error.message}`);
return results;
}
}
function formatDuration(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}${remainingSeconds}`;
}
function evaluatePerformance(duration) {
if (duration < 60) {
return '🟢 优秀 - 执行时间在1分钟以内';
} else if (duration < 90) {
return '🟡 良好 - 执行时间在1.5分钟以内';
} else if (duration < 120) {
return '🟠 一般 - 执行时间在2分钟以内';
} else {
return '🔴 需要优化 - 执行时间超过2分钟';
}
}
function saveResults(results) {
const history = [];
if (fs.existsSync(RESULTS_FILE)) {
const data = fs.readFileSync(RESULTS_FILE, 'utf8');
try {
history.push(...JSON.parse(data));
} catch (e) {
console.warn('⚠️ 无法解析历史结果文件');
}
}
history.push(results);
if (history.length > 10) {
history.shift();
}
fs.writeFileSync(RESULTS_FILE, JSON.stringify(history, null, 2));
console.log('\n📈 性能趋势分析:');
analyzePerformanceTrend(history);
}
function analyzePerformanceTrend(history) {
if (history.length < 2) {
console.log(' 需要更多测试数据来分析趋势');
return;
}
const successfulTests = history.filter(r => r.success);
if (successfulTests.length < 2) {
console.log(' 需要更多成功的测试数据来分析趋势');
return;
}
const durations = successfulTests.map(r => r.duration);
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
const minDuration = Math.min(...durations);
const maxDuration = Math.max(...durations);
console.log(` 平均执行时间: ${formatDuration(avgDuration)}`);
console.log(` 最快执行时间: ${formatDuration(minDuration)}`);
console.log(` 最慢执行时间: ${formatDuration(maxDuration)}`);
const recentTests = successfulTests.slice(-3);
if (recentTests.length >= 2) {
const recentAvg = recentTests.reduce((a, b) => a + b.duration, 0) / recentTests.length;
const olderTests = successfulTests.slice(0, -3);
if (olderTests.length > 0) {
const olderAvg = olderTests.reduce((a, b) => a + b.duration, 0) / olderTests.length;
const improvement = ((olderAvg - recentAvg) / olderAvg * 100).toFixed(1);
if (improvement > 0) {
console.log(` 📉 性能提升: ${improvement}%`);
} else {
console.log(` 📈 性能下降: ${Math.abs(improvement)}%`);
}
}
}
}
if (require.main === module) {
measureE2ETestPerformance();
}
module.exports = { measureE2ETestPerformance };
@@ -0,0 +1,337 @@
#!/usr/bin/env node
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
const RESULTS_FILE = path.join(__dirname, '../performance-test-results.json');
class PerformanceTester {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.results = [];
}
async testEndpoint(endpoint, method = 'GET', body = null) {
const url = `${this.baseUrl}${endpoint}`;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (body) {
options.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(body));
}
const protocol = url.startsWith('https') ? https : http;
const req = protocol.request(url, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
const endTime = Date.now();
const duration = endTime - startTime;
resolve({
endpoint,
method,
statusCode: res.statusCode,
duration,
success: res.statusCode >= 200 && res.statusCode < 300,
dataSize: data.length
});
});
});
req.on('error', (error) => {
const endTime = Date.now();
const duration = endTime - startTime;
resolve({
endpoint,
method,
statusCode: 0,
duration,
success: false,
error: error.message
});
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
async runLoadTest(endpoint, concurrentRequests = 10, totalRequests = 100) {
console.log(`\n📊 开始负载测试: ${endpoint}`);
console.log(` 并发数: ${concurrentRequests}`);
console.log(` 总请求数: ${totalRequests}\n`);
const results = [];
const startTime = Date.now();
for (let i = 0; i < totalRequests; i += concurrentRequests) {
const batch = Math.min(concurrentRequests, totalRequests - i);
const promises = [];
for (let j = 0; j < batch; j++) {
promises.push(this.testEndpoint(endpoint));
}
const batchResults = await Promise.all(promises);
results.push(...batchResults);
console.log(` 进度: ${Math.min(i + batch, totalRequests)}/${totalRequests} 请求已完成`);
}
const endTime = Date.now();
const totalDuration = endTime - startTime;
const successfulRequests = results.filter(r => r.success);
const failedRequests = results.filter(r => !r.success);
const durations = successfulRequests.map(r => r.duration);
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
const p95Duration = this.calculatePercentile(durations, 95);
const p99Duration = this.calculatePercentile(durations, 99);
const throughput = (successfulRequests.length / totalDuration) * 1000;
return {
endpoint,
concurrentRequests,
totalRequests,
successfulRequests: successfulRequests.length,
failedRequests: failedRequests.length,
successRate: (successfulRequests.length / totalRequests * 100).toFixed(2),
totalDuration,
avgDuration,
minDuration,
maxDuration,
p95Duration,
p99Duration,
throughput: throughput.toFixed(2),
results
};
}
calculatePercentile(values, percentile) {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
async runPerformanceTests() {
console.log('🚀 开始性能测试...\n');
const endpoints = [
{ path: '/api/auth/login', method: 'POST', body: { username: 'admin', password: 'admin123' } },
{ path: '/api/users', method: 'GET' },
{ path: '/api/roles', method: 'GET' },
{ path: '/api/menus', method: 'GET' },
{ path: '/api/dicts', method: 'GET' },
];
for (const endpoint of endpoints) {
console.log(`\n📡 测试端点: ${endpoint.method} ${endpoint.path}`);
const results = [];
const iterations = 10;
for (let i = 0; i < iterations; i++) {
const result = await this.testEndpoint(endpoint.path, endpoint.method, endpoint.body);
results.push(result);
console.log(` ${i + 1}/${iterations}: ${result.duration}ms - ${result.success ? '✅' : '❌'}`);
}
const durations = results.filter(r => r.success).map(r => r.duration);
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
const successRate = (results.filter(r => r.success).length / results.length * 100).toFixed(2);
this.results.push({
endpoint: endpoint.path,
method: endpoint.method,
avgDuration,
minDuration,
maxDuration,
successRate,
status: this.evaluatePerformance(avgDuration)
});
}
this.saveResults();
this.printSummary();
}
evaluatePerformance(avgDuration) {
if (avgDuration < 100) {
return '🟢 优秀';
} else if (avgDuration < 300) {
return '🟡 良好';
} else if (avgDuration < 500) {
return '🟠 一般';
} else {
return '🔴 需要优化';
}
}
saveResults() {
const timestamp = new Date().toISOString();
const data = {
timestamp,
performanceTests: this.results,
loadTests: this.loadTestResults
};
const history = [];
if (fs.existsSync(RESULTS_FILE)) {
try {
history.push(...JSON.parse(fs.readFileSync(RESULTS_FILE, 'utf8')));
} catch (e) {
console.warn('⚠️ 无法解析历史结果文件');
}
}
history.push(data);
if (history.length > 20) {
history.shift();
}
fs.writeFileSync(RESULTS_FILE, JSON.stringify(history, null, 2));
}
printSummary() {
console.log('\n📊 性能测试摘要:');
console.log('═══════════════════════════════════════');
const table = this.results.map(r => ({
端点: r.endpoint,
方法: r.method,
平均: `${r.avgDuration.toFixed(0)}ms`,
最小: `${r.minDuration}ms`,
最大: `${r.maxDuration}ms`,
成功率: `${r.successRate}%`,
状态: r.status
}));
console.table(table);
if (this.loadTestResults) {
console.log('\n📈 负载测试摘要:');
console.log('═══════════════════════════════════════');
const loadTable = this.loadTestResults.map(r => ({
端点: r.endpoint,
总请求: r.totalRequests,
成功: r.successfulRequests,
失败: r.failedRequests,
成功率: `${r.successRate}%`,
平均响应: `${r.avgDuration.toFixed(0)}ms`,
P95: `${r.p95Duration.toFixed(0)}ms`,
P99: `${r.p99Duration.toFixed(0)}ms`,
吞吐量: `${r.throughput} req/s`
}));
console.table(loadTable);
}
console.log('\n💡 性能优化建议:');
this.printRecommendations();
}
printRecommendations() {
const slowEndpoints = this.results.filter(r => r.avgDuration > 300);
if (slowEndpoints.length > 0) {
console.log(' ⚠️ 以下端点响应时间较长,建议优化:');
slowEndpoints.forEach(r => {
console.log(` - ${r.endpoint}: ${r.avgDuration.toFixed(0)}ms`);
});
}
const lowSuccessRate = this.results.filter(r => parseFloat(r.successRate) < 95);
if (lowSuccessRate.length > 0) {
console.log(' ⚠️ 以下端点成功率较低,建议检查:');
lowSuccessRate.forEach(r => {
console.log(` - ${r.endpoint}: ${r.successRate}%`);
});
}
if (slowEndpoints.length === 0 && lowSuccessRate.length === 0) {
console.log(' ✅ 所有端点性能良好,无需优化');
}
}
async runLoadTests() {
console.log('\n📊 开始负载测试...\n');
const endpoints = ['/api/users', '/api/roles', '/api/menus'];
this.loadTestResults = [];
for (const endpoint of endpoints) {
const result = await this.runLoadTest(endpoint, 10, 100);
this.loadTestResults.push(result);
console.log(`\n📈 ${endpoint} 负载测试结果:`);
console.log(` 成功率: ${result.successRate}%`);
console.log(` 平均响应时间: ${result.avgDuration.toFixed(0)}ms`);
console.log(` P95响应时间: ${result.p95Duration.toFixed(0)}ms`);
console.log(` P99响应时间: ${result.p99Duration.toFixed(0)}ms`);
console.log(` 吞吐量: ${result.throughput} req/s`);
}
this.saveResults();
}
}
async function main() {
const tester = new PerformanceTester(API_BASE_URL);
const command = process.argv[2];
switch (command) {
case 'performance':
await tester.runPerformanceTests();
break;
case 'load':
await tester.runLoadTests();
break;
case 'all':
await tester.runPerformanceTests();
await tester.runLoadTests();
break;
default:
console.log('使用方法:');
console.log(' node scripts/performance-test.js performance - 运行性能测试');
console.log(' node scripts/performance-test.js load - 运行负载测试');
console.log(' node scripts/performance-test.js all - 运行所有测试');
console.log('\n环境变量:');
console.log(' API_BASE_URL - API基础URL (默认: http://localhost:8080)');
}
}
if (require.main === module) {
main();
}
module.exports = PerformanceTester;
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Playwright E2E Headless 模式测试脚本
# 用于完整的端到端测试和UAT测试
set -e
echo "========================================"
echo "Playwright E2E Headless 测试脚本"
echo "========================================"
# 设置工作目录
cd /Users/zhangxiang/Codes/Novalon/novalon-manage-system/novalon-manage-web
# 检查前端开发服务器
echo "🔍 检查前端开发服务器..."
if ! lsof -ti:3001 > /dev/null; then
echo "❌ 前端开发服务器未运行,启动中..."
npm run dev > /tmp/frontend.log 2>&1 &
echo "✅ 前端开发服务器已启动(PID: $!"
sleep 10
fi
# 检查后端服务
echo "🔍 检查后端服务..."
if ! lsof -ti:8080 > /dev/null; then
echo "❌ 后端服务未运行,启动中..."
cd /Users/zhangxiang/Codes/Novalon/novalon-manage-system/novalon-manage-api
mvn spring-boot:run -pl manage-gateway > /tmp/gateway.log 2>&1 &
echo "✅ 后端服务已启动(PID: $!"
sleep 30
fi
# 回到前端目录
cd /Users/zhangxiang/Codes/Novalon/novalon-manage-system/novalon-manage-web
# 运行 E2E 测试(Headless 模式)
echo "🚀 运行 E2E 测试(Headless 模式)..."
PLAYWRIGHT_HEADLESS=true npx playwright test --project=chromium --reporter=list
# 生成测试报告
echo "📊 生成测试报告..."
npx playwright show-report playwright-report
echo "✅ E2E Headless 测试完成!"
echo "_report: playwright-report/index.html"