feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
"""
|
||||
增强版报告生成器模块
|
||||
支持趋势分析、历史报告对比、性能基准对比等功能
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
import statistics
|
||||
|
||||
from .reporter import TestResult, TestSummary, ReportGenerator
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrendData:
|
||||
"""趋势数据"""
|
||||
date: str
|
||||
total: int
|
||||
passed: int
|
||||
failed: int
|
||||
pass_rate: float
|
||||
avg_response_time: float
|
||||
total_time: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceBenchmark:
|
||||
"""性能基准数据"""
|
||||
endpoint: str
|
||||
method: str
|
||||
avg_response_time: float
|
||||
min_response_time: float
|
||||
max_response_time: float
|
||||
p95_response_time: float
|
||||
p99_response_time: float
|
||||
|
||||
|
||||
class EnhancedReportGenerator(ReportGenerator):
|
||||
"""增强版报告生成器"""
|
||||
|
||||
def __init__(self, output_dir: str = "reports", history_dir: str = "reports/history"):
|
||||
"""
|
||||
初始化增强版报告生成器
|
||||
|
||||
Args:
|
||||
output_dir: 报告输出目录
|
||||
history_dir: 历史报告目录
|
||||
"""
|
||||
super().__init__(output_dir)
|
||||
self.history_dir = Path(history_dir)
|
||||
self.history_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def generate_trend_report(self, days: int = 7) -> str:
|
||||
"""
|
||||
生成趋势分析报告
|
||||
|
||||
Args:
|
||||
days: 分析最近几天的数据
|
||||
|
||||
Returns:
|
||||
报告文件路径
|
||||
"""
|
||||
trend_data = self._load_trend_data(days)
|
||||
|
||||
if not trend_data:
|
||||
return ""
|
||||
|
||||
report_path = self.output_dir / f"trend_report_{datetime.now().strftime('%Y%m%d')}.html"
|
||||
|
||||
html_content = self._generate_trend_html(trend_data)
|
||||
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return str(report_path)
|
||||
|
||||
def generate_comparison_report(self, current_results: List[TestResult],
|
||||
previous_results: List[TestResult] = None) -> str:
|
||||
"""
|
||||
生成对比报告
|
||||
|
||||
Args:
|
||||
current_results: 当前测试结果
|
||||
previous_results: 之前的测试结果
|
||||
|
||||
Returns:
|
||||
报告文件路径
|
||||
"""
|
||||
if previous_results is None:
|
||||
previous_results = self._load_latest_results()
|
||||
|
||||
if not previous_results:
|
||||
return ""
|
||||
|
||||
comparison_data = self._compare_results(current_results, previous_results)
|
||||
|
||||
report_path = self.output_dir / f"comparison_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
||||
|
||||
html_content = self._generate_comparison_html(comparison_data)
|
||||
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return str(report_path)
|
||||
|
||||
def generate_performance_report(self, results: List[TestResult],
|
||||
benchmarks: Dict[str, PerformanceBenchmark] = None) -> str:
|
||||
"""
|
||||
生成性能报告
|
||||
|
||||
Args:
|
||||
results: 测试结果列表
|
||||
benchmarks: 性能基准数据
|
||||
|
||||
Returns:
|
||||
报告文件路径
|
||||
"""
|
||||
performance_data = self._analyze_performance(results)
|
||||
|
||||
if benchmarks:
|
||||
performance_data = self._compare_with_benchmarks(performance_data, benchmarks)
|
||||
|
||||
report_path = self.output_dir / f"performance_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
||||
|
||||
html_content = self._generate_performance_html(performance_data)
|
||||
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return str(report_path)
|
||||
|
||||
def _load_trend_data(self, days: int) -> List[TrendData]:
|
||||
"""
|
||||
加载趋势数据
|
||||
|
||||
Args:
|
||||
days: 加载最近几天的数据
|
||||
|
||||
Returns:
|
||||
趋势数据列表
|
||||
"""
|
||||
trend_data = []
|
||||
|
||||
for i in range(days):
|
||||
date = datetime.now() - timedelta(days=i)
|
||||
date_str = date.strftime('%Y%m%d')
|
||||
|
||||
report_file = self.history_dir / f"test_report_{date_str}.json"
|
||||
|
||||
if report_file.exists():
|
||||
with open(report_file, 'r', encoding='utf-8') as f:
|
||||
report_data = json.load(f)
|
||||
|
||||
summary = report_data.get('summary', {})
|
||||
results = report_data.get('results', [])
|
||||
|
||||
avg_response_time = statistics.mean([r['response_time'] for r in results]) if results else 0
|
||||
|
||||
trend_data.append(TrendData(
|
||||
date=date.strftime('%Y-%m-%d'),
|
||||
total=summary.get('total', 0),
|
||||
passed=summary.get('passed', 0),
|
||||
failed=summary.get('failed', 0),
|
||||
pass_rate=summary.get('pass_rate', 0),
|
||||
avg_response_time=avg_response_time,
|
||||
total_time=summary.get('total_time', 0)
|
||||
))
|
||||
|
||||
return sorted(trend_data, key=lambda x: x.date)
|
||||
|
||||
def _load_latest_results(self) -> List[TestResult]:
|
||||
"""
|
||||
加载最新的测试结果
|
||||
|
||||
Returns:
|
||||
测试结果列表
|
||||
"""
|
||||
report_files = list(self.history_dir.glob("test_report_*.json"))
|
||||
|
||||
if not report_files:
|
||||
return []
|
||||
|
||||
latest_file = max(report_files, key=lambda f: f.stat().st_mtime)
|
||||
|
||||
with open(latest_file, 'r', encoding='utf-8') as f:
|
||||
report_data = json.load(f)
|
||||
|
||||
return [TestResult(**r) for r in report_data.get('results', [])]
|
||||
|
||||
def _compare_results(self, current: List[TestResult], previous: List[TestResult]) -> Dict[str, Any]:
|
||||
"""
|
||||
对比测试结果
|
||||
|
||||
Args:
|
||||
current: 当前测试结果
|
||||
previous: 之前的测试结果
|
||||
|
||||
Returns:
|
||||
对比数据
|
||||
"""
|
||||
current_summary = self._calculate_summary(current)
|
||||
previous_summary = self._calculate_summary(previous)
|
||||
|
||||
return {
|
||||
"current": current_summary,
|
||||
"previous": previous_summary,
|
||||
"changes": {
|
||||
"total": current_summary["total"] - previous_summary["total"],
|
||||
"passed": current_summary["passed"] - previous_summary["passed"],
|
||||
"failed": current_summary["failed"] - previous_summary["failed"],
|
||||
"pass_rate": current_summary["pass_rate"] - previous_summary["pass_rate"],
|
||||
"avg_response_time": current_summary["avg_response_time"] - previous_summary["avg_response_time"]
|
||||
}
|
||||
}
|
||||
|
||||
def _analyze_performance(self, results: List[TestResult]) -> Dict[str, Any]:
|
||||
"""
|
||||
分析性能数据
|
||||
|
||||
Args:
|
||||
results: 测试结果列表
|
||||
|
||||
Returns:
|
||||
性能分析数据
|
||||
"""
|
||||
response_times = [r.response_time for r in results if r.response_time > 0]
|
||||
|
||||
if not response_times:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"avg_response_time": statistics.mean(response_times),
|
||||
"min_response_time": min(response_times),
|
||||
"max_response_time": max(response_times),
|
||||
"median_response_time": statistics.median(response_times),
|
||||
"std_response_time": statistics.stdev(response_times) if len(response_times) > 1 else 0,
|
||||
"p95_response_time": self._calculate_percentile(response_times, 95),
|
||||
"p99_response_time": self._calculate_percentile(response_times, 99),
|
||||
"total_tests": len(results),
|
||||
"failed_tests": sum(1 for r in results if not r.success)
|
||||
}
|
||||
|
||||
def _compare_with_benchmarks(self, performance_data: Dict[str, Any],
|
||||
benchmarks: Dict[str, PerformanceBenchmark]) -> Dict[str, Any]:
|
||||
"""
|
||||
与性能基准对比
|
||||
|
||||
Args:
|
||||
performance_data: 性能数据
|
||||
benchmarks: 性能基准
|
||||
|
||||
Returns:
|
||||
对比后的性能数据
|
||||
"""
|
||||
comparisons = []
|
||||
|
||||
for endpoint, benchmark in benchmarks.items():
|
||||
current_avg = performance_data.get("avg_response_time", 0)
|
||||
|
||||
comparison = {
|
||||
"endpoint": endpoint,
|
||||
"method": benchmark.method,
|
||||
"benchmark_avg": benchmark.avg_response_time,
|
||||
"current_avg": current_avg,
|
||||
"diff": current_avg - benchmark.avg_response_time,
|
||||
"diff_percent": ((current_avg - benchmark.avg_response_time) / benchmark.avg_response_time * 100) if benchmark.avg_response_time > 0 else 0,
|
||||
"status": "good" if current_avg <= benchmark.avg_response_time * 1.2 else "warning" if current_avg <= benchmark.avg_response_time * 1.5 else "critical"
|
||||
}
|
||||
|
||||
comparisons.append(comparison)
|
||||
|
||||
performance_data["benchmarks"] = comparisons
|
||||
|
||||
return performance_data
|
||||
|
||||
def _calculate_percentile(self, data: List[float], percentile: int) -> float:
|
||||
"""
|
||||
计算百分位数
|
||||
|
||||
Args:
|
||||
data: 数据列表
|
||||
percentile: 百分位数
|
||||
|
||||
Returns:
|
||||
百分位数值
|
||||
"""
|
||||
sorted_data = sorted(data)
|
||||
index = (percentile / 100) * (len(sorted_data) - 1)
|
||||
|
||||
if index.is_integer():
|
||||
return sorted_data[int(index)]
|
||||
else:
|
||||
lower = sorted_data[int(index)]
|
||||
upper = sorted_data[int(index) + 1]
|
||||
return lower + (upper - lower) * (index - int(index))
|
||||
|
||||
def _calculate_summary(self, results: List[TestResult]) -> Dict[str, Any]:
|
||||
"""
|
||||
计算测试摘要
|
||||
|
||||
Args:
|
||||
results: 测试结果列表
|
||||
|
||||
Returns:
|
||||
测试摘要
|
||||
"""
|
||||
total = len(results)
|
||||
passed = sum(1 for r in results if r.success)
|
||||
failed = total - passed
|
||||
response_times = [r.response_time for r in results if r.response_time > 0]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"passed": passed,
|
||||
"failed": failed,
|
||||
"pass_rate": (passed / total * 100) if total > 0 else 0,
|
||||
"avg_response_time": statistics.mean(response_times) if response_times else 0
|
||||
}
|
||||
|
||||
def _generate_trend_html(self, trend_data: List[TrendData]) -> str:
|
||||
"""生成趋势分析HTML"""
|
||||
# 这里简化实现,实际应该使用图表库(如Chart.js)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>趋势分析报告</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
||||
.trend-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
||||
.trend-table th, .trend-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
||||
.trend-table th {{ background-color: #4CAF50; color: white; }}
|
||||
.trend-table tr:nth-child(even) {{ background-color: #f2f2f2; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>测试趋势分析报告</h1>
|
||||
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
|
||||
<table class="trend-table">
|
||||
<tr>
|
||||
<th>日期</th>
|
||||
<th>总数</th>
|
||||
<th>通过</th>
|
||||
<th>失败</th>
|
||||
<th>通过率</th>
|
||||
<th>平均响应时间</th>
|
||||
</tr>
|
||||
{"".join([f"""
|
||||
<tr>
|
||||
<td>{d.date}</td>
|
||||
<td>{d.total}</td>
|
||||
<td>{d.passed}</td>
|
||||
<td>{d.failed}</td>
|
||||
<td>{d.pass_rate:.2f}%</td>
|
||||
<td>{d.avg_response_time:.2f}ms</td>
|
||||
</tr>
|
||||
""" for d in trend_data])}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def _generate_comparison_html(self, comparison_data: Dict[str, Any]) -> str:
|
||||
"""生成对比分析HTML"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>测试对比报告</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
||||
.comparison-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
||||
.comparison-table th, .comparison-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
||||
.comparison-table th {{ background-color: #4CAF50; color: white; }}
|
||||
.positive {{ color: green; }}
|
||||
.negative {{ color: red; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>测试对比报告</h1>
|
||||
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
|
||||
<table class="trend-table">
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>当前</th>
|
||||
<th>之前</th>
|
||||
<th>变化</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>总数</td>
|
||||
<td>{comparison_data['current']['total']}</td>
|
||||
<td>{comparison_data['previous']['total']}</td>
|
||||
<td class="{'positive' if comparison_data['changes']['total'] >= 0 else 'negative'}">
|
||||
{comparison_data['changes']['total']:+d}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>通过</td>
|
||||
<td>{comparison_data['current']['passed']}</td>
|
||||
<td>{comparison_data['previous']['passed']}</td>
|
||||
<td class="{'positive' if comparison_data['changes']['passed'] >= 0 else 'negative'}">
|
||||
{comparison_data['changes']['passed']:+d}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败</td>
|
||||
<td>{comparison_data['current']['failed']}</td>
|
||||
<td>{comparison_data['previous']['failed']}</td>
|
||||
<td class="{'negative' if comparison_data['changes']['failed'] > 0 else 'positive'}">
|
||||
{comparison_data['changes']['failed']:+d}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>通过率</td>
|
||||
<td>{comparison_data['current']['pass_rate']:.2f}%</td>
|
||||
<td>{comparison_data['previous']['pass_rate']:.2f}%</td>
|
||||
<td class="{'positive' if comparison_data['changes']['pass_rate'] >= 0 else 'negative'}">
|
||||
{comparison_data['changes']['pass_rate']:+.2f}%
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def _generate_performance_html(self, performance_data: Dict[str, Any]) -> str:
|
||||
"""生成性能分析HTML"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>性能分析报告</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
||||
.performance-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
||||
.performance-table th, .performance-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
||||
.performance-table th {{ background-color: #4CAF50; color: white; }}
|
||||
.good {{ color: green; }}
|
||||
.warning {{ color: orange; }}
|
||||
.critical {{ color: red; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>性能分析报告</h1>
|
||||
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
|
||||
<h2>性能指标</h2>
|
||||
<table class="performance-table">
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>值</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>平均响应时间</td>
|
||||
<td>{performance_data.get('avg_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最小响应时间</td>
|
||||
<td>{performance_data.get('min_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最大响应时间</td>
|
||||
<td>{performance_data.get('max_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>中位数响应时间</td>
|
||||
<td>{performance_data.get('median_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P95响应时间</td>
|
||||
<td>{performance_data.get('p95_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P99响应时间</td>
|
||||
<td>{performance_data.get('p99_response_time', 0):.2f}ms</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def save_to_history(self, results: List[TestResult], summary: TestSummary) -> None:
|
||||
"""
|
||||
保存测试结果到历史记录
|
||||
|
||||
Args:
|
||||
results: 测试结果列表
|
||||
summary: 测试摘要
|
||||
"""
|
||||
date_str = datetime.now().strftime('%Y%m%d')
|
||||
history_file = self.history_dir / f"test_report_{date_str}.json"
|
||||
|
||||
report_data = {
|
||||
"summary": summary.to_dict(),
|
||||
"results": [r.to_dict() for r in results],
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(history_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(report_data, f, ensure_ascii=False, indent=2)
|
||||
Reference in New Issue
Block a user