feat: 定制需求提交时发送邮件通知
- 新增 nodemailer 依赖,通过腾讯企业邮 SMTP 发送邮件 - 新建 server/utils/email.js 邮件发送工具 - 定制表单提交后企业微信和邮件通知并行发送 - 不配置 SMTP 环境变量时静默跳过,不影响原有流程 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -20,6 +20,7 @@
|
||||
|
||||
import { checkRateLimit } from '../utils/rateLimit'
|
||||
import { notifyWecom } from '../utils/notify'
|
||||
import { notifyEmail } from '../utils/email'
|
||||
import { useDB, schema } from '../utils/db.js'
|
||||
|
||||
// 手机号正则
|
||||
@@ -93,8 +94,8 @@ export default defineEventHandler(async (event) => {
|
||||
.returning()
|
||||
.all()
|
||||
|
||||
// ── 飞书通知(配置后生效)────────────────────────────
|
||||
await notifyWecom('🌿 新定制需求 - 呼籁旅行', {
|
||||
// ── 通知(配置后生效)────────────────────────────────
|
||||
const notifyFields = {
|
||||
'编号': String(record.id),
|
||||
'姓名': formData.name,
|
||||
'手机': formData.phone,
|
||||
@@ -105,7 +106,12 @@ export default defineEventHandler(async (event) => {
|
||||
'特殊需求': formData.activities.length ? formData.activities.join('、') : '无',
|
||||
'预算': formData.budget || '未选择',
|
||||
'补充说明': formData.remarks || '无',
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
notifyWecom('🌿 新定制需求 - 呼籁旅行', notifyFields),
|
||||
notifyEmail('🌿 新定制需求 - 呼籁旅行', notifyFields),
|
||||
])
|
||||
|
||||
// ── 返回 ──────────────────────────────────────────────
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 邮件通知工具
|
||||
* 通过 SMTP 发送邮件通知(腾讯企业邮)
|
||||
*
|
||||
* 环境变量:
|
||||
* SMTP_HOST - SMTP 服务器地址(默认 smtp.exmail.qq.com)
|
||||
* SMTP_PORT - SMTP 端口(默认 465)
|
||||
* SMTP_USER - 发件邮箱账号
|
||||
* SMTP_PASS - 发件邮箱密码 / 授权码
|
||||
* NOTIFY_EMAIL - 收件邮箱(默认 lt@1814.love)
|
||||
*
|
||||
* 不配置 SMTP_USER / SMTP_PASS 则静默跳过
|
||||
*/
|
||||
|
||||
import { createTransport } from 'nodemailer'
|
||||
|
||||
let transporter = null
|
||||
|
||||
function getTransporter() {
|
||||
if (transporter) return transporter
|
||||
|
||||
const user = process.env.SMTP_USER
|
||||
const pass = process.env.SMTP_PASS
|
||||
if (!user || !pass) return null
|
||||
|
||||
transporter = createTransport({
|
||||
host: process.env.SMTP_HOST || 'smtp.exmail.qq.com',
|
||||
port: Number(process.env.SMTP_PORT) || 465,
|
||||
secure: true,
|
||||
auth: { user, pass },
|
||||
})
|
||||
|
||||
return transporter
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件通知
|
||||
* @param {string} subject - 邮件主题
|
||||
* @param {Record<string, string>} fields - { 字段名: 字段值 }
|
||||
*/
|
||||
export async function notifyEmail(subject, fields) {
|
||||
const t = getTransporter()
|
||||
if (!t) return
|
||||
|
||||
const rows = Object.entries(fields)
|
||||
.map(([k, v]) => `<tr><td style="padding:8px 12px;border:1px solid #e5e7eb;font-weight:600;white-space:nowrap;background:#f9fafb;">${k}</td><td style="padding:8px 12px;border:1px solid #e5e7eb;">${v}</td></tr>`)
|
||||
.join('')
|
||||
|
||||
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||
|
||||
const html = `
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;">
|
||||
<h2 style="color:#3a7d44;border-bottom:2px solid #3a7d44;padding-bottom:8px;">${subject}</h2>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:14px;">${rows}</table>
|
||||
<p style="color:#9ca3af;font-size:12px;margin-top:16px;">提交时间:${now}</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
const to = process.env.NOTIFY_EMAIL || 'lt@1814.love'
|
||||
|
||||
try {
|
||||
await t.sendMail({
|
||||
from: `"呼籁旅行" <${process.env.SMTP_USER}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[email] 邮件发送失败:', err?.message)
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户