前端发送页面到邮箱

543 阅读5分钟

需求

根据给到的资料,设计邮件模板,并且将生成的页面发送到用户的邮箱中。

思路

对需求进行分析,可以拆分成多个小目标。

  1. 根据需求设计并实现静态邮件模板页面
  2. 实现邮件页面的动态生成
  3. 发送邮件

实践

step1 邮件模板静态页面

根据给到的参考资料,要实现的是数据报表,使用table实现。 实现的页面如下:

image.png

step2 实现邮件模板的动态生成

动态生成前端页面有多种方法,根据复杂度、灵活性和项目需求可以选择不同的方案。常见的方式有:

  1. 字符串拼接/模板字面量。最简单直接的方式,适合简单的HTML结构。
  2. DOM API 动态创建元素,适合需要在生成前后进行DOM操作的场景:
  3. 使用模板引擎(例如:EJS)
  4. 使用现代前端框架 (React, Vue等)
  5. Web Components
  6. 使用专用HTML生成库

我要实现的是一个简单的邮件模板页面,所以字符串模板或模板字面量是最优选。具体实现如下:

// report.js
const reportData = {
  baseData: {
    day: {
      deplete: 12500,
      new: 1200,
      dau: 3475,
      ad_revenue: 10000,
      ad_money: 10000,
      gross_income: 20000,
      profit: "",
    },
    month: {
      deplete: 12500,
      new: 1200,
      dau: "",
      ad_revenue: 10000,
      ad_money: 10000,
      gross_income: 20000,
      profit: 30032,
    },
    total: {
      deplete: 12500,
      new: 1200,
      dau: 3475,
      ad_revenue: 10000,
      ad_money: 10000,
      gross_income: 20000,
      profit: 234533,
    },
  },
  deliveryData: {
    seven_days: {
      deplete: 12500,
      new: 1200,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      day_1_ROI: 0.78,
    },
    chain: {
      deplete: 0.15,
      new: 0.22,
      enrollment_costs: -0.05,
      new_payment_rate: 0.007,
      paid_costs: 0.033,
      day_1_ROI: -0.78,
    },
    last_week: {
      deplete: 12500,
      new: 1200,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      day_1_ROI: 0.78,
    },
    year_on_year: {
      deplete: 0.15,
      new: 0.22,
      enrollment_costs: -0.05,
      new_payment_rate: 0.007,
      paid_costs: 0.033,
      day_1_ROI: -0.78,
    },
    month: {
      deplete: 12500,
      new: 1200,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      day_1_ROI: 0.78,
    },
    total: {
      deplete: 12500,
      new: 1200,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      day_1_ROI: 0.78,
    },
  },
  performanceData: {
    seven_days: {
      load_time: 1872,
      conversion_rate: 0.221,
    },
    chain: { load_time: 0.01872, conversion_rate: 0.0221 },
  },
  retainData: {
    register: {
      new: 0.452,
      seven_days: 0.45,
      chain: 0.58,
      last_week: 0.54,
      year_on_year: 0.52,
    },
    pay: {
      new: 0.452,
      seven_days: 0.45,
      chain: 0.58,
      last_week: 0.54,
      year_on_year: 0.52,
    },
  },
  revenueDataDetail: [
    {
      date: "2025-05-06",
      total_revenue: 10000,
      ad_revenue: 5000,
      ad_money: 5000,
      ad_money1: 5000,
      deplete: 12500,
      new: 1200,
      dau: 3475,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      pay_arpu: 0.001,
      active_arpu: 0.001,
      day_1_ROI: 0.78,
      day_3_ROI: 0.78,
      day_7_ROI: 0.78,
      day_14_ROI: 0.78,
      day_30_ROI: 0.78,
      day_60_ROI: 0.78,
      day_90_ROI: 0.78,
    },
    {
      date: "2025-05-07",
      total_revenue: 10000,
      ad_revenue: 5000,
      ad_money: 5000,
      ad_money1: 5000,
      deplete: 12500,
      new: 1200,
      dau: 3475,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      pay_arpu: 0.001,
      active_arpu: 0.001,
      day_1_ROI: 0.78,
      day_3_ROI: 0.78,
      day_7_ROI: 0.78,
      day_14_ROI: 0.78,
      day_30_ROI: 0.78,
      day_60_ROI: 0.78,
      day_90_ROI: 0.78,
    },
    {
      date: "2025-05-08",
      total_revenue: 10000,
      ad_revenue: 5000,
      ad_money: 5000,
      ad_money1: 5000,
      deplete: 12500,
      new: 1200,
      dau: 3475,
      enrollment_costs: 10.42,
      new_payment_rate: 0.052,
      paid_costs: 4.5,
      pay_arpu: 0.001,
      active_arpu: 0.001,
      day_1_ROI: 0.78,
      day_3_ROI: 0.78,
      day_7_ROI: 0.78,
      day_14_ROI: 0.78,
      day_30_ROI: 0.78,
      day_60_ROI: 0.78,
      day_90_ROI: 0.78,
    },
  ],
  retainDataDetail: [
    {
      date: "2025-05-06",
      new: 12344,
      ret_1: 0.45,
      ret_3: 0.45,
      ret_7: 0.45,
      ret_14: 0.45,
      ret_30: 0.45,
      ret_60: 0.45,
      ret_90: 0.45,
      first_pay_rate: 0.45,
      first_pay_num: 1000,
      pay_1: 0.45,
      pay_3: 0.45,
      pay_7: 0.45,
      pay_14: 0.45,
      pay_30: 0.45,
      pay_60: 0.45,
      pay_90: 0.45,
    },
    {
      date: "2025-05-07",
      new: 12344,
      ret_1: 0.45,
      ret_3: 0.45,
      ret_7: 0.45,
      ret_14: 0.45,
      ret_30: 0.45,
      ret_60: 0.45,
      ret_90: 0.45,
      first_pay_rate: 0.45,
      first_pay_num: 1000,
      pay_1: 0.45,
      pay_3: 0.45,
      pay_7: 0.45,
      pay_14: 0.45,
      pay_30: 0.45,
      pay_60: 0.45,
      pay_90: 0.45,
    },
    {
      date: "2025-05-08",
      new: 12344,
      ret_1: 0.45,
      ret_3: 0.45,
      ret_7: 0.45,
      ret_14: 0.45,
      ret_30: 0.45,
      ret_60: 0.45,
      ret_90: 0.45,
      first_pay_rate: 0.45,
      first_pay_num: 1000,
      pay_1: 0.45,
      pay_3: 0.45,
      pay_7: 0.45,
      pay_14: 0.45,
      pay_30: 0.45,
      pay_60: 0.45,
      pay_90: 0.45,
    },
  ],
};

//获取日期
const getDate = (type) => {
  const weekday = [
    "星期日",
    "星期一",
    "星期二",
    "星期三",
    "星期四",
    "星期五",
    "星期六",
  ];
  const date = new Date();
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const week = weekday[date.getDay()];

  return type === 1
    ? `${year}年${month}月${day}日 ${week}`
    : `${month}月${day}日 ${week}`;
};

// 辅助函数
const formatNumber = (num) => {
  if (num === "" || num === null || num === undefined) return "-";
  if (typeof num === "number") {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  }
  return num;
};
const formatPercent = (num) => {
  if (num === "" || num === null || num === undefined) return "-";
  if (typeof num === "number") {
    return (num * 100).toFixed(2) + "%";
  }
  return num;
};
const formatChange = (change) => {
  if (change === "" || change === null || change === undefined) return "-";
  if (change > 0)
    return `<span class="positive">↑${(change * 100).toFixed(2)}%</span>`;
  if (change < 0)
    return `<span class="negative">↓${Math.abs(change * 100).toFixed(
      2
    )}%</span>`;
  return `→0%`;
};
const formatROI = (roi) => {
  if (roi === "" || roi === null || roi === undefined) return "-";
  return roi.toFixed(2);
};

generatoReport = function () {
  const data = reportData;
  return `
  <!DOCTYPE html>
  <html>
  <head>
    <style>
      body,table,td { margin: 0;padding: 0;border: 0;line-height: 1.5;}
      html {background-color: #f0f2f5;}
      .container {max-width: 1000px;margin: 40px auto;background-color: #ffffff;border-radius: 12px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);}
      .header {padding: 32px;background: linear-gradient(135deg, #2c3e50, #3498db);color: white;border-radius: 12px 12px 0 0;}
      .title { font-size: 24px; font-weight: 700; margin: 0;}
      .sub-title { font-size: 14px;color: #e0f3ff; margin-top: 8px;}
      .content {padding: 32px;}
      .section { margin-bottom: 32px; }
      .section-title {font-size: 18px;font-weight: 600;color: #2c3e50;border-left: 4px solid #3498db;padding-left: 12px;margin-bottom: 20px;}
      .data-table {width: 100%;border-collapse: collapse;margin-top: 16px;font-size: 14px;}
      .data-table th,.data-table td {padding: 12px;text-align: center;border: 1px solid #e0e0e0;}
      .data-table th {background-color: #f0f2f5;color: #333;}
      .data-table tr:hover {background-color: #f8f9fa;}
      .positive { color: #f5222d; }  /* 红色表示正向变化 */
      .negative { color: #52c41a; }  /* 绿色表示负向变化 */
    </style>
  </head>
  <body>
    <div class="container">
      <div class="header">
        <h1 class="title">产品运营数据分析报表</h1>
        <p class="sub-title">${getDate(1)}</p>
      </div>
      <div class="content">
        <div class="section">
          <h2 class="section-title">1. 基本数据</h2>
          <table class="data-table">
            <colgroup>
              <col style="width:120px"></col>
            </colgroup>
            <tbody>
              <tr>
                <th rowspan="2">日期</th>
                <th rowspan="2">消耗</th>
                <th rowspan="2">新增</th>
                <th rowspan="2">DAU</th>
                <th colspan="3">收入</th>
                <th rowspan="2">毛利</th>
              </tr>
              <tr>
                <td>广告收入</td>
                <td>广告金</td>
                <td>总收入</td>
              </tr>
              <tr>
                <td>${getDate(2)}</td>
                <td>${formatNumber(data.baseData.day.deplete)}</td>
                <td>${formatNumber(data.baseData.day.new)}</td>
                <td>${formatNumber(data.baseData.day.dau)}</td>
                <td>${formatNumber(data.baseData.day.ad_revenue)}</td>
                <td>${formatNumber(data.baseData.day.ad_money)}</td>
                <td>${formatNumber(data.baseData.day.gross_income)}</td>
                <td>${formatNumber(data.baseData.day.profit)}</td>
              </tr>
              <tr>
                <td>本月累计</td>
                <td>${formatNumber(data.baseData.month.deplete)}</td>
                <td>${formatNumber(data.baseData.month.new)}</td>
                <td>${formatNumber(data.baseData.month.dau)}</td>
                <td>${formatNumber(data.baseData.month.ad_revenue)}</td>
                <td>${formatNumber(data.baseData.month.ad_money)}</td>
                <td>${formatNumber(data.baseData.month.gross_income)}</td>
                <td>${formatNumber(data.baseData.month.profit)}</td>
              </tr>
              <tr>
                <td>历史累计</td>
                <td>${formatNumber(data.baseData.total.deplete)}</td>
                <td>${formatNumber(data.baseData.total.new)}</td>
                <td>${formatNumber(data.baseData.total.dau)}</td>
                <td>${formatNumber(data.baseData.total.ad_revenue)}</td>
                <td>${formatNumber(data.baseData.total.ad_money)}</td>
                <td>${formatNumber(data.baseData.total.gross_income)}</td>
                <td>${formatNumber(data.baseData.total.profit)}</td>
              </tr>
            </tbody>
          </table>
        </div>

        <div class="section">
          <h2 class="section-title">2. 投放数据</h2>
          <table class="data-table">
            <colgroup>
              <col style="width:120px"></col>
            </colgroup>
            <tbody>
              <tr>
                <th>日期</th>
                <th>指标</th>
                <th>消耗</th>
                <th>新增</th>
                <th>注册成本</th>
                <th>新增付费率</th>
                <th>付费成本</th>
                <th>首日ROI(内购)</th>
              </tr>
              <tr>
                <td rowspan="4">${getDate(2)}</td>
                <td>近7日日均</td>
                <td>${formatNumber(data.deliveryData.seven_days.deplete)}</td>
                <td>${formatNumber(data.deliveryData.seven_days.new)}</td>
                <td>${formatNumber(
                  data.deliveryData.seven_days.enrollment_costs
                )}</td>
                <td>${formatPercent(
                  data.deliveryData.seven_days.new_payment_rate
                )}</td>
                <td>${formatNumber(
                  data.deliveryData.seven_days.paid_costs
                )}</td>
                <td>${formatROI(data.deliveryData.seven_days.day_1_ROI)}</td>
              </tr>
              <tr>
                <td>环比</td>
                <td>${formatChange(data.deliveryData.chain.deplete)}</td>
                <td>${formatChange(data.deliveryData.chain.new)}</td>
                <td>${formatChange(
                  data.deliveryData.chain.enrollment_costs
                )}</td>
                <td>${formatChange(
                  data.deliveryData.chain.new_payment_rate
                )}</td>
                <td>${formatChange(data.deliveryData.chain.paid_costs)}</td>
                <td>${formatChange(data.deliveryData.chain.day_1_ROI)}</td>
              </tr>
              <tr>
                <td>上周同期</td>
                <td>${formatNumber(data.deliveryData.last_week.deplete)}</td>
                <td>${formatNumber(data.deliveryData.last_week.new)}</td>
                <td>${formatNumber(
                  data.deliveryData.last_week.enrollment_costs
                )}</td>
                <td>${formatPercent(
                  data.deliveryData.last_week.new_payment_rate
                )}</td>
                <td>${formatNumber(data.deliveryData.last_week.paid_costs)}</td>
                <td>${formatROI(data.deliveryData.last_week.day_1_ROI)}</td>
              </tr>
              <tr>
                <td>同比</td>
                <td>${formatChange(data.deliveryData.year_on_year.deplete)}</td>
                <td>${formatChange(data.deliveryData.year_on_year.new)}</td>
                <td>${formatChange(
                  data.deliveryData.year_on_year.enrollment_costs
                )}</td>
                <td>${formatChange(
                  data.deliveryData.year_on_year.new_payment_rate
                )}</td>
                <td>${formatChange(
                  data.deliveryData.year_on_year.paid_costs
                )}</td>
                <td>${formatChange(
                  data.deliveryData.year_on_year.day_1_ROI
                )}</td>
              </tr>
              <tr>
                <td>本月累计</td>
                  <td></td>
                <td>${formatNumber(data.deliveryData.month.deplete)}</td>
                <td>${formatNumber(data.deliveryData.month.new)}</td>
                <td>${formatNumber(
                  data.deliveryData.month.enrollment_costs
                )}</td>
                <td>${formatPercent(
                  data.deliveryData.month.new_payment_rate
                )}</td>
                <td>${formatNumber(data.deliveryData.month.paid_costs)}</td>
                <td>${formatROI(data.deliveryData.month.day_1_ROI)}</td>
              </tr>
              <tr>
                <td>历史累计</td>
                <td></td>
                <td>${formatNumber(data.deliveryData.total.deplete)}</td>
                <td>${formatNumber(data.deliveryData.total.new)}</td>
                <td>${formatNumber(
                  data.deliveryData.total.enrollment_costs
                )}</td>
                <td>${formatPercent(
                  data.deliveryData.total.new_payment_rate
                )}</td>
                <td>${formatNumber(data.deliveryData.total.paid_costs)}</td>
                <td>${formatROI(data.deliveryData.total.day_1_ROI)}</td>
              </tr>
            </tbody>
          </table>
        </div>

        <div class="section">
          <h2 class="section-title">3. 性能数据</h2>
          <table class="data-table">
            <colgroup>
              <col style="width:120px"></col>
            </colgroup>
            <tr>
              <th>日期</th>
              <th>指标</th>
              <th>加载时长ms</th>
              <th>转化率</th>
            </tr>
            <tr>
              <td rowspan="2">${getDate(2)}</td>
              <td>近7日日均</td>
              <td>${formatNumber(
                data.performanceData.seven_days.load_time
              )}</td>
              <td>${formatPercent(
                data.performanceData.seven_days.conversion_rate
              )}</td>
            </tr>
            <tr>
              <td>环比</td>
              <td>${formatChange(data.performanceData.chain.load_time)}</td>
              <td>${formatChange(
                data.performanceData.chain.conversion_rate
              )}</td>
            </tr>
          </table>
        </div>

        <div class="section">
          <h2 class="section-title">4. 留存数据</h2>
          <table class="data-table">
            <colgroup>
              <col style="width:120px"></col>
            </colgroup>
            <tr>
              <th>指标</th>
              <th>最新数据</th>
              <th>近7日日均</th>
              <th>环比</th>
              <th>上周同期</th>
              <th>同比</th>
            </tr>
            <tr>
              <td>注册次留</td>
              <td>${formatPercent(data.retainData.register.new)}</td>
              <td>${formatPercent(data.retainData.register.seven_days)}</td>
              <td>${formatChange(data.retainData.register.chain)}</td>
              <td>${formatPercent(data.retainData.register.last_week)}</td>
              <td>${formatChange(data.retainData.register.year_on_year)}</td>
            </tr>
            <tr>
              <td>付费次留</td>
              <td>${formatPercent(data.retainData.pay.new)}</td>
              <td>${formatPercent(data.retainData.pay.seven_days)}</td>
              <td>${formatChange(data.retainData.pay.chain)}</td>
              <td>${formatPercent(data.retainData.pay.last_week)}</td>
              <td>${formatChange(data.retainData.pay.year_on_year)}</td>
            </tr>
          </table>
        </div>

        <div class="section">
          <h2 class="section-title">5. 详细数据-收入</h2>
          <div style="overflow-x: auto">
            <table class="data-table" style="min-width:1800px">
              <colgroup>
                <col style="width:100px"></col>
                <col span=9 style="width:90px"></col>
                <col style="width:100px"></col>
                <col style="width:120px"></col>
                <col style="width:90px"></col>
              </colgroup>
              <tr>
                <th rowspan="2">日期</th>
                <th colspan="4">收入</th>
                <th rowspan="2">消耗</th>
                <th rowspan="2">新增</th>
                <th rowspan="2">DAU</th>
                <th rowspan="2">注册成本</th>
                <th rowspan="2">付费成本</th>
                <th rowspan="2">首日付费率</th>
                <th rowspan="2">新增付费Arpu</th>
                <th rowspan="2">活跃Arpu</th>
                <th colspan="7">内购ROI</th>
              </tr>
              <tr>
                <th>总收入</th>
                <th>内购收入</th>
                <th>广告收入</th>
                <th>广告金</th>
                <th>首日</th>
                <th>3日</th>
                <th>7日</th>
                <th>14日</th>
                <th>30日</th>
                <th>60日</th>
                <th>90日</th>
              </tr>
              ${data.revenueDataDetail
                .map(
                  (item) => `
                <tr>
                  <td>${item.date}</td>
                  <td>${formatNumber(item.total_revenue)}</td>
                  <td>-</td>
                  <td>${formatNumber(item.ad_revenue)}</td>
                  <td>${formatNumber(item.ad_money)}</td>
                  <td>${formatNumber(item.deplete)}</td>
                  <td>${formatNumber(item.new)}</td>
                  <td>${formatNumber(item.dau)}</td>
                  <td>${formatNumber(item.enrollment_costs)}</td>
                  <td>${formatNumber(item.paid_costs)}</td>
                  <td>${formatPercent(item.new_payment_rate)}</td>
                  <td>${formatNumber(item.pay_arpu)}</td>
                  <td>${formatNumber(item.active_arpu)}</td>
                  <td>${formatROI(item.day_1_ROI)}</td>
                  <td>${formatROI(item.day_3_ROI)}</td>
                  <td>${formatROI(item.day_7_ROI)}</td>
                  <td>${formatROI(item.day_14_ROI)}</td>
                  <td>${formatROI(item.day_30_ROI)}</td>
                  <td>${formatROI(item.day_60_ROI)}</td>
                  <td>${formatROI(item.day_90_ROI)}</td>
                </tr>
              `
                )
                .join("")}
            </table>
          </div> 
        </div>

        <div class="section">
          <h2 class="section-title">6. 详细数据-留存</h2>
          <div style="overflow-x: auto">
            <table class="data-table" style="min-width: 1600px">
              <colgroup>
                <col style="width:100px"></col>
                <col style="width:80px"></col>
                <col span=7 style="width:80px"></col>
                <col span=2 style="width:110px"></col>
              </colgroup>
              <tr>
                <th rowspan="2">日期</th>
                <th rowspan="2">新增</th>
                <th colspan="7">留存</th>
                <th rowspan="2">首日付费率</th>
                <th rowspan="2">首日付费人数</th>
                <th colspan="7">付费留存</th>
              </tr>
              <tr>
                <th>次留</th>
                <th>3留</th>
                <th>7留</th>
                <th>14留</th>
                <th>30留</th>
                <th>60留</th>
                <th>90留</th>
                <th>次留</th>
                <th>3留</th>
                <th>7留</th>
                <th>14留</th>
                <th>30留</th>
                <th>60留</th>
                <th>90留</th>
              </tr>
              ${data.retainDataDetail
                .map(
                  (item) => `
                <tr>
                  <td>${item.date}</td>
                  <td>${formatNumber(item.new)}</td>
                  <td>${formatPercent(item.ret_1)}</td>
                  <td>${formatPercent(item.ret_3)}</td>
                  <td>${formatPercent(item.ret_7)}</td>
                  <td>${formatPercent(item.ret_14)}</td>
                  <td>${formatPercent(item.ret_30)}</td>
                  <td>${formatPercent(item.ret_60)}</td>
                  <td>${formatPercent(item.ret_90)}</td>
                  <td>${formatPercent(item.first_pay_rate)}</td>
                  <td>${formatNumber(item.first_pay_num)}</td>
                  <td>${formatPercent(item.pay_1)}</td>
                  <td>${formatPercent(item.pay_3)}</td>
                  <td>${formatPercent(item.pay_7)}</td>
                  <td>${formatPercent(item.pay_14)}</td>
                  <td>${formatPercent(item.pay_30)}</td>
                  <td>${formatPercent(item.pay_60)}</td>
                  <td>${formatPercent(item.pay_90)}</td>
                </tr>
              `
                )
                .join("")}
            </table>
          </div>
        </div>
      </div>
    </div>
  </body>
  </html>
  `;
};

exports.getDate = getDate;
exports.generatoReport = generatoReport;

step3 发送邮件

(正式应该是由后端发送,现在主要是前端在测试)

我这里选择借助Node.js来实现将页面内容发送到用户邮箱的功能。实现步骤

  1. 前端部分。 report.js

  2. Node.js后端部分

首先安装所需依赖: npm install express body-parser nodemailer cors 。运行过程中如果出现报错,要注意node版本

然后创建服务器代码:

const express = require("express");
const bodyParser = require("body-parser");
const nodemailer = require("nodemailer");
const cors = require("cors"); // 引入cors模块,解决跨域
const app = express();

// 引入前端模板页面
const { generatoReport, getDate } = require("./generate-report"); 

// 使用cors中间件
app.use(cors()); 

app.use(bodyParser.json());

// 配置邮件服务 (这里以qq为例)
const transporter = nodemailer.createTransport({
  host: "smtp.qq.com", // QQ邮箱的SMTP服务器地址
  port: 465, // QQ邮箱的SSL端口(加密方式)
  secure: true, // 使用SSL加密
  auth: {
    user: "xxxxxxx@qq.com", // 你的QQ邮箱
    pass: "xxxxxxx", // 不是QQ密码,是SMTP授权码
  },
});

app.post("/api/send-email", async (req, res) => {
  const html = generatoReport();
  try {
    // 发送邮件
    await transporter.sendMail({
      from: "xxxxxxx@qq.com",
      to: "xxxxxx",
      subject: `【${getDate(1)}】产品运营数据分析报表`,
      html: html,
      text: "您的邮箱客户端不支持HTML邮件,请使用支持HTML的邮件客户端查看。",
    });
    res.json({ success: true });
    return;
  } catch (error) {
    console.error("邮件发送错误:", error);
    res.status(500).json({
      success: false,
      message: "邮件发送失败: " + error.message,
    });
  }
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

最终实现的效果如下:

image.png