测试组/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>
      <a href="{report_url}" class="report-btn">点击查看详细报告</a>
        <div class="card">
          <span class="label">📈 通过率</span>
          <span class="value success">{success_rate}</span>
    </div>
      </div>
      <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>