From 536b18a7c5d53d72d78ffab579ff24ac9146d5ab Mon Sep 17 00:00:00 2001
From: hyb <kk_huangyangbo@163.com>
Date: Tue, 20 Jan 2026 09:39:42 +0000
Subject: [PATCH] 接口自动化平台优化登录页面和首页; 项目看板增加多个统计数据和详细数据信息,看板布局和样式优化
---
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py | 535 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 488 insertions(+), 47 deletions(-)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py"
index 6b4adde..65f39cb 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py"
@@ -10,6 +10,7 @@
from typing import Dict
from django.conf import settings
import time
+from datetime import datetime
def parse_message(summary: Dict, **kwargs):
@@ -25,14 +26,79 @@
pass_count = summary["stat"]["successes"]
fail_count = summary["stat"]["failures"]
error_count = summary["stat"]["errors"]
+ skipped_count = summary["stat"].get("skipped", 0)
+ expected_failures_count = summary["stat"].get("expectedFailures", 0)
+ unexpected_successes_count = summary["stat"].get("unexpectedSuccesses", 0)
duration = "%.2fs" % summary["time"]["duration"]
report_id = summary["report_id"]
base_url = settings.IM_REPORT_SETTING.get("base_url")
port = settings.IM_REPORT_SETTING.get("port")
report_url = f"{base_url}:{port}/api/lunarlink/reports/{report_id}"
executed = rows_count
- fail_rate = "{:.2%}".format(fail_count / executed)
+ fail_rate = "{:.2%}".format(fail_count / executed) if executed > 0 else "0.00%"
+ success_rate = "{:.2%}".format(pass_count / executed) if executed > 0 else "0.00%"
case_count = kwargs.get("case_count")
+ creator = kwargs.get("creator", "")
+ updater = kwargs.get("updater", "")
+
+ start_at = summary.get("time", {}).get("start_at", 0)
+ if start_at:
+ start_time = datetime.fromtimestamp(start_at).strftime("%Y-%m-%d %H:%M:%S")
+ end_time = datetime.fromtimestamp(start_at + summary["time"]["duration"]).strftime("%Y-%m-%d %H:%M:%S")
+ else:
+ start_time = ""
+ end_time = ""
+
+ setup_hooks_duration = summary.get("time", {}).get("setup_hooks_duration", 0)
+ teardown_hooks_duration = summary.get("time", {}).get("teardown_hooks_duration", 0)
+
+ platform_info = summary.get("platform", {})
+ python_version = platform_info.get("python_version", "")
+ httprunner_version = platform_info.get("httprunner_version", "")
+
+ avg_duration = 0
+ if executed > 0:
+ avg_duration = summary["time"]["duration"] / executed
+
+ failed_cases = []
+ error_cases = []
+
+ details = summary.get("details", [])
+ for detail in details:
+ records = detail.get("records", [])
+ for record in records:
+ if record.get("status") in ["failure", "error"]:
+ case_name = record.get("name", "未知用例")
+ case_status = record.get("status", "")
+ meta_data = record.get("meta_data", {})
+ case_url = meta_data.get("request", {}).get("url", "")
+ case_method = meta_data.get("request", {}).get("method", "")
+ case_error = record.get("attachment", "")
+
+ if case_error:
+ case_error_lines = case_error.split('\n')
+ if len(case_error_lines) > 1:
+ case_error = case_error_lines[0]
+ if len(case_error_lines) > 2:
+ case_error += f"\n... (共{len(case_error_lines)}行错误信息)"
+ case_error = case_error.strip()
+ else:
+ case_error = "无错误信息"
+
+ case_info = {
+ "name": case_name,
+ "url": case_url,
+ "method": case_method,
+ "error": case_error[:150] if case_error else "无错误信息"
+ }
+
+ if case_status == "failure":
+ failed_cases.append(case_info)
+ elif case_status == "error":
+ error_cases.append(case_info)
+
+ failed_cases = failed_cases[:10]
+ error_cases = error_cases[:10]
return {
"task_name": task_name,
@@ -42,7 +108,22 @@
"error_count": error_count,
"fail_count": fail_count,
"fail_rate": fail_rate,
+ "success_rate": success_rate,
"report_url": report_url,
+ "creator": creator,
+ "updater": updater,
+ "start_time": start_time,
+ "end_time": end_time,
+ "skipped_count": skipped_count,
+ "expected_failures_count": expected_failures_count,
+ "unexpected_successes_count": unexpected_successes_count,
+ "setup_hooks_duration": setup_hooks_duration,
+ "teardown_hooks_duration": teardown_hooks_duration,
+ "python_version": python_version,
+ "httprunner_version": httprunner_version,
+ "avg_duration": avg_duration,
+ "failed_cases": failed_cases,
+ "error_cases": error_cases,
}
@@ -55,6 +136,21 @@
fail_count,
fail_rate,
report_url,
+ success_rate,
+ creator,
+ updater,
+ start_time,
+ failed_cases,
+ error_cases,
+ end_time,
+ skipped_count,
+ expected_failures_count,
+ unexpected_successes_count,
+ setup_hooks_duration,
+ teardown_hooks_duration,
+ python_version,
+ httprunner_version,
+ avg_duration,
):
"""
定制邮件报告消息模板(高级优化版)
@@ -67,72 +163,172 @@
:param fail_count: 失败接口个数
:param fail_rate: 失败比例
:param report_url: 报告链接
+ :param success_rate: 成功率
+ :param creator: 创建人
+ :param updater: 更新人
+ :param start_time: 开始时间
+ :param failed_cases: 失败用例列表
+ :param error_cases: 异常用例列表
+ :param end_time: 结束时间
+ :param skipped_count: 跳过用例数
+ :param expected_failures_count: 预期失败数
+ :param unexpected_successes_count: 意外成功数
+ :param setup_hooks_duration: 前置钩子耗时
+ :param teardown_hooks_duration: 后置钩子耗时
+ :param python_version: Python版本
+ :param httprunner_version: HttpRunner版本
+ :param avg_duration: 平均用例耗时
:return: 邮件主题及 HTML 内容
"""
email_subject = f"【自动化测试报告】{task_name}"
+
+ status_icon = "✅" if fail_count == 0 and error_count == 0 else ("⚠️" if fail_count == 0 else "❌")
+
+ if error_count == 0 and fail_count == 0:
+ status_text = "测试通过"
+ status_color = "#2ecc71"
+ status_bg = "#e8f8f5"
+ elif fail_count == 0:
+ status_text = "部分异常"
+ status_color = "#f39c12"
+ status_bg = "#fef5e7"
+ else:
+ status_text = "测试失败"
+ status_color = "#e74c3c"
+ status_bg = "#fdedec"
+
+ failed_cases_html = ""
+ if failed_cases:
+ failed_cases_html = "<div class='case-list'>"
+ for idx, case in enumerate(failed_cases, 1):
+ failed_cases_html += f"""
+ <div class='case-item'>
+ <div class='case-header'>
+ <span class='case-index'>{idx}</span>
+ <span class='case-name'>{case['name']}</span>
+ </div>
+ <div class='case-details'>
+ <span class='case-method'>{case['method']}</span>
+ <span class='case-url'>{case['url'][:60]}...</span>
+ </div>
+ <div class='case-error'>❌ {case['error']}</div>
+ </div>
+ """
+ failed_cases_html += "</div>"
+
+ error_cases_html = ""
+ if error_cases:
+ error_cases_html = "<div class='case-list'>"
+ for idx, case in enumerate(error_cases, 1):
+ error_cases_html += f"""
+ <div class='case-item'>
+ <div class='case-header'>
+ <span class='case-index'>{idx}</span>
+ <span class='case-name'>{case['name']}</span>
+ </div>
+ <div class='case-details'>
+ <span class='case-method'>{case['method']}</span>
+ <span class='case-url'>{case['url'][:60]}...</span>
+ </div>
+ <div class='case-error'>⚠️ {case['error']}</div>
+ </div>
+ """
+ error_cases_html += "</div>"
+
email_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>自动化测试报告</title>
- <!-- 引入 Google 字体 -->
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
<style>
body {{
margin: 0;
padding: 0;
background-color: #f5f7fa;
- font-family: 'Roboto', sans-serif;
+ font-family: 'Roboto', 'Microsoft YaHei', sans-serif;
color: #333;
}}
.container {{
- max-width: 700px;
+ max-width: 800px;
margin: 40px auto;
background: #fff;
- border-radius: 12px;
+ border-radius: 16px;
overflow: hidden;
- box-shadow: 0 8px 24px rgba(0,0,0,0.1);
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}}
.header {{
- background: linear-gradient(135deg, #4facfe, #00f2fe);
- padding: 30px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 40px 30px;
text-align: center;
color: #fff;
}}
.header h1 {{
margin: 0;
- font-size: 28px;
+ font-size: 32px;
font-weight: 700;
+ letter-spacing: 1px;
}}
- .header p {{
- margin: 8px 0 0;
+ .header .subtitle {{
+ margin: 12px 0 0;
font-size: 16px;
+ opacity: 0.95;
+ }}
+ .status-badge {{
+ display: inline-block;
+ margin-top: 20px;
+ padding: 8px 24px;
+ background: rgba(255,255,255,0.2);
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
}}
.content {{
- padding: 30px 40px;
+ padding: 40px 50px;
+ }}
+ .section-title {{
+ font-size: 18px;
+ font-weight: 600;
+ color: #2c3e50;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid #f0f0f0;
}}
.summary-grid {{
display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin-bottom: 30px;
+ }}
+ .summary-grid-extended {{
+ display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
- margin-top: 20px;
+ margin-bottom: 30px;
}}
.card {{
- background: #f9fafc;
- border-radius: 8px;
- padding: 20px;
+ background: #f8f9fa;
+ border-radius: 12px;
+ padding: 24px 16px;
text-align: center;
- box-shadow: 0 2px 6px rgba(0,0,0,0.05);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ }}
+ .card:hover {{
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}}
.card .label {{
- font-size: 14px;
- color: #777;
- margin-bottom: 8px;
+ font-size: 13px;
+ color: #7f8c8d;
+ margin-bottom: 10px;
display: block;
+ font-weight: 500;
}}
.card .value {{
- font-size: 24px;
+ font-size: 28px;
font-weight: 700;
+ line-height: 1;
}}
.value.success {{
color: #2ecc71;
@@ -143,40 +339,178 @@
.value.warning {{
color: #f39c12;
}}
+ .value.primary {{
+ color: #667eea;
+ }}
+ .info-table {{
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 30px;
+ }}
+ .info-table td {{
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+ }}
+ .info-table td:first-child {{
+ color: #7f8c8d;
+ font-weight: 500;
+ width: 120px;
+ }}
+ .info-table td:last-child {{
+ color: #2c3e50;
+ font-weight: 600;
+ }}
+ .progress-bar {{
+ width: 100%;
+ height: 8px;
+ background: #e0e0e0;
+ border-radius: 4px;
+ overflow: hidden;
+ margin: 20px 0;
+ }}
+ .progress-fill {{
+ height: 100%;
+ background: linear-gradient(90deg, #2ecc71, #27ae60);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ }}
+ .case-list {{
+ margin-top: 15px;
+ }}
+ .case-item {{
+ background: #fff;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 12px;
+ transition: box-shadow 0.2s ease;
+ }}
+ .case-item:hover {{
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+ }}
+ .case-header {{
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ }}
+ .case-index {{
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: #667eea;
+ color: #fff;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: 600;
+ margin-right: 10px;
+ }}
+ .case-name {{
+ font-weight: 600;
+ color: #2c3e50;
+ font-size: 14px;
+ flex: 1;
+ }}
+ .case-details {{
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+ }}
+ .case-method {{
+ display: inline-block;
+ padding: 4px 10px;
+ background: #e8f4f8;
+ color: #2980b9;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 600;
+ }}
+ .case-url {{
+ flex: 1;
+ color: #7f8c8d;
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }}
+ .case-error {{
+ background: #fdedec;
+ color: #c0392b;
+ padding: 10px 12px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-family: 'Courier New', monospace;
+ white-space: pre-wrap;
+ word-break: break-word;
+ line-height: 1.5;
+ }}
+ .no-cases {{
+ text-align: center;
+ padding: 30px;
+ color: #95a5a6;
+ font-size: 14px;
+ }}
.report-btn {{
display: block;
- width: 240px;
+ width: 280px;
margin: 30px auto 0;
text-align: center;
- padding: 15px;
- background-color: #4a90e2;
+ padding: 16px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
text-decoration: none;
- border-radius: 6px;
- font-weight: 500;
- transition: background 0.3s ease;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 16px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}}
.report-btn:hover {{
- background-color: #3b7dd8;
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}}
.footer {{
text-align: center;
- font-size: 12px;
- color: #aaa;
- padding: 20px;
- background: #f0f2f5;
+ font-size: 13px;
+ color: #95a5a6;
+ padding: 25px;
+ background: #f8f9fa;
+ border-top: 1px solid #e9ecef;
+ }}
+ .divider {{
+ height: 1px;
+ background: #e9ecef;
+ margin: 30px 0;
+ }}
+ .icon {{
+ font-size: 20px;
+ margin-right: 8px;
}}
@media (max-width: 768px) {{
.container {{
margin: 20px;
}}
+ .content {{
+ padding: 30px 25px;
+ }}
.summary-grid {{
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(2, 1fr);
+ }}
+ .summary-grid-extended {{
+ grid-template-columns: repeat(2, 1fr);
}}
}}
@media (max-width: 480px) {{
.summary-grid {{
grid-template-columns: 1fr;
+ }}
+ .summary-grid-extended {{
+ grid-template-columns: 1fr;
+ }}
+ .header h1 {{
+ font-size: 24px;
}}
}}
</style>
@@ -184,40 +518,147 @@
<body>
<div class="container">
<div class="header">
- <h1>自动化测试报告</h1>
- <p>{task_name}</p>
+ <h1>🚀 自动化测试报告</h1>
+ <p class="subtitle">{task_name}</p>
+ <div class="status-badge">{status_icon} {status_text}</div>
</div>
+
<div class="content">
+ <div class="section-title">📊 测试概览</div>
<div class="summary-grid">
<div class="card">
<span class="label">总耗时</span>
- <span class="value">{duration}</span>
+ <span class="value primary">{duration}</span>
</div>
<div class="card">
- <span class="label">用例个数</span>
+ <span class="label">用例总数</span>
<span class="value">{case_count}</span>
</div>
<div class="card">
- <span class="label">失败比例</span>
- <span class="value warning">{fail_rate}</span>
+ <span class="label">成功率</span>
+ <span class="value success">{success_rate}</span>
</div>
<div class="card">
- <span class="label">成功接口</span>
+ <span class="label">失败率</span>
+ <span class="value error">{fail_rate}</span>
+ </div>
+ <div class="card">
+ <span class="label">✅ 成功</span>
<span class="value success">{pass_count}</span>
</div>
<div class="card">
- <span class="label">异常接口</span>
- <span class="value error">{error_count}</span>
- </div>
- <div class="card">
- <span class="label">失败接口</span>
+ <span class="label">❌ 失败</span>
<span class="value error">{fail_count}</span>
</div>
+ <div class="card">
+ <span class="label">⚠️ 异常</span>
+ <span class="value warning">{error_count}</span>
+ </div>
+ <div class="card">
+ <span class="label">📈 通过率</span>
+ <span class="value success">{success_rate}</span>
+ </div>
</div>
- <a href="{report_url}" class="report-btn">点击查看详细报告</a>
+
+ <div class="summary-grid-extended">
+ <div class="card">
+ <span class="label">⏭️ 跳过</span>
+ <span class="value" style="color: #95a5a6;">{skipped_count}</span>
+ </div>
+ <div class="card">
+ <span class="label">🎯 预期失败</span>
+ <span class="value" style="color: #f39c12;">{expected_failures_count}</span>
+ </div>
+ <div class="card">
+ <span class="label">🎉 意外成功</span>
+ <span class="value" style="color: #3498db;">{unexpected_successes_count}</span>
+ </div>
+ </div>
+
+ <div class="summary-grid-extended">
+ <div class="card">
+ <span class="label">⚡ 平均耗时</span>
+ <span class="value primary">{avg_duration:.3f}s</span>
+ </div>
+ <div class="card">
+ <span class="label">🔧 前置钩子</span>
+ <span class="value" style="color: #7f8c8d;">{setup_hooks_duration:.3f}s</span>
+ </div>
+ <div class="card">
+ <span class="label">🔧 后置钩子</span>
+ <span class="value" style="color: #7f8c8d;">{teardown_hooks_duration:.3f}s</span>
+ </div>
+ </div>
+
+ <div class="divider"></div>
+
+ <div class="section-title">📋 详细信息</div>
+ <table class="info-table">
+ <tr>
+ <td><span class="icon">📝</span>任务名称</td>
+ <td>{task_name}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">⏱️</span>执行耗时</td>
+ <td>{duration}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">⚡</span>平均耗时</td>
+ <td>{avg_duration:.3f}s</td>
+ </tr>
+ <tr>
+ <td><span class="icon">📅</span>开始时间</td>
+ <td>{start_time if start_time else '未记录'}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">🏁</span>结束时间</td>
+ <td>{end_time if end_time else '未记录'}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">👤</span>创建人</td>
+ <td>{creator if creator else '未记录'}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">✏️</span>更新人</td>
+ <td>{updater if updater else '未记录'}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">🐍</span>Python版本</td>
+ <td>{python_version if python_version else '未记录'}</td>
+ </tr>
+ <tr>
+ <td><span class="icon">🏃</span>HttpRunner版本</td>
+ <td>{httprunner_version if httprunner_version else '未记录'}</td>
+ </tr>
+ </table>
+
+ <div class="divider"></div>
+
+ <div class="section-title">📈 测试结果分析</div>
+ <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
+ <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
+ <span style="color: #7f8c8d; font-size: 14px;">测试通过率</span>
+ <span style="color: #2ecc71; font-weight: 700; font-size: 18px;">{success_rate}</span>
+ </div>
+ <div class="progress-bar">
+ <div class="progress-fill" style="width: {success_rate};"></div>
+ </div>
+ <div style="display: flex; justify-content: space-between; font-size: 12px; color: #95a5a6;">
+ <span>成功: {pass_count}</span>
+ <span>失败: {fail_count}</span>
+ <span>异常: {error_count}</span>
+ </div>
+ </div>
+
+ {f'<div class="divider"></div><div class="section-title">❌ 失败用例列表</div>{failed_cases_html}' if failed_cases else ''}
+ {f'<div class="divider"></div><div class="section-title">⚠️ 异常用例列表</div>{error_cases_html}' if error_cases else ''}
+
+ <a href="{report_url}" class="report-btn">👉 点击查看详细报告</a>
</div>
+
<div class="footer">
- 本邮件由系统自动发出,请勿回复
+ <p>本邮件由系统自动发出,请勿回复</p>
+ <p style="margin-top: 8px;">如有疑问,请联系相关负责人</p>
</div>
</div>
</body>
--
Gitblit v1.9.1