新增官网咨询企业微信通知

This commit is contained in:
Rei
2026-09-30 17:33:15 +08:00
parent bc8fc0dfce
commit f35ee24510
4 changed files with 135 additions and 2 deletions
+16 -2
View File
@@ -11,6 +11,7 @@ import PDFDocument from 'pdfkit';
import { initDB, getDB, UPLOAD_DIR } from './db.js';
import { signToken, verifyToken, authMiddleware } from './middleware/auth.js';
import { createContentIngestRouter } from './content-ingest.js';
import { sendContactLeadToWeCom } from './wecom-notifier.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
@@ -112,7 +113,7 @@ const requestBuckets = new Map();
function rateLimit({ windowMs = 60_000, max = 30 } = {}) {
return (req, res, next) => {
const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
const key = `${req.ip || forwarded || req.socket.remoteAddress || 'unknown'}:${req.path}`;
const key = `${forwarded || req.ip || req.socket.remoteAddress || 'unknown'}:${req.path}`;
const now = Date.now();
const old = requestBuckets.get(key);
if (!old || now - old.startedAt >= windowMs) requestBuckets.set(key, { startedAt: now, count: 1 });
@@ -124,18 +125,31 @@ function rateLimit({ windowMs = 60_000, max = 30 } = {}) {
};
}
const publicRateLimit = rateLimit({ windowMs: 60_000, max: 60 });
const contactLeadRateLimit = rateLimit({ windowMs: 60_000, max: 10 });
app.use('/api/diagnosis', publicRateLimit);
app.use('/api/internal/content/v1', createContentIngestRouter());
app.use('/api/contact/leads', contactLeadRateLimit);
app.post('/api/contact/leads', (req, res) => {
const { name, company = '', phone, need = '' } = req.body || {};
if (!name || !phone) return res.status(400).json({ code: 400, msg: '姓名和手机号必填' });
if (!/^1[3-9]\d{9}$/.test(String(phone))) return res.status(400).json({ code: 400, msg: '手机号格式错误' });
const id = genId('contact');
const createdAt = nowISO();
const lead = {
name: String(name).trim().slice(0, 80),
company: String(company).trim().slice(0, 160),
phone: String(phone),
need: String(need).trim().slice(0, 2000),
createdAt
};
db.run(`INSERT INTO contact_leads (id, name, company, phone, need, created_at) VALUES (?,?,?,?,?,?)`,
[id, String(name).trim().slice(0, 80), String(company).trim().slice(0, 160), String(phone), String(need).trim().slice(0, 2000), nowISO()], err => {
[id, lead.name, lead.company, lead.phone, lead.need, createdAt], err => {
if (err) return res.status(500).json({ code: 500, msg: '提交失败,请稍后再试' });
res.json({ code: 0, msg: '提交成功' });
sendContactLeadToWeCom(lead).catch(error => {
console.error('[企业微信通知失败]', error.message);
});
});
});
+59
View File
@@ -0,0 +1,59 @@
const DEFAULT_TIMEOUT_MS = 5000;
function cleanLine(value, maxLength) {
return String(value ?? '')
.replace(/[\r\n\t]+/g, ' ')
.replace(/\s{2,}/g, ' ')
.trim()
.slice(0, maxLength);
}
export function formatContactLeadMessage(lead, { siteUrl = 'https://www.ameiba.cn' } = {}) {
const submittedAt = new Date(lead.createdAt || Date.now()).toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false
});
return [
'【官网新咨询】',
'',
`姓名:${cleanLine(lead.name, 80) || '未填写'}`,
`企业:${cleanLine(lead.company, 160) || '未填写'}`,
`联系电话:${cleanLine(lead.phone, 30) || '未填写'}`,
`需求描述:${cleanLine(lead.need, 2000) || '未填写'}`,
`提交时间:${submittedAt}`,
`来源:${cleanLine(siteUrl, 200)}`
].join('\n');
}
export async function sendContactLeadToWeCom(lead, {
webhookUrl = process.env.WECOM_BOT_WEBHOOK,
siteUrl = process.env.PUBLIC_SITE_URL || 'https://www.ameiba.cn',
timeoutMs = Number(process.env.WECOM_BOT_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
fetchImpl = globalThis.fetch
} = {}) {
if (!webhookUrl) return { skipped: true };
if (typeof fetchImpl !== 'function') throw new Error('当前 Node.js 环境不支持 fetch');
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msgtype: 'text',
text: { content: formatContactLeadMessage(lead, { siteUrl }) }
}),
signal: controller.signal
});
const result = await response.json().catch(() => ({}));
if (!response.ok || result.errcode !== 0) {
throw new Error(`企业微信返回错误:${result.errmsg || response.status}`);
}
return { sent: true };
} finally {
clearTimeout(timer);
}
}