feat(round-12): 实体卡外贸生产工单/雷达多渠道归因漏斗/AI地道外联草拟/现代图片自适应协商 (87套件1007测试全绿)
这个提交包含在:
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
AiOutreachDraftService,
|
||||
OutreachDraftRequest,
|
||||
formatOutreachPrompt,
|
||||
} from '../../../src/modules/ai/service/outreach';
|
||||
|
||||
describe('Point 47: AI 销售潜客智能跟进邮件/消息地道草拟引擎', () => {
|
||||
let service: AiOutreachDraftService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new AiOutreachDraftService();
|
||||
});
|
||||
|
||||
describe('1. formatOutreachPrompt (出海商务邮件地道提示词组装)', () => {
|
||||
it('英语场景应包含地道商务开场、兴趣针对性提及与日程预约呼吁', () => {
|
||||
const req: OutreachDraftRequest = {
|
||||
leadName: 'Arthur Pendelton',
|
||||
company: 'Global Retail Alliance',
|
||||
targetLanguage: 'en',
|
||||
interestTopics: ['Bento Grid Micro-portal', 'Smart NFC Card'],
|
||||
calendarUrl: 'https://calendar.google.com/render?action=TEMPLATE...',
|
||||
tone: 'professional',
|
||||
};
|
||||
|
||||
const prompt = formatOutreachPrompt(req);
|
||||
expect(prompt).toContain('Arthur Pendelton');
|
||||
expect(prompt).toContain('Global Retail Alliance');
|
||||
expect(prompt).toContain('Bento Grid Micro-portal');
|
||||
expect(prompt).toContain('professional');
|
||||
expect(prompt).toContain('Call to Action');
|
||||
});
|
||||
|
||||
it('阿拉伯语场景应要求符合中东商业礼仪与敬语规范', () => {
|
||||
const req: OutreachDraftRequest = {
|
||||
leadName: 'Sheikh Tariq Al-Mansoor',
|
||||
company: 'Riyadh Holdings',
|
||||
targetLanguage: 'ar',
|
||||
interestTopics: ['NFC VIP Cards'],
|
||||
tone: 'respectful',
|
||||
};
|
||||
|
||||
const prompt = formatOutreachPrompt(req);
|
||||
expect(prompt).toContain('Sheikh Tariq Al-Mansoor');
|
||||
expect(prompt).toContain('Arabic');
|
||||
expect(prompt).toContain('respectful');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. AiOutreachDraftService 草稿生成与结构化解析', () => {
|
||||
it('应生成包含邮件主题、正文与预约链接的结构化草案', async () => {
|
||||
const draft = await service.generateDraft({
|
||||
leadName: 'Elena Rostova',
|
||||
company: 'Nordic Trade Hub',
|
||||
targetLanguage: 'en',
|
||||
interestTopics: ['Visitor Radar CRM'],
|
||||
senderName: 'Alex Zheng',
|
||||
senderTitle: 'VP of International Sales',
|
||||
});
|
||||
|
||||
expect(draft.subject).toBeDefined();
|
||||
expect(draft.subject.length).toBeGreaterThan(0);
|
||||
expect(draft.body).toContain('Elena Rostova');
|
||||
expect(draft.body).toContain('Visitor Radar CRM');
|
||||
expect(draft.body).toContain('Alex Zheng');
|
||||
expect(draft.targetLanguage).toBe('en');
|
||||
});
|
||||
|
||||
it('支持生成 WhatsApp 极简短消息格式 (short_im)', async () => {
|
||||
const draft = await service.generateDraft({
|
||||
leadName: 'Carlos Gomez',
|
||||
company: 'Madrid Ventures',
|
||||
targetLanguage: 'es',
|
||||
channel: 'whatsapp',
|
||||
senderName: 'Alex',
|
||||
});
|
||||
|
||||
expect(draft.channel).toBe('whatsapp');
|
||||
expect(draft.body).toContain('Carlos Gomez');
|
||||
expect(draft.body.length).toBeLessThan(300); // 移动端短消息克制简洁
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
negotiateOptimalMediaUrl,
|
||||
MediaNegotiationContext,
|
||||
SupportedImageFormat,
|
||||
} from '../../../src/modules/card/service/media-negotiator';
|
||||
|
||||
describe('Point 48: 移动微官网现代图片自适应协商与弱网资源压缩', () => {
|
||||
const originalUrl = 'https://cdn.omnicard.ai/uploads/cards/vip-avatar.png';
|
||||
|
||||
describe('1. 格式协商 (Image Format Negotiation: AVIF > WebP > JPEG/PNG)', () => {
|
||||
it('客户端声明支持 AVIF 时应优先协商为 AVIF 格式', () => {
|
||||
const ctx: MediaNegotiationContext = {
|
||||
acceptHeader: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
networkType: '4g',
|
||||
};
|
||||
|
||||
const result = negotiateOptimalMediaUrl(originalUrl, ctx);
|
||||
expect(result.format).toBe('avif');
|
||||
expect(result.optimizedUrl).toContain('format=avif');
|
||||
});
|
||||
|
||||
it('客户端不支持 AVIF 但支持 WebP 时应协商为 WebP', () => {
|
||||
const ctx: MediaNegotiationContext = {
|
||||
acceptHeader: 'image/webp,*/*',
|
||||
networkType: 'wifi',
|
||||
};
|
||||
|
||||
const result = negotiateOptimalMediaUrl(originalUrl, ctx);
|
||||
expect(result.format).toBe('webp');
|
||||
expect(result.optimizedUrl).toContain('format=webp');
|
||||
});
|
||||
|
||||
it('旧版客户端不包含新现代格式时保留原有格式或转为 jpg', () => {
|
||||
const ctx: MediaNegotiationContext = {
|
||||
acceptHeader: 'image/png,image/*,*/*',
|
||||
};
|
||||
|
||||
const result = negotiateOptimalMediaUrl(originalUrl, ctx);
|
||||
expect(result.format).toBe('png');
|
||||
expect(result.optimizedUrl).toContain('format=png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 网络环境与屏幕自适应 (Network & DPR Adaptive Scaling)', () => {
|
||||
it('弱网 (2g/3g) 环境应自动降低图片质量与宽度以保障极致加载速度', () => {
|
||||
const ctx: MediaNegotiationContext = {
|
||||
acceptHeader: 'image/webp,*/*',
|
||||
networkType: '3g',
|
||||
targetWidth: 800,
|
||||
};
|
||||
|
||||
const result = negotiateOptimalMediaUrl(originalUrl, ctx);
|
||||
expect(result.quality).toBeLessThanOrEqual(70);
|
||||
expect(result.width).toBeLessThanOrEqual(600);
|
||||
expect(result.optimizedUrl).toContain(`w=${result.width}`);
|
||||
expect(result.optimizedUrl).toContain(`q=${result.quality}`);
|
||||
});
|
||||
|
||||
it('高速 WiFi 且高 DPR (dpr >= 2) 时应保障高清晰度', () => {
|
||||
const ctx: MediaNegotiationContext = {
|
||||
acceptHeader: 'image/avif,image/webp',
|
||||
networkType: 'wifi',
|
||||
targetWidth: 400,
|
||||
dpr: 2,
|
||||
};
|
||||
|
||||
const result = negotiateOptimalMediaUrl(originalUrl, ctx);
|
||||
expect(result.width).toBe(800); // 400 * 2 = 800
|
||||
expect(result.quality).toBeGreaterThanOrEqual(85);
|
||||
expect(result.optimizedUrl).toContain('w=800');
|
||||
});
|
||||
|
||||
it('无效或空 URL 时应安全回退', () => {
|
||||
const res = negotiateOptimalMediaUrl('', {});
|
||||
expect(res.optimizedUrl).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
RadarAttributionService,
|
||||
normalizeTrafficChannel,
|
||||
calculateAttributionFunnel,
|
||||
ChannelAttributionEvent,
|
||||
} from '../../../src/modules/crm/service/attribution';
|
||||
|
||||
describe('Point 46: 访客雷达多渠道来源归因与出海转化漏斗分析', () => {
|
||||
let service: RadarAttributionService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RadarAttributionService();
|
||||
});
|
||||
|
||||
describe('1. 流量渠道归一化解析 (normalizeTrafficChannel)', () => {
|
||||
it('应精准识别 NFC 碰一碰与展位二维码', () => {
|
||||
expect(normalizeTrafficChannel('nfc', 'tap')).toBe('nfc_tap');
|
||||
expect(normalizeTrafficChannel('booth', 'qrcode')).toBe('booth_qr');
|
||||
});
|
||||
|
||||
it('应精准识别 WhatsApp 与 LinkedIn 社交出海渠道', () => {
|
||||
expect(normalizeTrafficChannel('whatsapp', 'im')).toBe('whatsapp_chat');
|
||||
expect(normalizeTrafficChannel('linkedin', 'social')).toBe('linkedin_share');
|
||||
expect(normalizeTrafficChannel('email', 'sig')).toBe('email_signature');
|
||||
});
|
||||
|
||||
it('未知或直接访问归入 direct_or_organic', () => {
|
||||
expect(normalizeTrafficChannel('', '')).toBe('direct_or_organic');
|
||||
expect(normalizeTrafficChannel(undefined, undefined)).toBe('direct_or_organic');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculateAttributionFunnel (5阶段留资转化漏斗计算)', () => {
|
||||
const mockEvents: ChannelAttributionEvent[] = [
|
||||
// 访客 1: 全链路完成
|
||||
{ visitorId: 'v1', channel: 'nfc_tap', stage: 'impression' },
|
||||
{ visitorId: 'v1', channel: 'nfc_tap', stage: 'interactive' },
|
||||
{ visitorId: 'v1', channel: 'nfc_tap', stage: 'ai_consult' },
|
||||
{ visitorId: 'v1', channel: 'nfc_tap', stage: 'lead_submitted' },
|
||||
{ visitorId: 'v1', channel: 'nfc_tap', stage: 'meeting_booked' },
|
||||
|
||||
// 访客 2: 停留于 AI 咨询
|
||||
{ visitorId: 'v2', channel: 'nfc_tap', stage: 'impression' },
|
||||
{ visitorId: 'v2', channel: 'nfc_tap', stage: 'interactive' },
|
||||
{ visitorId: 'v2', channel: 'nfc_tap', stage: 'ai_consult' },
|
||||
|
||||
// 访客 3: WhatsApp 渠道完成留资
|
||||
{ visitorId: 'v3', channel: 'whatsapp_chat', stage: 'impression' },
|
||||
{ visitorId: 'v3', channel: 'whatsapp_chat', stage: 'lead_submitted' },
|
||||
|
||||
// 访客 4: 仅浏览展位
|
||||
{ visitorId: 'v4', channel: 'booth_qr', stage: 'impression' },
|
||||
];
|
||||
|
||||
it('应精确统计各漏斗阶段的独立访客数与流失率', () => {
|
||||
const funnel = calculateAttributionFunnel(mockEvents);
|
||||
|
||||
expect(funnel.totalVisitors).toBe(4);
|
||||
expect(funnel.stages.impression).toBe(4);
|
||||
expect(funnel.stages.interactive).toBe(2);
|
||||
expect(funnel.stages.ai_consult).toBe(2);
|
||||
expect(funnel.stages.lead_submitted).toBe(2);
|
||||
expect(funnel.stages.meeting_booked).toBe(1);
|
||||
|
||||
// 留资总体转化率: 2 / 4 = 50%
|
||||
expect(funnel.overallConversionRate).toBe(0.5);
|
||||
});
|
||||
|
||||
it('应支持按渠道拆分多维度漏斗表现', () => {
|
||||
const breakdown = service.analyzeChannelPerformance(mockEvents);
|
||||
|
||||
expect(breakdown['nfc_tap']).toBeDefined();
|
||||
expect(breakdown['nfc_tap'].visitors).toBe(2);
|
||||
expect(breakdown['nfc_tap'].leads).toBe(1);
|
||||
expect(breakdown['nfc_tap'].leadRate).toBe(0.5);
|
||||
|
||||
expect(breakdown['whatsapp_chat'].visitors).toBe(1);
|
||||
expect(breakdown['whatsapp_chat'].leads).toBe(1);
|
||||
expect(breakdown['whatsapp_chat'].leadRate).toBe(1.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
NfcManufacturingService,
|
||||
ManufacturingBatchStatus,
|
||||
CreateManufacturingBatchDto,
|
||||
calculateBatchYieldRate,
|
||||
canTransitionBatchStatus,
|
||||
} from '../../../src/modules/nfc/service/manufacturing';
|
||||
|
||||
describe('Point 45: 跨境实体卡供应链生产批次工单生命周期与良率检验', () => {
|
||||
let service: NfcManufacturingService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new NfcManufacturingService();
|
||||
});
|
||||
|
||||
describe('1. 生产工单状态机流转规则 (canTransitionBatchStatus)', () => {
|
||||
it('应允许合法正向生命周期流转: draft -> ordered -> manufacturing -> qa_inspected -> in_stock', () => {
|
||||
expect(canTransitionBatchStatus('draft', 'ordered')).toBe(true);
|
||||
expect(canTransitionBatchStatus('ordered', 'manufacturing')).toBe(true);
|
||||
expect(canTransitionBatchStatus('manufacturing', 'qa_inspected')).toBe(true);
|
||||
expect(canTransitionBatchStatus('qa_inspected', 'in_stock')).toBe(true);
|
||||
});
|
||||
|
||||
it('质检不合格可流转为 rejected 废品或返工', () => {
|
||||
expect(canTransitionBatchStatus('qa_inspected', 'rejected')).toBe(true);
|
||||
expect(canTransitionBatchStatus('manufacturing', 'cancelled')).toBe(true);
|
||||
expect(canTransitionBatchStatus('draft', 'cancelled')).toBe(true);
|
||||
});
|
||||
|
||||
it('逆向或跨阶段非法流转应坚决拒绝', () => {
|
||||
expect(canTransitionBatchStatus('in_stock', 'draft')).toBe(false);
|
||||
expect(canTransitionBatchStatus('draft', 'in_stock')).toBe(false);
|
||||
expect(canTransitionBatchStatus('rejected', 'in_stock')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculateBatchYieldRate (出厂质检良率与损耗统计)', () => {
|
||||
it('应精确计算生产良品率 (Yield Rate)', () => {
|
||||
// 计划生产 1000 张,实际合格 985 张,报废 15 张
|
||||
const result = calculateBatchYieldRate(1000, 985, 15);
|
||||
expect(result.yieldRate).toBe(0.985);
|
||||
expect(result.defectRate).toBe(0.015);
|
||||
expect(result.isQualified).toBe(true); // 默认合格线 >= 95%
|
||||
});
|
||||
|
||||
it('良率低于阈值 (如 95%) 时应标记为未达标', () => {
|
||||
const result = calculateBatchYieldRate(1000, 920, 80, 0.95);
|
||||
expect(result.yieldRate).toBe(0.92);
|
||||
expect(result.isQualified).toBe(false);
|
||||
expect(result.defectCount).toBe(80);
|
||||
});
|
||||
|
||||
it('非法入参处理 (如数量为负或超过总数) 应安全抛出异常', () => {
|
||||
expect(() => calculateBatchYieldRate(0, 0, 0)).toThrow();
|
||||
expect(() => calculateBatchYieldRate(100, 105, 0)).toThrow();
|
||||
expect(() => calculateBatchYieldRate(100, -5, 0)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. NfcManufacturingService 批次工单管理', () => {
|
||||
it('应能创建生产批次并初始化为 draft 状态', () => {
|
||||
const batchDto: CreateManufacturingBatchDto = {
|
||||
batchNo: 'MFG-2026-Q4-001',
|
||||
materialType: 'MATTE_BLACK_METAL', // 磨砂黑金属卡
|
||||
targetQuantity: 500,
|
||||
factoryName: 'Shenzhen Smart Chip Technology Co.',
|
||||
chipModel: 'NTAG216',
|
||||
cardFinishing: 'GOLD_LASER_ENGRAVING', // 激光金雕
|
||||
};
|
||||
|
||||
const batch = service.createBatch(batchDto);
|
||||
expect(batch.id).toBeDefined();
|
||||
expect(batch.batchNo).toBe('MFG-2026-Q4-001');
|
||||
expect(batch.status).toBe('draft');
|
||||
expect(batch.targetQuantity).toBe(500);
|
||||
expect(batch.qualifiedQuantity).toBe(0);
|
||||
});
|
||||
|
||||
it('更新生产状态并在达到 qa_inspected 时记录质检指标', () => {
|
||||
const batch = service.createBatch({
|
||||
batchNo: 'MFG-2026-Q4-002',
|
||||
materialType: 'ECO_BAMBOO_WOOD', // 环保竹木卡
|
||||
targetQuantity: 200,
|
||||
factoryName: 'EcoCard Global Ltd.',
|
||||
chipModel: 'NTAG213',
|
||||
});
|
||||
|
||||
// 流转为 ordered -> manufacturing
|
||||
service.updateBatchStatus(batch.batchNo, 'ordered');
|
||||
service.updateBatchStatus(batch.batchNo, 'manufacturing');
|
||||
|
||||
// 质检录入
|
||||
const updated = service.recordBatchInspection(batch.batchNo, 196, 4);
|
||||
expect(updated.status).toBe('qa_inspected');
|
||||
expect(updated.qualifiedQuantity).toBe(196);
|
||||
expect(updated.defectQuantity).toBe(4);
|
||||
expect(updated.yieldRate).toBe(0.98);
|
||||
|
||||
// 入库
|
||||
const inStock = service.updateBatchStatus(batch.batchNo, 'in_stock');
|
||||
expect(inStock.status).toBe('in_stock');
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户