- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
267 行
8.7 KiB
TypeScript
267 行
8.7 KiB
TypeScript
import { CONTROLLER_KEY, getClassMetadata } from '@midwayjs/core';
|
||
import { CurdOption } from '@cool-midway/core';
|
||
import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admin/lead';
|
||
import { AdminCrmRadarController } from '../../../src/modules/crm/controller/admin/radar';
|
||
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
|
||
import { CrmTrackEntity } from '../../../src/modules/crm/entity/track';
|
||
import { CrmLeadService } from '../../../src/modules/crm/service/lead';
|
||
import { CrmRadarService } from '../../../src/modules/crm/service/radar';
|
||
import { CoolCommException } from '@cool-midway/core';
|
||
|
||
/**
|
||
* CoolController 元数据(脚本路径解析为异步)
|
||
*/
|
||
interface CoolControllerMeta {
|
||
prefix?: string;
|
||
curdOption?: CurdOption;
|
||
}
|
||
|
||
/**
|
||
* 等待 CoolController 将 CRUD 配置写入类元数据
|
||
*/
|
||
async function waitCoolMeta(
|
||
target: new (...args: never[]) => unknown
|
||
): Promise<CoolControllerMeta> {
|
||
const started = Date.now();
|
||
while (Date.now() - started < 2000) {
|
||
const meta = getClassMetadata(CONTROLLER_KEY, target) as
|
||
| CoolControllerMeta
|
||
| undefined;
|
||
if (meta?.curdOption) {
|
||
return meta;
|
||
}
|
||
await new Promise<void>(resolve => setImmediate(resolve));
|
||
}
|
||
throw new Error(`${target.name} 的 CoolController 元数据未就绪`);
|
||
}
|
||
|
||
function serviceApiMethods(apis: CurdOption['serviceApis'] = []): string[] {
|
||
return apis.map(api => (typeof api === 'string' ? api : api.method));
|
||
}
|
||
|
||
describe('CRM 管理后台 CRUD 控制器 (admin/crm)', () => {
|
||
describe('AdminCrmLeadController /admin/crm/lead', () => {
|
||
let meta: CoolControllerMeta;
|
||
let option: CurdOption;
|
||
|
||
beforeAll(async () => {
|
||
meta = await waitCoolMeta(AdminCrmLeadController);
|
||
option = meta.curdOption as CurdOption;
|
||
});
|
||
|
||
it('路由前缀应为 /admin/crm/lead', () => {
|
||
expect(meta.prefix).toBe('/admin/crm/lead');
|
||
});
|
||
|
||
it('应开放完整 CRUD:add / delete / update / info / list / page', () => {
|
||
expect(option.api).toEqual([
|
||
'add',
|
||
'delete',
|
||
'update',
|
||
'info',
|
||
'list',
|
||
'page',
|
||
]);
|
||
});
|
||
|
||
it('应绑定线索实体与线索服务', () => {
|
||
expect(option.entity).toBe(CrmLeadEntity);
|
||
expect(option.service).toBe(CrmLeadService);
|
||
});
|
||
|
||
it('分页查询应支持姓名/公司/邮箱/电话模糊检索', () => {
|
||
const pageQueryOp = option.pageQueryOp as {
|
||
keyWordLikeFields?: string[];
|
||
fieldEq?: string[];
|
||
addOrderBy?: Record<string, string>;
|
||
};
|
||
expect(pageQueryOp.keyWordLikeFields).toEqual([
|
||
'a.name',
|
||
'a.company',
|
||
'a.email',
|
||
'a.phone',
|
||
]);
|
||
});
|
||
|
||
it('分页查询应支持状态/来源/名片精确筛选,并按创建时间倒序', () => {
|
||
const pageQueryOp = option.pageQueryOp as {
|
||
keyWordLikeFields?: string[];
|
||
fieldEq?: string[];
|
||
addOrderBy?: Record<string, string>;
|
||
};
|
||
expect(pageQueryOp.fieldEq).toEqual(['a.status', 'a.source', 'a.cardId']);
|
||
expect(pageQueryOp.addOrderBy).toEqual({ createTime: 'desc' });
|
||
});
|
||
|
||
it('应将 updateStatus 注册为 serviceApi(线索状态流转)', () => {
|
||
const methods = serviceApiMethods(option.serviceApis);
|
||
expect(methods).toContain('updateStatus');
|
||
const named = (option.serviceApis || []).find(
|
||
api => typeof api !== 'string' && api.method === 'updateStatus'
|
||
) as { method: string; summary: string } | undefined;
|
||
expect(named?.summary).toBe('线索状态流转');
|
||
});
|
||
});
|
||
|
||
describe('AdminCrmRadarController /admin/crm/radar', () => {
|
||
let meta: CoolControllerMeta;
|
||
let option: CurdOption;
|
||
|
||
beforeAll(async () => {
|
||
meta = await waitCoolMeta(AdminCrmRadarController);
|
||
option = meta.curdOption as CurdOption;
|
||
});
|
||
|
||
it('路由前缀应为 /admin/crm/radar', () => {
|
||
expect(meta.prefix).toBe('/admin/crm/radar');
|
||
});
|
||
|
||
it('足迹只读管理:开放 info / list / page / delete,不含 add', () => {
|
||
expect(option.api).toEqual(['info', 'list', 'page', 'delete']);
|
||
expect(option.api).not.toContain('add');
|
||
});
|
||
|
||
it('应绑定足迹实体与雷达服务', () => {
|
||
expect(option.entity).toBe(CrmTrackEntity);
|
||
expect(option.service).toBe(CrmRadarService);
|
||
});
|
||
|
||
it('分页查询应支持国家/城市/访客/IP 模糊检索', () => {
|
||
const pageQueryOp = option.pageQueryOp as {
|
||
keyWordLikeFields?: string[];
|
||
fieldEq?: string[];
|
||
addOrderBy?: Record<string, string>;
|
||
};
|
||
expect(pageQueryOp.keyWordLikeFields).toEqual([
|
||
'a.country',
|
||
'a.city',
|
||
'a.visitorId',
|
||
'a.ip',
|
||
]);
|
||
});
|
||
|
||
it('分页查询应支持名片/触达方式/国家精确筛选,并按 id 倒序', () => {
|
||
const pageQueryOp = option.pageQueryOp as {
|
||
keyWordLikeFields?: string[];
|
||
fieldEq?: string[];
|
||
addOrderBy?: Record<string, string>;
|
||
};
|
||
expect(pageQueryOp.fieldEq).toEqual([
|
||
'a.cardId',
|
||
'a.touchType',
|
||
'a.country',
|
||
]);
|
||
expect(pageQueryOp.addOrderBy).toEqual({ id: 'desc' });
|
||
});
|
||
|
||
it('应将 getRecentTracks 注册为 serviceApi(最近足迹)', () => {
|
||
const methods = serviceApiMethods(option.serviceApis);
|
||
expect(methods).toContain('getRecentTracks');
|
||
const named = (option.serviceApis || []).find(
|
||
api => typeof api !== 'string' && api.method === 'getRecentTracks'
|
||
) as { method: string; summary: string } | undefined;
|
||
expect(named?.summary).toBe('最近足迹');
|
||
});
|
||
});
|
||
|
||
describe('serviceApis POST body 单参数对象转调', () => {
|
||
it('updateStatus 接收 { id, status } 时应转调已有双参签名', async () => {
|
||
const saved: CrmLeadEntity[] = [];
|
||
const leadService = new CrmLeadService();
|
||
leadService.crmLeadEntity = {
|
||
save: jest.fn(async (item: CrmLeadEntity) => {
|
||
if (!item.id) {
|
||
item.id = 1;
|
||
}
|
||
const idx = saved.findIndex(row => row.id === item.id);
|
||
if (idx >= 0) {
|
||
saved[idx] = item;
|
||
} else {
|
||
saved.push(item);
|
||
}
|
||
return item;
|
||
}),
|
||
findOne: jest.fn(async (options: { where?: { id?: number } }) => {
|
||
const id = options?.where?.id;
|
||
return saved.find(row => row.id === Number(id)) || null;
|
||
}),
|
||
} as unknown as CrmLeadService['crmLeadEntity'];
|
||
|
||
const created = await leadService.submitLead({
|
||
cardId: 101,
|
||
name: 'Admin Flow',
|
||
email: 'admin@example.com',
|
||
});
|
||
expect(created.status).toBe('new');
|
||
|
||
const updated = await leadService.updateStatus({
|
||
id: Number(created.id),
|
||
status: 'contacted',
|
||
});
|
||
expect(updated.status).toBe('contacted');
|
||
});
|
||
|
||
it('getRecentTracks 接收 { cardId, limit } 时应转调已有双参签名', async () => {
|
||
const saved: CrmTrackEntity[] = [];
|
||
for (let i = 1; i <= 4; i++) {
|
||
const track = new CrmTrackEntity();
|
||
track.id = i;
|
||
track.cardId = 202;
|
||
track.visitorId = `vis_${i}`;
|
||
track.touchType = 'link';
|
||
saved.push(track);
|
||
}
|
||
|
||
const radarService = new CrmRadarService();
|
||
radarService.crmTrackEntity = {
|
||
find: jest.fn(
|
||
async (options: {
|
||
where?: { cardId?: number };
|
||
order?: { id?: string };
|
||
take?: number;
|
||
}) => {
|
||
let list = saved.filter(
|
||
row => row.cardId === Number(options?.where?.cardId)
|
||
);
|
||
if (options?.order?.id === 'DESC') {
|
||
list = [...list].sort((a, b) => Number(b.id) - Number(a.id));
|
||
}
|
||
if (options?.take) {
|
||
list = list.slice(0, options.take);
|
||
}
|
||
return list;
|
||
}
|
||
),
|
||
} as unknown as CrmRadarService['crmTrackEntity'];
|
||
|
||
const list = await radarService.getRecentTracks({
|
||
cardId: 202,
|
||
limit: 2,
|
||
});
|
||
expect(list).toHaveLength(2);
|
||
expect(list[0].id).toBe(4);
|
||
expect(list[1].id).toBe(3);
|
||
});
|
||
|
||
it('updateStatus 旧双参签名仍可用,非法状态仍拒绝', async () => {
|
||
const leadService = new CrmLeadService();
|
||
leadService.crmLeadEntity = {
|
||
save: jest.fn(async (item: CrmLeadEntity) => item),
|
||
findOne: jest.fn(async () => {
|
||
const lead = new CrmLeadEntity();
|
||
lead.id = 7;
|
||
lead.status = 'new';
|
||
return lead;
|
||
}),
|
||
} as unknown as CrmLeadService['crmLeadEntity'];
|
||
|
||
const updated = await leadService.updateStatus(7, 'won');
|
||
expect(updated.status).toBe('won');
|
||
|
||
await expect(
|
||
leadService.updateStatus(7, 'invalid' as 'new')
|
||
).rejects.toThrow(CoolCommException);
|
||
});
|
||
});
|
||
});
|