refactor(i18n): 全工程面向国际用户与全球现代商务重构,彻底清理出海跨境等单向视角表达并对齐自动化测试
这个提交包含在:
@@ -6,7 +6,7 @@ import {
|
||||
MeetingEventOptions,
|
||||
} from '../../src/modules/card/service/calendar';
|
||||
|
||||
describe('Point 42: 出海潜客跨国会议 Google Calendar / Outlook 一键预约同步', () => {
|
||||
describe('Point 42: 跨国会议与潜客预约 Google Calendar / Outlook 一键同步', () => {
|
||||
let calendarService: CardCalendarService;
|
||||
|
||||
const mockMeeting: MeetingEventOptions = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ContactWindowInfo,
|
||||
} from '../../src/comm/i18n/geo';
|
||||
|
||||
describe('Point 39: 出海潜客多时区识别与最佳联系时间窗口推荐 (Best Contact Time Window)', () => {
|
||||
describe('Point 39: 潜客多时区识别与最佳联系时间窗口推荐 (Best Contact Time Window)', () => {
|
||||
describe('1. 国际多国家/时区时间计算', () => {
|
||||
it('应准确计算中东阿联酋 (UTC+4) 当地时间与联系窗口', () => {
|
||||
// 假设 UTC 时间为 06:00 (北京时间 14:00,阿联酋当地时间为 10:00)
|
||||
@@ -20,7 +20,7 @@ describe('Point 39: 出海潜客多时区识别与最佳联系时间窗口推荐
|
||||
expect(windowInfo.suggestionText).toContain('当地工作时间');
|
||||
});
|
||||
|
||||
it('当海外客商处于深夜休息时段 (如欧洲当地 23:00) 时,建议勿扰', () => {
|
||||
it('当国际客商处于深夜休息时段 (如欧洲当地 23:00) 时,建议勿扰', () => {
|
||||
// 假设 UTC 时间为 22:00 (欧洲 CET UTC+1 当地为 23:00)
|
||||
const baseUtcDate = new Date(Date.UTC(2026, 8, 21, 22, 0, 0));
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'corporate_navy',
|
||||
remark: '内部高管名片,核心出海带队',
|
||||
remark: '内部高管名片,国际业务核心',
|
||||
internalConfig: '{"rateLimit": 1000, "debug": false}',
|
||||
});
|
||||
|
||||
@@ -425,7 +425,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '个人简介与身份卡',
|
||||
content: '专注于中东与欧洲出海数字商务拓客',
|
||||
content: '专注于中东与欧洲数字商务合作',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -440,7 +440,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '协助头部光伏企业落地阿布扎比',
|
||||
status: 1,
|
||||
},
|
||||
@@ -448,8 +448,8 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '2026出海白皮书',
|
||||
content: '深度解读跨境智能名片获客漏斗',
|
||||
title: '2026全球数字名片白皮书',
|
||||
content: '深度解读全球智能名片获客漏斗',
|
||||
status: 1,
|
||||
},
|
||||
];
|
||||
@@ -488,14 +488,14 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
it('P04: 博客文章发布与 SEO OpenGraph 社交卡片自动提炼', async () => {
|
||||
const rawHtmlContent = `
|
||||
<article>
|
||||
<h1>出海获客新引擎:2026 跨境数字名片实战洞察</h1>
|
||||
<p>在全球数字化出海与国际展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,出海销售代表能够在 30 秒内建立品牌信任,并让每一位海外客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
|
||||
<h1>全球获客新引擎:2026 数字名片实战洞察</h1>
|
||||
<p>在全球数字化与国际商务展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,销售代表能够在 30 秒内建立品牌信任,并让每一位国际客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
|
||||
</article>
|
||||
`;
|
||||
|
||||
const articleEntity = await articleRepo.save({
|
||||
cardId: sharedCardId,
|
||||
title: '出海获客新引擎:2026 跨境数字名片实战洞察',
|
||||
title: '全球获客新引擎:2026 数字名片实战洞察',
|
||||
content: rawHtmlContent,
|
||||
coverImage: 'https://cdn.omnicard.ai/articles/og-banner.png',
|
||||
isPublished: true,
|
||||
@@ -506,7 +506,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
|
||||
expect(ogMeta.title).toBe(articleEntity.title);
|
||||
expect(ogMeta.image).toBe('https://cdn.omnicard.ai/articles/og-banner.png');
|
||||
expect(ogMeta.description).toContain('在全球数字化出海与国际展会场景中');
|
||||
expect(ogMeta.description).toContain('在全球数字化与国际商务展会场景中');
|
||||
expect(ogMeta.description.length).toBeLessThanOrEqual(160);
|
||||
expect(ogMeta.description).not.toContain('<p>');
|
||||
});
|
||||
@@ -746,7 +746,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
it('P13: 名片 AI 顾问人设、基调与留资触发阈值配置', async () => {
|
||||
const config = await aiConciergeService.saveByCardId(sharedCardId, {
|
||||
personaType: 'trade_expert',
|
||||
welcomeMsg: '您好!我是 Alex 的 AI 出海顾问,请问有什么可以协助您的?',
|
||||
welcomeMsg: '您好!我是 Alex 的 AI 商务顾问,请问有什么可以协助您的?',
|
||||
triggerLeadThreshold: 75,
|
||||
promptTemplate: 'You are an export trade consultant specialized in clean energy solutions.',
|
||||
toneStyle: 'professional',
|
||||
@@ -758,14 +758,14 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
expect(config.triggerLeadThreshold).toBe(75);
|
||||
|
||||
const queried = await aiConciergeService.getByCardId(sharedCardId);
|
||||
expect(queried?.welcomeMsg).toContain('Alex 的 AI 出海顾问');
|
||||
expect(queried?.welcomeMsg).toContain('Alex 的 AI 商务顾问');
|
||||
});
|
||||
|
||||
it('P14: 知识库 RAG 滑动窗口文本分块与重叠切片算法', () => {
|
||||
const rawDocText = `
|
||||
OmniCard AI 致力于为全球商务出海团队提供次世代数字名片与微官网解决方案。
|
||||
OmniCard AI 致力于为全球商务团队提供次世代数字名片与微官网解决方案。
|
||||
支持中东、欧洲及北美地区的主流商务支付与 CRM 系统无缝互联。
|
||||
我们针对新能源、智能硬件及跨境电商客户提供定制化的展会获客方案,
|
||||
我们针对新能源、智能硬件及全球电商客户提供定制化的展会获客方案,
|
||||
配合实体 NFC 智能卡片,可实现秒级留资并自动将高意向线索同步至公海池。
|
||||
`.trim();
|
||||
|
||||
@@ -782,7 +782,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
const testChunks = [
|
||||
{
|
||||
index: 0,
|
||||
content: '公司主营产品包含新能源储能电站与光伏逆变器海外解决方案。',
|
||||
content: '公司主营产品包含新能源储能电站与光伏逆变器全球解决方案。',
|
||||
length: 30,
|
||||
},
|
||||
{
|
||||
@@ -808,7 +808,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
expect(results[0].score).toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it('P16: 外贸行业术语库 (Glossary) 导入与出海多语言润色替换', async () => {
|
||||
it('P16: 行业专有术语库 (Glossary) 导入与国际多语言润色替换', async () => {
|
||||
const importRes = await aiGlossaryService.batchImport([
|
||||
{
|
||||
sourceTerm: 'digital business card',
|
||||
@@ -1103,15 +1103,15 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
// =========================================================================
|
||||
describe('主干流 6: 多租户与商业化 (Points 27-30)', () => {
|
||||
it('P27: 组织架构部门树多层级递归与成员名片分配 (TeamMembersService)', async () => {
|
||||
// 1. 初始化部门树结构:总部 -> 海外销售中心 -> 中东拓展组
|
||||
// 1. 初始化部门树结构:总部 -> 国际销售中心 -> 中东拓展组
|
||||
const headDept = await deptRepo.save({
|
||||
name: '全球出海总部',
|
||||
name: '全球总部',
|
||||
parentId: null as any,
|
||||
orderNum: 1,
|
||||
});
|
||||
|
||||
const salesDept = await deptRepo.save({
|
||||
name: '海外拓展事业群',
|
||||
name: '全球拓展事业群',
|
||||
parentId: headDept.id,
|
||||
orderNum: 2,
|
||||
});
|
||||
|
||||
@@ -54,13 +54,13 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
|
||||
it('2.1 buildSystemPrompt 组装名片主、人设与知识库切片', () => {
|
||||
const card = {
|
||||
name: '张总',
|
||||
company: '华强出海科技',
|
||||
title: '海外商务总监',
|
||||
company: '华强全球科技',
|
||||
title: '国际商务总监',
|
||||
};
|
||||
const concierge = {
|
||||
personaType: 'trade_expert' as const,
|
||||
toneStyle: 'professional' as const,
|
||||
promptTemplate: '你是一名资深出海贸易专家,精通中东与东南亚市场。',
|
||||
promptTemplate: '你是一名资深国际贸易专家,精通中东与东南亚市场。',
|
||||
};
|
||||
const chunks = [
|
||||
{ index: 1, content: '支持信用证结算与 FOB 迪拜港条款。' },
|
||||
@@ -70,9 +70,9 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
|
||||
const systemPrompt = service.buildSystemPrompt(card, concierge, chunks);
|
||||
|
||||
expect(systemPrompt).toContain('张总');
|
||||
expect(systemPrompt).toContain('华强出海科技');
|
||||
expect(systemPrompt).toContain('海外商务总监');
|
||||
expect(systemPrompt).toContain('你是一名资深出海贸易专家');
|
||||
expect(systemPrompt).toContain('华强全球科技');
|
||||
expect(systemPrompt).toContain('国际商务总监');
|
||||
expect(systemPrompt).toContain('你是一名资深国际贸易专家');
|
||||
expect(systemPrompt).toContain('支持信用证结算与 FOB 迪拜港条款。');
|
||||
expect(systemPrompt).toContain('最小起订量 (MOQ) 为 500 台。');
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ function localizeReq(
|
||||
};
|
||||
}
|
||||
|
||||
describe('T39: AI 出海润色 (modules/ai localize)', () => {
|
||||
describe('T39: AI 多语言本地化润色 (modules/ai localize)', () => {
|
||||
describe('1. AiLocalizeService.localize 入参校验', () => {
|
||||
let service: AiLocalizeService;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Point 47: AI 销售潜客智能跟进邮件/消息地道草拟引擎',
|
||||
service = new AiOutreachDraftService();
|
||||
});
|
||||
|
||||
describe('1. formatOutreachPrompt (出海商务邮件地道提示词组装)', () => {
|
||||
describe('1. formatOutreachPrompt (全球商务邮件地道提示词组装)', () => {
|
||||
it('英语场景应包含地道商务开场、兴趣针对性提及与日程预约呼吁', () => {
|
||||
const req: OutreachDraftRequest = {
|
||||
leadName: 'Arthur Pendelton',
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Point 07: AI 知识库分块切片与召回沙盒 (modules/ai knowledg
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
content: '雷达系统支持出海 CRM 访客轨迹追踪、停留时长与意向度自动评分。',
|
||||
content: '雷达系统支持智能 CRM 访客轨迹追踪、停留时长与意向度自动评分。',
|
||||
length: 33,
|
||||
},
|
||||
{
|
||||
@@ -150,10 +150,10 @@ describe('Point 07: AI 知识库分块切片与召回沙盒 (modules/ai knowledg
|
||||
|
||||
it('应兼容接收纯字符串数组 chunks', () => {
|
||||
const stringChunks = [
|
||||
'出海多语言支持英文、中文、阿拉伯语',
|
||||
'国际多语言支持英文、中文、阿拉伯语',
|
||||
'国内网络环境配置说明',
|
||||
];
|
||||
const results = service.simulateRetrieval('阿拉伯语 出海', stringChunks, 2);
|
||||
const results = service.simulateRetrieval('阿拉伯语 国际多语言', stringChunks, 2);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].content).toContain('阿拉伯语');
|
||||
expect(results[0].score).toBeGreaterThan(0);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Point 61 (P1): AI 对话客商意图与情绪极性分析引擎', () =
|
||||
analyzer = new AiSentimentIntentAnalyzer();
|
||||
});
|
||||
|
||||
describe('1. analyzeConversationIntent (出海商务意图识别)', () => {
|
||||
describe('1. analyzeConversationIntent (商务意图识别)', () => {
|
||||
it('包含价格、报价、起订量或购买关键词应识别为 PURCHASE_INTENT', () => {
|
||||
const msgs = [
|
||||
'Hello, what is the MOQ for 500 NFC metal cards?',
|
||||
|
||||
@@ -190,7 +190,7 @@ function seedBlocks(): Array<Partial<CardBentoEntity>> {
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '个人简介与身份卡',
|
||||
content: '全栈出海专家',
|
||||
content: '全球商务专家',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -211,7 +211,7 @@ function seedBlocks(): Array<Partial<CardBentoEntity>> {
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
sortOrder: 2,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '中东客户成功交付',
|
||||
status: 0,
|
||||
},
|
||||
@@ -334,7 +334,7 @@ describe('后台 Bento 画布 CRUD (Admin Card Bento CoolController)', () => {
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '企业出海宣传片',
|
||||
title: '企业宣传片',
|
||||
mediaUrl: 'https://cdn.omnicard.ai/video/intro.mp4',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('后台博客文章与动态发布中心 (Card Article CoolController &
|
||||
it('完整文章入参时,应正确映射 title、summary 与 coverImage', () => {
|
||||
const article = {
|
||||
cardId: 101,
|
||||
title: '出海战略新视界:2026 跨境数字名片白皮书',
|
||||
title: '全球商务新视界:2026 数字名片白皮书',
|
||||
summary: '本文深度剖析全球化商务场景下 NFC 与微官网结合的数字化获客新路径。',
|
||||
content: '<p>正文内容...</p>',
|
||||
coverImage: 'https://cdn.omnicard.ai/articles/cover-1.jpg',
|
||||
@@ -91,18 +91,18 @@ describe('后台博客文章与动态发布中心 (Card Article CoolController &
|
||||
const longHtml = `
|
||||
<article>
|
||||
<h1>一级标题</h1>
|
||||
<p>在全球数字化浪潮席卷而来的今天,<strong>智能名片</strong>已经不仅是身份凭证,更是出海企业的第一道数字门面与线索转化漏斗。通过多语言自适应与 NFC 瞬时触达,销售代表可以在海外展会现场以极低摩擦完成商机留资与精准雷达跟进,大幅提升商机转化效率与客户体验。</p>
|
||||
<p>在全球数字化浪潮席卷而来的今天,<strong>智能名片</strong>已经不仅是身份凭证,更是现代企业的第一道数字门面与线索转化漏斗。通过多语言自适应与 NFC 瞬时触达,销售代表可以在国际展会现场以极低摩擦完成商机留资与精准雷达跟进,大幅提升商机转化效率与客户体验。</p>
|
||||
</article>
|
||||
`;
|
||||
const article = {
|
||||
cardId: 101,
|
||||
title: '出海营销指南',
|
||||
title: '全球营销指南',
|
||||
content: longHtml,
|
||||
isPublished: true,
|
||||
};
|
||||
|
||||
const og = service.generateOgMeta(article);
|
||||
expect(og.title).toBe('出海营销指南');
|
||||
expect(og.title).toBe('全球营销指南');
|
||||
expect(og.description.length).toBeLessThanOrEqual(160);
|
||||
expect(og.description).not.toContain('<p>');
|
||||
expect(og.description).not.toContain('<strong>');
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../src/modules/card/service/batch-locale';
|
||||
import { CardLocaleEntity } from '../../../src/modules/card/entity/locale';
|
||||
|
||||
describe('Point 43: 出海团队多语言名片批量生成向导与多语种字段镜像校验', () => {
|
||||
describe('Point 43: 跨国团队多语言名片批量生成向导与多语种字段镜像校验', () => {
|
||||
let service: CardBatchLocaleService;
|
||||
let mockRepository: any;
|
||||
let savedRecords: CardLocaleEntity[];
|
||||
@@ -80,8 +80,8 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
cardId: 101,
|
||||
baseFields: {
|
||||
name: '张三',
|
||||
title: '出海销售总监',
|
||||
company: '深圳数字智能出海科技',
|
||||
title: '国际业务总监',
|
||||
company: '数字智能科技',
|
||||
slogan: '智能连接全球',
|
||||
},
|
||||
},
|
||||
@@ -90,7 +90,7 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
baseFields: {
|
||||
name: '李四',
|
||||
title: '中东区域代表',
|
||||
company: '深圳数字智能出海科技',
|
||||
company: '数字智能科技',
|
||||
slogan: '专注海湾六国',
|
||||
},
|
||||
},
|
||||
@@ -164,7 +164,7 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. inspectTeamLocaleCoverage (出海团队语言覆盖率与就绪度诊断)', () => {
|
||||
describe('3. inspectTeamLocaleCoverage (跨国团队语言覆盖率与就绪度诊断)', () => {
|
||||
it('应精准统计团队名片各语言的覆盖率与缺失名单', async () => {
|
||||
savedRecords.push(
|
||||
{ id: 1, cardId: 101, locale: 'en', name: 'Alex' } as CardLocaleEntity,
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '个人简介与身份卡',
|
||||
content: '全栈出海专家',
|
||||
content: '全球商务专家',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -174,7 +174,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
sortOrder: 2,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '中东客户成功交付',
|
||||
status: 0, // 禁用
|
||||
},
|
||||
@@ -275,7 +275,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '企业出海宣传片',
|
||||
title: '企业宣传片',
|
||||
mediaUrl: 'https://cdn.omnicard.ai/video/intro.mp4',
|
||||
},
|
||||
];
|
||||
@@ -336,7 +336,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
type: 'form' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '外贸留资表单',
|
||||
title: '商务留资表单',
|
||||
metaJson: JSON.stringify({
|
||||
fields: ['name', 'email', 'country', 'requirement'],
|
||||
buttonText: 'Get Quote',
|
||||
@@ -383,12 +383,12 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
const created = await service.saveBlock({
|
||||
cardId: 100,
|
||||
type: 'article',
|
||||
title: '出海白皮书',
|
||||
title: '全球商务白皮书',
|
||||
colSpan: 1,
|
||||
rowSpan: 1,
|
||||
});
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.title).toBe('出海白皮书');
|
||||
expect(created.title).toBe('全球商务白皮书');
|
||||
});
|
||||
|
||||
it('deleteBlock 应能成功删除单个块', async () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const mockMeeting: MeetingEventOptions = {
|
||||
uid: 'omnicard-meet-20260920-001@omnicard.ai',
|
||||
summary: 'OmniCard AI 商务拓展战略会谈 (Alex & Partner)',
|
||||
description: '讨论海外微官网落地方案, NFC 实体卡发行与访客雷达 CRM 对接。\n请准时参会。',
|
||||
description: '讨论国际微官网落地方案, NFC 实体卡发行与访客雷达 CRM 对接。\n请准时参会。',
|
||||
location: 'Zoom 会议室 (ID: 888-999-000), 新加坡滨海湾金融中心',
|
||||
startTime: new Date(Date.UTC(2026, 8, 20, 14, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2026, 8, 20, 15, 0, 0)),
|
||||
@@ -122,7 +122,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
expect(ics).toContain('DTSTART:20260920T140000Z\r\n');
|
||||
expect(ics).toContain('DTEND:20260920T150000Z\r\n');
|
||||
expect(ics).toContain('SUMMARY:OmniCard AI 商务拓展战略会谈 (Alex & Partner)\r\n');
|
||||
expect(ics).toContain('DESCRIPTION:讨论海外微官网落地方案\\, NFC 实体卡发行与访客雷达 CRM 对接。\\n请准时参会。\r\n');
|
||||
expect(ics).toContain('DESCRIPTION:讨论国际微官网落地方案\\, NFC 实体卡发行与访客雷达 CRM 对接。\\n请准时参会。\r\n');
|
||||
expect(ics).toContain('LOCATION:Zoom 会议室 (ID: 888-999-000)\\, 新加坡滨海湾金融中心\r\n');
|
||||
expect(ics).toContain('ORGANIZER;CN=Alex Zheng:mailto:alex@omnicard.ai\r\n');
|
||||
expect(ics).toContain('URL:https://omnicard.ai/p/alex-zheng\r\n');
|
||||
@@ -193,10 +193,10 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
portalController.ctx = mockCtx;
|
||||
|
||||
const result = await portalController.meetingIcs({
|
||||
summary: '全球合作伙伴出海研讨会',
|
||||
summary: '全球合作伙伴研讨会',
|
||||
startTime: '2026-09-22T09:00:00Z',
|
||||
endTime: '2026-09-22T10:00:00Z',
|
||||
description: '探讨 2026 年品牌海外本土化与数字名片生态布局',
|
||||
description: '探讨 2026 年品牌全球本土化与数字名片生态布局',
|
||||
location: '线上 Google Meet',
|
||||
organizerName: 'Alex Zheng',
|
||||
organizerEmail: 'alex@omnicard.ai',
|
||||
@@ -211,7 +211,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const icsOutput = (responseBody as string) || (result as string);
|
||||
expect(typeof icsOutput).toBe('string');
|
||||
expect(icsOutput).toContain('BEGIN:VCALENDAR');
|
||||
expect(icsOutput).toContain('SUMMARY:全球合作伙伴出海研讨会');
|
||||
expect(icsOutput).toContain('SUMMARY:全球合作伙伴研讨会');
|
||||
expect(icsOutput).toContain('DTSTART:20260922T090000Z');
|
||||
expect(icsOutput).toContain('DTEND:20260922T100000Z');
|
||||
expect(icsOutput).toContain('ORGANIZER;CN=Alex Zheng:mailto:alex@omnicard.ai');
|
||||
@@ -266,8 +266,8 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
it('应正确从 ICS 字符串反向提取会议核心属性', () => {
|
||||
const meeting: MeetingEventOptions = {
|
||||
uid: 'roundtrip-test-001@omnicard.ai',
|
||||
summary: '出海战略合作洽谈',
|
||||
description: '讨论微官网与海外本地化支付通道',
|
||||
summary: '全球战略合作洽谈',
|
||||
description: '讨论微官网与全球本地化支付通道',
|
||||
location: '线上 Google Meet',
|
||||
startTime: '2026-09-25T08:00:00Z',
|
||||
endTime: '2026-09-25T09:00:00Z',
|
||||
@@ -282,8 +282,8 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const parsed = parseIcsEvent(ics);
|
||||
|
||||
expect(parsed.uid).toBe('roundtrip-test-001@omnicard.ai');
|
||||
expect(parsed.summary).toBe('出海战略合作洽谈');
|
||||
expect(parsed.description).toBe('讨论微官网与海外本地化支付通道');
|
||||
expect(parsed.summary).toBe('全球战略合作洽谈');
|
||||
expect(parsed.description).toBe('讨论微官网与全球本地化支付通道');
|
||||
expect(parsed.location).toBe('线上 Google Meet');
|
||||
expect(parsed.status).toBe('CONFIRMED');
|
||||
expect(typeof parsed.organizer).toBe('object');
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
adminCardCasesCurdOption,
|
||||
} from '../../../src/modules/card/controller/admin/cases';
|
||||
|
||||
describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
describe('后台案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
const entityFile = path.join(
|
||||
__dirname,
|
||||
'../../../src/modules/card/entity/cases.ts'
|
||||
@@ -59,10 +59,10 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
});
|
||||
|
||||
describe('2. 实体字段契约 (CardCasesEntity)', () => {
|
||||
it('实体实例应支持出海案例必需契约字段', () => {
|
||||
it('实体实例应支持案例必需契约字段', () => {
|
||||
const item = new CardCasesEntity();
|
||||
item.cardId = 1001;
|
||||
item.title = '智能电动滑板车欧美 DTC 出海';
|
||||
item.title = '智能电动滑板车欧美 DTC 全球拓展';
|
||||
item.client = 'VoltRide Global';
|
||||
item.duration = '2024.01 - 2024.06';
|
||||
item.metrics = '+320% ROI, 45,000+ 留资量';
|
||||
@@ -72,7 +72,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
item.sortOrder = 1;
|
||||
|
||||
expect(item.cardId).toBe(1001);
|
||||
expect(item.title).toBe('智能电动滑板车欧美 DTC 出海');
|
||||
expect(item.title).toBe('智能电动滑板车欧美 DTC 全球拓展');
|
||||
expect(item.client).toBe('VoltRide Global');
|
||||
expect(item.duration).toBe('2024.01 - 2024.06');
|
||||
expect(item.metrics).toBe('+320% ROI, 45,000+ 留资量');
|
||||
@@ -92,7 +92,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
Object.assign(new CardCasesEntity(), {
|
||||
id: 1,
|
||||
cardId: 101,
|
||||
title: '跨境智能硬件众筹出海',
|
||||
title: '全球智能硬件众筹发布',
|
||||
client: 'TechNova',
|
||||
duration: '3个月',
|
||||
metrics: '+280% 众筹目标达成',
|
||||
@@ -103,7 +103,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
Object.assign(new CardCasesEntity(), {
|
||||
id: 2,
|
||||
cardId: 101,
|
||||
title: '东南亚 TikTok 跨境直播获客',
|
||||
title: '东南亚 TikTok 国际直播获客',
|
||||
client: 'FashionGo',
|
||||
duration: '6个月',
|
||||
metrics: '千万级曝光, +150% GMV',
|
||||
@@ -163,9 +163,9 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
it('getCasesByCardId 应根据名片 cardId 筛选并按 sortOrder 升序排列', async () => {
|
||||
const cases = await service.getCasesByCardId(101);
|
||||
expect(cases).toHaveLength(2);
|
||||
expect(cases[0].title).toBe('东南亚 TikTok 跨境直播获客');
|
||||
expect(cases[0].title).toBe('东南亚 TikTok 国际直播获客');
|
||||
expect(cases[0].sortOrder).toBe(1);
|
||||
expect(cases[1].title).toBe('跨境智能硬件众筹出海');
|
||||
expect(cases[1].title).toBe('全球智能硬件众筹发布');
|
||||
expect(cases[1].sortOrder).toBe(2);
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
it('创建案例并能查询出最新案例', async () => {
|
||||
const created = await service.cardCasesEntity.save({
|
||||
cardId: 101,
|
||||
title: '拉美电商本土化品牌出海',
|
||||
title: '拉美电商本土化品牌打造',
|
||||
client: 'LatamDirect',
|
||||
duration: '4个月',
|
||||
metrics: '+90% 复购率',
|
||||
@@ -187,7 +187,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
});
|
||||
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.title).toBe('拉美电商本土化品牌出海');
|
||||
expect(created.title).toBe('拉美电商本土化品牌打造');
|
||||
|
||||
const found = await service.cardCasesEntity.findOneBy({ id: created.id });
|
||||
expect(found).not.toBeNull();
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
themeCode: 'corporate_navy',
|
||||
// 敏感内部数据
|
||||
userId: 9999,
|
||||
remark: '重点出海客户核心高管,年费 VIP 专属',
|
||||
remark: '重点合作客户核心高管,年费 VIP 专属',
|
||||
internalConfig: '{"rateLimit": 1000, "debug": true, "internalTenantSecret": "sk_live_xyz"}',
|
||||
createTime: new Date(),
|
||||
updateTime: new Date(),
|
||||
@@ -71,7 +71,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
colSpan: 1,
|
||||
rowSpan: 1,
|
||||
sortOrder: 3,
|
||||
title: '出海 SaaS 落地指南',
|
||||
title: '全球 SaaS 落地指南',
|
||||
content: '深度解读全球数字名片与微官网搭建...',
|
||||
status: 1, // 有效
|
||||
internalNotes: '文章需由法务团队二次审核', // 敏感
|
||||
@@ -84,7 +84,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '关于我',
|
||||
content: '10 年全球出海商务拓展经验',
|
||||
content: '10 年全球商务拓展经验',
|
||||
status: 1, // 有效
|
||||
internalNotes: '置顶 Hero 块',
|
||||
} as CardBentoEntity,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PosterElementBox,
|
||||
} from '../../../src/modules/card/service/poster-layout';
|
||||
|
||||
describe('Point 54: 出海展会多语言电子邀请海报轻量级排版引擎', () => {
|
||||
describe('Point 54: 商务展会多语言电子邀请海报轻量级排版引擎', () => {
|
||||
let engine: CardPosterLayoutEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { MOCK_CARDS_DATA, MockCardBundle } from '../../../src/modules/card/mock/seed-cards';
|
||||
import { CardPortalService } from '../../../src/modules/card/service/portal';
|
||||
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
|
||||
import { CardBentoEntity } from '../../../src/modules/card/entity/bento';
|
||||
|
||||
describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试', () => {
|
||||
it('1. 应包含至少 5 组不同国际行业的高保真名片数据包', () => {
|
||||
expect(Array.isArray(MOCK_CARDS_DATA)).toBe(true);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
const slugs = MOCK_CARDS_DATA.map(item => item.card.slug);
|
||||
const uniqueSlugs = new Set(slugs);
|
||||
expect(uniqueSlugs.size).toBe(MOCK_CARDS_DATA.length); // Slug 必须唯一
|
||||
});
|
||||
|
||||
it('2. 每张名片的基础信息必须完整且配有高分辨率头像与国际化联系方式', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { card } = bundle;
|
||||
expect(card.id).toBeDefined();
|
||||
expect(card.name).toBeTruthy();
|
||||
expect(card.title).toBeTruthy();
|
||||
expect(card.company).toBeTruthy();
|
||||
expect(card.slogan).toBeTruthy();
|
||||
expect(card.status).toBe('published');
|
||||
|
||||
// 配图验证:头像必须是合法图片 URL (http/https)
|
||||
expect(card.avatarUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp|svg)(\?.*)?$/i);
|
||||
|
||||
// 国际化联系方式
|
||||
expect(card.phone).toMatch(/^\+[1-9]\d{1,14}$/); // E.164 规范
|
||||
expect(card.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||
expect(card.timezone).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('3. 每张名片必须包含有效的视频 Bento 块,且配有可播放视频流与专属海报封面', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { bentoBlocks } = bundle;
|
||||
expect(Array.isArray(bentoBlocks)).toBe(true);
|
||||
expect(bentoBlocks.length).toBeGreaterThan(0);
|
||||
|
||||
// 必须包含至少一个 video 类型的 Bento 块
|
||||
const videoBlocks = bentoBlocks.filter(b => b.type === 'video');
|
||||
expect(videoBlocks.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
videoBlocks.forEach(videoBlock => {
|
||||
expect(videoBlock.title).toBeTruthy();
|
||||
// mediaUrl 作为视频封面海报,必须是合法图片 URL
|
||||
expect(videoBlock.mediaUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
// linkUrl 或 metaJson.videoUrl 必须是可播放的视频源 (mp4/m3u8/webm)
|
||||
let videoUrl = videoBlock.linkUrl;
|
||||
if (typeof videoBlock.metaJson === 'object' && videoBlock.metaJson !== null) {
|
||||
videoUrl = (videoBlock.metaJson as any).videoUrl || videoUrl;
|
||||
} else if (typeof videoBlock.metaJson === 'string') {
|
||||
const parsed = JSON.parse(videoBlock.metaJson);
|
||||
videoUrl = parsed.videoUrl || videoUrl;
|
||||
}
|
||||
expect(videoUrl).toMatch(/^https?:\/\/.+\.(mp4|webm|m3u8)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('4. 每张名片必须包含图文案例与文章,且均配置了真实多媒体封面配图', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
// 检查案例
|
||||
expect(Array.isArray(bundle.cases)).toBe(true);
|
||||
expect(bundle.cases.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.cases.forEach(c => {
|
||||
expect(c.title).toBeTruthy();
|
||||
expect(c.client).toBeTruthy();
|
||||
expect(c.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
// 检查文章
|
||||
expect(Array.isArray(bundle.articles)).toBe(true);
|
||||
expect(bundle.articles.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.articles.forEach(a => {
|
||||
expect(a.title).toBeTruthy();
|
||||
expect(a.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染视频与图文块', async () => {
|
||||
const cardInfoRepo: any = {
|
||||
findOne: jest.fn().mockImplementation(async options => {
|
||||
const slug = options?.where?.slug;
|
||||
const id = options?.where?.id;
|
||||
const found = MOCK_CARDS_DATA.find(
|
||||
b => (slug && b.card.slug === slug) || (id && b.card.id === Number(id))
|
||||
);
|
||||
return found ? ({ ...found.card } as CardInfoEntity) : null;
|
||||
}),
|
||||
};
|
||||
|
||||
const cardBentoRepo: any = {
|
||||
find: jest.fn().mockImplementation(async options => {
|
||||
const cardId = options?.where?.cardId;
|
||||
const found = MOCK_CARDS_DATA.find(b => b.card.id === Number(cardId));
|
||||
return found ? found.bentoBlocks.map(blk => ({ ...blk } as CardBentoEntity)) : [];
|
||||
}),
|
||||
};
|
||||
|
||||
const portalService = new CardPortalService();
|
||||
portalService.cardInfoEntity = cardInfoRepo;
|
||||
portalService.cardBentoEntity = cardBentoRepo;
|
||||
CardPortalService.clearAllCache();
|
||||
|
||||
for (const bundle of MOCK_CARDS_DATA) {
|
||||
const detail = await portalService.getPortalDetail(bundle.card.slug);
|
||||
expect(detail).toBeDefined();
|
||||
expect(detail.name).toBe(bundle.card.name);
|
||||
expect(detail.avatarUrl).toBe(bundle.card.avatarUrl);
|
||||
|
||||
// 验证视频块是否存在于公开微官网聚合结果中
|
||||
const videoBlock = detail.bentoBlocks.find(b => b.type === 'video');
|
||||
expect(videoBlock).toBeDefined();
|
||||
expect(videoBlock?.mediaUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ChannelAttributionEvent,
|
||||
} from '../../../src/modules/crm/service/attribution';
|
||||
|
||||
describe('Point 46: 访客雷达多渠道来源归因与出海转化漏斗分析', () => {
|
||||
describe('Point 46: 访客雷达多渠道来源归因与转化漏斗分析', () => {
|
||||
let service: RadarAttributionService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -18,7 +18,7 @@ describe('Point 46: 访客雷达多渠道来源归因与出海转化漏斗分析
|
||||
expect(normalizeTrafficChannel('booth', 'qrcode')).toBe('booth_qr');
|
||||
});
|
||||
|
||||
it('应精准识别 WhatsApp 与 LinkedIn 社交出海渠道', () => {
|
||||
it('应精准识别 WhatsApp 与 LinkedIn 国际社交渠道', () => {
|
||||
expect(normalizeTrafficChannel('whatsapp', 'im')).toBe('whatsapp_chat');
|
||||
expect(normalizeTrafficChannel('linkedin', 'social')).toBe('linkedin_share');
|
||||
expect(normalizeTrafficChannel('email', 'sig')).toBe('email_signature');
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TrafficFilterResult,
|
||||
} from '../../../src/modules/crm/service/bot-filter';
|
||||
|
||||
describe('Point 63 (P2): 访客雷达异常流量与出海社交抓取机器爬虫清洗过滤', () => {
|
||||
describe('Point 63 (P2): 访客雷达异常流量与社交抓取机器爬虫清洗过滤', () => {
|
||||
let service: RadarBotFilterService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admi
|
||||
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
|
||||
import { RadarTrack } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('【BTP-20】线索流转自动化与出海 Webhook / 实时推送集成测试', () => {
|
||||
describe('【BTP-20】线索流转自动化与外部 Webhook / 实时推送集成测试', () => {
|
||||
let syncService: CrmSyncService;
|
||||
let pushService: CrmRadarPushService;
|
||||
let mockLead: CrmLeadEntity;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
GdprUserDataBundle,
|
||||
} from '../../../src/modules/crm/service/compliance';
|
||||
|
||||
describe('Point 53: 海外合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => {
|
||||
describe('Point 53: 国际合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => {
|
||||
let service: ComplianceGdprService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CompanyProfileResult,
|
||||
} from '../../../src/modules/crm/service/enrichment';
|
||||
|
||||
describe('Point 49: 出海潜客企业邮箱域名解析与商业主页自动富化', () => {
|
||||
describe('Point 49: 潜客企业邮箱域名解析与商业主页自动富化', () => {
|
||||
let service: LeadCompanyEnrichmentService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Point 55: 全球访客雷达国家城市坐标聚合与热力散点引
|
||||
service = new RadarGeoHeatmapService();
|
||||
});
|
||||
|
||||
describe('1. resolveCityCoordinates (出海核心商务枢纽城市经纬度解析)', () => {
|
||||
describe('1. resolveCityCoordinates (全球核心商务枢纽城市经纬度解析)', () => {
|
||||
it('应精准解析迪拜、利雅得、新加坡与伦敦经纬度', () => {
|
||||
const dubai = resolveCityCoordinates('AE', 'Dubai');
|
||||
expect(dubai).toEqual([55.27, 25.20]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LeadBehaviorTrigger,
|
||||
} from '../../../src/modules/crm/service/lead-automation';
|
||||
|
||||
describe('Point 58 (P1): 出海潜客线索行为自动化阶段推进引擎', () => {
|
||||
describe('Point 58 (P1): 潜客线索行为自动化阶段推进引擎', () => {
|
||||
let service: LeadStageAutomationService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('多租户安全闭环: 公开留资与雷达足迹自动绑定名片
|
||||
it('公开留资未显式传入 tenantId 时,必须根据 cardId 自动追溯绑定归属名片的 tenantId 和 userId', async () => {
|
||||
const lead = await leadService.submitLead({
|
||||
cardId: 101,
|
||||
name: '海外采购商',
|
||||
name: '国际采购商',
|
||||
phone: '+971-50-9876543',
|
||||
company: 'Dubai Trading LLC',
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('多租户安全闭环: 公开留资与雷达足迹自动绑定名片
|
||||
const lead = await leadService.submitLead({
|
||||
cardId: 101,
|
||||
tenantId: 999,
|
||||
name: '海外采购商',
|
||||
name: '国际采购商',
|
||||
phone: '+971-50-9876543',
|
||||
} as any);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LeadWebhookPayload,
|
||||
} from '../../../src/modules/crm/service/webhook-dispatcher';
|
||||
|
||||
describe('Point 51: 出海商机实时 Webhook 触发器与 HMAC-SHA256 签名分发', () => {
|
||||
describe('Point 51: 商机实时 Webhook 触发器与 HMAC-SHA256 签名分发', () => {
|
||||
let dispatcher: LeadWebhookDispatcher;
|
||||
const mockSecret = 'whsec_8f9a2b4c6e1d7g5h3j8k9l0m';
|
||||
const mockPayload: LeadWebhookPayload = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AdminCrmRadarController } from '../../../src/modules/crm/controller/admin/radar';
|
||||
import {
|
||||
buildWorkplaceOverview,
|
||||
calculateTrackIntentScore,
|
||||
CrmRadarService,
|
||||
} from '../../../src/modules/crm/service/radar';
|
||||
import { CrmTrackEntity } from '../../../src/modules/crm/entity/track';
|
||||
@@ -152,4 +153,97 @@ describe('商务概览工作台聚合 (getWorkplaceOverview)', () => {
|
||||
expect(res.data.kpis.viewCount).toBe(2);
|
||||
expect(controller.crmRadarService.getWorkplaceOverview).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('数据专家重构:科学意向度与会话降噪增强', () => {
|
||||
it('科学意向分算法 (calculateTrackIntentScore) 必须符合多因子阶梯模型', () => {
|
||||
// 1. 直链 0 秒跳出,无任何浏览与滚动:低意向分 (10分)
|
||||
const bounceScore = calculateTrackIntentScore({
|
||||
touchType: 'link',
|
||||
stayDurationMs: 0,
|
||||
scrollDepth: 0,
|
||||
viewedBlocks: [],
|
||||
});
|
||||
expect(bounceScore).toBe(10);
|
||||
|
||||
// 2. NFC 面对面触碰初始分:天然高权重 (35分)
|
||||
const nfcInitialScore = calculateTrackIntentScore({
|
||||
touchType: 'nfc',
|
||||
stayDurationMs: 0,
|
||||
});
|
||||
expect(nfcInitialScore).toBe(35);
|
||||
|
||||
// 3. QR 扫码 + 30秒研读 + 2个模块 + 深度滚动:高意向买家 (25 + 20 + 10 + 10 = 65分)
|
||||
const engagedScore = calculateTrackIntentScore({
|
||||
touchType: 'qr',
|
||||
stayDurationMs: 30000,
|
||||
viewedBlocks: ['hero', 'products'],
|
||||
scrollDepth: 75,
|
||||
});
|
||||
expect(engagedScore).toBe(65);
|
||||
|
||||
// 4. NFC 触碰 + 150秒深读 + 4个模块 + 深度滚动:极高意向买家 (封顶 99分)
|
||||
const hotScore = calculateTrackIntentScore({
|
||||
touchType: 'nfc',
|
||||
stayDurationMs: 150000,
|
||||
viewedBlocks: ['hero', 'products', 'cases', 'contact'],
|
||||
scrollDepth: 95,
|
||||
});
|
||||
expect(hotScore).toBe(99);
|
||||
});
|
||||
|
||||
it('buildWorkplaceOverview 应实现会话去噪与合并,并填充名片名称和本地标记', () => {
|
||||
const cardList = [
|
||||
{ id: 1, name: 'Alex Chen', title: '全球商务总监' },
|
||||
];
|
||||
|
||||
// 同一访客 vis_101 先产生了 0 秒 PV,又更新了 45 秒有效停留
|
||||
const t1 = track({
|
||||
id: 10,
|
||||
cardId: 1,
|
||||
touchType: 'link',
|
||||
country: 'Local',
|
||||
city: 'Local',
|
||||
stayDurationMs: 0,
|
||||
createTime: new Date('2026-09-21T10:00:00'),
|
||||
});
|
||||
t1.visitorId = 'vis_101';
|
||||
|
||||
const t2 = track({
|
||||
id: 11,
|
||||
cardId: 1,
|
||||
touchType: 'link',
|
||||
country: 'Local',
|
||||
city: 'Local',
|
||||
stayDurationMs: 45000,
|
||||
viewedBlocks: ['products'],
|
||||
createTime: new Date('2026-09-21T10:01:00'),
|
||||
});
|
||||
t2.visitorId = 'vis_101';
|
||||
|
||||
const result = buildWorkplaceOverview(
|
||||
[t1, t2],
|
||||
[],
|
||||
now,
|
||||
10,
|
||||
cardList
|
||||
);
|
||||
|
||||
// 验证 1:去噪合并后同一会话只保留 1 条记录
|
||||
expect(result.radarFeed).toHaveLength(1);
|
||||
const feedItem = result.radarFeed[0];
|
||||
|
||||
// 验证 2:停留时长采用合并后的最高值 45 秒
|
||||
expect(feedItem.stayDurationMs).toBe(45000);
|
||||
expect(feedItem.viewedBlocks).toContain('products');
|
||||
|
||||
// 验证 3:名片名称正确注入
|
||||
expect(feedItem.cardTitle).toBe('Alex Chen · 全球商务总监');
|
||||
|
||||
// 验证 4:识别为本地环境
|
||||
expect(feedItem.isLocal).toBe(true);
|
||||
|
||||
// 验证 5:意向分由后端自动算出并大于 0 秒跳出分
|
||||
expect(feedItem.intentScore).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
canTransitionBatchStatus,
|
||||
} from '../../../src/modules/nfc/service/manufacturing';
|
||||
|
||||
describe('Point 45: 跨境实体卡供应链生产批次工单生命周期与良率检验', () => {
|
||||
describe('Point 45: 国际实体卡供应链生产批次工单生命周期与良率检验', () => {
|
||||
let service: NfcManufacturingService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../src/modules/team/service/currency-tax';
|
||||
import { TaxCalculationResult } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算', () => {
|
||||
describe('Point 57 (P1): 跨国多币种汇率换算与国际发票税制计算', () => {
|
||||
let service: CurrencyTaxCalculatorService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -35,7 +35,7 @@ describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算'
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculateInternationalTax (出海合规发票税制计算)', () => {
|
||||
describe('2. calculateInternationalTax (国际合规发票税制计算)', () => {
|
||||
it('阿联酋企业或个人消费应按 5% VAT 计税', () => {
|
||||
const res = calculateInternationalTax({
|
||||
amount: 1000,
|
||||
@@ -50,7 +50,7 @@ describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算'
|
||||
expect(res.taxName).toBe('UAE VAT');
|
||||
});
|
||||
|
||||
it('欧盟企业间跨境 B2B (Reverse Charge) 应适用反向征税 0% VAT', () => {
|
||||
it('欧盟企业间跨国 B2B (Reverse Charge) 应适用反向征税 0% VAT', () => {
|
||||
const res = calculateInternationalTax({
|
||||
amount: 2000,
|
||||
currency: 'EUR',
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
assertLeadAccessPermission,
|
||||
} from '../../../src/modules/team/service/data-scope';
|
||||
|
||||
describe('Point 52: 出海多租户团队成员细粒度数据隔离与销售管辖范围保护', () => {
|
||||
describe('Point 52: 多租户团队成员细粒度数据隔离与销售管辖范围保护', () => {
|
||||
let service: TeamDataScopeService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('【财务与运营新账统计】TeamFinanceAnalyticsService 单元测
|
||||
expect(Array.isArray(result.planDistribution)).toBe(true);
|
||||
expect(result.planDistribution.some((item) => item.plan === 'pro')).toBe(true);
|
||||
|
||||
// 出海区域分布
|
||||
// 国际区域分布
|
||||
expect(Array.isArray(result.regionalRevenue)).toBe(true);
|
||||
|
||||
// 流水明细
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Point 19: 离职资产交接工单流程与历史线索迁移 单元
|
||||
lead1.id = 1;
|
||||
lead1.userId = 1001;
|
||||
lead1.cardId = 101;
|
||||
lead1.name = '重点出海客户 A';
|
||||
lead1.name = '重点合作客户 A';
|
||||
lead1.isPool = false;
|
||||
lead1.status = 'contacted';
|
||||
leads.push(lead1);
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Point 28: 展会现场团队获客实时排行榜大屏广播 (Live Ra
|
||||
u1.id = 101;
|
||||
u1.name = 'Alex Rivera';
|
||||
u1.tenantId = 1;
|
||||
u1.departmentName = '出海一队';
|
||||
u1.departmentName = '国际业务一队';
|
||||
|
||||
const c1 = new CardInfoEntity();
|
||||
c1.id = 1;
|
||||
@@ -103,7 +103,7 @@ describe('Point 28: 展会现场团队获客实时排行榜大屏广播 (Live Ra
|
||||
u2.id = 102;
|
||||
u2.name = 'Sarah Wang';
|
||||
u2.tenantId = 1;
|
||||
u2.departmentName = '出海二队';
|
||||
u2.departmentName = '国际业务二队';
|
||||
|
||||
const c2 = new CardInfoEntity();
|
||||
c2.id = 2;
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
users = [];
|
||||
cards = [];
|
||||
|
||||
// 预置部门数据:总部 (1) -> 研发中心 (2) -> 前端组 (3); 总部 (1) -> 销售出海中心 (4)
|
||||
// 预置部门数据:总部 (1) -> 研发中心 (2) -> 前端组 (3); 总部 (1) -> 国际业务拓展部 (4)
|
||||
const d1 = new BaseSysDepartmentEntity();
|
||||
d1.id = 1;
|
||||
d1.name = '全球商务总部';
|
||||
@@ -56,7 +56,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
|
||||
const d2 = new BaseSysDepartmentEntity();
|
||||
d2.id = 2;
|
||||
d2.name = '海外研发中心';
|
||||
d2.name = '国际研发中心';
|
||||
d2.parentId = 1;
|
||||
d2.orderNum = 10;
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
|
||||
const d4 = new BaseSysDepartmentEntity();
|
||||
d4.id = 4;
|
||||
d4.name = '出海业务拓展部';
|
||||
d4.name = '国际业务拓展部';
|
||||
d4.parentId = 1;
|
||||
d4.orderNum = 20;
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
expect(root.title).toBe('全球商务总部');
|
||||
expect(root.children?.length).toBe(2);
|
||||
|
||||
// children 排序应为 海外研发中心 (orderNum: 10) 和 出海业务拓展部 (orderNum: 20)
|
||||
// children 排序应为 国际研发中心 (orderNum: 10) 和 国际业务拓展部 (orderNum: 20)
|
||||
expect(root.children?.[0].id).toBe(2);
|
||||
expect(root.children?.[0].children?.length).toBe(1); // 前端组 (3)
|
||||
expect(root.children?.[0].children?.[0].id).toBe(3);
|
||||
@@ -211,7 +211,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
// 人数统计验证
|
||||
expect(root.memberCount).toBe(1); // u1 在总部
|
||||
expect(root.children?.[0].children?.[0].memberCount).toBe(1); // u2 在前端组
|
||||
expect(root.children?.[1].memberCount).toBe(1); // u3 在出海业务部
|
||||
expect(root.children?.[1].memberCount).toBe(1); // u3 在国际业务部
|
||||
});
|
||||
|
||||
it('2.2 addDepartment 校验部门名称必填并成功入库', async () => {
|
||||
|
||||
@@ -34,6 +34,15 @@ type ArticleDetailState = {
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
||||
authorName?: string;
|
||||
authorTitle?: string;
|
||||
authorCompany?: string;
|
||||
authorAvatar?: string;
|
||||
readTimeMinutes?: number;
|
||||
summaryText?: string;
|
||||
scrollDepth?: number;
|
||||
enterTime?: number;
|
||||
visitorId?: string;
|
||||
};
|
||||
|
||||
type ArticleShareFields = {
|
||||
@@ -65,6 +74,15 @@ type ArticleDetailModule = {
|
||||
getArticleShareFields: (state: ArticleDetailState, pageUrl: string) => ArticleShareFields;
|
||||
loadArticleDetail: (state: ArticleDetailState) => Promise<BentoBlockFixture | null>;
|
||||
copyArticleShareLink: (state: ArticleDetailState, pageUrl?: string) => boolean;
|
||||
resolveArticleDetailText: (key: string, locale?: string) => string;
|
||||
updateArticleScrollDepth: (
|
||||
state: ArticleDetailState,
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
windowHeight: number
|
||||
) => number;
|
||||
reportArticleStay: (state: ArticleDetailState, scrollDepth?: number) => Promise<unknown>;
|
||||
navToAuthorPortal: (state: ArticleDetailState) => void;
|
||||
};
|
||||
|
||||
type UniToastOption = {
|
||||
@@ -453,4 +471,112 @@ describe('OmniCard AI 用户端文章详情业务逻辑测试 (useArticleDetail.
|
||||
expect(toastCalls.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 专栏作者微展台与核心摘要导读', () => {
|
||||
it('4.1 加载文章时能一并解析名片作者信息与核心摘要', async () => {
|
||||
const {
|
||||
createArticleDetailState,
|
||||
applyArticleDetailOptions,
|
||||
loadArticleDetail,
|
||||
} = loadUseArticleDetail();
|
||||
|
||||
const state = createArticleDetailState();
|
||||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||||
await loadArticleDetail(state);
|
||||
|
||||
expect(state.authorName).toBe('Alex Vance');
|
||||
expect(state.authorTitle).toBe('Managing Director & Global Partner');
|
||||
expect(state.authorCompany).toBe('Nexus Tech Global Ltd');
|
||||
expect(state.readTimeMinutes).toBeGreaterThanOrEqual(1);
|
||||
expect(state.summaryText?.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 阅读滑屏深度计算与雷达停留时长上报', () => {
|
||||
it('5.1 updateArticleScrollDepth 正确计算并单调递增滑屏百分比', () => {
|
||||
const { createArticleDetailState, updateArticleScrollDepth } = loadUseArticleDetail();
|
||||
const state = createArticleDetailState();
|
||||
|
||||
expect(state.scrollDepth).toBe(0);
|
||||
// 顶部
|
||||
updateArticleScrollDepth(state, 0, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(0);
|
||||
|
||||
// 中间 (scrollTop = 700 / maxScroll = 1400 => 50%)
|
||||
updateArticleScrollDepth(state, 700, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(50);
|
||||
|
||||
// 向上回滚时保持最大深度
|
||||
updateArticleScrollDepth(state, 200, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(50);
|
||||
|
||||
// 触底 (100%)
|
||||
updateArticleScrollDepth(state, 1400, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(100);
|
||||
});
|
||||
|
||||
it('5.2 reportArticleStay 将停留时长与阅读深度正确上报至雷达', async () => {
|
||||
const {
|
||||
createArticleDetailState,
|
||||
applyArticleDetailOptions,
|
||||
loadArticleDetail,
|
||||
reportArticleStay,
|
||||
} = loadUseArticleDetail();
|
||||
|
||||
const state = createArticleDetailState();
|
||||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||||
await loadArticleDetail(state);
|
||||
|
||||
mockRequestResponse = {
|
||||
statusCode: 200,
|
||||
data: {
|
||||
code: 1000,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 'track-1',
|
||||
cardId: 'card-101',
|
||||
visitorId: 'vis-test',
|
||||
touchType: 'link',
|
||||
stayDurationMs: 1200,
|
||||
scrollDepth: 75,
|
||||
viewedBlocks: ['3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
state.enterTime = Date.now() - 1500;
|
||||
state.scrollDepth = 75;
|
||||
|
||||
await reportArticleStay(state, 75);
|
||||
|
||||
expect(lastRequestOptions?.url).toContain('/open/crm/radar/track');
|
||||
const body = lastRequestOptions?.data as Record<string, unknown>;
|
||||
expect(body.cardId).toBe('card-101');
|
||||
expect(body.touchType).toBe('link');
|
||||
expect(body.scrollDepth).toBe(75);
|
||||
expect(body.viewedBlocks).toEqual(['3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('6. 多语言本地化文案体系', () => {
|
||||
it('6.1 覆盖 zh-CN, en-US, ar-SA, es-ES 等语种的核心文案', () => {
|
||||
const { resolveArticleDetailText } = loadUseArticleDetail();
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'zh-CN')).toBe('深度洞见');
|
||||
expect(resolveArticleDetailText('summary', 'zh-CN')).toBe('核心导读');
|
||||
expect(resolveArticleDetailText('author', 'zh-CN')).toBe('专栏作者');
|
||||
expect(resolveArticleDetailText('viewPortal', 'zh-CN')).toBe('查看微官网');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'en-US')).toBe('Thought Leadership');
|
||||
expect(resolveArticleDetailText('summary', 'en-US')).toBe('Key Takeaways');
|
||||
expect(resolveArticleDetailText('author', 'en-US')).toBe('Author');
|
||||
expect(resolveArticleDetailText('viewPortal', 'en-US')).toBe('View Profile');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'ar-SA')).toBe('رؤى متعمقة');
|
||||
expect(resolveArticleDetailText('summary', 'ar-SA')).toBe('أبرز النقاط');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'es-ES')).toBe('Perspectivas');
|
||||
expect(resolveArticleDetailText('summary', 'es-ES')).toBe('Puntos clave');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,12 @@ type MockBentoBlock = {
|
||||
metrics?: CaseKpi[];
|
||||
pdfUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
client?: string;
|
||||
industry?: string;
|
||||
region?: string;
|
||||
challenge?: string;
|
||||
solution?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -89,6 +95,8 @@ type UseCaseDetailModule = {
|
||||
buildExchangePath: (cardId: string) => string;
|
||||
openCaseExchange: (cardId: string) => void;
|
||||
goCaseBack: () => void;
|
||||
copyCaseExternalLink: (url: string) => boolean;
|
||||
resolveCaseDetailText: (key: string) => string;
|
||||
};
|
||||
|
||||
function loadUseCaseDetail(): UseCaseDetailModule {
|
||||
@@ -371,4 +379,72 @@ describe('OmniCard AI 用户端案例详情业务逻辑测试 (useCaseDetail.uts
|
||||
expect(navigateBackCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. Bento Grid 模块化结构化数据与国际化解析', () => {
|
||||
it('5.1 支持从 metaJson 解析 client, industry, region, challenge, solution', () => {
|
||||
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
|
||||
loadUseCaseDetail();
|
||||
const card = buildMockPortal();
|
||||
card.bentoBlocks[1].metaJson = {
|
||||
client: 'Global Logistics Corp',
|
||||
industry: 'Supply Chain IoT',
|
||||
region: 'Middle East & North Africa',
|
||||
challenge: 'High latency in cross-border tracking and data silos.',
|
||||
solution: 'Deployed dual-frequency NFC sensors and cloud-native mesh.',
|
||||
kpis: [{ label: 'On-time Delivery', value: '99.8%' }],
|
||||
};
|
||||
|
||||
const state = createCaseDetailState();
|
||||
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
|
||||
applyPortalCase(state, card);
|
||||
|
||||
expect((state as any).client).toBe('Global Logistics Corp');
|
||||
expect((state as any).industry).toBe('Supply Chain IoT');
|
||||
expect((state as any).region).toBe('Middle East & North Africa');
|
||||
expect((state as any).challenge).toBe(
|
||||
'High latency in cross-border tracking and data silos.'
|
||||
);
|
||||
expect((state as any).solution).toBe(
|
||||
'Deployed dual-frequency NFC sensors and cloud-native mesh.'
|
||||
);
|
||||
expect(state.kpis).toEqual([{ label: 'On-time Delivery', value: '99.8%' }]);
|
||||
});
|
||||
|
||||
it('5.2 resolveCaseDetailText 正确解析新增的 Bento 模组词条', () => {
|
||||
const { resolveCaseDetailText } = loadUseCaseDetail();
|
||||
|
||||
expect(resolveCaseDetailText('badge')).toBeDefined();
|
||||
expect(resolveCaseDetailText('overview')).toBeDefined();
|
||||
expect(resolveCaseDetailText('challenge')).toBeDefined();
|
||||
expect(resolveCaseDetailText('solution')).toBeDefined();
|
||||
expect(resolveCaseDetailText('kpis')).toBeDefined();
|
||||
expect(resolveCaseDetailText('resources')).toBeDefined();
|
||||
expect(resolveCaseDetailText('pdfAction')).toBeDefined();
|
||||
expect(resolveCaseDetailText('videoAction')).toBeDefined();
|
||||
});
|
||||
|
||||
it('5.3 copyCaseExternalLink 触发 uni.setClipboardData 与 Toast 提示', () => {
|
||||
let clipboardText = '';
|
||||
let toastCalled = false;
|
||||
(global as { uni?: Record<string, unknown> }).uni = {
|
||||
setClipboardData: (opts: { data: string; success?: () => void }) => {
|
||||
clipboardText = opts.data;
|
||||
opts.success?.();
|
||||
},
|
||||
showToast: () => {
|
||||
toastCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const { copyCaseExternalLink } = loadUseCaseDetail();
|
||||
const res = copyCaseExternalLink('https://cdn.omnicard.ai/doc.pdf');
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(clipboardText).toBe('https://cdn.omnicard.ai/doc.pdf');
|
||||
expect(toastCalled).toBe(true);
|
||||
|
||||
const emptyRes = copyCaseExternalLink('');
|
||||
expect(emptyRes).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 严格遵循 TDD 测试驱动规范
|
||||
*/
|
||||
|
||||
describe('Point 56: 移动端 App / H5 出海名片 Universal Link 深度链接路由分发解析器', () => {
|
||||
describe('Point 56: 移动端 App / H5 智能数字名片 Universal Link 深度链接路由分发解析器', () => {
|
||||
let router: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 访客端多语言动态切换与 RTL 镜像契约测试 (TDD)
|
||||
* 严格按照 AGENTS.md 与出海国际化 WCAG 标准规范
|
||||
* 严格按照 AGENTS.md 与国际化 WCAG 标准规范
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
@@ -173,7 +173,7 @@ describe('Point 04: 访客端多语言动态切换与 RTL 镜像无感适配', (
|
||||
});
|
||||
|
||||
describe('4. 默认 GeoIP 语言回退 (resolveGeoLocale)', () => {
|
||||
it('根据出海访客的国家码准确映射主流语言或回退 en-US', () => {
|
||||
it('根据访客的国家码准确映射主流语言或回退 en-US', () => {
|
||||
const { resolveGeoLocale } = require(LOCALE_UTS) as {
|
||||
resolveGeoLocale: (countryCode?: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 移动微官网离线 LocalStorage 极速预加载与骨架屏单测套件 (TDD)
|
||||
* 严格遵循 AGENTS.md 规范与出海高性能架构契约
|
||||
* 严格遵循 AGENTS.md 规范与高性能架构契约
|
||||
*/
|
||||
|
||||
export interface MockBentoBlock {
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('uni-app 框架自带原生 tabBar 规范测试 (TDD Red -> Green)', ()
|
||||
|
||||
// 关键规范:tabBar.list[0].pagePath 必须与 pages[0].path 一致,原生 tabBar 才能在首屏正常加载
|
||||
expect(config.tabBar.list[0].pagePath).toBe(config.pages[0].path);
|
||||
expect(config.tabBar.list[0].text).toBe('官网');
|
||||
expect(config.tabBar.list[0].text).toBe('名片');
|
||||
|
||||
// 校验颜色 Token
|
||||
expect(config.tabBar.color.toLowerCase()).toBe('#667085');
|
||||
|
||||
在新工单中引用
屏蔽一个用户