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; stageRates: Record; 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(); const stageSets: Record> = { 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 = { 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 = { 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 { const channelMap: Record; leads: Set; meetings: Set }> = {}; 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 = {}; 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; } }