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 = { country: 'United Arab Emirates', city: 'Dubai', touchType: 'nfc', stayDurationMs: 320000, viewedBlocks: ['hero', 'case_study', 'product_demo', 'contact'], }; const mockLead: Partial = { 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 = { 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 = {}; leadService.crmLeadEntity = { save: jest.fn().mockImplementation(async (entity: CrmLeadEntity) => { Object.assign(mockSavedEntity, entity); return entity; }), } as unknown as Repository; 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(); }); }); });