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