1309 行
52 KiB
TypeScript
1309 行
52 KiB
TypeScript
import * as crypto from 'crypto';
|
||
import * as AdmZip from 'adm-zip';
|
||
|
||
// ==========================================
|
||
// 1. 实体与契约导入
|
||
// ==========================================
|
||
import { CardInfoEntity } from '../../src/modules/card/entity/info';
|
||
import { CardBentoEntity } from '../../src/modules/card/entity/bento';
|
||
import { CardCasesEntity } from '../../src/modules/card/entity/cases';
|
||
import { CardArticleEntity } from '../../src/modules/card/entity/article';
|
||
import { CardLocaleEntity } from '../../src/modules/card/entity/locale';
|
||
import { CrmTrackEntity } from '../../src/modules/crm/entity/track';
|
||
import { CrmLeadEntity } from '../../src/modules/crm/entity/lead';
|
||
import { CrmLeadAuditEntity } from '../../src/modules/crm/entity/audit';
|
||
import { AiConciergeEntity } from '../../src/modules/ai/entity/concierge';
|
||
import { AiGlossaryEntity } from '../../src/modules/ai/entity/glossary';
|
||
import { NfcDeviceEntity } from '../../src/modules/nfc/entity/device';
|
||
import { NfcRuleEntity } from '../../src/modules/nfc/entity/rule';
|
||
import { BaseSysDepartmentEntity } from '../../src/modules/base/entity/sys/department';
|
||
import { BaseSysUserEntity } from '../../src/modules/base/entity/sys/user';
|
||
import { TeamDomainEntity } from '../../src/modules/team/entity/domain';
|
||
import { TeamSubscriptionEntity } from '../../src/modules/team/entity/subscription';
|
||
import { TeamInvoiceEntity } from '../../src/modules/team/entity/invoice';
|
||
import { TeamHandoverLogEntity } from '../../src/modules/team/entity/handover-log';
|
||
|
||
import {
|
||
BentoBlockType,
|
||
BentoColSpan,
|
||
BentoRowSpan,
|
||
RadarTrack,
|
||
StripeWebhookEvent,
|
||
} from '../../src/comm/types/omni';
|
||
|
||
// ==========================================
|
||
// 2. 真实服务类导入 (全真实调用,拒绝 mock 空壳)
|
||
// ==========================================
|
||
import { CardBentoService } from '../../src/modules/card/service/bento';
|
||
import { CardArticleService } from '../../src/modules/card/service/article';
|
||
import { CardLocaleService } from '../../src/modules/card/service/locale';
|
||
import {
|
||
CardCalendarService,
|
||
generateIcsEvent,
|
||
parseIcsEvent,
|
||
} from '../../src/modules/card/service/calendar';
|
||
import { CardPortalService } from '../../src/modules/card/service/portal';
|
||
import { NfcAntiTamperService } from '../../src/modules/nfc/service/anti-tamper';
|
||
import { CrmRadarService } from '../../src/modules/crm/service/radar';
|
||
import { CrmRadarPushService, RadarPushPayload } from '../../src/modules/crm/service/push';
|
||
import { AiConciergeService } from '../../src/modules/ai/service/concierge';
|
||
import { AiKnowledgeService } from '../../src/modules/ai/service/knowledge';
|
||
import { AiGlossaryService } from '../../src/modules/ai/service/glossary';
|
||
import { AiChatService } from '../../src/modules/ai/service/chat';
|
||
import { CrmLeadScoreService } from '../../src/modules/crm/service/lead-score';
|
||
import { CrmSyncService } from '../../src/modules/crm/service/sync';
|
||
import { CrmLeadPoolService } from '../../src/modules/crm/service/lead-pool';
|
||
import { TeamHandoverService } from '../../src/modules/team/service/handover';
|
||
import { NfcDeviceService } from '../../src/modules/nfc/service/device';
|
||
import { NfcRuleService } from '../../src/modules/nfc/service/rule';
|
||
import { NfcSecurityService } from '../../src/modules/nfc/service/security';
|
||
import { NfcBatchExportService } from '../../src/modules/nfc/service/batch-export';
|
||
import { TeamMembersService } from '../../src/modules/team/service/members';
|
||
import { TeamDomainService } from '../../src/modules/team/service/domain';
|
||
import { TeamStripeService } from '../../src/modules/team/service/stripe';
|
||
import { TeamQuotaService } from '../../src/modules/team/service/quota';
|
||
import { TeamInvoiceService } from '../../src/modules/team/service/invoice';
|
||
|
||
/**
|
||
* 高保真通用的内存仓储工厂,真实执行增删改查、排序、OR/AND过滤及关联查找
|
||
*/
|
||
class MemoryRepository<T extends { id?: any; createTime?: any; updateTime?: any }> {
|
||
public data: T[] = [];
|
||
private currentId = 1;
|
||
|
||
constructor(initial: Partial<T>[] = []) {
|
||
this.data = initial.map(item => this.createEntity(item));
|
||
if (this.data.length > 0) {
|
||
this.currentId = Math.max(...this.data.map(d => Number(d.id) || 0)) + 1;
|
||
}
|
||
}
|
||
|
||
private createEntity(item: Partial<T>): T {
|
||
const entity = {
|
||
id: item.id || this.currentId++,
|
||
createTime: item.createTime || new Date(),
|
||
updateTime: new Date(),
|
||
status: (item as any).status !== undefined ? (item as any).status : 1,
|
||
...item,
|
||
} as unknown as T;
|
||
return entity;
|
||
}
|
||
|
||
private matchCondition(item: any, where: any): boolean {
|
||
if (!where) return true;
|
||
if (Array.isArray(where)) {
|
||
return where.some(cond => this.matchCondition(item, cond));
|
||
}
|
||
for (const key of Object.keys(where)) {
|
||
const expected = where[key];
|
||
if (expected === undefined) continue;
|
||
|
||
// 支持 TypeORM 操作符或带 _value 的过滤
|
||
if (expected && typeof expected === 'object' && ('_value' in expected || '_type' in expected)) {
|
||
const val = expected._value !== undefined ? expected._value : expected;
|
||
if (expected._type === 'moreThanOrEqual' || expected._type === 'moreThan') {
|
||
if (new Date(item[key]) < new Date(val)) return false;
|
||
} else if (item[key] !== val) {
|
||
return false;
|
||
}
|
||
} else if (item[key] !== expected) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async find(options?: any): Promise<T[]> {
|
||
let result = [...this.data];
|
||
if (options?.where) {
|
||
result = result.filter(item => this.matchCondition(item, options.where));
|
||
}
|
||
|
||
if (options?.order) {
|
||
const orderKeys = Object.keys(options.order);
|
||
result.sort((a: any, b: any) => {
|
||
for (const key of orderKeys) {
|
||
const dir = String(options.order[key] || 'ASC').toUpperCase();
|
||
if (a[key] < b[key]) return dir === 'ASC' ? -1 : 1;
|
||
if (a[key] > b[key]) return dir === 'ASC' ? 1 : -1;
|
||
}
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
return result.map(d => ({ ...d }));
|
||
}
|
||
|
||
async findOne(options?: any): Promise<T | null> {
|
||
const list = await this.find(options);
|
||
return list.length > 0 ? ({ ...list[0] } as T) : null;
|
||
}
|
||
|
||
async findOneBy(where: any): Promise<T | null> {
|
||
return this.findOne({ where });
|
||
}
|
||
|
||
async save(entityOrEntities: any): Promise<any> {
|
||
if (Array.isArray(entityOrEntities)) {
|
||
const list: T[] = [];
|
||
for (const item of entityOrEntities) {
|
||
list.push(await this.save(item));
|
||
}
|
||
return list;
|
||
}
|
||
|
||
const item = entityOrEntities;
|
||
if (item.id) {
|
||
const index = this.data.findIndex(d => Number(d.id) === Number(item.id));
|
||
if (index !== -1) {
|
||
this.data[index] = {
|
||
...this.data[index],
|
||
...item,
|
||
updateTime: new Date(),
|
||
};
|
||
return { ...this.data[index] };
|
||
}
|
||
}
|
||
|
||
const newEntity = this.createEntity(item);
|
||
this.data.push(newEntity);
|
||
return { ...newEntity };
|
||
}
|
||
|
||
async insert(item: any): Promise<{ identifiers: [{ id: any }] }> {
|
||
const saved = await this.save(item);
|
||
return { identifiers: [{ id: saved.id }] };
|
||
}
|
||
|
||
async count(options?: any): Promise<number> {
|
||
const list = await this.find(options);
|
||
return list.length;
|
||
}
|
||
|
||
async delete(condition: any): Promise<{ affected?: number }> {
|
||
const initialLen = this.data.length;
|
||
if (typeof condition === 'number' || typeof condition === 'string') {
|
||
this.data = this.data.filter(d => Number(d.id) !== Number(condition));
|
||
} else if (Array.isArray(condition)) {
|
||
const idSet = new Set(condition.map(id => Number(id)));
|
||
this.data = this.data.filter(d => !idSet.has(Number(d.id)));
|
||
} else if (condition && typeof condition === 'object') {
|
||
this.data = this.data.filter(item => !this.matchCondition(item, condition));
|
||
}
|
||
return { affected: initialLen - this.data.length };
|
||
}
|
||
|
||
async update(criteria: any, partial: any): Promise<{ affected?: number }> {
|
||
const id = typeof criteria === 'object' ? criteria.id : criteria;
|
||
const target = this.data.find(d => Number(d.id) === Number(id));
|
||
if (target) {
|
||
if (typeof criteria === 'object') {
|
||
for (const k of Object.keys(criteria)) {
|
||
if (target[k] !== criteria[k]) return { affected: 0 };
|
||
}
|
||
}
|
||
Object.assign(target, partial, { updateTime: new Date() });
|
||
return { affected: 1 };
|
||
}
|
||
return { affected: 0 };
|
||
}
|
||
|
||
clear(): void {
|
||
this.data = [];
|
||
this.currentId = 1;
|
||
}
|
||
}
|
||
|
||
describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||
// 全局持久化数据仓储 (在 6 大主干流各环节中无缝贯通)
|
||
let cardRepo: MemoryRepository<CardInfoEntity>;
|
||
let bentoRepo: MemoryRepository<CardBentoEntity>;
|
||
let casesRepo: MemoryRepository<CardCasesEntity>;
|
||
let articleRepo: MemoryRepository<CardArticleEntity>;
|
||
let localeRepo: MemoryRepository<CardLocaleEntity>;
|
||
let trackRepo: MemoryRepository<CrmTrackEntity>;
|
||
let leadRepo: MemoryRepository<CrmLeadEntity>;
|
||
let leadAuditRepo: MemoryRepository<CrmLeadAuditEntity>;
|
||
let conciergeRepo: MemoryRepository<AiConciergeEntity>;
|
||
let glossaryRepo: MemoryRepository<AiGlossaryEntity>;
|
||
let nfcDeviceRepo: MemoryRepository<NfcDeviceEntity>;
|
||
let nfcRuleRepo: MemoryRepository<NfcRuleEntity>;
|
||
let deptRepo: MemoryRepository<BaseSysDepartmentEntity>;
|
||
let userRepo: MemoryRepository<BaseSysUserEntity>;
|
||
let domainRepo: MemoryRepository<TeamDomainEntity>;
|
||
let subRepo: MemoryRepository<TeamSubscriptionEntity>;
|
||
let invoiceRepo: MemoryRepository<TeamInvoiceEntity>;
|
||
let handoverLogRepo: MemoryRepository<TeamHandoverLogEntity>;
|
||
|
||
// 业务服务实例 (真实逻辑)
|
||
let bentoService: CardBentoService;
|
||
let articleService: CardArticleService;
|
||
let localeService: CardLocaleService;
|
||
let calendarService: CardCalendarService;
|
||
let portalService: CardPortalService;
|
||
let nfcAntiTamperService: NfcAntiTamperService;
|
||
let crmRadarService: CrmRadarService;
|
||
let crmRadarPushService: CrmRadarPushService;
|
||
let aiConciergeService: AiConciergeService;
|
||
let aiKnowledgeService: AiKnowledgeService;
|
||
let aiGlossaryService: AiGlossaryService;
|
||
let aiChatService: AiChatService;
|
||
let crmLeadScoreService: CrmLeadScoreService;
|
||
let crmSyncService: CrmSyncService;
|
||
let crmLeadPoolService: CrmLeadPoolService;
|
||
let teamHandoverService: TeamHandoverService;
|
||
let nfcDeviceService: NfcDeviceService;
|
||
let nfcRuleService: NfcRuleService;
|
||
let nfcSecurityService: NfcSecurityService;
|
||
let nfcBatchExportService: NfcBatchExportService;
|
||
let teamMembersService: TeamMembersService;
|
||
let teamDomainService: TeamDomainService;
|
||
let teamStripeService: TeamStripeService;
|
||
let teamQuotaService: TeamQuotaService;
|
||
let teamInvoiceService: TeamInvoiceService;
|
||
|
||
// 全链路贯穿共享上下文数据 (跨流传递)
|
||
let sharedTenantId: number;
|
||
let sharedCardId: number;
|
||
let sharedCardSlug: string;
|
||
let sharedSalesUserId: number;
|
||
let sharedSuccessorUserId: number;
|
||
let sharedNfcUid: string;
|
||
let sharedNfcSecret: string;
|
||
let sharedDeviceToken: string;
|
||
let sharedVisitorId: string;
|
||
let sharedLeadId: number;
|
||
|
||
beforeAll(() => {
|
||
// 1. 初始化所有仓储
|
||
cardRepo = new MemoryRepository<CardInfoEntity>();
|
||
bentoRepo = new MemoryRepository<CardBentoEntity>();
|
||
casesRepo = new MemoryRepository<CardCasesEntity>();
|
||
articleRepo = new MemoryRepository<CardArticleEntity>();
|
||
localeRepo = new MemoryRepository<CardLocaleEntity>();
|
||
trackRepo = new MemoryRepository<CrmTrackEntity>();
|
||
leadRepo = new MemoryRepository<CrmLeadEntity>();
|
||
leadAuditRepo = new MemoryRepository<CrmLeadAuditEntity>();
|
||
conciergeRepo = new MemoryRepository<AiConciergeEntity>();
|
||
glossaryRepo = new MemoryRepository<AiGlossaryEntity>();
|
||
nfcDeviceRepo = new MemoryRepository<NfcDeviceEntity>();
|
||
nfcRuleRepo = new MemoryRepository<NfcRuleEntity>();
|
||
deptRepo = new MemoryRepository<BaseSysDepartmentEntity>();
|
||
userRepo = new MemoryRepository<BaseSysUserEntity>();
|
||
domainRepo = new MemoryRepository<TeamDomainEntity>();
|
||
subRepo = new MemoryRepository<TeamSubscriptionEntity>();
|
||
invoiceRepo = new MemoryRepository<TeamInvoiceEntity>();
|
||
handoverLogRepo = new MemoryRepository<TeamHandoverLogEntity>();
|
||
|
||
// 2. 初始化各真实业务 Service 并挂载真实仓储
|
||
bentoService = new CardBentoService();
|
||
bentoService.cardBentoEntity = bentoRepo as any;
|
||
|
||
articleService = new CardArticleService();
|
||
articleService.cardArticleEntity = articleRepo as any;
|
||
|
||
localeService = new CardLocaleService();
|
||
localeService.cardLocaleEntity = localeRepo as any;
|
||
|
||
calendarService = new CardCalendarService();
|
||
|
||
portalService = new CardPortalService();
|
||
portalService.cardInfoEntity = cardRepo as any;
|
||
portalService.cardBentoEntity = bentoRepo as any;
|
||
CardPortalService.clearAllCache();
|
||
|
||
nfcAntiTamperService = new NfcAntiTamperService();
|
||
|
||
crmRadarService = new CrmRadarService();
|
||
crmRadarService.crmTrackEntity = trackRepo as any;
|
||
|
||
crmRadarPushService = new CrmRadarPushService();
|
||
|
||
aiConciergeService = new AiConciergeService();
|
||
aiConciergeService.aiConciergeEntity = conciergeRepo as any;
|
||
|
||
aiKnowledgeService = new AiKnowledgeService();
|
||
|
||
aiGlossaryService = new AiGlossaryService();
|
||
aiGlossaryService.aiGlossaryEntity = glossaryRepo as any;
|
||
|
||
aiChatService = new AiChatService();
|
||
|
||
crmLeadScoreService = new CrmLeadScoreService();
|
||
|
||
crmSyncService = new CrmSyncService();
|
||
|
||
crmLeadPoolService = new CrmLeadPoolService();
|
||
crmLeadPoolService.crmLeadEntity = leadRepo as any;
|
||
crmLeadPoolService.crmLeadAuditEntity = leadAuditRepo as any;
|
||
|
||
teamHandoverService = new TeamHandoverService();
|
||
teamHandoverService.cardInfoEntity = cardRepo as any;
|
||
teamHandoverService.teamHandoverLogEntity = handoverLogRepo as any;
|
||
teamHandoverService.crmLeadEntity = leadRepo as any;
|
||
teamHandoverService.crmLeadAuditEntity = leadAuditRepo as any;
|
||
|
||
nfcDeviceService = new NfcDeviceService();
|
||
nfcDeviceService.nfcDeviceEntity = nfcDeviceRepo as any;
|
||
|
||
nfcRuleService = new NfcRuleService();
|
||
nfcRuleService.nfcRuleEntity = nfcRuleRepo as any;
|
||
|
||
nfcSecurityService = new NfcSecurityService();
|
||
nfcSecurityService.nfcDeviceEntity = nfcDeviceRepo as any;
|
||
|
||
nfcBatchExportService = new NfcBatchExportService();
|
||
|
||
teamMembersService = new TeamMembersService();
|
||
teamMembersService.baseSysDepartmentEntity = deptRepo as any;
|
||
teamMembersService.baseSysUserEntity = userRepo as any;
|
||
teamMembersService.cardInfoEntity = cardRepo as any;
|
||
|
||
teamDomainService = new TeamDomainService();
|
||
teamDomainService.teamDomainEntity = domainRepo as any;
|
||
|
||
teamStripeService = new TeamStripeService();
|
||
teamStripeService.teamSubscriptionEntity = subRepo as any;
|
||
teamStripeService.webhookSecret = 'whsec_e2e_live_test_secret_key_888';
|
||
|
||
teamQuotaService = new TeamQuotaService();
|
||
teamQuotaService.cardInfoEntity = cardRepo as any;
|
||
teamQuotaService.baseSysUserEntity = userRepo as any;
|
||
teamQuotaService.nfcDeviceEntity = nfcDeviceRepo as any;
|
||
|
||
teamInvoiceService = new TeamInvoiceService();
|
||
teamInvoiceService.teamInvoiceEntity = invoiceRepo as any;
|
||
|
||
// 基础全局种子变量
|
||
sharedTenantId = 1001;
|
||
sharedSalesUserId = 801;
|
||
sharedSuccessorUserId = 802;
|
||
sharedNfcUid = '04:A1:B2:C3:D4:E5:F6';
|
||
sharedNfcSecret = 'k9#mZ$8vP!xL2@qR5*wY8^uN1(bC4)eT';
|
||
sharedVisitorId = 'vis_dubai_investor_99';
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 1:微官网搭建与展品套件 (P01 ~ P08)
|
||
// 名片 ➔ Bento 布局 ➔ 案例作品 ➔ 博客发布 ➔ 双语发音 ➔ RTL 多语言 ➔ ICS 日历 ➔ SEO OG
|
||
// =========================================================================
|
||
describe('主干流 1: 微官网搭建与展品套件 (Points 01-08)', () => {
|
||
it('P01: 创建名片基础档案与发布状态', async () => {
|
||
const newCard = await cardRepo.save({
|
||
tenantId: sharedTenantId,
|
||
userId: sharedSalesUserId,
|
||
slug: 'alex-zheng',
|
||
name: 'Alex Zheng',
|
||
title: 'VP of Global Expansion',
|
||
company: 'OmniCard Global Inc.',
|
||
slogan: 'Empowering Global Business Connections',
|
||
avatarUrl: 'https://cdn.omnicard.ai/avatars/alex.png',
|
||
phone: '+86-13800138000',
|
||
email: 'alex@omnicard.ai',
|
||
whatsapp: '+8613800138000',
|
||
linkedin: 'https://linkedin.com/in/alex-zheng',
|
||
timezone: 'Asia/Shanghai',
|
||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3',
|
||
status: 'published',
|
||
themeCode: 'corporate_navy',
|
||
remark: '内部高管名片,国际业务核心',
|
||
internalConfig: '{"rateLimit": 1000, "debug": false}',
|
||
});
|
||
|
||
expect(newCard.id).toBeDefined();
|
||
expect(newCard.slug).toBe('alex-zheng');
|
||
expect(newCard.status).toBe('published');
|
||
|
||
sharedCardId = newCard.id;
|
||
sharedCardSlug = newCard.slug;
|
||
});
|
||
|
||
it('P02: Bento Grid 便当盒栅格多维布局编排与物理尺寸自动约束', async () => {
|
||
const layoutBlocks = [
|
||
{
|
||
type: 'hero' as BentoBlockType,
|
||
colSpan: 2 as BentoColSpan,
|
||
rowSpan: 1 as BentoRowSpan,
|
||
title: '个人简介与身份卡',
|
||
content: '专注于中东与欧洲数字商务合作',
|
||
status: 1,
|
||
},
|
||
{
|
||
type: 'social' as BentoBlockType,
|
||
colSpan: 1 as BentoColSpan,
|
||
rowSpan: 1 as BentoRowSpan,
|
||
title: '商务社交矩阵',
|
||
content: 'WhatsApp & LinkedIn',
|
||
status: 1,
|
||
},
|
||
{
|
||
type: 'case' as BentoBlockType,
|
||
colSpan: 2 as BentoColSpan,
|
||
rowSpan: 2 as BentoRowSpan,
|
||
title: '新能源国际合作案例',
|
||
content: '协助头部光伏企业落地阿布扎比',
|
||
status: 1,
|
||
},
|
||
{
|
||
type: 'article' as BentoBlockType,
|
||
colSpan: 1 as BentoColSpan,
|
||
rowSpan: 1 as BentoRowSpan,
|
||
title: '2026全球数字名片白皮书',
|
||
content: '深度解读全球智能名片获客漏斗',
|
||
status: 1,
|
||
},
|
||
];
|
||
|
||
// 真实调用 CardBentoService.batchSaveLayout 自动重排与持久化
|
||
const savedBlocks = await bentoService.batchSaveLayout(sharedCardId, layoutBlocks);
|
||
|
||
expect(savedBlocks).toHaveLength(4);
|
||
expect(savedBlocks[0].sortOrder).toBe(0);
|
||
expect(savedBlocks[0].type).toBe('hero');
|
||
expect(savedBlocks[1].sortOrder).toBe(1);
|
||
expect(savedBlocks[1].type).toBe('social');
|
||
|
||
// 真实从数据源验证排序
|
||
const activeBlocks = await bentoService.getBlocksByCardId(sharedCardId, { status: 1 });
|
||
expect(activeBlocks.length).toBe(4);
|
||
expect(activeBlocks[2].type).toBe('case');
|
||
});
|
||
|
||
it('P03: 展厅案例作品集持久化与 KPI 指标挂载', async () => {
|
||
const demoCase = await casesRepo.save({
|
||
cardId: sharedCardId,
|
||
title: '阿联酋新能源峰会智能签到',
|
||
clientName: 'Gulf Solar Tech',
|
||
metricsKpi: '触碰转化率 +320%',
|
||
coverImage: 'https://cdn.omnicard.ai/cases/gulf-solar.jpg',
|
||
mediaUrl: 'https://cdn.omnicard.ai/videos/summit-recap.mp4',
|
||
status: 1,
|
||
sortOrder: 1,
|
||
});
|
||
|
||
expect(demoCase.id).toBeDefined();
|
||
expect(demoCase.metricsKpi).toBe('触碰转化率 +320%');
|
||
});
|
||
|
||
it('P04: 博客文章发布与 SEO OpenGraph 社交卡片自动提炼', async () => {
|
||
const rawHtmlContent = `
|
||
<article>
|
||
<h1>全球获客新引擎:2026 数字名片实战洞察</h1>
|
||
<p>在全球数字化与国际商务展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,销售代表能够在 30 秒内建立品牌信任,并让每一位国际客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
|
||
</article>
|
||
`;
|
||
|
||
const articleEntity = await articleRepo.save({
|
||
cardId: sharedCardId,
|
||
title: '全球获客新引擎:2026 数字名片实战洞察',
|
||
content: rawHtmlContent,
|
||
coverImage: 'https://cdn.omnicard.ai/articles/og-banner.png',
|
||
isPublished: true,
|
||
});
|
||
|
||
// 真实调用 CardArticleService 提取 OpenGraph 社交卡片元数据
|
||
const ogMeta = articleService.generateOgMeta(articleEntity);
|
||
|
||
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.length).toBeLessThanOrEqual(160);
|
||
expect(ogMeta.description).not.toContain('<p>');
|
||
});
|
||
|
||
it('P05: 双语人名读音与语音条配置真实性校验', async () => {
|
||
const card = await cardRepo.findOneBy({ id: sharedCardId });
|
||
expect(card).not.toBeNull();
|
||
expect(card?.audioPronunciationUrl).toBe(
|
||
'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3'
|
||
);
|
||
expect(card?.audioPronunciationUrl).toMatch(/\.mp3$/i);
|
||
});
|
||
|
||
it('P06: RTL 多语言阿语分支独立配置与检索 (ar-AE)', async () => {
|
||
const arabicLocale = await localeService.upsert({
|
||
cardId: sharedCardId,
|
||
locale: 'ar-AE',
|
||
name: 'أليكس تشنغ',
|
||
title: 'نائب الرئيس للتوسع العالمي',
|
||
company: 'أومني كارد جلوبال',
|
||
slogan: 'خبير في الحلول الرقمية الذكية للشركات العابرة للحدود',
|
||
});
|
||
|
||
expect(arabicLocale.cardId).toBe(sharedCardId);
|
||
expect(arabicLocale.locale).toBe('ar-AE');
|
||
|
||
// 真实检索阿语内容
|
||
const queried = await localeService.getByCardIdAndLocale(sharedCardId, 'ar-AE');
|
||
expect(queried).not.toBeNull();
|
||
expect(queried?.title).toBe('نائب الرئيس للتوسع العالمي');
|
||
});
|
||
|
||
it('P07: 国际商务日程 RFC 5545 ICS 日历订阅事件生成与解析', () => {
|
||
const meetingStart = new Date('2026-10-15T10:00:00.000Z');
|
||
const meetingEnd = new Date('2026-10-15T11:00:00.000Z');
|
||
|
||
const icsString = generateIcsEvent({
|
||
uid: 'omnicard-meet-20261015-001@omnicard.ai',
|
||
summary: 'OmniCard AI 商务合作洽谈会 (Alex Zheng & Dubai Partner)',
|
||
description: '探讨中东区域独家分销与 NFC 智能硬件批量发行方案。\n请提前 5 分钟接入。',
|
||
location: '迪拜世界贸易中心 (DWTC) 会议厅 3B',
|
||
startTime: meetingStart,
|
||
endTime: meetingEnd,
|
||
organizer: {
|
||
name: 'Alex Zheng',
|
||
email: 'alex@omnicard.ai',
|
||
},
|
||
url: 'https://omnicard.ai/p/alex-zheng',
|
||
});
|
||
|
||
expect(icsString).toContain('BEGIN:VCALENDAR');
|
||
expect(icsString).toContain('BEGIN:VEVENT');
|
||
expect(icsString).toContain('UID:omnicard-meet-20261015-001@omnicard.ai');
|
||
expect(icsString).toContain('SUMMARY:OmniCard AI 商务合作洽谈会');
|
||
expect(icsString).toContain('END:VEVENT');
|
||
expect(icsString).toContain('END:VCALENDAR');
|
||
|
||
// 真实反向解析校验
|
||
const parsed = parseIcsEvent(icsString);
|
||
expect(parsed.uid).toBe('omnicard-meet-20261015-001@omnicard.ai');
|
||
expect(parsed.summary).toContain('Alex Zheng & Dubai Partner');
|
||
expect(parsed.location).toContain('DWTC');
|
||
});
|
||
|
||
it('P08: 访客端公开微官网全景聚合输出与敏感信息脱敏校验', async () => {
|
||
const portalDetail = await portalService.getPortalDetail(sharedCardSlug, 'ar-AE');
|
||
|
||
expect(portalDetail.id).toBe(sharedCardId);
|
||
expect(portalDetail.name).toBe('Alex Zheng');
|
||
expect(portalDetail.slug).toBe('alex-zheng');
|
||
expect(portalDetail.audioPronunciationUrl).toBeDefined();
|
||
expect(portalDetail.bentoBlocks.length).toBe(4);
|
||
expect(portalDetail.suggestedLocale).toBe('ar-AE');
|
||
|
||
// 安全脱敏校验:内部机密字段严禁暴露给访客前端
|
||
expect((portalDetail as any).internalConfig).toBeUndefined();
|
||
expect((portalDetail as any).remark).toBeUndefined();
|
||
expect((portalDetail as any).userId).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 2:触卡与访客雷达 (P09 ~ P12)
|
||
// NFC 防伪验签 ➔ 访客雷达埋点 ➔ 国家分布聚合 ➔ 实时推送
|
||
// =========================================================================
|
||
describe('主干流 2: 触卡与访客雷达 (Points 09-12)', () => {
|
||
it('P09: NFC 实体卡动态防伪 HMAC-SHA256 签名生成、验签与防重放', () => {
|
||
// 1. 生成动态防伪令牌 (时间戳 + HMAC 签名)
|
||
sharedDeviceToken = nfcAntiTamperService.generateDeviceToken(
|
||
sharedNfcUid,
|
||
sharedCardId,
|
||
sharedNfcSecret
|
||
);
|
||
expect(sharedDeviceToken).toContain('.');
|
||
const [ts, sig] = sharedDeviceToken.split('.');
|
||
expect(Number(ts)).toBeGreaterThan(0);
|
||
expect(sig).toHaveLength(64);
|
||
|
||
// 2. 真实首次验签通过
|
||
const validRes = nfcAntiTamperService.verifyDeviceToken(
|
||
sharedNfcUid,
|
||
sharedCardId,
|
||
sharedDeviceToken,
|
||
sharedNfcSecret
|
||
);
|
||
expect(validRes.valid).toBe(true);
|
||
|
||
// 3. 防重放攻击测试:相同令牌二次使用应直接拦截
|
||
const replayRes = nfcAntiTamperService.verifyDeviceToken(
|
||
sharedNfcUid,
|
||
sharedCardId,
|
||
sharedDeviceToken,
|
||
sharedNfcSecret
|
||
);
|
||
expect(replayRes.valid).toBe(false);
|
||
expect(replayRes.reason).toMatch(/重放|已使用/);
|
||
|
||
// 4. 恶意篡改拦截:篡改 UID 进行验签被拦截并入安全审计日志
|
||
const fakeToken = nfcAntiTamperService.generateDeviceToken(
|
||
sharedNfcUid,
|
||
sharedCardId,
|
||
sharedNfcSecret
|
||
);
|
||
const tamperedRes = nfcAntiTamperService.verifyDeviceToken(
|
||
'04:FF:FF:FF:FF:FF:FF',
|
||
sharedCardId,
|
||
fakeToken,
|
||
sharedNfcSecret
|
||
);
|
||
expect(tamperedRes.valid).toBe(false);
|
||
expect(tamperedRes.reason).toMatch(/签名无效|篡改/);
|
||
|
||
const logs = nfcAntiTamperService.getAuditLogs();
|
||
expect(logs.length).toBeGreaterThanOrEqual(2);
|
||
});
|
||
|
||
it('P10: 访客雷达实时足迹埋点与 IP 地理位置推断 (recordTrack)', async () => {
|
||
// 模拟迪拜买家触碰 NFC 卡后产生的浏览埋点
|
||
const track1 = await crmRadarService.recordTrack({
|
||
cardId: sharedCardId,
|
||
visitorId: sharedVisitorId,
|
||
ip: '194.170.1.1',
|
||
country: 'United Arab Emirates',
|
||
city: 'Dubai',
|
||
touchType: 'nfc',
|
||
stayDurationMs: 320000, // 320 秒 (深度停留)
|
||
scrollDepth: 95,
|
||
viewedBlocks: ['hero', 'social', 'case', 'article'],
|
||
tenantId: sharedTenantId,
|
||
});
|
||
|
||
expect(track1.id).toBeDefined();
|
||
expect(track1.touchType).toBe('nfc');
|
||
expect(track1.country).toBe('United Arab Emirates');
|
||
expect(track1.stayDurationMs).toBe(320000);
|
||
|
||
// 模拟另外两名国际访客浏览 (新加坡与美国)
|
||
await crmRadarService.recordTrack({
|
||
cardId: sharedCardId,
|
||
visitorId: 'vis_sg_01',
|
||
ip: '202.166.1.1',
|
||
country: 'Singapore',
|
||
city: 'Singapore',
|
||
touchType: 'qr',
|
||
stayDurationMs: 150000,
|
||
scrollDepth: 70,
|
||
viewedBlocks: ['hero', 'case'],
|
||
tenantId: sharedTenantId,
|
||
});
|
||
|
||
await crmRadarService.recordTrack({
|
||
cardId: sharedCardId,
|
||
visitorId: 'vis_us_02',
|
||
ip: '8.8.8.8',
|
||
country: 'United States',
|
||
city: 'Mountain View',
|
||
touchType: 'link',
|
||
stayDurationMs: 45000,
|
||
scrollDepth: 50,
|
||
viewedBlocks: ['hero'],
|
||
tenantId: sharedTenantId,
|
||
});
|
||
});
|
||
|
||
it('P11: 全球雷达热力分布与多国访客聚合统计', async () => {
|
||
const distribution = await crmRadarService.getCountryDistribution(sharedCardId);
|
||
|
||
expect(distribution).toHaveLength(3);
|
||
// 验证国家与人次统计
|
||
const countries = distribution.map(d => d.country);
|
||
expect(countries).toContain('United Arab Emirates');
|
||
expect(countries).toContain('Singapore');
|
||
expect(countries).toContain('United States');
|
||
|
||
// 验证平均停留时长最高者为阿联酋访客 (320000ms)
|
||
const uaeItem = distribution.find(d => d.country === 'United Arab Emirates');
|
||
expect(uaeItem?.avgStayDurationMs).toBe(320000);
|
||
expect(uaeItem?.percentage).toBeCloseTo(33.33, 0);
|
||
});
|
||
|
||
it('P12: 实时雷达 Push 广播通知流订阅与派发', done => {
|
||
const sampleTrack: RadarTrack = {
|
||
id: 99,
|
||
tenantId: sharedTenantId,
|
||
cardId: sharedCardId,
|
||
visitorId: sharedVisitorId,
|
||
ip: '194.170.1.1',
|
||
country: 'United Arab Emirates',
|
||
city: 'Dubai',
|
||
touchType: 'nfc',
|
||
stayDurationMs: 320000,
|
||
scrollDepth: 95,
|
||
viewedBlocks: ['hero', 'case'],
|
||
createdAt: new Date().toISOString(),
|
||
};
|
||
|
||
// 销售管家端订阅该名片的雷达事件
|
||
crmRadarPushService.subscribe(sharedCardId, (payload: RadarPushPayload) => {
|
||
expect(payload.event).toBe('radar.visitor');
|
||
expect(payload.cardId).toBe(sharedCardId);
|
||
expect(payload.country).toBe('United Arab Emirates');
|
||
expect(payload.touchType).toBe('nfc');
|
||
expect(payload.stayDurationMs).toBe(320000);
|
||
done();
|
||
});
|
||
|
||
// 触发雷达广播推送
|
||
crmRadarPushService.publish(sampleTrack);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 3:AI 智能中枢 (P13 ~ P17)
|
||
// 人设配置 ➔ RAG 切片与沙盒召回 ➔ 术语库润色 ➔ 流式对话与留资卡片
|
||
// =========================================================================
|
||
describe('主干流 3: AI 智能中枢 (Points 13-17)', () => {
|
||
it('P13: 名片 AI 顾问人设、基调与留资触发阈值配置', async () => {
|
||
const config = await aiConciergeService.saveByCardId(sharedCardId, {
|
||
personaType: 'trade_expert',
|
||
welcomeMsg: '您好!我是 Alex 的 AI 商务顾问,请问有什么可以协助您的?',
|
||
triggerLeadThreshold: 75,
|
||
promptTemplate: 'You are an export trade consultant specialized in clean energy solutions.',
|
||
toneStyle: 'professional',
|
||
isEnabled: true,
|
||
});
|
||
|
||
expect(config.cardId).toBe(sharedCardId);
|
||
expect(config.personaType).toBe('trade_expert');
|
||
expect(config.triggerLeadThreshold).toBe(75);
|
||
|
||
const queried = await aiConciergeService.getByCardId(sharedCardId);
|
||
expect(queried?.welcomeMsg).toContain('Alex 的 AI 商务顾问');
|
||
});
|
||
|
||
it('P14: 知识库 RAG 滑动窗口文本分块与重叠切片算法', () => {
|
||
const rawDocText = `
|
||
OmniCard AI 致力于为全球商务团队提供次世代数字名片与微官网解决方案。
|
||
支持中东、欧洲及北美地区的主流商务支付与 CRM 系统无缝互联。
|
||
我们针对新能源、智能硬件及全球电商客户提供定制化的展会获客方案,
|
||
配合实体 NFC 智能卡片,可实现秒级留资并自动将高意向线索同步至公海池。
|
||
`.trim();
|
||
|
||
const chunks = aiKnowledgeService.splitTextToChunks(rawDocText, 50, 15);
|
||
|
||
expect(chunks.length).toBeGreaterThan(1);
|
||
expect(chunks[0].index).toBe(0);
|
||
expect(chunks[0].length).toBeLessThanOrEqual(50);
|
||
// 验证滑动重叠覆盖
|
||
expect(chunks[0].content.slice(-15)).toBe(chunks[1].content.slice(0, 15));
|
||
});
|
||
|
||
it('P15: 知识库沙盒召回模拟与关键词相似度排序', () => {
|
||
const testChunks = [
|
||
{
|
||
index: 0,
|
||
content: '公司主营产品包含新能源储能电站与光伏逆变器全球解决方案。',
|
||
length: 30,
|
||
},
|
||
{
|
||
index: 1,
|
||
content: '在中东阿联酋及沙特地区,我们提供 7x24 小时的当地运维支持。',
|
||
length: 31,
|
||
},
|
||
{
|
||
index: 2,
|
||
content: '欧洲市场支持符合 GDPR 的数据合规导出与多语言商务发票。',
|
||
length: 29,
|
||
},
|
||
];
|
||
|
||
// 访客搜索:“中东阿联酋支持”
|
||
const query = '中东 阿联酋 支持';
|
||
const results = aiKnowledgeService.simulateRetrieval(query, testChunks, 2);
|
||
|
||
expect(results).toHaveLength(2);
|
||
// 第一个命中必须是第 1 块 (中东阿联酋运维支持)
|
||
expect(results[0].index).toBe(1);
|
||
expect(results[0].content).toContain('中东阿联酋');
|
||
expect(results[0].score).toBeGreaterThan(0.5);
|
||
});
|
||
|
||
it('P16: 行业专有术语库 (Glossary) 导入与国际多语言润色替换', async () => {
|
||
const importRes = await aiGlossaryService.batchImport([
|
||
{
|
||
sourceTerm: 'digital business card',
|
||
targetTerm: '智能数字名片',
|
||
sourceLang: 'en',
|
||
targetLang: 'zh-CN',
|
||
domain: 'tech',
|
||
notes: '品牌核心定义',
|
||
},
|
||
{
|
||
sourceTerm: 'NFC card fleet',
|
||
targetTerm: 'NFC 智能卡舰队',
|
||
sourceLang: 'en',
|
||
targetLang: 'zh-CN',
|
||
domain: 'hardware',
|
||
},
|
||
]);
|
||
|
||
expect(importRes.inserted).toBe(2);
|
||
|
||
// 真实调用术语库替换
|
||
const replaced = await aiGlossaryService.replaceTerms(
|
||
'Welcome to our digital business card solution',
|
||
'zh-CN',
|
||
'tech'
|
||
);
|
||
expect(replaced).toContain('智能数字名片');
|
||
});
|
||
|
||
it('P17: AI 顾问流式对话意图识别与高意向留资卡片引导', async () => {
|
||
// 访客询问价格与合作方案
|
||
const reply = await aiChatService.ask({
|
||
cardId: sharedCardId,
|
||
locale: 'zh-CN',
|
||
messages: [
|
||
{ role: 'user', content: '请问你们 Enterprise 企业版怎么报价?我们想洽谈合作。' },
|
||
],
|
||
});
|
||
|
||
expect(reply.shouldCollectLead).toBe(true);
|
||
expect(reply.reply).toContain('留下您的姓名与联系方式');
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 4:客户 CRM 转化 (P18 ~ P22)
|
||
// 多维加权意向算分 ➔ 外部 CRM 推送 ➔ 销售公海池流转 ➔ 离职交接
|
||
// =========================================================================
|
||
describe('主干流 4: 客户 CRM 转化 (Points 18-22)', () => {
|
||
it('P18: 多维加权意向算分引擎 (CrmLeadScoreService)', () => {
|
||
// 结合前面访客行为计算加权得分:
|
||
// 停留 >300s (stayScore: 40) + 浏览4个模块 (moduleScore: 20) + 交互全触发 (interactionScore: 40) = 100分
|
||
const scoreResult = crmLeadScoreService.calculateScore({
|
||
stayDurationSeconds: 320,
|
||
viewedBlockCount: 4,
|
||
clickedWhatsApp: true,
|
||
downloadedVCard: true,
|
||
submittedLead: true,
|
||
});
|
||
|
||
expect(scoreResult.score).toBe(100);
|
||
expect(scoreResult.level).toBe('Hot');
|
||
expect(scoreResult.breakdown.stayScore).toBe(40);
|
||
expect(scoreResult.breakdown.moduleScore).toBe(20);
|
||
expect(scoreResult.breakdown.interactionScore).toBe(80);
|
||
});
|
||
|
||
it('P19: 沉淀高意向潜客全景画像建档', async () => {
|
||
const lead = await leadRepo.save({
|
||
cardId: sharedCardId,
|
||
userId: sharedSalesUserId, // 初始归属于销售 Alex
|
||
name: 'Tariq Al-Mansoor',
|
||
company: 'Gulf Horizon Trading LLC',
|
||
phone: '+971-50-9876543',
|
||
email: 'tariq@gulfhorizon.ae',
|
||
intentScore: 95,
|
||
status: 'hot',
|
||
source: 'nfc',
|
||
isPool: false,
|
||
lastFollowUpTime: new Date(),
|
||
notes: '阿联酋大客户,重点推进企业微官网采购',
|
||
});
|
||
|
||
expect(lead.id).toBeDefined();
|
||
expect(lead.intentScore).toBe(95);
|
||
expect(lead.status).toBe('hot');
|
||
sharedLeadId = lead.id;
|
||
});
|
||
|
||
it('P20: 国际主流外部 CRM (HubSpot & Salesforce) 格式映射与 Webhook 签名生成', () => {
|
||
const currentLead = leadRepo.data.find(l => l.id === sharedLeadId);
|
||
expect(currentLead).toBeDefined();
|
||
|
||
// 1. HubSpot Contacts v3 契约转换
|
||
const hubspotPayload = crmSyncService.formatToHubSpot(currentLead!);
|
||
expect(hubspotPayload.properties.email).toBe('tariq@gulfhorizon.ae');
|
||
expect(hubspotPayload.properties.firstname).toBe('Tariq');
|
||
expect(hubspotPayload.properties.lastname).toBe('Al-Mansoor');
|
||
expect(hubspotPayload.properties.company).toBe('Gulf Horizon Trading LLC');
|
||
expect(hubspotPayload.properties.lifecyclestage).toBe('lead');
|
||
|
||
// 2. Salesforce Lead sObject 契约转换
|
||
const sfPayload = crmSyncService.formatToSalesforce(currentLead!);
|
||
expect(sfPayload.FirstName).toBe('Tariq');
|
||
expect(sfPayload.LastName).toBe('Al-Mansoor');
|
||
expect(sfPayload.Email).toBe('tariq@gulfhorizon.ae');
|
||
expect(sfPayload.Status).toBe('Open - Not Contacted');
|
||
|
||
// 3. Webhook 签名生成与验证 (HMAC-SHA256)
|
||
const webhookSecret = 'crm_webhook_secret_key_999';
|
||
const webhookSignature = crmSyncService.generateSignature(
|
||
JSON.stringify(currentLead),
|
||
webhookSecret
|
||
);
|
||
expect(webhookSignature).toHaveLength(64);
|
||
});
|
||
|
||
it('P21: 团队公海池流转与 CAS 销售抢单认领', async () => {
|
||
// 1. 销售将线索释放退回到企业公海池
|
||
const poolResult = await crmLeadPoolService.recycleToPool(
|
||
sharedLeadId,
|
||
'转交团队公海供区域销售抢单'
|
||
);
|
||
expect(poolResult.isPool).toBe(true);
|
||
expect(poolResult.userId).toBeNull();
|
||
|
||
// 验证产生审计日志
|
||
const auditsAfterPool = leadAuditRepo.data.filter(a => a.leadId === sharedLeadId);
|
||
expect(auditsAfterPool.some(a => a.action === 'recycle')).toBe(true);
|
||
|
||
// 2. 另一位新接手销售 (sharedSuccessorUserId) 抢单捞取线索
|
||
const claimed = await crmLeadPoolService.claimLead(sharedLeadId, sharedSuccessorUserId);
|
||
expect(claimed.isPool).toBe(false);
|
||
expect(claimed.userId).toBe(sharedSuccessorUserId);
|
||
|
||
// 验证抢单后不可被重复认领
|
||
await expect(
|
||
crmLeadPoolService.claimLead(sharedLeadId, 9999)
|
||
).rejects.toThrow(/已被其他销售认领|不在公海池/);
|
||
});
|
||
|
||
it('P22: 员工离职资产交接工单、短链安全重定向与历史线索迁移', async () => {
|
||
// 模拟原销售 Alex (userId: 801) 离职,交接给接任者 (userId: 802)
|
||
// 名片短链重定向到接替人的名片,历史线索全部迁移
|
||
const handoverLog = await teamHandoverService.executeHandover({
|
||
fromUserId: sharedSalesUserId,
|
||
toUserId: sharedSuccessorUserId,
|
||
cardId: sharedCardId,
|
||
redirectType: 'card',
|
||
redirectTarget: 'successor-card-slug',
|
||
leadAction: 'transfer',
|
||
operatorId: 1,
|
||
remark: 'Alex 正常离职交接,客户与名片一键转接给 Bob',
|
||
});
|
||
|
||
expect(handoverLog.id).toBeDefined();
|
||
expect(handoverLog.handoverNo).toMatch(/^HO\d+/);
|
||
expect(handoverLog.status).toBe('completed');
|
||
|
||
// 验证名片重定向配置生效
|
||
const updatedCard = await cardRepo.findOneBy({ id: sharedCardId });
|
||
expect(updatedCard?.redirectType).toBe('card');
|
||
expect(updatedCard?.redirectTarget).toBe('successor-card-slug');
|
||
expect(updatedCard?.userId).toBe(sharedSuccessorUserId);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 5:NFC 硬件生命周期 (P23 ~ P26)
|
||
// 设备入库 ➔ 场景规则匹配 ➔ 远程挂失解冻 ➔ 批量制卡 Zip 导出
|
||
// =========================================================================
|
||
describe('主干流 5: NFC 硬件生命周期 (Points 23-26)', () => {
|
||
it('P23: NFC 实体卡设备台账入库与绑定', async () => {
|
||
const device = await nfcDeviceRepo.save({
|
||
tenantId: sharedTenantId,
|
||
uid: sharedNfcUid,
|
||
cardId: sharedCardId,
|
||
batchNo: 'EXPO-2026-DXB',
|
||
material: 'metal',
|
||
status: 'bound',
|
||
sceneMode: 'business',
|
||
});
|
||
|
||
expect(device.id).toBeDefined();
|
||
expect(device.uid).toBe(sharedNfcUid);
|
||
expect(device.material).toBe('metal');
|
||
expect(device.status).toBe('bound');
|
||
});
|
||
|
||
it('P24: 场景化动态时间规则匹配与名片路由 (NfcRuleService)', async () => {
|
||
// 配置规则:工作日 09:00 - 18:00 路由到商务微官网 (cardId: 101)
|
||
// 其他非工作时段路由到个人社交名片 (cardId: 202)
|
||
await nfcRuleRepo.save({
|
||
tenantId: sharedTenantId,
|
||
ruleName: '日间商务展会模式',
|
||
targetCardId: sharedCardId,
|
||
sceneMode: 'business',
|
||
startTime: '09:00',
|
||
endTime: '18:00',
|
||
enabled: true,
|
||
});
|
||
|
||
await nfcRuleRepo.save({
|
||
tenantId: sharedTenantId,
|
||
ruleName: '晚间晚宴社交模式',
|
||
targetCardId: 202,
|
||
sceneMode: 'social',
|
||
startTime: '19:00',
|
||
endTime: '23:59',
|
||
enabled: true,
|
||
});
|
||
|
||
// 1. 模拟上午 11:30 触碰卡片
|
||
const dayResult = await nfcRuleService.resolveScene({
|
||
tenantId: sharedTenantId,
|
||
now: '2026-09-21T11:30:00',
|
||
});
|
||
expect(dayResult).not.toBeNull();
|
||
expect(dayResult?.targetCardId).toBe(sharedCardId);
|
||
expect(dayResult?.sceneMode).toBe('business');
|
||
|
||
// 2. 模拟晚上 21:00 触碰卡片
|
||
const nightResult = await nfcRuleService.resolveScene({
|
||
tenantId: sharedTenantId,
|
||
now: '2026-09-21T21:00:00',
|
||
});
|
||
expect(nightResult).not.toBeNull();
|
||
expect(nightResult?.targetCardId).toBe(202);
|
||
expect(nightResult?.sceneMode).toBe('social');
|
||
});
|
||
|
||
it('P25: NFC 实体卡远程一键挂失冻结与解冻恢复 (NfcSecurityService)', async () => {
|
||
// 1. 模拟卡片在展会遗失,远程一键挂失冻结
|
||
const reportLostRes = await nfcSecurityService.freeze({ uid: sharedNfcUid });
|
||
expect(reportLostRes.status).toBe('suspended');
|
||
|
||
// 查库验证设备状态被冻结为 suspended
|
||
const lostDevice = await nfcDeviceRepo.findOneBy({ uid: sharedNfcUid });
|
||
expect(lostDevice?.status).toBe('suspended');
|
||
|
||
// 2. 找回卡片后远程解冻,恢复绑定正常状态 (active)
|
||
const recoverRes = await nfcSecurityService.unfreeze({ uid: sharedNfcUid });
|
||
expect(recoverRes.status).toBe('active');
|
||
|
||
const activeDevice = await nfcDeviceRepo.findOneBy({ uid: sharedNfcUid });
|
||
expect(activeDevice?.status).toBe('active');
|
||
});
|
||
|
||
it(
|
||
'P26: 展会批量制卡清单 CSV 与二维码离线 ZIP 压缩包归档导出',
|
||
async () => {
|
||
const mockBatchDevices = [
|
||
{
|
||
uid: sharedNfcUid,
|
||
slug: sharedCardSlug,
|
||
cardTitle: 'Alex Zheng - VP Global',
|
||
},
|
||
{
|
||
uid: '04:11:22:33:44:55:66',
|
||
slug: 'bob-engineer',
|
||
cardTitle: 'Bob Successor - Lead Dev',
|
||
},
|
||
];
|
||
|
||
const batchResult = await nfcBatchExportService.generateBatchPack(
|
||
'EXPO-2026-DXB',
|
||
mockBatchDevices
|
||
);
|
||
|
||
expect(batchResult.totalCount).toBe(2);
|
||
expect(Buffer.isBuffer(batchResult.zipBuffer)).toBe(true);
|
||
expect(batchResult.zipBuffer.length).toBeGreaterThan(0);
|
||
|
||
// 解压验证 ZIP 归档包结构
|
||
const zip = new AdmZip(batchResult.zipBuffer);
|
||
const zipEntries = zip.getEntries();
|
||
expect(zipEntries.length).toBeGreaterThanOrEqual(3);
|
||
|
||
// 校验存在 manifest.csv 清单文件
|
||
const manifestEntry = zipEntries.find(e => e.entryName.includes('manifest.csv'));
|
||
expect(manifestEntry).toBeDefined();
|
||
const csvText = manifestEntry?.getData().toString('utf8');
|
||
expect(csvText).toContain('出厂防伪码');
|
||
expect(csvText).toContain(sharedNfcUid);
|
||
expect(csvText).toContain('Alex Zheng - VP Global');
|
||
}, 15000);
|
||
});
|
||
|
||
// =========================================================================
|
||
// 主干流 6:多租户与商业化 (P27 ~ P30)
|
||
// 组织架构与名片分配 ➔ 独立域名与 SSL 校验 ➔ Stripe 订阅 Webhook ➔ 坐席配额核销 ➔ 国际多币种发票
|
||
// =========================================================================
|
||
describe('主干流 6: 多租户与商业化 (Points 27-30)', () => {
|
||
it('P27: 组织架构部门树多层级递归与成员名片分配 (TeamMembersService)', async () => {
|
||
// 1. 初始化部门树结构:总部 -> 国际销售中心 -> 中东拓展组
|
||
const headDept = await deptRepo.save({
|
||
name: '全球总部',
|
||
parentId: null as any,
|
||
orderNum: 1,
|
||
});
|
||
|
||
const salesDept = await deptRepo.save({
|
||
name: '全球拓展事业群',
|
||
parentId: headDept.id,
|
||
orderNum: 2,
|
||
});
|
||
|
||
const meDept = await deptRepo.save({
|
||
name: '中东战区组',
|
||
parentId: salesDept.id,
|
||
orderNum: 3,
|
||
});
|
||
|
||
// 2. 获取树形结构验证
|
||
const tree = await teamMembersService.getDepartmentTree();
|
||
expect(tree.length).toBeGreaterThanOrEqual(1);
|
||
const rootNode = tree.find(t => t.id === headDept.id);
|
||
expect(rootNode).toBeDefined();
|
||
expect(rootNode?.children?.length).toBeGreaterThanOrEqual(1);
|
||
|
||
// 3. 将销售成员 Bob 挂载到中东战区组并分配专属名片
|
||
await userRepo.save({
|
||
id: sharedSuccessorUserId,
|
||
name: 'Bob Successor',
|
||
username: 'bob_global',
|
||
departmentId: meDept.id,
|
||
status: 1,
|
||
});
|
||
|
||
const assigned = await teamMembersService.assignCard({
|
||
memberId: sharedSuccessorUserId,
|
||
cardId: sharedCardId,
|
||
});
|
||
expect(assigned.cardId).toBe(sharedCardId);
|
||
expect(assigned.memberId).toBe(sharedSuccessorUserId);
|
||
});
|
||
|
||
it('P28: 企业独立二级域名绑定向导与 SSL 证书有效性状态检测 (TeamDomainService)', async () => {
|
||
const domainName = 'card.omnicard-global.com';
|
||
|
||
// 1. 域名规范严格正则校验 (拦截非法端口、协议与保留地址)
|
||
const validCheck = teamDomainService.validateDomainFormat(domainName);
|
||
expect(validCheck.valid).toBe(true);
|
||
|
||
const invalidCheck = teamDomainService.validateDomainFormat('http://bad.domain.com:8080');
|
||
expect(invalidCheck.valid).toBe(false);
|
||
|
||
// 2. 绑定域名与生成 CNAME 解析目标
|
||
const bound = await domainRepo.save({
|
||
tenantId: sharedTenantId,
|
||
customDomain: domainName,
|
||
cnameTarget: 'cname.omnicard.ai',
|
||
sslStatus: 'pending',
|
||
});
|
||
expect(bound.customDomain).toBe(domainName);
|
||
expect(bound.cnameTarget).toBe('cname.omnicard.ai');
|
||
|
||
// 3. SSL 证书校验检测
|
||
const sslResult = await teamDomainService.checkSslCertificate(domainName, 443, {
|
||
mockCert: {
|
||
issuer: "Let's Encrypt Authority X3",
|
||
validFrom: new Date(Date.now() - 30 * 86400000).toISOString(),
|
||
validTo: new Date(Date.now() + 60 * 86400000).toISOString(),
|
||
authorized: true,
|
||
},
|
||
});
|
||
expect(sslResult.valid).toBe(true);
|
||
expect(sslResult.daysRemaining).toBeGreaterThan(0);
|
||
expect(sslResult.issuer).toBe("Let's Encrypt Authority X3");
|
||
});
|
||
|
||
it('P29: Stripe 国际信用卡结算会话创建与 Webhook 自动化入账 (TeamStripeService)', async () => {
|
||
// 1. 创建 Enterprise 年付结算会话
|
||
const session = await teamStripeService.createCheckoutSession({
|
||
tenantId: sharedTenantId,
|
||
plan: 'enterprise',
|
||
billingCycle: 'yearly',
|
||
});
|
||
expect(session.amount).toBe(99000); // $990.00 / 年
|
||
expect(session.sessionId).toMatch(/^cs_test_/);
|
||
expect(session.url).toContain(session.sessionId);
|
||
|
||
// 2. 模拟 Stripe 官方 Webhook 支付成功通知
|
||
const webhookPayload: StripeWebhookEvent = {
|
||
id: 'evt_stripe_payment_2026_001',
|
||
type: 'checkout.session.completed',
|
||
created: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
object: {
|
||
id: session.sessionId,
|
||
client_reference_id: String(sharedTenantId),
|
||
customer: 'cus_stripe_dubai_001',
|
||
subscription: 'sub_enterprise_live_001',
|
||
metadata: {
|
||
tenantId: String(sharedTenantId),
|
||
plan: 'enterprise',
|
||
billingCycle: 'yearly',
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
// 签名生成与真实 Webhook 处理
|
||
const headerTimestamp = Math.floor(Date.now() / 1000);
|
||
const rawPayloadString = JSON.stringify(webhookPayload);
|
||
const hmac = crypto.createHmac('sha256', teamStripeService.webhookSecret);
|
||
hmac.update(`${headerTimestamp}.${rawPayloadString}`, 'utf8');
|
||
const signature = hmac.digest('hex');
|
||
const stripeSigHeader = `t=${headerTimestamp},v1=${signature}`;
|
||
|
||
const verified = teamStripeService.verifyWebhookSignature(
|
||
rawPayloadString,
|
||
stripeSigHeader
|
||
);
|
||
expect(verified).toBe(true);
|
||
|
||
// 处理入账流转
|
||
const handleResult = await teamStripeService.handleWebhookEvent(webhookPayload);
|
||
expect(handleResult.success).toBe(true);
|
||
expect(handleResult.status).toBe('active');
|
||
expect(handleResult.plan).toBe('enterprise');
|
||
|
||
// 校验租户订阅数据已真实写入
|
||
const subscription = await subRepo.findOneBy({ tenantId: sharedTenantId });
|
||
expect(subscription?.status).toBe('active');
|
||
expect(subscription?.planType).toBe('enterprise');
|
||
});
|
||
|
||
it('P30: 坐席配额核销、80% 警戒线预警、升舱差价折算与国际多币种标准发票', async () => {
|
||
// 1. 配额检查:企业版已用 85 个坐席 / 上限 100 个 -> 触发 80% 警戒线
|
||
const quotaCheck = await teamQuotaService.checkQuota(sharedTenantId, 'seats', {
|
||
plan: 'enterprise',
|
||
currentUsed: 85,
|
||
});
|
||
expect(quotaCheck.limit).toBe(100);
|
||
expect(quotaCheck.used).toBe(85);
|
||
expect(quotaCheck.percentage).toBe(85);
|
||
expect(quotaCheck.warning).toBe(true); // 超过 80% 预警
|
||
expect(quotaCheck.exceeded).toBe(false);
|
||
|
||
// 2. 套餐升级差价折算 (Pro 升级至 Enterprise,剩余 180 天 / 全年 365 天)
|
||
const proration = teamQuotaService.calculateUpgradeProration(
|
||
'pro',
|
||
'enterprise',
|
||
180,
|
||
365
|
||
);
|
||
expect(proration.upgradeDiffAmount).toBeGreaterThan(0);
|
||
expect(proration.targetPlan).toBe('enterprise');
|
||
|
||
// 3. 国际多币种发票生成与税额精确拆解
|
||
const taxCalc = teamInvoiceService.calculateTaxAndTotals({
|
||
subtotal: 990.0,
|
||
taxRate: 0.05, // 5% 增值税
|
||
isTaxInclusive: false,
|
||
});
|
||
expect(taxCalc.subtotal).toBe(990.0);
|
||
expect(taxCalc.taxAmount).toBe(49.5);
|
||
expect(taxCalc.totalAmount).toBe(1039.5);
|
||
|
||
const invoiceDate = new Date('2026-09-20');
|
||
const invoiceNo = teamInvoiceService.generateInvoiceNumber(invoiceDate, 1);
|
||
expect(invoiceNo).toBe('INV-20260920-0001');
|
||
|
||
const invoice = await teamInvoiceService.generateInvoice(sharedTenantId, {
|
||
orderId: 'ORDER-2026-STRIPE-001',
|
||
currency: 'USD',
|
||
subtotal: 990.0,
|
||
taxRate: 0.05,
|
||
title: 'OmniCard Enterprise Annual Subscription (100 Seats)',
|
||
items: [
|
||
{
|
||
description: 'Enterprise Plan (Yearly)',
|
||
quantity: 1,
|
||
unitPrice: 990.0,
|
||
amount: 990.0,
|
||
},
|
||
],
|
||
billingInfo: {
|
||
companyName: 'Gulf Horizon Trading LLC',
|
||
taxId: 'AE-VAT-1002938481',
|
||
email: 'tariq@gulfhorizon.ae',
|
||
address: 'Downtown Dubai, UAE',
|
||
},
|
||
});
|
||
|
||
expect(invoice.id).toBeDefined();
|
||
expect(invoice.invoiceNo).toMatch(/^INV-\d{8}-\d{4}$/);
|
||
expect(invoice.currency).toBe('USD');
|
||
expect(invoice.currencySymbol).toBe('$');
|
||
expect(invoice.totalAmount).toBe(1039.5);
|
||
expect(invoice.status).toBe('paid');
|
||
expect(invoice.htmlTemplate).toContain('INV-');
|
||
expect(invoice.textTemplate).toContain('Gulf Horizon Trading LLC');
|
||
});
|
||
});
|
||
});
|