feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -43,3 +43,90 @@ describe('性能与防 OOM: 雷达国家分布与时段统计原生 SQL 聚合
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户