Files
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

516 lines
15 KiB
Python

"""
报告生成器模块
"""
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