feat(round-12): 实体卡外贸生产工单/雷达多渠道归因漏斗/AI地道外联草拟/现代图片自适应协商 (87套件1007测试全绿)

这个提交包含在:
wikiboss
2026-09-21 02:06:55 +08:00
父节点 76f0dc17c0
当前提交 3cfbed9e6a
共有 8 个文件被更改,包括 897 次插入 和 0 次删除

查看文件

@@ -0,0 +1,111 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export interface OutreachDraftRequest {
leadName: string;
company?: string;
targetLanguage: string; // 'en' | 'ar' | 'es' | 'zh' | 'fr' | 'ja'
interestTopics?: string[];
calendarUrl?: string;
senderName?: string;
senderTitle?: string;
tone?: 'professional' | 'friendly' | 'respectful';
channel?: 'email' | 'whatsapp' | 'linkedin';
}
export interface OutreachDraftResponse {
channel: 'email' | 'whatsapp' | 'linkedin';
subject?: string;
body: string;
targetLanguage: string;
generatedAt: number;
}
/**
* 组装出海销售跟进提示词
*/
export function formatOutreachPrompt(req: OutreachDraftRequest): string {
const langCode = (req.targetLanguage || 'en').toLowerCase();
const langMap: Record<string, string> = {
ar: 'Arabic',
en: 'English',
es: 'Spanish',
fr: 'French',
ja: 'Japanese',
zh: 'Chinese',
};
const lang = langMap[langCode] || langCode;
const topics = req.interestTopics?.join(', ') || 'Smart Digital Cards & Mobile Portal';
const tone = req.tone || 'professional';
return `Task: Draft a high-converting B2B outreach message.
Target Language: ${lang} (${langCode})
Prospect Name: ${req.leadName}
Prospect Company: ${req.company || 'Their Esteemed Firm'}
Topics of Interest: ${topics}
Tone: ${tone}
Channel: ${req.channel || 'email'}
Call to Action: Include meeting booking link if available: ${req.calendarUrl || 'Available on request'}
Guidelines:
1. Swiss internationalist style: concise, credible, no hype, no emojis.
2. Maximize relevance to their browsing interests.`;
}
/**
* AI 销售潜客智能跟进邮件/消息地道草拟引擎
*/
@Provide()
export class AiOutreachDraftService extends BaseService {
/**
* 生成地道跟进草稿
*/
async generateDraft(req: OutreachDraftRequest): Promise<OutreachDraftResponse> {
const channel = req.channel || 'email';
const lang = (req.targetLanguage || 'en').toLowerCase();
const sender = req.senderName || 'OmniCard Team';
const topics = req.interestTopics && req.interestTopics.length > 0
? req.interestTopics.join(', ')
: 'OmniCard AI';
if (channel === 'whatsapp') {
let body = '';
if (lang === 'es') {
body = `Hola ${req.leadName}, un gusto saludarte. Vi tu interés en ${topics} en nuestra web. ¿Te gustaría agendar una breve llamada de 10 min? Saludos, ${sender}.`;
} else if (lang === 'ar') {
body = `مرحباً ${req.leadName}، تشرفنا باهتمامكم بـ ${topics}. يسعدنا ترتيب محادثة سريعة لمناقشة التفاصيل. تحياتي، ${sender}.`;
} else {
body = `Hi ${req.leadName}, great connecting. Noticed your interest in ${topics}. Would love to share a 10-min overview this week. Best, ${sender}.`;
}
return {
channel: 'whatsapp',
body,
targetLanguage: lang,
generatedAt: Date.now(),
};
}
// Default Email
let subject = '';
let body = '';
if (lang === 'ar') {
subject = `متابعة حلول ${topics} لشركة ${req.company || ''}`;
body = `عزيزي/عزيزتي ${req.leadName}،\n\nتحية طيبة وبعد،\n\nيسعدنا تواصلكم واهتمامكم بـ ${topics}. نود تقديم عرض مخصص يلبي تطلعات شركتكم الموقرة ${req.company || ''}.\n\nيمكنكم حجز موعد مناسب عبر الرابط:\n${req.calendarUrl || 'https://omnicard.ai/book'}\n\nوتفضلوا بقبول فائق الاحترام،\n${sender}\n${req.senderTitle || ''}`;
} else if (lang === 'es') {
subject = `Seguimiento de ${topics} para ${req.company || req.leadName}`;
body = `Estimado/a ${req.leadName},\n\nEspero que se encuentre bien. Me pongo en contacto tras notar su interés en ${topics}.\n\nNos encantaría explorar cómo nuestra solución puede potenciar las operaciones de ${req.company || 'su empresa'}.\n\nPuede seleccionar un horario conveniente aquí:\n${req.calendarUrl || 'https://omnicard.ai/book'}\n\nAtentamente,\n${sender}\n${req.senderTitle || ''}`;
} else {
subject = `Following up: ${topics} for ${req.company || req.leadName}`;
body = `Hi ${req.leadName},\n\nI hope this note finds you well.\n\nI noticed you explored our ${topics} at ${req.company || 'your organization'}. We have helped leading global trade teams streamline their international networking and lead conversion.\n\nFeel free to choose a time that suits your schedule:\n${req.calendarUrl || 'https://omnicard.ai/book'}\n\nBest regards,\n${sender}\n${req.senderTitle || ''}`;
}
return {
channel: 'email',
subject,
body,
targetLanguage: lang,
generatedAt: Date.now(),
};
}
}

查看文件

@@ -0,0 +1,91 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type SupportedImageFormat = 'avif' | 'webp' | 'png' | 'jpg';
export interface MediaNegotiationContext {
acceptHeader?: string;
networkType?: 'wifi' | '5g' | '4g' | '3g' | '2g' | 'unknown' | string;
targetWidth?: number;
dpr?: number;
}
export interface OptimizedMediaResult {
format: SupportedImageFormat;
width?: number;
quality: number;
optimizedUrl: string;
}
/**
* 协商最佳媒体交付 URL 与压缩参数
*/
export function negotiateOptimalMediaUrl(
url: string,
ctx: MediaNegotiationContext
): OptimizedMediaResult {
if (!url || typeof url !== 'string' || !url.trim()) {
return {
format: 'jpg',
quality: 80,
optimizedUrl: '',
};
}
const accept = (ctx.acceptHeader || '').toLowerCase();
const net = (ctx.networkType || 'wifi').toLowerCase();
const isSlowNetwork = net === '2g' || net === '3g';
// 1. 协商格式: avif > webp > 原图扩展名(png/jpg)
let format: SupportedImageFormat = 'jpg';
if (accept.includes('image/avif')) {
format = 'avif';
} else if (accept.includes('image/webp')) {
format = 'webp';
} else if (url.toLowerCase().endsWith('.png') || accept.includes('image/png')) {
format = 'png';
}
// 2. 协商质量与宽度
let quality = 85;
if (isSlowNetwork) {
quality = 65;
}
let width = ctx.targetWidth;
if (width != null && width > 0) {
const dpr = ctx.dpr && ctx.dpr > 1 ? ctx.dpr : 1;
if (isSlowNetwork) {
// 弱网降规 75%
width = Math.round(width * 0.75);
} else {
width = Math.round(width * dpr);
}
}
// 3. 构造优化 URL 参数
const separator = url.includes('?') ? '&' : '?';
const queryParts: string[] = [`format=${format}`, `q=${quality}`];
if (width != null && width > 0) {
queryParts.push(`w=${width}`);
}
const optimizedUrl = `${url}${separator}${queryParts.join('&')}`;
return {
format,
width,
quality,
optimizedUrl,
};
}
/**
* 微官网现代图片自适应协商服务
*/
@Provide()
export class PortalMediaNegotiatorService extends BaseService {
negotiate(url: string, ctx: MediaNegotiationContext): OptimizedMediaResult {
return negotiateOptimalMediaUrl(url, ctx);
}
}

查看文件

@@ -0,0 +1,169 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type TrafficChannelType =
| 'nfc_tap'
| 'booth_qr'
| 'whatsapp_chat'
| 'linkedin_share'
| 'email_signature'
| 'direct_or_organic';
export type FunnelStageType =
| 'impression'
| 'interactive'
| 'ai_consult'
| 'lead_submitted'
| 'meeting_booked';
export interface ChannelAttributionEvent {
visitorId: string;
channel: TrafficChannelType | string;
stage: FunnelStageType;
timestamp?: number;
}
export interface AttributionFunnelReport {
totalVisitors: number;
stages: Record<FunnelStageType, number>;
stageRates: Record<FunnelStageType, number>;
overallConversionRate: number;
}
export interface ChannelPerformanceItem {
channel: string;
visitors: number;
leads: number;
leadRate: number;
meetings: number;
}
/**
* 流量来源与渠道归一化解析
*/
export function normalizeTrafficChannel(
source?: string,
medium?: string
): TrafficChannelType {
const s = (source || '').toLowerCase().trim();
const m = (medium || '').toLowerCase().trim();
if (s.includes('nfc') || m.includes('nfc') || m.includes('tap')) {
return 'nfc_tap';
}
if (s.includes('booth') || m.includes('booth') || m.includes('qrcode')) {
return 'booth_qr';
}
if (s.includes('whatsapp') || m.includes('whatsapp')) {
return 'whatsapp_chat';
}
if (s.includes('linkedin') || m.includes('linkedin')) {
return 'linkedin_share';
}
if (s.includes('email') || m.includes('sig')) {
return 'email_signature';
}
return 'direct_or_organic';
}
/**
* 计算全链路出海留资转化漏斗指标
*/
export function calculateAttributionFunnel(
events: ChannelAttributionEvent[]
): AttributionFunnelReport {
const visitorSet = new Set<string>();
const stageSets: Record<FunnelStageType, Set<string>> = {
impression: new Set(),
interactive: new Set(),
ai_consult: new Set(),
lead_submitted: new Set(),
meeting_booked: new Set(),
};
for (const e of events) {
visitorSet.add(e.visitorId);
if (stageSets[e.stage]) {
stageSets[e.stage].add(e.visitorId);
}
}
const total = visitorSet.size;
const stages: Record<FunnelStageType, number> = {
impression: stageSets.impression.size,
interactive: stageSets.interactive.size,
ai_consult: stageSets.ai_consult.size,
lead_submitted: stageSets.lead_submitted.size,
meeting_booked: stageSets.meeting_booked.size,
};
const stageRates: Record<FunnelStageType, number> = {
impression: total > 0 ? Number((stages.impression / total).toFixed(2)) : 0,
interactive: total > 0 ? Number((stages.interactive / total).toFixed(2)) : 0,
ai_consult: total > 0 ? Number((stages.ai_consult / total).toFixed(2)) : 0,
lead_submitted: total > 0 ? Number((stages.lead_submitted / total).toFixed(2)) : 0,
meeting_booked: total > 0 ? Number((stages.meeting_booked / total).toFixed(2)) : 0,
};
const overallConversionRate =
total > 0 ? Number((stages.lead_submitted / total).toFixed(2)) : 0;
return {
totalVisitors: total,
stages,
stageRates,
overallConversionRate,
};
}
/**
* 访客雷达多渠道归因与出海转化分析服务
*/
@Provide()
export class RadarAttributionService extends BaseService {
/**
* 渠道转化表现分析
*/
analyzeChannelPerformance(
events: ChannelAttributionEvent[]
): Record<string, ChannelPerformanceItem> {
const channelMap: Record<string, { visitors: Set<string>; leads: Set<string>; meetings: Set<string> }> = {};
for (const e of events) {
const ch = e.channel;
if (!channelMap[ch]) {
channelMap[ch] = {
visitors: new Set(),
leads: new Set(),
meetings: new Set(),
};
}
channelMap[ch].visitors.add(e.visitorId);
if (e.stage === 'lead_submitted') {
channelMap[ch].leads.add(e.visitorId);
}
if (e.stage === 'meeting_booked') {
channelMap[ch].meetings.add(e.visitorId);
}
}
const report: Record<string, ChannelPerformanceItem> = {};
for (const [channel, data] of Object.entries(channelMap)) {
const vCount = data.visitors.size;
const lCount = data.leads.size;
const mCount = data.meetings.size;
const rate = vCount > 0 ? Number((lCount / vCount).toFixed(2)) : 0;
report[channel] = {
channel,
visitors: vCount,
leads: lCount,
leadRate: rate,
meetings: mCount,
};
}
return report;
}
}

查看文件

@@ -0,0 +1,180 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type ManufacturingBatchStatus =
| 'draft'
| 'ordered'
| 'manufacturing'
| 'qa_inspected'
| 'in_stock'
| 'rejected'
| 'cancelled';
export interface CreateManufacturingBatchDto {
batchNo: string;
materialType: string;
targetQuantity: number;
factoryName: string;
chipModel: string;
cardFinishing?: string;
}
export interface ManufacturingBatchRecord extends CreateManufacturingBatchDto {
id: string;
status: ManufacturingBatchStatus;
qualifiedQuantity: number;
defectQuantity: number;
yieldRate?: number;
createdAt: number;
updatedAt: number;
}
export interface BatchYieldResult {
yieldRate: number;
defectRate: number;
isQualified: boolean;
defectCount: number;
}
/**
* 生产批次工单生命周期合法转换图
*/
const ALLOWED_TRANSITIONS: Record<ManufacturingBatchStatus, ManufacturingBatchStatus[]> = {
draft: ['ordered', 'cancelled'],
ordered: ['manufacturing', 'cancelled'],
manufacturing: ['qa_inspected', 'cancelled'],
qa_inspected: ['in_stock', 'rejected'],
in_stock: [],
rejected: ['manufacturing'], // 返工重制
cancelled: [],
};
/**
* 判定工单状态机流转是否合法
*/
export function canTransitionBatchStatus(
from: ManufacturingBatchStatus,
to: ManufacturingBatchStatus
): boolean {
const allowed = ALLOWED_TRANSITIONS[from];
return allowed != null && allowed.includes(to);
}
/**
* 计算生产良率与报废率
*/
export function calculateBatchYieldRate(
totalPlanned: number,
qualified: number,
defect: number,
qualificationThreshold = 0.95
): BatchYieldResult {
if (totalPlanned <= 0) {
throw new Error('计划生产总量必须大于零');
}
if (qualified < 0 || defect < 0) {
throw new Error('合格数与不良品数不能为负数');
}
if (qualified > totalPlanned) {
throw new Error('合格品数量不能超出计划生产总量');
}
const inspectedTotal = qualified + defect;
if (inspectedTotal > totalPlanned * 1.05) {
// 允许少量备损溢产,超出过多则报错
throw new Error('质检总数超出合理批次生产总数范围');
}
const yieldRate = Number((qualified / totalPlanned).toFixed(3));
const defectRate = Number((defect / totalPlanned).toFixed(3));
const isQualified = yieldRate >= qualificationThreshold;
return {
yieldRate,
defectRate,
isQualified,
defectCount: defect,
};
}
/**
* 跨境实体卡供应链生产批次工单管理服务
*/
@Provide()
export class NfcManufacturingService extends BaseService {
private readonly batches = new Map<string, ManufacturingBatchRecord>();
/**
* 创建生产批次工单
*/
createBatch(dto: CreateManufacturingBatchDto): ManufacturingBatchRecord {
if (!dto.batchNo || !dto.targetQuantity || dto.targetQuantity <= 0) {
throw new Error('生产批次号与计划生产数量为必填项');
}
const now = Date.now();
const record: ManufacturingBatchRecord = {
id: `mfg_${now}_${Math.random().toString(36).slice(2, 6)}`,
...dto,
status: 'draft',
qualifiedQuantity: 0,
defectQuantity: 0,
createdAt: now,
updatedAt: now,
};
this.batches.set(dto.batchNo, record);
return record;
}
/**
* 获取生产工单
*/
getBatch(batchNo: string): ManufacturingBatchRecord | undefined {
return this.batches.get(batchNo);
}
/**
* 流转工单生命周期状态
*/
updateBatchStatus(
batchNo: string,
targetStatus: ManufacturingBatchStatus
): ManufacturingBatchRecord {
const batch = this.batches.get(batchNo);
if (!batch) {
throw new Error(`未找到批次工单: ${batchNo}`);
}
if (!canTransitionBatchStatus(batch.status, targetStatus)) {
throw new Error(`非法状态流转: 不能从 ${batch.status} 流转至 ${targetStatus}`);
}
batch.status = targetStatus;
batch.updatedAt = Date.now();
return batch;
}
/**
* 录入出厂质检数据
*/
recordBatchInspection(
batchNo: string,
qualified: number,
defect: number
): ManufacturingBatchRecord {
const batch = this.batches.get(batchNo);
if (!batch) {
throw new Error(`未找到批次工单: ${batchNo}`);
}
const stats = calculateBatchYieldRate(batch.targetQuantity, qualified, defect);
batch.qualifiedQuantity = qualified;
batch.defectQuantity = defect;
batch.yieldRate = stats.yieldRate;
batch.status = 'qa_inspected';
batch.updatedAt = Date.now();
return batch;
}
}

查看文件

@@ -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');
});
});
});