feat: 定制需求提交时发送邮件通知
- 新增 nodemailer 依赖,通过腾讯企业邮 SMTP 发送邮件 - 新建 server/utils/email.js 邮件发送工具 - 定制表单提交后企业微信和邮件通知并行发送 - 不配置 SMTP 环境变量时静默跳过,不影响原有流程 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
父节点
0f591eaf5b
当前提交
32cd352132
@ -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
自动生成的
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
普通文件
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)
|
||||
}
|
||||
}
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户