feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user