- 新增 nodemailer 依赖,通过腾讯企业邮 SMTP 发送邮件 - 新建 server/utils/email.js 邮件发送工具 - 定制表单提交后企业微信和邮件通知并行发送 - 不配置 SMTP 环境变量时静默跳过,不影响原有流程 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
123 行
4.4 KiB
JavaScript
123 行
4.4 KiB
JavaScript
/**
|
||
* POST /api/customize
|
||
* 定制表单提交接口
|
||
*
|
||
* 请求体:
|
||
* {
|
||
* name: string // 姓名(必填)
|
||
* phone: string // 手机号(必填)
|
||
* wechat?: string // 微信号(选填)
|
||
* adults: number // 成人数(必填)
|
||
* children: number // 儿童数
|
||
* childAges: number[] // 儿童年龄列表
|
||
* date?: string // 出行日期 YYYY-MM-DD
|
||
* duration?: string // 出行天数
|
||
* activities: string[]// 特殊需求标签
|
||
* budget?: string // 预算范围
|
||
* remarks?: string // 补充说明
|
||
* }
|
||
*/
|
||
|
||
import { checkRateLimit } from '../utils/rateLimit'
|
||
import { notifyWecom } from '../utils/notify'
|
||
import { notifyEmail } from '../utils/email'
|
||
import { useDB, schema } from '../utils/db.js'
|
||
|
||
// 手机号正则
|
||
const PHONE_RE = /^1[3-9]\d{9}$/
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
// ── 速率限制 ──────────────────────────────────────────
|
||
const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
|
||
const { allowed, remaining, resetAt } = checkRateLimit(`customize:${ip}`)
|
||
|
||
setResponseHeader(event, 'X-RateLimit-Remaining', String(remaining))
|
||
|
||
if (!allowed) {
|
||
const waitMin = Math.ceil((resetAt - Date.now()) / 60000)
|
||
throw createError({
|
||
statusCode: 429,
|
||
message: `提交过于频繁,请 ${waitMin} 分钟后再试`,
|
||
})
|
||
}
|
||
|
||
// ── 读取请求体 ─────────────────────────────────────────
|
||
const body = await readBody(event)
|
||
|
||
// ── 字段校验 ──────────────────────────────────────────
|
||
const errors = {}
|
||
|
||
if (!body?.name?.trim()) {
|
||
errors.name = '请填写您的姓名'
|
||
}
|
||
|
||
if (!body?.phone || !PHONE_RE.test(body.phone.trim())) {
|
||
errors.phone = '请填写正确的手机号码(11位大陆手机号)'
|
||
}
|
||
|
||
if (!Number.isInteger(body?.adults) || body.adults < 1 || body.adults > 20) {
|
||
errors.adults = '成人人数不合法'
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
throw createError({
|
||
statusCode: 422,
|
||
data: { errors },
|
||
message: '提交内容有误,请检查后重试',
|
||
})
|
||
}
|
||
|
||
// ── 构建表单数据 ────────────────────────────────────────
|
||
const formData = {
|
||
name: body.name.trim(),
|
||
phone: body.phone.trim(),
|
||
wechat: body.wechat?.trim() ?? '',
|
||
adults: body.adults,
|
||
children: body.children ?? 0,
|
||
childAges: Array.isArray(body.childAges) ? body.childAges : [],
|
||
date: body.date ?? '',
|
||
duration: body.duration ?? '',
|
||
activities: Array.isArray(body.activities) ? body.activities : [],
|
||
budget: body.budget ?? '',
|
||
remarks: body.remarks?.trim() ?? '',
|
||
source: 'customize-form',
|
||
}
|
||
|
||
// ── 写入数据库 ────────────────────────────────────────
|
||
const db = useDB()
|
||
const [record] = db.insert(schema.submissions)
|
||
.values({
|
||
type: 'customize',
|
||
data: JSON.stringify(formData),
|
||
ip,
|
||
})
|
||
.returning()
|
||
.all()
|
||
|
||
// ── 通知(配置后生效)────────────────────────────────
|
||
const notifyFields = {
|
||
'编号': String(record.id),
|
||
'姓名': formData.name,
|
||
'手机': formData.phone,
|
||
'微信': formData.wechat || '(同手机号)',
|
||
'出行人数': `成人 ${formData.adults} 人${formData.children > 0 ? `、儿童 ${formData.children} 人(${formData.childAges.join('、')}岁)` : ''}`,
|
||
'出行日期': formData.date || '未填写',
|
||
'天数': formData.duration || '未选择',
|
||
'特殊需求': formData.activities.length ? formData.activities.join('、') : '无',
|
||
'预算': formData.budget || '未选择',
|
||
'补充说明': formData.remarks || '无',
|
||
}
|
||
|
||
await Promise.all([
|
||
notifyWecom('🌿 新定制需求 - 呼籁旅行', notifyFields),
|
||
notifyEmail('🌿 新定制需求 - 呼籁旅行', notifyFields),
|
||
])
|
||
|
||
// ── 返回 ──────────────────────────────────────────────
|
||
return {
|
||
success: true,
|
||
id: record.id,
|
||
message: '需求已收到,定制师将在2小时内联系您',
|
||
}
|
||
})
|