文件
OmniCard/backend/test/modules/crm/radar-map.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

263 行
9.1 KiB
TypeScript

import { CrmTrackEntity } from '../../../src/modules/crm/entity/track';
import { CrmRadarService } from '../../../src/modules/crm/service/radar';
import { AdminCrmRadarController } from '../../../src/modules/crm/controller/admin/radar';
describe('Point 09: 全球访客雷达热力分布与大屏服务 (radar-map.test.ts)', () => {
let radarService: CrmRadarService;
let trackList: CrmTrackEntity[];
beforeEach(() => {
trackList = [];
// Mock TypeORM Repository
const mockRepo: any = {
find: jest.fn().mockImplementation(async (options?: any) => {
let list = [...trackList];
if (options?.where) {
const where = options.where;
if (where.cardId !== undefined && where.cardId !== null) {
list = list.filter((item) => Number(item.cardId) === Number(where.cardId));
}
if (where.createTime && typeof where.createTime === 'object') {
// 支持 TypeORM MoreThanOrEqual / Between 等操作符过滤
if (where.createTime._type === 'moreThanOrEqual' || where.createTime._value) {
const minTime = where.createTime._value || where.createTime;
list = list.filter((item) => new Date(item.createTime) >= new Date(minTime));
}
}
}
return list;
}),
createQueryBuilder: jest.fn().mockReturnValue(null),
};
radarService = new CrmRadarService();
radarService.crmTrackEntity = mockRepo;
});
describe('1. 国家分布统计 (getCountryDistribution)', () => {
it('空数据时应安全兜底返回空数组 []', async () => {
const result = await radarService.getCountryDistribution();
expect(result).toEqual([]);
});
it('应准确按国家聚合访客数、计算占比与平均停留时长,并按人次降序排列', async () => {
trackList = [
{
id: 1,
cardId: 10,
country: 'United States',
stayDurationMs: 30000,
createTime: new Date(),
} as CrmTrackEntity,
{
id: 2,
cardId: 10,
country: 'United States',
stayDurationMs: 10000,
createTime: new Date(),
} as CrmTrackEntity,
{
id: 3,
cardId: 10,
country: 'Singapore',
stayDurationMs: 20000,
createTime: new Date(),
} as CrmTrackEntity,
{
id: 4,
cardId: 10,
country: 'Germany',
stayDurationMs: 40000,
createTime: new Date(),
} as CrmTrackEntity,
];
const result = await radarService.getCountryDistribution();
expect(result.length).toBe(3);
// US 有 2 次访问 (50%), Singapore 1 次 (25%), Germany 1 次 (25%)
expect(result[0].country).toBe('United States');
expect(result[0].visitorCount).toBe(2);
expect(result[0].percentage).toBe(50);
expect(result[0].avgStayDurationMs).toBe(20000);
expect(result[1].visitorCount).toBe(1);
expect(result[1].percentage).toBe(25);
});
it('当国家字段为空或缺失时,应统一兜底为 Unknown', async () => {
trackList = [
{
id: 1,
cardId: 10,
country: '',
stayDurationMs: 5000,
createTime: new Date(),
} as CrmTrackEntity,
{
id: 2,
cardId: 10,
country: undefined,
stayDurationMs: 15000,
createTime: new Date(),
} as CrmTrackEntity,
];
const result = await radarService.getCountryDistribution();
expect(result.length).toBe(1);
expect(result[0].country).toBe('Unknown');
expect(result[0].visitorCount).toBe(2);
expect(result[0].percentage).toBe(100);
expect(result[0].avgStayDurationMs).toBe(10000);
});
it('应支持根据指定 cardId 过滤数据', async () => {
trackList = [
{
id: 1,
cardId: 10,
country: 'Japan',
stayDurationMs: 10000,
createTime: new Date(),
} as CrmTrackEntity,
{
id: 2,
cardId: 20,
country: 'Japan',
stayDurationMs: 20000,
createTime: new Date(),
} as CrmTrackEntity,
];
const result = await radarService.getCountryDistribution(10);
expect(result.length).toBe(1);
expect(result[0].visitorCount).toBe(1);
expect(result[0].country).toBe('Japan');
});
});
describe('2. 停留时长时段分布 (getHourlyStayStats)', () => {
it('返回结果必须严格包含 0 至 23 共 24 个时段,空时段以 0 填充兜底', async () => {
const stats = await radarService.getHourlyStayStats(7);
expect(stats.length).toBe(24);
for (let h = 0; h < 24; h++) {
expect(stats[h].hour).toBe(h);
expect(stats[h].visitorCount).toBe(0);
expect(stats[h].avgStayDurationMs).toBe(0);
}
});
it('应能正确将足迹按创建时间的小时进行归类与停留时长均值统计', async () => {
const now = new Date();
const dateHour14 = new Date(now);
dateHour14.setHours(14, 30, 0, 0);
const dateHour14_2 = new Date(now);
dateHour14_2.setHours(14, 45, 0, 0);
trackList = [
{
id: 1,
cardId: 10,
stayDurationMs: 10000,
createTime: dateHour14,
} as CrmTrackEntity,
{
id: 2,
cardId: 10,
stayDurationMs: 30000,
createTime: dateHour14_2,
} as CrmTrackEntity,
];
const stats = await radarService.getHourlyStayStats(7);
expect(stats.length).toBe(24);
expect(stats[14].visitorCount).toBe(2);
expect(stats[14].avgStayDurationMs).toBe(20000);
expect(stats[0].visitorCount).toBe(0);
});
});
describe('3. 终端渠道分析 (getDeviceStats)', () => {
it('空数据时应安全返回包含基础渠道为 0 的统计', async () => {
const result = await radarService.getDeviceStats();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThanOrEqual(3);
expect(result.every((item) => item.count === 0 && item.percentage === 0)).toBe(true);
});
it('应准确按 touchType (nfc, qr, link) 计算数量与占比', async () => {
trackList = [
{ id: 1, touchType: 'nfc', createTime: new Date() } as CrmTrackEntity,
{ id: 2, touchType: 'nfc', createTime: new Date() } as CrmTrackEntity,
{ id: 3, touchType: 'qr', createTime: new Date() } as CrmTrackEntity,
{ id: 4, touchType: 'link', createTime: new Date() } as CrmTrackEntity,
];
const result = await radarService.getDeviceStats();
const nfcItem = result.find((item) => item.type === 'nfc');
const qrItem = result.find((item) => item.type === 'qr');
const linkItem = result.find((item) => item.type === 'link');
expect(nfcItem?.count).toBe(2);
expect(nfcItem?.percentage).toBe(50);
expect(qrItem?.count).toBe(1);
expect(qrItem?.percentage).toBe(25);
expect(linkItem?.count).toBe(1);
expect(linkItem?.percentage).toBe(25);
});
});
describe('4. 控制器接口契约 (AdminCrmRadarController)', () => {
let controller: AdminCrmRadarController;
let mockService: any;
beforeEach(() => {
mockService = {
getCountryDistribution: jest.fn().mockResolvedValue([
{ country: 'Singapore', visitorCount: 10, percentage: 100, avgStayDurationMs: 15000 },
]),
getHourlyStayStats: jest.fn().mockResolvedValue([]),
getDeviceStats: jest.fn().mockResolvedValue([]),
getAnalytics: jest.fn().mockResolvedValue({
countryDistribution: [
{ country: 'Singapore', visitorCount: 10, percentage: 100, avgStayDurationMs: 15000 },
],
hourlyStats: [],
deviceStats: [],
summary: {
totalVisitors: 10,
avgStayDurationMs: 15000,
topCountry: 'Singapore',
countriesCount: 1,
},
}),
};
controller = new AdminCrmRadarController();
(controller as any).crmRadarService = mockService;
(controller as any).ok = (data: any) => ({ code: 1000, message: 'success', data });
(controller as any).fail = (msg: string, code = 1001) => ({ code, message: msg });
});
it('GET /admin/crm/radar/distribution 应返回标准 1000 成功响应与国家分布列表', async () => {
const res: any = await controller.distribution({ cardId: 10 });
expect(res.code).toBe(1000);
expect(res.data[0].country).toBe('Singapore');
expect(mockService.getCountryDistribution).toHaveBeenCalledWith(10);
});
it('GET/POST /admin/crm/radar/analytics 应聚合返回大屏综合数据与 KPI 汇总', async () => {
const res: any = await controller.analytics({ cardId: 10, days: 7 });
expect(res.code).toBe(1000);
expect(res.data.countryDistribution).toBeDefined();
expect(res.data.hourlyStats).toBeDefined();
expect(res.data.deviceStats).toBeDefined();
expect(res.data.summary).toBeDefined();
expect(res.data.summary.totalVisitors).toBe(10);
expect(res.data.summary.topCountry).toBe('Singapore');
});
});
});