import { CrmRadarService } from '../../../src/modules/crm/service/radar'; describe('性能与防 OOM: 雷达国家分布与时段统计原生 SQL 聚合机制', () => { let radarService: CrmRadarService; let mockQb: any; let mockTrackRepo: any; beforeEach(() => { mockQb = { select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(), groupBy: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), getRawMany: jest.fn().mockResolvedValue([ { country: 'United Arab Emirates', visitorCount: '30', avgStayDurationMs: '45000' }, { country: 'Germany', visitorCount: '20', avgStayDurationMs: '30000' }, { country: 'United States', visitorCount: '10', avgStayDurationMs: '20000' }, ]), }; mockTrackRepo = { createQueryBuilder: jest.fn().mockReturnValue(mockQb), find: jest.fn(), }; radarService = new CrmRadarService(); radarService.crmTrackEntity = mockTrackRepo; }); it('getCountryDistribution 必须使用 createQueryBuilder 原生 GROUP BY 聚合,严禁全表 find 加载至内存', async () => { const result = await radarService.getCountryDistribution(101); expect(mockTrackRepo.createQueryBuilder).toHaveBeenCalledWith('track'); expect(mockQb.select).toHaveBeenCalled(); expect(mockQb.groupBy).toHaveBeenCalled(); expect(mockQb.where).toHaveBeenCalledWith('track.card_id = :cardId', { cardId: 101 }); expect(mockTrackRepo.find).not.toHaveBeenCalled(); expect(result).toHaveLength(3); expect(result[0].country).toBe('United Arab Emirates'); expect(result[0].visitorCount).toBe(30); expect(result[0].percentage).toBe(50); // 30 / 60 * 100 expect(result[0].avgStayDurationMs).toBe(45000); }); }); describe('R25: 语言占比与设备占比必须来自足迹真实计数', () => { let radarService: CrmRadarService; let trackList: any[]; beforeEach(() => { trackList = []; radarService = new CrmRadarService(); radarService.crmTrackEntity = { find: jest.fn(async (options?: any) => { let list = [...trackList]; if (options?.where?.cardId !== undefined) { list = list.filter((t) => Number(t.cardId) === Number(options.where.cardId)); } return list; }), } as any; }); it('无足迹时语言占比与设备占比均返回空数组,不得填示例语言或零值渠道', async () => { const languages = await radarService.getLanguageStats(); const devices = await radarService.getDeviceStats(); expect(languages).toEqual([]); expect(devices).toEqual([]); expect(JSON.stringify(languages)).not.toMatch(/zh-CN|en-US|ar-SA|English|中文/i); expect(JSON.stringify(devices)).not.toMatch(/iPhone|Android|iOS|Samsung/i); }); it('足迹无 language/locale 字段时语言占比仍为空,不得用国家或固定比例冒充', async () => { trackList = [ { id: 1, cardId: 10, country: 'United States', touchType: 'nfc' }, { id: 2, cardId: 10, country: 'Germany', touchType: 'qr' }, ]; const languages = await radarService.getLanguageStats(10); expect(languages).toEqual([]); expect(languages.map((i) => i.percentage)).not.toEqual([40, 35, 25]); }); it('设备占比按 touchType 真实计数,仅返回有数据的类型与占比', async () => { trackList = [ { id: 1, cardId: 10, touchType: 'nfc' }, { id: 2, cardId: 10, touchType: 'nfc' }, { id: 3, cardId: 10, touchType: 'qr' }, { id: 4, cardId: 10, touchType: 'link' }, ]; const devices = await radarService.getDeviceStats(10); expect(devices).toHaveLength(3); const byType = Object.fromEntries(devices.map((d) => [d.type, d])); expect(byType.nfc.count).toBe(2); expect(byType.nfc.percentage).toBe(50); expect(byType.qr.count).toBe(1); expect(byType.qr.percentage).toBe(25); expect(byType.link.count).toBe(1); expect(byType.link.percentage).toBe(25); expect(devices.every((d) => d.count > 0)).toBe(true); }); it('仅出现部分渠道时不补齐未出现的 nfc/qr/link 零值行', async () => { trackList = [ { id: 1, cardId: 10, touchType: 'nfc' }, { id: 2, cardId: 10, touchType: 'nfc' }, ]; const devices = await radarService.getDeviceStats(10); expect(devices).toEqual([{ type: 'nfc', count: 2, percentage: 100 }]); }); it('若足迹带有 language 字段则按真实计数聚合语言占比', async () => { trackList = [ { id: 1, cardId: 10, language: 'en-US', touchType: 'link' }, { id: 2, cardId: 10, language: 'en-US', touchType: 'link' }, { id: 3, cardId: 10, language: 'ar-SA', touchType: 'qr' }, ]; const languages = await radarService.getLanguageStats(10); expect(languages).toHaveLength(2); const byLang = Object.fromEntries(languages.map((l) => [l.language, l])); expect(byLang['en-US'].count).toBe(2); expect(byLang['en-US'].percentage).toBeCloseTo(66.7, 0); expect(byLang['ar-SA'].count).toBe(1); expect(byLang['ar-SA'].percentage).toBeCloseTo(33.3, 0); }); });