feat: 定制需求提交时发送邮件通知

- 新增 nodemailer 依赖,通过腾讯企业邮 SMTP 发送邮件
- 新建 server/utils/email.js 邮件发送工具
- 定制表单提交后企业微信和邮件通知并行发送
- 不配置 SMTP 环境变量时静默跳过,不影响原有流程

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
Mimingguang 2026-03-25 09:50:59 +08:00
父节点 0f591eaf5b
当前提交 32cd352132
共有 4 个文件被更改,包括 90 次插入3 次删除

查看文件

@ -15,6 +15,7 @@
"drizzle-orm": "^0.45.1",
"less": "^4.6.4",
"naive-ui": "^2.44.1",
"nodemailer": "^8.0.3",
"nuxt": "^3.17.5"
},
"pnpm": {

9
pnpm-lock.yaml 自动生成的
查看文件

@ -23,6 +23,9 @@ importers:
naive-ui:
specifier: ^2.44.1
version: 2.44.1(vue@3.5.30(typescript@5.9.3))
nodemailer:
specifier: ^8.0.3
version: 8.0.3
nuxt:
specifier: ^3.17.5
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(ioredis@5.10.0)(less@4.6.4)(magicast@0.5.2)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
@ -2819,6 +2822,10 @@ packages:
node-releases@2.0.36:
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
nodemailer@8.0.3:
resolution: {integrity: sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==}
engines: {node: '>=6.0.0'}
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -6609,6 +6616,8 @@ snapshots:
node-releases@2.0.36: {}
nodemailer@8.0.3: {}
nopt@8.1.0:
dependencies:
abbrev: 3.0.1

查看文件

@ -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 {

71
server/utils/email.js 普通文件
查看文件

@ -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)
}
}