feat(admin-workplace): 工作台业务概览真实对接与全端认证集成 (更新远程代码仓配置与测试全绿)
这个提交包含在:
@@ -0,0 +1,155 @@
|
||||
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<CrmTrackEntity> & { 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<CrmLeadEntity> & { 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();
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户