- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
273 行
9.3 KiB
TypeScript
273 行
9.3 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { CONTROLLER_KEY, getClassMetadata } from '@midwayjs/core';
|
||
import { BaseController, CoolCommException, CurdOption } from '@cool-midway/core';
|
||
import { Repository } from 'typeorm';
|
||
import { AiGlossaryEntity } from '../../../src/modules/ai/entity/glossary';
|
||
import { AiGlossaryService } from '../../../src/modules/ai/service/glossary';
|
||
import { AiLocalizeService } from '../../../src/modules/ai/service/localize';
|
||
import {
|
||
AdminAiGlossaryController,
|
||
adminAiGlossaryCurdOption,
|
||
} from '../../../src/modules/ai/controller/admin/glossary';
|
||
import { AiGlossaryItem } from '../../../src/comm/types/omni';
|
||
|
||
interface CoolControllerMeta {
|
||
prefix?: string;
|
||
curdOption?: CurdOption;
|
||
}
|
||
|
||
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 createMockRepo(seedItems: AiGlossaryEntity[] = []): Repository<AiGlossaryEntity> {
|
||
let list = [...seedItems];
|
||
let autoId = 100;
|
||
|
||
return {
|
||
find: jest.fn(async (options?: { where?: Record<string, unknown> }) => {
|
||
if (!options?.where) {
|
||
return list;
|
||
}
|
||
return list.filter(item => {
|
||
for (const [k, v] of Object.entries(options.where ?? {})) {
|
||
if (v !== undefined && (item as unknown as Record<string, unknown>)[k] !== v) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}),
|
||
save: jest.fn(async (entityOrEntities: AiGlossaryEntity | AiGlossaryEntity[]) => {
|
||
if (Array.isArray(entityOrEntities)) {
|
||
for (const item of entityOrEntities) {
|
||
if (!item.id) {
|
||
item.id = ++autoId;
|
||
}
|
||
list.push(item);
|
||
}
|
||
return entityOrEntities;
|
||
}
|
||
if (!entityOrEntities.id) {
|
||
entityOrEntities.id = ++autoId;
|
||
}
|
||
list.push(entityOrEntities);
|
||
return entityOrEntities;
|
||
}),
|
||
insert: jest.fn(async (item: AiGlossaryEntity) => {
|
||
if (!item.id) {
|
||
item.id = ++autoId;
|
||
}
|
||
list.push(item);
|
||
return { identifiers: [{ id: item.id }] };
|
||
}),
|
||
} as unknown as Repository<AiGlossaryEntity>;
|
||
}
|
||
|
||
describe('Point 06: 后台多语言 AI 润色中心与行业术语库 (Glossary)', () => {
|
||
describe('1. 实体 AiGlossaryEntity 定义与字段校验', () => {
|
||
it('实体属性与映射表名 ai_glossary 校验', () => {
|
||
const entity = new AiGlossaryEntity();
|
||
entity.sourceTerm = 'digital business card';
|
||
entity.targetTerm = '智能数字名片';
|
||
entity.sourceLang = 'en';
|
||
entity.targetLang = 'zh-CN';
|
||
entity.domain = 'tech';
|
||
entity.notes = '核心品牌术语';
|
||
|
||
expect(entity.sourceTerm).toBe('digital business card');
|
||
expect(entity.targetTerm).toBe('智能数字名片');
|
||
expect(entity.sourceLang).toBe('en');
|
||
expect(entity.targetLang).toBe('zh-CN');
|
||
expect(entity.domain).toBe('tech');
|
||
expect(entity.notes).toBe('核心品牌术语');
|
||
});
|
||
});
|
||
|
||
describe('2. AiGlossaryService 批量导入与增删改查', () => {
|
||
let service: AiGlossaryService;
|
||
let mockRepo: Repository<AiGlossaryEntity>;
|
||
|
||
beforeEach(() => {
|
||
service = new AiGlossaryService();
|
||
mockRepo = createMockRepo();
|
||
service.aiGlossaryEntity = mockRepo;
|
||
});
|
||
|
||
it('batchImport 空列表应抛异常或返回0', async () => {
|
||
await expect(service.batchImport([])).rejects.toThrow(CoolCommException);
|
||
});
|
||
|
||
it('batchImport 忽略缺少 sourceTerm 或 targetTerm 的无效项', async () => {
|
||
const result = await service.batchImport([
|
||
{ sourceTerm: '', targetTerm: '无效' },
|
||
{ sourceTerm: 'CRM', targetTerm: '' },
|
||
{ sourceTerm: 'NFC Card', targetTerm: 'NFC 实体卡', targetLang: 'zh-CN' },
|
||
]);
|
||
expect(result.total).toBe(3);
|
||
expect(result.inserted).toBe(1);
|
||
});
|
||
|
||
it('batchImport 批量导入多项成功', async () => {
|
||
const items: Array<Partial<AiGlossaryItem>> = [
|
||
{ sourceTerm: 'OmniCard', targetTerm: '奥秘名片', targetLang: 'zh-CN' },
|
||
{ sourceTerm: 'Radar', targetTerm: '访客雷达', targetLang: 'zh-CN', domain: 'crm' },
|
||
];
|
||
const result = await service.batchImport(items);
|
||
expect(result.inserted).toBe(2);
|
||
expect(result.total).toBe(2);
|
||
});
|
||
});
|
||
|
||
describe('3. AiGlossaryService 术语匹配替换 (贪心长词优先 & 领域匹配)', () => {
|
||
let service: AiGlossaryService;
|
||
|
||
beforeEach(() => {
|
||
const seed: AiGlossaryEntity[] = [
|
||
{
|
||
id: 1,
|
||
sourceTerm: 'digital card',
|
||
targetTerm: '电子名片',
|
||
targetLang: 'zh-CN',
|
||
domain: 'general',
|
||
} as AiGlossaryEntity,
|
||
{
|
||
id: 2,
|
||
sourceTerm: 'smart digital card',
|
||
targetTerm: '全球智能数字名片',
|
||
targetLang: 'zh-CN',
|
||
domain: 'tech',
|
||
} as AiGlossaryEntity,
|
||
{
|
||
id: 3,
|
||
sourceTerm: 'lead',
|
||
targetTerm: '高意向商机',
|
||
targetLang: 'zh-CN',
|
||
domain: 'crm',
|
||
} as AiGlossaryEntity,
|
||
{
|
||
id: 4,
|
||
sourceTerm: 'lead',
|
||
targetTerm: '线索',
|
||
targetLang: 'zh-CN',
|
||
domain: 'general',
|
||
} as AiGlossaryEntity,
|
||
{
|
||
id: 5,
|
||
sourceTerm: 'بطاقة',
|
||
targetTerm: 'بطاقة ذكية',
|
||
targetLang: 'ar-SA',
|
||
} as AiGlossaryEntity,
|
||
];
|
||
service = new AiGlossaryService();
|
||
const mockRepo = createMockRepo(seed);
|
||
service.aiGlossaryEntity = mockRepo;
|
||
});
|
||
|
||
it('长词优先贪心匹配:smart digital card 应被替换为 全球智能数字名片 而非 电子名片', async () => {
|
||
const input = 'This is our smart digital card platform.';
|
||
const output = await service.replaceTerms(input, 'zh-CN', 'tech');
|
||
expect(output).toContain('全球智能数字名片');
|
||
expect(output).not.toContain('电子名片');
|
||
});
|
||
|
||
it('领域匹配优先:domain 为 crm 时 lead 替换为 高意向商机,否则为 线索', async () => {
|
||
const input = 'We captured a new lead yesterday.';
|
||
const crmOutput = await service.replaceTerms(input, 'zh-CN', 'crm');
|
||
expect(crmOutput).toContain('高意向商机');
|
||
|
||
const generalOutput = await service.replaceTerms(input, 'zh-CN', 'general');
|
||
expect(generalOutput).toContain('线索');
|
||
});
|
||
|
||
it('跨目标语言隔离:目标语言 ar-SA 不应应用 zh-CN 术语', async () => {
|
||
const input = 'smart digital card and بطاقة';
|
||
const output = await service.replaceTerms(input, 'ar-SA');
|
||
expect(output).toContain('بطاقة ذكية');
|
||
expect(output).not.toContain('全球智能数字名片');
|
||
});
|
||
});
|
||
|
||
describe('4. AiLocalizeService 引用术语库并强行替换', () => {
|
||
let localizeService: AiLocalizeService;
|
||
let glossaryService: AiGlossaryService;
|
||
|
||
beforeEach(() => {
|
||
glossaryService = new AiGlossaryService();
|
||
const seed: AiGlossaryEntity[] = [
|
||
{
|
||
id: 1,
|
||
sourceTerm: 'digital cards',
|
||
targetTerm: '智能数字名片',
|
||
targetLang: 'zh-CN',
|
||
} as AiGlossaryEntity,
|
||
];
|
||
const mockRepo = createMockRepo(seed);
|
||
glossaryService.aiGlossaryEntity = mockRepo;
|
||
|
||
localizeService = new AiLocalizeService();
|
||
localizeService.aiGlossaryService = glossaryService;
|
||
});
|
||
|
||
it('润色时应将术语强制替换为术语库配置的目标词', async () => {
|
||
const result = await localizeService.localize({
|
||
text: 'We help global teams share digital cards.',
|
||
targetLocale: 'zh-CN',
|
||
tone: 'professional',
|
||
});
|
||
expect(result.locale).toBe('zh-CN');
|
||
expect(result.text).toContain('智能数字名片');
|
||
});
|
||
});
|
||
|
||
describe('5. AdminAiGlossaryController 路由与元数据校验', () => {
|
||
const controllerFile = path.join(
|
||
__dirname,
|
||
'../../../src/modules/ai/controller/admin/glossary.ts'
|
||
);
|
||
|
||
let meta: CoolControllerMeta;
|
||
let option: CurdOption;
|
||
|
||
beforeAll(async () => {
|
||
meta = await waitCoolMeta(AdminAiGlossaryController);
|
||
option = meta.curdOption as CurdOption;
|
||
});
|
||
|
||
it('文件路径必须符合 /admin/ai/glossary 规范', () => {
|
||
expect(fs.existsSync(controllerFile)).toBe(true);
|
||
expect(controllerFile.replace(/\\/g, '/')).toMatch(
|
||
/modules\/ai\/controller\/admin\/glossary\.ts$/
|
||
);
|
||
});
|
||
|
||
it('路由前缀必须为 /admin/ai/glossary', () => {
|
||
expect(meta.prefix).toBe('/admin/ai/glossary');
|
||
});
|
||
|
||
it('控制器应继承 BaseController 并开启 CRUD 与 serviceApis', () => {
|
||
expect(AdminAiGlossaryController.prototype).toBeInstanceOf(BaseController);
|
||
expect(option.service).toBe(AiGlossaryService);
|
||
expect(adminAiGlossaryCurdOption.service).toBe(AiGlossaryService);
|
||
expect(option.entity).toBe(AiGlossaryEntity);
|
||
expect(option.api).toEqual(
|
||
expect.arrayContaining(['add', 'delete', 'update', 'info', 'list', 'page'])
|
||
);
|
||
expect(option.serviceApis).toEqual(expect.arrayContaining(['batchImport']));
|
||
});
|
||
});
|
||
});
|