import { AdminCrmRadarController } from '../../../src/modules/crm/controller/admin/radar'; import { buildWorkplaceOverview, CrmRadarService, } from '../../../src/modules/crm/service/radar'; import { CrmTrackEntity } from '../../../src/modules/crm/entity/track'; import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead'; function track(partial: Partial & { id: number; createTime: Date }): CrmTrackEntity { const item = new CrmTrackEntity(); item.id = partial.id; item.cardId = partial.cardId ?? 1; item.country = partial.country ?? 'AE'; item.city = partial.city ?? 'Dubai'; item.touchType = partial.touchType ?? 'link'; item.stayDurationMs = partial.stayDurationMs ?? 30000; item.viewedBlocks = partial.viewedBlocks ?? ['hero']; item.createTime = partial.createTime; return item; } function lead( partial: Partial & { id: number; createTime: Date } ): CrmLeadEntity { const item = new CrmLeadEntity(); item.id = partial.id; item.status = partial.status ?? 'new'; item.createTime = partial.createTime; return item; } describe('商务概览工作台聚合 (getWorkplaceOverview)', () => { const now = new Date('2026-09-21T15:00:00'); const today = new Date('2026-09-21T09:30:00'); const yesterday = new Date('2026-09-20T11:00:00'); const older = new Date('2026-09-18T08:00:00'); it('应按今日窗口统计触碰量、浏览量、新线索,并计算成交转化', () => { const result = buildWorkplaceOverview( [ track({ id: 1, touchType: 'nfc', createTime: today, country: 'AE', city: 'Dubai' }), track({ id: 2, touchType: 'qr', createTime: today, country: 'SG', city: 'Singapore' }), track({ id: 3, touchType: 'nfc', createTime: yesterday }), track({ id: 4, touchType: 'link', createTime: older }), ], [ lead({ id: 1, status: 'new', createTime: today }), lead({ id: 2, status: 'won', createTime: older }), lead({ id: 3, status: 'archived', createTime: older }), lead({ id: 4, status: 'contacted', createTime: yesterday }), ], now, 10 ); expect(result.kpis.tapCount).toBe(1); expect(result.kpis.viewCount).toBe(2); expect(result.kpis.newLeadCount).toBe(1); expect(result.kpis.conversionRate).toBe(33.3); expect(result.kpis.tapDelta).toBe(0); expect(result.kpis.viewDelta).toBe(1); expect(result.kpis.newLeadDelta).toBe(0); }); it('空数据时应返回零值 KPI 与空雷达动态', () => { const result = buildWorkplaceOverview([], [], now, 12); expect(result.kpis.tapCount).toBe(0); expect(result.kpis.viewCount).toBe(0); expect(result.kpis.newLeadCount).toBe(0); expect(result.kpis.conversionRate).toBe(0); expect(result.radarFeed).toEqual([]); }); it('雷达动态应按时间倒序截断,且不要求 cardId', () => { const result = buildWorkplaceOverview( [ track({ id: 1, createTime: yesterday, country: 'US', city: 'Austin' }), track({ id: 3, createTime: today, country: 'DE', city: 'Berlin', viewedBlocks: ['cases'] }), track({ id: 2, createTime: today, country: 'JP', city: 'Tokyo' }), ], [], now, 2 ); expect(result.radarFeed).toHaveLength(2); expect(result.radarFeed[0].id).toBe(3); expect(result.radarFeed[0].city).toBe('Berlin'); expect(result.radarFeed[1].id).toBe(2); }); it('归档线索不计入成交转化分母', () => { const result = buildWorkplaceOverview( [], [ lead({ id: 1, status: 'won', createTime: older }), lead({ id: 2, status: 'archived', createTime: older }), lead({ id: 3, status: 'archived', createTime: older }), ], now ); expect(result.kpis.conversionRate).toBe(100); }); it('服务应聚合足迹与线索仓库并返回工作台结果', async () => { const service = new CrmRadarService(); const tracks = [ track({ id: 9, touchType: 'nfc', createTime: today, viewedBlocks: ['hero', 'cases'] }), ]; const leads = [lead({ id: 7, status: 'new', createTime: today })]; service.crmTrackEntity = { find: jest.fn().mockResolvedValue(tracks), } as never; service.crmLeadEntity = { find: jest.fn().mockResolvedValue(leads), } as never; const result = await service.getWorkplaceOverview({ feedLimit: 8 }); expect(result.kpis.tapCount).toBe(1); expect(result.kpis.newLeadCount).toBe(1); expect(result.radarFeed[0].viewedBlocks).toEqual(['hero', 'cases']); }); it('管理端控制器必须暴露 /workplace 聚合接口', async () => { const controller = new AdminCrmRadarController(); const overview = { kpis: { tapCount: 1, viewCount: 2, newLeadCount: 1, conversionRate: 10, tapDelta: 0, viewDelta: 1, newLeadDelta: 0, }, radarFeed: [], }; controller.crmRadarService = { getWorkplaceOverview: jest.fn().mockResolvedValue(overview), } as never; (controller as unknown as { ok: (data: unknown) => unknown }).ok = (data) => ({ code: 1000, data, }); const res = (await controller.workplace({}, 12)) as unknown as { code: number; data: typeof overview; }; expect(res.code).toBe(1000); expect(res.data.kpis.viewCount).toBe(2); expect(controller.crmRadarService.getWorkplaceOverview).toHaveBeenCalled(); }); });