完善咨询表单与企业微信诉求推送
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
export const OTHER_CONTACT_NEED = '其他个性需求';
|
||||
|
||||
export const CONTACT_NEED_OPTIONS = [
|
||||
'IT数字化部署',
|
||||
'AI智能体部署',
|
||||
'经营体系建设',
|
||||
'项目融资需求',
|
||||
'实缴合规管理',
|
||||
'获客自动化部署',
|
||||
OTHER_CONTACT_NEED
|
||||
];
|
||||
|
||||
export class ContactLeadValidationError extends Error {}
|
||||
|
||||
function clean(value, maxLength) {
|
||||
return String(value ?? '').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
export function normalizeContactLeadInput(input = {}) {
|
||||
const name = clean(input.name, 80);
|
||||
const company = clean(input.company, 160);
|
||||
const phone = clean(input.phone, 20);
|
||||
const otherNeed = clean(input.otherNeed, 1000);
|
||||
const rawNeeds = Array.isArray(input.needs) ? input.needs : [];
|
||||
const needs = [...new Set(rawNeeds.map(item => clean(item, 40)).filter(Boolean))];
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new ContactLeadValidationError('请输入合法的11位中国大陆手机号');
|
||||
}
|
||||
if (needs.length === 0) {
|
||||
throw new ContactLeadValidationError('请至少选择一项核心诉求');
|
||||
}
|
||||
if (needs.some(item => !CONTACT_NEED_OPTIONS.includes(item))) {
|
||||
throw new ContactLeadValidationError('核心诉求包含无效选项');
|
||||
}
|
||||
if (needs.includes(OTHER_CONTACT_NEED) && !otherNeed) {
|
||||
throw new ContactLeadValidationError('请填写其他个性需求');
|
||||
}
|
||||
|
||||
const need = needs
|
||||
.map(item => item === OTHER_CONTACT_NEED ? `${OTHER_CONTACT_NEED}:${otherNeed}` : item)
|
||||
.join(';');
|
||||
|
||||
return { name, company, phone, needs, otherNeed, need };
|
||||
}
|
||||
+11
-10
@@ -12,6 +12,7 @@ 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';
|
||||
import { ContactLeadValidationError, normalizeContactLeadInput } from './contact-leads.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
@@ -131,18 +132,18 @@ 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: '手机号格式错误' });
|
||||
let lead;
|
||||
try {
|
||||
lead = normalizeContactLeadInput(req.body);
|
||||
} catch (error) {
|
||||
if (error instanceof ContactLeadValidationError) {
|
||||
return res.status(400).json({ code: 400, msg: error.message });
|
||||
}
|
||||
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
|
||||
};
|
||||
lead.createdAt = createdAt;
|
||||
db.run(`INSERT INTO contact_leads (id, name, company, phone, need, created_at) VALUES (?,?,?,?,?,?)`,
|
||||
[id, lead.name, lead.company, lead.phone, lead.need, createdAt], err => {
|
||||
if (err) return res.status(500).json({ code: 500, msg: '提交失败,请稍后再试' });
|
||||
|
||||
@@ -14,13 +14,19 @@ export function formatContactLeadMessage(lead, { siteUrl = 'https://www.ameiba.c
|
||||
hour12: false
|
||||
});
|
||||
|
||||
const needs = Array.isArray(lead.needs) && lead.needs.length > 0
|
||||
? lead.needs.map(item => `• ${cleanLine(item, 40)}`)
|
||||
: [`• ${cleanLine(lead.need, 2000) || '未填写'}`];
|
||||
if (lead.otherNeed) needs.push(` 说明:${cleanLine(lead.otherNeed, 1000)}`);
|
||||
|
||||
return [
|
||||
'【官网新咨询】',
|
||||
'',
|
||||
`姓名:${cleanLine(lead.name, 80) || '未填写'}`,
|
||||
`企业:${cleanLine(lead.company, 160) || '未填写'}`,
|
||||
`您的称呼:${cleanLine(lead.name, 80) || '未填写'}`,
|
||||
`联系电话:${cleanLine(lead.phone, 30) || '未填写'}`,
|
||||
`需求描述:${cleanLine(lead.need, 2000) || '未填写'}`,
|
||||
`企业名:${cleanLine(lead.company, 160) || '未填写'}`,
|
||||
'核心诉求:',
|
||||
...needs,
|
||||
`提交时间:${submittedAt}`,
|
||||
`来源:${cleanLine(siteUrl, 200)}`
|
||||
].join('\n');
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
ContactLeadValidationError,
|
||||
normalizeContactLeadInput
|
||||
} from '../src/contact-leads.js';
|
||||
|
||||
test('规范化有效咨询表单并允许称呼和企业名为空', () => {
|
||||
const result = normalizeContactLeadInput({
|
||||
phone: '13800138000',
|
||||
needs: ['IT数字化部署', 'AI智能体部署']
|
||||
});
|
||||
|
||||
assert.equal(result.name, '');
|
||||
assert.equal(result.company, '');
|
||||
assert.equal(result.phone, '13800138000');
|
||||
assert.equal(result.need, 'IT数字化部署;AI智能体部署');
|
||||
});
|
||||
|
||||
test('拒绝不合法的中国大陆手机号', () => {
|
||||
for (const phone of ['12800138000', '1380013800', '23800138000', '138001380000']) {
|
||||
assert.throws(
|
||||
() => normalizeContactLeadInput({ phone, needs: ['经营体系建设'] }),
|
||||
ContactLeadValidationError
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('核心诉求至少选择一项且只接受预设选项', () => {
|
||||
assert.throws(
|
||||
() => normalizeContactLeadInput({ phone: '13800138000', needs: [] }),
|
||||
/至少选择一项/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeContactLeadInput({ phone: '13800138000', needs: ['未知选项'] }),
|
||||
/无效选项/
|
||||
);
|
||||
});
|
||||
|
||||
test('选择其他个性需求时必须填写说明', () => {
|
||||
assert.throws(
|
||||
() => normalizeContactLeadInput({ phone: '13800138000', needs: ['其他个性需求'] }),
|
||||
/填写其他个性需求/
|
||||
);
|
||||
|
||||
const result = normalizeContactLeadInput({
|
||||
phone: '13800138000',
|
||||
needs: ['其他个性需求'],
|
||||
otherNeed: '希望先进行现场访谈'
|
||||
});
|
||||
assert.equal(result.need, '其他个性需求:希望先进行现场访谈');
|
||||
});
|
||||
@@ -7,15 +7,17 @@ test('咨询消息包含完整字段并清理换行', () => {
|
||||
name: '张三\n测试',
|
||||
company: '',
|
||||
phone: '13800138000',
|
||||
need: '希望\r\n了解企业进化方案',
|
||||
needs: ['IT数字化部署', '其他个性需求'],
|
||||
otherNeed: '希望\r\n了解企业进化方案',
|
||||
createdAt: '2026-09-30T07:30:00.000Z'
|
||||
});
|
||||
|
||||
assert.match(message, /【官网新咨询】/);
|
||||
assert.match(message, /姓名:张三 测试/);
|
||||
assert.match(message, /企业:未填写/);
|
||||
assert.match(message, /您的称呼:张三 测试/);
|
||||
assert.match(message, /企业名:未填写/);
|
||||
assert.match(message, /联系电话:13800138000/);
|
||||
assert.match(message, /需求描述:希望 了解企业进化方案/);
|
||||
assert.match(message, /核心诉求:\n• IT数字化部署\n• 其他个性需求/);
|
||||
assert.match(message, /说明:希望 了解企业进化方案/);
|
||||
assert.match(message, /提交时间:2026\/9\/30 15:30:00/);
|
||||
});
|
||||
|
||||
@@ -41,7 +43,7 @@ test('按企业微信文本消息格式发送', async () => {
|
||||
assert.equal(request.url, 'https://example.test/webhook');
|
||||
const body = JSON.parse(request.options.body);
|
||||
assert.equal(body.msgtype, 'text');
|
||||
assert.match(body.text.content, /姓名:李四/);
|
||||
assert.match(body.text.content, /您的称呼:李四/);
|
||||
});
|
||||
|
||||
test('企业微信返回错误时抛出异常', async () => {
|
||||
|
||||
Reference in New Issue
Block a user