feat(round-12): 实体卡外贸生产工单/雷达多渠道归因漏斗/AI地道外联草拟/现代图片自适应协商 (87套件1007测试全绿)
这个提交包含在:
@@ -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;
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户