diff --git a/package.json b/package.json index 4363927..1942a25 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 552dd97..a060c4c 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/server/api/customize.post.js b/server/api/customize.post.js index 6192dd6..7478f26 100644 --- a/server/api/customize.post.js +++ b/server/api/customize.post.js @@ -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 { diff --git a/server/utils/email.js b/server/utils/email.js new file mode 100644 index 0000000..49c290c --- /dev/null +++ b/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} fields - { 字段名: 字段值 } + */ +export async function notifyEmail(subject, fields) { + const t = getTransporter() + if (!t) return + + const rows = Object.entries(fields) + .map(([k, v]) => `${k}${v}`) + .join('') + + const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + + const html = ` +
+

${subject}

+ ${rows}
+

提交时间:${now}

+
+ ` + + 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) + } +}