feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1 @@
"""工具模块"""
@@ -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)
@@ -0,0 +1,156 @@
"""
日志工具模块
"""
import logging
import sys
from pathlib import Path
from typing import Optional
from datetime import datetime
class TestLogger:
"""测试日志记录器"""
def __init__(self, name: str = "test", log_file: str = None, level: str = "INFO", console: bool = True):
"""
初始化日志记录器
Args:
name: 日志记录器名称
log_file: 日志文件路径
level: 日志级别
console: 是否输出到控制台
"""
self.logger = logging.getLogger(name)
self.logger.setLevel(getattr(logging, level.upper()))
# 清除现有的处理器
self.logger.handlers.clear()
# 创建格式化器
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 添加文件处理器
if log_file:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
# 添加控制台处理器
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
def debug(self, message: str) -> None:
"""记录调试信息"""
self.logger.debug(message)
def info(self, message: str) -> None:
"""记录信息"""
self.logger.info(message)
def warning(self, message: str) -> None:
"""记录警告"""
self.logger.warning(message)
def error(self, message: str) -> None:
"""记录错误"""
self.logger.error(message)
def critical(self, message: str) -> None:
"""记录严重错误"""
self.logger.critical(message)
def exception(self, message: str) -> None:
"""记录异常信息"""
self.logger.exception(message)
def log_test_start(self, test_name: str) -> None:
"""记录测试开始"""
self.info(f"{'='*60}")
self.info(f"开始测试: {test_name}")
self.info(f"{'='*60}")
def log_test_end(self, test_name: str, passed: bool, duration: float = None) -> None:
"""记录测试结束"""
status = "✅ 通过" if passed else "❌ 失败"
duration_str = f" (耗时: {duration:.2f}ms)" if duration else ""
self.info(f"测试结束: {test_name} - {status}{duration_str}")
def log_request(self, method: str, url: str, data: dict = None, headers: dict = None) -> None:
"""记录请求信息"""
self.info(f"发送请求: {method} {url}")
if data:
self.info(f"请求数据: {data}")
if headers:
self.info(f"请求头: {headers}")
def log_response(self, status_code: int, response_time: float, data: dict = None) -> None:
"""记录响应信息"""
self.info(f"响应状态码: {status_code}")
self.info(f"响应时间: {response_time:.2f}ms")
if data:
self.info(f"响应数据: {data}")
def log_validation(self, rule_type: str, passed: bool, message: str = "") -> None:
"""记录验证结果"""
status = "" if passed else ""
self.info(f"{status} 验证规则 [{rule_type}]: {message or '通过'}")
def log_error(self, error: Exception) -> None:
"""记录错误详情"""
self.error(f"错误类型: {type(error).__name__}")
self.error(f"错误信息: {str(error)}")
self.exception("错误堆栈:")
def log_summary(self, total: int, passed: int, failed: int, skipped: int = 0, duration: float = None) -> None:
"""记录测试摘要"""
self.info(f"{'='*60}")
self.info(f"测试摘要:")
self.info(f" 总数: {total}")
self.info(f" 通过: {passed}")
self.info(f" 失败: {failed}")
self.info(f" 跳过: {skipped}")
if duration:
self.info(f" 总耗时: {duration:.2f}ms")
self.info(f" 通过率: {(passed/total*100):.2f}%")
self.info(f"{'='*60}")
class LoggerFactory:
"""日志记录器工厂"""
_loggers = {}
@classmethod
def get_logger(cls, name: str = "test", log_file: str = None, level: str = "INFO", console: bool = True) -> TestLogger:
"""
获取日志记录器实例
Args:
name: 日志记录器名称
log_file: 日志文件路径
level: 日志级别
console: 是否输出到控制台
Returns:
日志记录器实例
"""
key = f"{name}_{log_file}_{level}_{console}"
if key not in cls._loggers:
cls._loggers[key] = TestLogger(name, log_file, level, console)
return cls._loggers[key]
@classmethod
def clear_all(cls) -> None:
"""清除所有日志记录器"""
cls._loggers.clear()
@@ -0,0 +1,516 @@
"""
报告生成器模块
"""
import json
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, List
from dataclasses import dataclass, asdict
@dataclass
class TestResult:
"""测试结果数据类"""
test_name: str
test_type: str
url: str
method: str
status_code: int
response_time: float
success: bool
error_message: str
request_data: Dict[str, Any]
response_data: Dict[str, Any]
timestamp: str
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return asdict(self)
@dataclass
class TestSummary:
"""测试摘要数据类"""
total: int
passed: int
failed: int
skipped: int
pass_rate: float
total_time: float
start_time: str
end_time: str
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return asdict(self)
class ReportGenerator:
"""测试报告生成器"""
def __init__(self, output_dir: str = "reports"):
"""
初始化报告生成器
Args:
output_dir: 报告输出目录
"""
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def generate_json_report(self, results: List[TestResult], summary: TestSummary, filename: str = None) -> str:
"""
生成JSON格式报告
Args:
results: 测试结果列表
summary: 测试摘要
filename: 报告文件名
Returns:
报告文件路径
"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"test_report_{timestamp}.json"
report_path = self.output_dir / filename
report_data = {
"summary": summary.to_dict(),
"results": [result.to_dict() for result in results],
"generated_at": datetime.now().isoformat()
}
with open(report_path, 'w', encoding='utf-8') as f:
json.dump(report_data, f, ensure_ascii=False, indent=2)
return str(report_path)
def generate_html_report(self, results: List[TestResult], summary: TestSummary, filename: str = None) -> str:
"""
生成HTML格式报告
Args:
results: 测试结果列表
summary: 测试摘要
filename: 报告文件名
Returns:
报告文件路径
"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"test_report_{timestamp}.html"
report_path = self.output_dir / filename
html_content = self._generate_html_content(results, summary)
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html_content)
return str(report_path)
def _generate_html_content(self, results: List[TestResult], summary: TestSummary) -> str:
"""
生成HTML内容
Args:
results: 测试结果列表
summary: 测试摘要
Returns:
HTML内容
"""
passed_results = [r for r in results if r.success]
failed_results = [r for r in results if not r.success]
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试报告 - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
color: #333;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
font-size: 28px;
margin-bottom: 10px;
}}
.header p {{
font-size: 14px;
opacity: 0.9;
}}
.summary {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background: #f8f9fa;
}}
.summary-card {{
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}}
.summary-card h3 {{
font-size: 14px;
color: #666;
margin-bottom: 10px;
}}
.summary-card .value {{
font-size: 32px;
font-weight: bold;
color: #333;
}}
.summary-card.passed .value {{
color: #52c41a;
}}
.summary-card.failed .value {{
color: #ff4d4f;
}}
.progress-bar {{
grid-column: 1 / -1;
height: 30px;
background: #e8e8e8;
border-radius: 15px;
overflow: hidden;
margin-top: 20px;
}}
.progress-fill {{
height: 100%;
background: linear-gradient(90deg, #52c41a 0%, #73d13d 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
transition: width 0.3s ease;
}}
.results {{
padding: 30px;
}}
.results h2 {{
font-size: 24px;
margin-bottom: 20px;
color: #333;
}}
.test-item {{
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}}
.test-item.passed {{
border-left: 4px solid #52c41a;
}}
.test-item.failed {{
border-left: 4px solid #ff4d4f;
}}
.test-header {{
padding: 15px 20px;
background: #fafafa;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}}
.test-header:hover {{
background: #f0f0f0;
}}
.test-name {{
font-weight: bold;
color: #333;
}}
.test-status {{
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}}
.test-status.passed {{
background: #f6ffed;
color: #52c41a;
}}
.test-status.failed {{
background: #fff1f0;
color: #ff4d4f;
}}
.test-details {{
padding: 20px;
display: none;
border-top: 1px solid #e8e8e8;
}}
.test-item.expanded .test-details {{
display: block;
}}
.detail-row {{
margin-bottom: 10px;
}}
.detail-label {{
font-weight: bold;
color: #666;
margin-right: 10px;
}}
.detail-value {{
color: #333;
}}
.error-message {{
background: #fff1f0;
color: #ff4d4f;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}}
.code-block {{
background: #f5f5f5;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
margin-top: 10px;
}}
.footer {{
text-align: center;
padding: 20px;
background: #f8f9fa;
color: #666;
font-size: 14px;
}}
@media (max-width: 768px) {{
.summary {{
grid-template-columns: 1fr;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>API测试报告</h1>
<p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
</div>
<div class="summary">
<div class="summary-card">
<h3>测试总数</h3>
<div class="value">{summary.total}</div>
</div>
<div class="summary-card passed">
<h3>通过</h3>
<div class="value">{summary.passed}</div>
</div>
<div class="summary-card failed">
<h3>失败</h3>
<div class="value">{summary.failed}</div>
</div>
<div class="summary-card">
<h3>跳过</h3>
<div class="value">{summary.skipped}</div>
</div>
<div class="summary-card">
<h3>通过率</h3>
<div class="value">{summary.pass_rate:.1f}%</div>
</div>
<div class="summary-card">
<h3>总耗时</h3>
<div class="value">{summary.total_time:.0f}ms</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {summary.pass_rate}%">
{summary.pass_rate:.1f}%
</div>
</div>
</div>
<div class="results">
<h2>测试结果</h2>
<div id="test-results">
"""
for result in results:
status_class = "passed" if result.success else "failed"
status_text = "通过" if result.success else "失败"
html += f"""
<div class="test-item {status_class}" onclick="toggleDetails(this)">
<div class="test-header">
<span class="test-name">{result.test_name}</span>
<span class="test-status {status_class}">{status_text}</span>
</div>
<div class="test-details">
<div class="detail-row">
<span class="detail-label">测试类型:</span>
<span class="detail-value">{result.test_type}</span>
</div>
<div class="detail-row">
<span class="detail-label">请求方法:</span>
<span class="detail-value">{result.method}</span>
</div>
<div class="detail-row">
<span class="detail-label">请求URL:</span>
<span class="detail-value">{result.url}</span>
</div>
<div class="detail-row">
<span class="detail-label">状态码:</span>
<span class="detail-value">{result.status_code}</span>
</div>
<div class="detail-row">
<span class="detail-label">响应时间:</span>
<span class="detail-value">{result.response_time:.2f}ms</span>
</div>
<div class="detail-row">
<span class="detail-label">时间戳:</span>
<span class="detail-value">{result.timestamp}</span>
</div>
{self._generate_request_details(result)}
{self._generate_response_details(result)}
{self._generate_error_details(result)}
</div>
</div>
"""
html += """
</div>
</div>
<div class="footer">
<p>Everything is Suitable - API测试工具</p>
</div>
</div>
<script>
function toggleDetails(element) {
element.classList.toggle('expanded');
}
</script>
</body>
</html>
"""
return html
def _generate_request_details(self, result: TestResult) -> str:
"""生成请求详情"""
if not result.request_data:
return ""
import json
return f"""
<div class="detail-row">
<span class="detail-label">请求数据:</span>
</div>
<div class="code-block">{json.dumps(result.request_data, ensure_ascii=False, indent=2)}</div>
"""
def _generate_response_details(self, result: TestResult) -> str:
"""生成响应详情"""
if not result.response_data:
return ""
import json
return f"""
<div class="detail-row">
<span class="detail-label">响应数据:</span>
</div>
<div class="code-block">{json.dumps(result.response_data, ensure_ascii=False, indent=2)}</div>
"""
def _generate_error_details(self, result: TestResult) -> str:
"""生成错误详情"""
if result.success or not result.error_message:
return ""
return f"""
<div class="error-message">
<strong>错误信息:</strong> {result.error_message}
</div>
"""
def generate_all_reports(self, results: List[TestResult], summary: TestSummary) -> Dict[str, str]:
"""
生成所有格式的报告
Args:
results: 测试结果列表
summary: 测试摘要
Returns:
报告文件路径字典
"""
reports = {}
# 生成JSON报告
json_path = self.generate_json_report(results, summary)
reports['json'] = json_path
# 生成HTML报告
html_path = self.generate_html_report(results, summary)
reports['html'] = html_path
return reports