文件
OmniCard/backend/test/modules/crm/radar-aggregation.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

133 行
5.0 KiB
TypeScript

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);
});
});