补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
133 行
5.0 KiB
TypeScript
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);
|
|
});
|
|
});
|