feat: 完善出海商机管道快速录入、RTL服务端首屏渲染与AI顾问留资动效闭环
这个提交包含在:
@@ -204,6 +204,22 @@ describe('Point 29-B: 社交爬虫探测与微官网短链服务端渲染集成
|
||||
expect(html).toContain('Hack & Co "Global"');
|
||||
});
|
||||
|
||||
it('当名片推荐语言为阿拉伯语等 RTL 语言时,服务端首屏 HTML 必须注入 dir="rtl" 属性', () => {
|
||||
const arabicCard: CardPortalDetail = {
|
||||
...mockAlexCardDetail,
|
||||
suggestedLocale: 'ar',
|
||||
};
|
||||
const html = cardSeoService.renderCardHtml(arabicCard);
|
||||
expect(html).toContain('<html lang="ar" dir="rtl">');
|
||||
|
||||
const uaeCard: CardPortalDetail = {
|
||||
...mockAlexCardDetail,
|
||||
suggestedLocale: 'ar-AE',
|
||||
};
|
||||
const uaeHtml = cardSeoService.renderCardHtml(uaeCard);
|
||||
expect(uaeHtml).toContain('<html lang="ar-AE" dir="rtl">');
|
||||
});
|
||||
|
||||
it('生成 404 兜底 HTML 时应提示名片已下线或不存在', () => {
|
||||
const notFoundHtml = cardSeoService.renderNotFoundHtml('名片不存在或已下线');
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { CrmSyncService } from '../../../src/modules/crm/service/sync';
|
||||
import { CrmRadarPushService } from '../../../src/modules/crm/service/push';
|
||||
import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admin/lead';
|
||||
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
|
||||
import { RadarTrack } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('【BTP-20】线索流转自动化与出海 Webhook / 实时推送集成测试', () => {
|
||||
let syncService: CrmSyncService;
|
||||
let pushService: CrmRadarPushService;
|
||||
let mockLead: CrmLeadEntity;
|
||||
|
||||
beforeEach(() => {
|
||||
syncService = new CrmSyncService();
|
||||
pushService = new CrmRadarPushService();
|
||||
mockLead = new CrmLeadEntity();
|
||||
mockLead.id = 101;
|
||||
mockLead.name = 'John Doe';
|
||||
mockLead.company = 'Acme Global Corp';
|
||||
mockLead.email = 'john.doe@acme.com';
|
||||
mockLead.phone = '+1-555-0199';
|
||||
mockLead.status = 'hot';
|
||||
mockLead.intentScore = 88;
|
||||
mockLead.source = 'nfc';
|
||||
});
|
||||
|
||||
it('1. Webhook HMAC-SHA256 签名生成与防伪防篡改验证', () => {
|
||||
const secret = 'whsec_omnicard_lead_flow_test_2026';
|
||||
const payload = {
|
||||
event: 'crm.lead.status_changed',
|
||||
timestamp: 1789924567,
|
||||
data: {
|
||||
leadId: mockLead.id,
|
||||
previousStatus: 'new',
|
||||
currentStatus: 'hot',
|
||||
lead: mockLead,
|
||||
},
|
||||
};
|
||||
|
||||
const signature = syncService.generateSignature(payload, secret);
|
||||
expect(signature).toBeDefined();
|
||||
expect(signature).toHaveLength(64);
|
||||
expect(/^[a-f0-9]{64}$/.test(signature)).toBe(true);
|
||||
|
||||
// 篡改测试
|
||||
const tamperedPayload = { ...payload, timestamp: 1789924568 };
|
||||
const tamperedSignature = syncService.generateSignature(tamperedPayload, secret);
|
||||
expect(tamperedSignature).not.toBe(signature);
|
||||
});
|
||||
|
||||
it('2. 国际 CRM 契约格式转换 (HubSpot & Salesforce)', () => {
|
||||
const hubspot = syncService.formatToHubSpot(mockLead);
|
||||
expect(hubspot.properties.firstname).toBe('John');
|
||||
expect(hubspot.properties.lastname).toBe('Doe');
|
||||
expect(hubspot.properties.company).toBe('Acme Global Corp');
|
||||
expect(hubspot.properties.lifecyclestage).toBe('lead');
|
||||
|
||||
const sf = syncService.formatToSalesforce(mockLead);
|
||||
expect(sf.FirstName).toBe('John');
|
||||
expect(sf.LastName).toBe('Doe');
|
||||
expect(sf.Company).toBe('Acme Global Corp');
|
||||
expect(sf.Status).toBe('Open - Not Contacted');
|
||||
});
|
||||
|
||||
it('3. 雷达实时 Push 渠道广播与定向名片监听机制 (CrmRadarPushService)', () => {
|
||||
const received: any[] = [];
|
||||
pushService.subscribe(101, (p) => {
|
||||
received.push(p);
|
||||
});
|
||||
|
||||
const track: RadarTrack = {
|
||||
id: 1,
|
||||
tenantId: 1,
|
||||
cardId: 101,
|
||||
visitorId: 'vis_dubai_101',
|
||||
ip: '185.220.101.5',
|
||||
country: 'AE',
|
||||
city: 'Dubai',
|
||||
touchType: 'nfc',
|
||||
stayDurationMs: 15400,
|
||||
scrollDepth: 85,
|
||||
viewedBlocks: ['hero', 'form', 'contact'],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const pushResult = pushService.publish(track);
|
||||
expect(pushResult.event).toBe('radar.visitor');
|
||||
expect(pushResult.cardId).toBe(101);
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].visitorId).toBe('vis_dubai_101');
|
||||
expect(received[0].country).toBe('AE');
|
||||
expect(received[0].city).toBe('Dubai');
|
||||
});
|
||||
|
||||
it('4. 控制器同步接口 syncExternal 校验与 Webhook 调用', async () => {
|
||||
const mockRepo: any = {
|
||||
findOne: jest.fn().mockResolvedValue(mockLead),
|
||||
};
|
||||
const controller = new AdminCrmLeadController();
|
||||
controller.crmLeadEntity = mockRepo;
|
||||
controller.crmSyncService = syncService;
|
||||
|
||||
// 模拟目标 Webhook URL 发送
|
||||
jest.spyOn(syncService, 'sendWebhook').mockResolvedValueOnce({
|
||||
success: true,
|
||||
attempts: 1,
|
||||
data: { received: true },
|
||||
});
|
||||
|
||||
const res: any = await controller.syncExternal({
|
||||
id: 101,
|
||||
target: 'webhook',
|
||||
webhookUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
||||
webhookSecret: 'secret_lead_flow_2026',
|
||||
});
|
||||
|
||||
expect(res.code).toBe(1000);
|
||||
expect(res.data.target).toBe('webhook');
|
||||
expect(res.data.leadId).toBe(101);
|
||||
expect(res.data.result.success).toBe(true);
|
||||
expect(res.data.result.attempts).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NfcDeviceEntity } from '../../../src/modules/nfc/entity/device';
|
||||
import { NfcDeviceService } from '../../../src/modules/nfc/service/device';
|
||||
import { AdminNfcDeviceController } from '../../../src/modules/nfc/controller/admin/device';
|
||||
import { CoolCommException } from '@cool-midway/core';
|
||||
|
||||
describe('Point 36: NFC 硬件卡片国际物流发货追踪与全生命周期流转 (NFC Fleet Shipping)', () => {
|
||||
let service: NfcDeviceService;
|
||||
let mockRepo: any;
|
||||
let savedDevices: any[];
|
||||
|
||||
beforeEach(() => {
|
||||
savedDevices = [
|
||||
{
|
||||
id: 1,
|
||||
uid: 'UID-NFC-GLOBAL-001',
|
||||
batchNo: 'BATCH-2026-EU-01',
|
||||
material: 'wood',
|
||||
status: 'unbound',
|
||||
sceneMode: 'business',
|
||||
shippingStatus: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
mockRepo = {
|
||||
findOneBy: jest.fn(async (query: { id: number }) => {
|
||||
return savedDevices.find(d => d.id === query.id) || null;
|
||||
}),
|
||||
save: jest.fn(async (device: any) => {
|
||||
const index = savedDevices.findIndex(d => d.id === device.id);
|
||||
if (index >= 0) {
|
||||
savedDevices[index] = { ...savedDevices[index], ...device };
|
||||
return savedDevices[index];
|
||||
}
|
||||
savedDevices.push(device);
|
||||
return device;
|
||||
}),
|
||||
};
|
||||
|
||||
service = new NfcDeviceService();
|
||||
(service as any).nfcDeviceEntity = mockRepo;
|
||||
});
|
||||
|
||||
describe('1. 实体字段契约与默认状态', () => {
|
||||
it('实体实例应包含 shippingStatus, carrier, trackingNumber 等物流追踪属性', () => {
|
||||
const entity = new NfcDeviceEntity();
|
||||
entity.shippingStatus = 'pending';
|
||||
entity.carrier = 'dhl';
|
||||
entity.trackingNumber = 'DHL-987654321';
|
||||
expect(entity.shippingStatus).toBe('pending');
|
||||
expect(entity.carrier).toBe('dhl');
|
||||
expect(entity.trackingNumber).toBe('DHL-987654321');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. markShipped 业务逻辑与防呆校验', () => {
|
||||
it('正常标记发货时,更新承运商、运单号与发货状态', async () => {
|
||||
const result = await service.markShipped(1, 'dhl', 'DHL-987654321');
|
||||
expect(result.shippingStatus).toBe('shipped');
|
||||
expect(result.carrier).toBe('dhl');
|
||||
expect(result.trackingNumber).toBe('DHL-987654321');
|
||||
expect(result.shippedAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('设备不存在时应抛出异常', async () => {
|
||||
await expect(service.markShipped(999, 'fedex', 'FX-123')).rejects.toThrow(
|
||||
CoolCommException
|
||||
);
|
||||
});
|
||||
|
||||
it('缺少运单号时应抛出参数异常', async () => {
|
||||
await expect(service.markShipped(1, 'dhl', '')).rejects.toThrow(
|
||||
'缺少物流运单号'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 控制器 ship API 存在性与契约', () => {
|
||||
it('AdminNfcDeviceController 必须提供 ship 路由方法', () => {
|
||||
const controller = new AdminNfcDeviceController();
|
||||
expect(typeof (controller as any).ship).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -242,5 +242,7 @@ describe('OmniCard AI 门户顾问对话页契约 (ai-chat.uvue)', () => {
|
||||
false
|
||||
);
|
||||
expect((pageSource.match(/primary-cta-btn/g) || []).length).toBeGreaterThan(0);
|
||||
expect(pageSource.includes('ai-lead-card-submitted')).toBe(true);
|
||||
expect(pageSource.includes('cubic-bezier(0.2, 0.8, 0.2, 1)')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户