feat: 实现联系表单提交与邮件通知功能

添加联系表单提交接口和邮件通知功能,支持从环境变量读取 SMTP 配置
重构 SEO 配置到 site.json,新增 robots.txt 和 sitemap.xml 生成
更新公司电话并添加 PM2 生产运行配置
This commit is contained in:
JanYork
2026-03-18 13:20:40 +08:00
parent 59e80bf938
commit e2850586a9
20 changed files with 872 additions and 28 deletions

116
lib/contact-mailer.ts Normal file
View File

@@ -0,0 +1,116 @@
import nodemailer from "nodemailer"
import type { ContactFormData } from "@/lib/contact-form"
import type { ContactMailerConfig } from "@/lib/contact-mailer-config"
type ContactNotificationMeta = {
submittedAt: string
sourceUrl: string
ipAddress: string
userAgent: string
}
function escapeHtml(value: string) {
return value
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#39;")
}
function formatOptionalField(value: string) {
return value || "未填写"
}
export async function sendContactNotification({
config,
payload,
meta,
}: {
config: ContactMailerConfig
payload: ContactFormData
meta: ContactNotificationMeta
}) {
const transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
auth: {
user: config.smtp.auth.user,
pass: config.smtp.auth.pass,
},
})
const subject = `${config.notification.subjectPrefix} - ${payload.company} / ${payload.name}`
const text = [
"官网获取初步方案表单有新提交。",
"",
`姓名:${payload.name}`,
`公司 / 品牌:${payload.company}`,
`电话:${payload.phone}`,
`门店规模:${formatOptionalField(payload.scale)}`,
`需求描述:${formatOptionalField(payload.message)}`,
"",
`提交时间:${meta.submittedAt}`,
`来源页面:${meta.sourceUrl}`,
`来源 IP${meta.ipAddress}`,
`User-Agent${meta.userAgent}`,
].join("\n")
const html = `
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.7;">
<h2 style="margin: 0 0 16px;">官网获取初步方案表单有新提交</h2>
<table style="border-collapse: collapse; width: 100%; max-width: 720px;">
<tbody>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; width: 160px; background: #f9fafb;">姓名</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(payload.name)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">公司 / 品牌</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(payload.company)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">电话</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(payload.phone)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">门店规模</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(formatOptionalField(payload.scale))}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">需求描述</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; white-space: pre-wrap;">${escapeHtml(formatOptionalField(payload.message))}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">提交时间</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(meta.submittedAt)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">来源页面</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(meta.sourceUrl)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">来源 IP</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(meta.ipAddress)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb; background: #f9fafb;">User-Agent</td>
<td style="padding: 8px 12px; border: 1px solid #e5e7eb;">${escapeHtml(meta.userAgent)}</td>
</tr>
</tbody>
</table>
</div>
`
await transporter.sendMail({
from: {
name: config.smtp.from.name,
address: config.smtp.from.email,
},
to: config.notification.to,
subject,
text,
html,
})
}