文件
OmniCard/backend/test/modules/crm/lead-score.test.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

347 行
14 KiB
TypeScript

import {
CrmLeadScoreService,
LeadIntentGrade,
LeadScoreBehaviorInput,
} from '../../../src/modules/crm/service/lead-score';
import { CrmLeadService } from '../../../src/modules/crm/service/lead';
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
import { RadarTrack, LeadEntity } from '../../../src/comm/types/omni';
import { Repository } from 'typeorm';
describe('Point 10: 线索智能意向度评分引擎 (lead-score.ts) 单元测试', () => {
let scoreService: CrmLeadScoreService;
const mockTrack: Partial<RadarTrack> = {
country: 'United Arab Emirates',
city: 'Dubai',
touchType: 'nfc',
stayDurationMs: 320000,
viewedBlocks: ['hero', 'case_study', 'product_demo', 'contact'],
};
const mockLead: Partial<LeadEntity> = {
name: 'Tariq Al-Mansoor',
company: 'Gulf Horizon Trading',
phone: '+971-50-9876543',
email: 'tariq@gulfhorizon.ae',
source: 'nfc',
notes: '对中东区域独家代理感兴趣',
};
beforeEach(() => {
scoreService = new CrmLeadScoreService();
});
// ==========================================
// 1. 停留时长加权规则测试 (>30s +15, >120s +30, >300s +40)
// ==========================================
describe('1. 停留时长加权规则', () => {
it('停留时长 <= 30秒时,得分应为 0', () => {
expect(scoreService.calculateScore({ stayDurationSeconds: 0 }).score).toBe(0);
expect(scoreService.calculateScore({ stayDurationSeconds: 15 }).score).toBe(0);
expect(scoreService.calculateScore({ stayDurationSeconds: 30 }).score).toBe(0);
});
it('停留时长 > 30秒且 <= 120秒时,得分应加 15', () => {
expect(scoreService.calculateScore({ stayDurationSeconds: 31 }).score).toBe(15);
expect(scoreService.calculateScore({ stayDurationSeconds: 60 }).score).toBe(15);
expect(scoreService.calculateScore({ stayDurationSeconds: 120 }).score).toBe(15);
});
it('停留时长 > 120秒且 <= 300秒时,得分应加 30', () => {
expect(scoreService.calculateScore({ stayDurationSeconds: 121 }).score).toBe(30);
expect(scoreService.calculateScore({ stayDurationSeconds: 200 }).score).toBe(30);
expect(scoreService.calculateScore({ stayDurationSeconds: 300 }).score).toBe(30);
});
it('停留时长 > 300秒时,得分应加 40', () => {
expect(scoreService.calculateScore({ stayDurationSeconds: 301 }).score).toBe(40);
expect(scoreService.calculateScore({ stayDurationSeconds: 600 }).score).toBe(40);
});
it('支持毫秒输入 (stayDurationMs) 并正确转换为秒级加权', () => {
// 25000ms = 25s <= 30s -> 0
expect(scoreService.calculateScore({ stayDurationMs: 25000 }).score).toBe(0);
// 45000ms = 45s > 30s -> 15
expect(scoreService.calculateScore({ stayDurationMs: 45000 }).score).toBe(15);
// 150000ms = 150s > 120s -> 30
expect(scoreService.calculateScore({ stayDurationMs: 150000 }).score).toBe(30);
// 360000ms = 360s > 300s -> 40
expect(scoreService.calculateScore({ stayDurationMs: 360000 }).score).toBe(40);
});
});
// ==========================================
// 2. 浏览模块深度测试 (每个查看模块 +5,上限 20)
// ==========================================
describe('2. 浏览模块深度加权规则', () => {
it('查看 0 个模块时,得分应为 0', () => {
expect(scoreService.calculateScore({ viewedBlockCount: 0 }).score).toBe(0);
expect(scoreService.calculateScore({ viewedBlocks: [] }).score).toBe(0);
});
it('查看 1 至 3 个模块时,按每个模块 +5 累加', () => {
expect(scoreService.calculateScore({ viewedBlockCount: 1 }).score).toBe(5);
expect(scoreService.calculateScore({ viewedBlockCount: 2 }).score).toBe(10);
expect(scoreService.calculateScore({ viewedBlockCount: 3 }).score).toBe(15);
});
it('查看 4 个及以上模块时,得分上限截断为 20', () => {
expect(scoreService.calculateScore({ viewedBlockCount: 4 }).score).toBe(20);
expect(scoreService.calculateScore({ viewedBlockCount: 5 }).score).toBe(20);
expect(scoreService.calculateScore({ viewedBlockCount: 10 }).score).toBe(20);
});
it('传入 viewedBlocks 数组时应自动去重并计算', () => {
const blocksWithDuplicates = ['hero', 'case', 'hero', 'social', 'case'];
// 去重后为 3 个不同模块: hero, case, social -> 15分
const res = scoreService.calculateScore({ viewedBlocks: blocksWithDuplicates });
expect(res.score).toBe(15);
expect(res.breakdown.moduleScore).toBe(15);
});
});
// ==========================================
// 3. 高价值互动加权测试 (WhatsApp +25, vCard +20, 留资 +35)
// ==========================================
describe('3. 高价值互动加权规则', () => {
it('点击 WhatsApp 加 25 分', () => {
const res = scoreService.calculateScore({ clickedWhatsApp: true });
expect(res.score).toBe(25);
expect(res.breakdown.interactionScore).toBe(25);
});
it('下载 vCard 加 20 分', () => {
const res = scoreService.calculateScore({ downloadedVCard: true });
expect(res.score).toBe(20);
expect(res.breakdown.interactionScore).toBe(20);
});
it('主动留资加 35 分', () => {
const res = scoreService.calculateScore({ submittedLead: true });
expect(res.score).toBe(35);
expect(res.breakdown.interactionScore).toBe(35);
});
it('多个高价值互动可叠加加分', () => {
// WhatsApp (25) + vCard (20) = 45
const res = scoreService.calculateScore({
clickedWhatsApp: true,
downloadedVCard: true,
});
expect(res.score).toBe(45);
expect(res.breakdown.interactionScore).toBe(45);
});
});
// ==========================================
// 4. 综合加权、边界与异常防御测试
// ==========================================
describe('4. 综合加权与边界防御保护 (0~100 截断、负数与异常)', () => {
it('无任何行为时,应得 0 分并判定为 Cold', () => {
const res = scoreService.calculateScore({});
expect(res.score).toBe(0);
expect(res.level).toBe('Cold');
expect(res.breakdown).toEqual({
stayScore: 0,
moduleScore: 0,
interactionScore: 0,
});
});
it('各维度组合加权正确', () => {
// 停留 150s (+30) + 模块 2个 (+10) + WhatsApp (+25) = 65
const res = scoreService.calculateScore({
stayDurationSeconds: 150,
viewedBlockCount: 2,
clickedWhatsApp: true,
});
expect(res.score).toBe(65);
expect(res.level).toBe('Warm');
expect(res.breakdown).toEqual({
stayScore: 30,
moduleScore: 10,
interactionScore: 25,
});
});
it('高维度满额且叠加所有互动时,总分严格截断在 100 分上限', () => {
// 停留 >300s (40) + 模块 >=4 (20) + WhatsApp (25) + vCard (20) + 留资 (35) = 140
const res = scoreService.calculateScore({
stayDurationSeconds: 350,
viewedBlockCount: 5,
clickedWhatsApp: true,
downloadedVCard: true,
submittedLead: true,
});
expect(res.score).toBe(100);
expect(res.level).toBe('Hot');
});
it('负数与非法值输入时具备防御性保护,最小分为 0', () => {
const res = scoreService.calculateScore({
stayDurationSeconds: -50,
stayDurationMs: -10000,
viewedBlockCount: -5,
});
expect(res.score).toBe(0);
expect(res.level).toBe('Cold');
expect(res.breakdown.stayScore).toBe(0);
expect(res.breakdown.moduleScore).toBe(0);
});
it('NaN 或异常参数输入时优雅兜底', () => {
const res = scoreService.calculateScore({
stayDurationSeconds: NaN,
viewedBlockCount: NaN,
});
expect(res.score).toBe(0);
expect(res.level).toBe('Cold');
});
});
// ==========================================
// 5. 分级判定测试 (>=75 Hot, 40~74 Warm, <40 Cold)
// ==========================================
describe('5. 意向度分级判定规范 (Hot / Warm / Cold)', () => {
it('分值 < 40 分级为 Cold', () => {
expect(scoreService.resolveIntentLevel(0)).toBe('Cold');
expect(scoreService.resolveIntentLevel(15)).toBe('Cold');
expect(scoreService.resolveIntentLevel(35)).toBe('Cold');
expect(scoreService.resolveIntentLevel(39)).toBe('Cold');
});
it('分值 40 ~ 74 分级为 Warm', () => {
expect(scoreService.resolveIntentLevel(40)).toBe('Warm');
expect(scoreService.resolveIntentLevel(50)).toBe('Warm');
expect(scoreService.resolveIntentLevel(65)).toBe('Warm');
expect(scoreService.resolveIntentLevel(74)).toBe('Warm');
});
it('分值 >= 75 分级为 Hot', () => {
expect(scoreService.resolveIntentLevel(75)).toBe('Hot');
expect(scoreService.resolveIntentLevel(80)).toBe('Hot');
expect(scoreService.resolveIntentLevel(100)).toBe('Hot');
});
});
// ==========================================
// 6. 诊断报告与破冰首语生成测试 (generateDiagnosticReport)
// ==========================================
describe('6. 意向诊断报告与破冰首语生成引擎 (generateDiagnosticReport)', () => {
it('为 Hot 级别线索生成高意向跟进建议与破冰首语', () => {
const report = scoreService.generateDiagnosticReport(85, mockTrack, mockLead);
expect(report.score).toBe(85);
expect(report.level).toBe('Hot');
expect(report.suggestion).toBeDefined();
expect(report.suggestion.length).toBeGreaterThan(10);
expect(report.icebreaker).toBeDefined();
expect(report.icebreaker).toContain('Tariq');
expect(report.icebreaker).toContain('Gulf Horizon Trading');
expect(report.highlights.length).toBeGreaterThan(0);
});
it('为 Warm 级别线索生成培育建议与破冰首语', () => {
const warmLead: Partial<LeadEntity> = {
name: 'Sarah Chen',
company: 'Apex Solutions',
};
const report = scoreService.generateDiagnosticReport(55, mockTrack, warmLead);
expect(report.score).toBe(55);
expect(report.level).toBe('Warm');
expect(report.suggestion).toBeDefined();
expect(report.icebreaker).toContain('Sarah');
expect(report.icebreaker).toContain('Apex Solutions');
});
it('为 Cold 级别线索生成自动化触达建议与通用破冰首语', () => {
const report = scoreService.generateDiagnosticReport(20, null, null);
expect(report.score).toBe(20);
expect(report.level).toBe('Cold');
expect(report.suggestion).toBeDefined();
expect(report.icebreaker).toBeDefined();
});
it('支持直接传入 LeadScoreResult 对象并生成诊断报告', () => {
const scoreResult = scoreService.calculateScore({
stayDurationSeconds: 320,
viewedBlockCount: 4,
clickedWhatsApp: true,
}); // 40 + 20 + 25 = 85 -> Hot
const report = scoreService.generateDiagnosticReport(
scoreResult,
mockTrack,
mockLead
);
expect(report.score).toBe(85);
expect(report.level).toBe('Hot');
});
});
// ==========================================
// 7. CrmLeadService 集成智能评分引擎验证
// ==========================================
describe('7. CrmLeadService 评分引擎集成验证', () => {
it('CrmLeadService 拥有 leadScoreService 属性或集成方法', () => {
const leadService = new CrmLeadService();
expect(leadService).toBeDefined();
const scoreServiceInstance =
leadService.leadScoreService || new CrmLeadScoreService();
expect(scoreServiceInstance).toBeInstanceOf(CrmLeadScoreService);
});
it('CrmLeadService.calculateLeadScore 能够正确代理评分', () => {
const leadService = new CrmLeadService();
const result = leadService.calculateLeadScore({
stayDurationSeconds: 150,
viewedBlockCount: 2,
clickedWhatsApp: true,
});
expect(result.score).toBe(65);
expect(result.level).toBe('Warm');
expect(result.breakdown).toEqual({
stayScore: 30,
moduleScore: 10,
interactionScore: 25,
});
});
it('CrmLeadService.generateDiagnosticReport 能够正确生成报告', () => {
const leadService = new CrmLeadService();
const report = leadService.generateDiagnosticReport(80, mockTrack, mockLead);
expect(report.score).toBe(80);
expect(report.level).toBe('Hot');
expect(report.icebreaker).toContain('Tariq');
});
it('CrmLeadService.submitLead 携带互动特征时自动调用新引擎算分并判定 hot 状态', async () => {
const leadService = new CrmLeadService();
const mockSavedEntity: Partial<CrmLeadEntity> = {};
leadService.crmLeadEntity = {
save: jest.fn().mockImplementation(async (entity: CrmLeadEntity) => {
Object.assign(mockSavedEntity, entity);
return entity;
}),
} as unknown as Repository<CrmLeadEntity>;
const saved = await leadService.submitLead({
cardId: 202,
name: 'Alexander Weber',
email: 'alex@weber-machinery.de',
stayDurationMs: 310000, // >300s -> 40分
viewedBlocks: ['hero', 'case_1', 'video', 'contact'], // 4模块 -> 20分
clickedWhatsApp: true, // +25分
// 自动叠加留资 +35分 -> 40 + 20 + 25 + 35 = 120 -> 截断 100分
});
expect(saved.intentScore).toBe(100);
expect(saved.status).toBe('hot');
expect(leadService.crmLeadEntity.save).toHaveBeenCalled();
});
});
});