- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
100 行
3.2 KiB
JavaScript
100 行
3.2 KiB
JavaScript
/**
|
||
* POST /api/contact
|
||
* 联系表单提交接口(通用留言)
|
||
*
|
||
* 请求体:
|
||
* {
|
||
* name: string // 姓名(必填)
|
||
* phone: string // 手机号(必填)
|
||
* subject?: string // 咨询主题
|
||
* message: string // 留言内容(必填)
|
||
* }
|
||
*/
|
||
|
||
import { checkRateLimit } from '../utils/rateLimit'
|
||
import { notifyWecom } from '../utils/notify'
|
||
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(`contact:${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 = '请填写正确的手机号码'
|
||
}
|
||
|
||
if (!body?.message?.trim() || body.message.trim().length < 5) {
|
||
errors.message = '请填写留言内容(至少5个字)'
|
||
}
|
||
|
||
if (body?.message?.trim().length > 1000) {
|
||
errors.message = '留言内容不超过1000字'
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
throw createError({
|
||
statusCode: 422,
|
||
data: { errors },
|
||
message: '提交内容有误,请检查后重试',
|
||
})
|
||
}
|
||
|
||
// ── 构建表单数据 ────────────────────────────────────────
|
||
const formData = {
|
||
name: body.name.trim(),
|
||
phone: body.phone.trim(),
|
||
subject: body.subject?.trim() ?? '',
|
||
message: body.message.trim(),
|
||
source: 'contact-form',
|
||
}
|
||
|
||
// ── 写入数据库 ────────────────────────────────────────
|
||
const db = useDB()
|
||
const [record] = db.insert(schema.submissions)
|
||
.values({
|
||
type: 'contact',
|
||
data: JSON.stringify(formData),
|
||
ip,
|
||
})
|
||
.returning()
|
||
.all()
|
||
|
||
// ── 飞书通知 ──────────────────────────────────────────
|
||
await notifyWecom('📩 新联系留言 - 呼籁旅行', {
|
||
'编号': String(record.id),
|
||
'姓名': formData.name,
|
||
'手机': formData.phone,
|
||
'主题': formData.subject || '(未填)',
|
||
'留言': formData.message,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
id: record.id,
|
||
message: '留言已收到,我们会尽快与您联系',
|
||
}
|
||
})
|