- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
347 行
14 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|