import { Provide } from '@midwayjs/core'; import { BaseService, CoolCommException } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { CrmLeadEntity } from '../entity/lead'; import { LeadSource, LeadStatus } from '../../../comm/types/omni'; /** * 意向度评分计算结果接口 */ export interface IntentScoreResult { score: number; level: 'low' | 'medium' | 'hot'; } /** * 留资入参 DTO */ export interface SubmitLeadDTO { cardId: number | string; visitorId?: string; name: string; company?: string; phone?: string; email?: string; status?: LeadStatus; intentScore?: number; source?: LeadSource; notes?: string; stayDurationMs?: number; scrollDepth?: number; blockCount?: number; } /** * 潜客线索服务 * 负责线索沉淀、多维行为意向度打标与跟进流转 */ @Provide() export class CrmLeadService extends BaseService { @InjectEntityModel(CrmLeadEntity) crmLeadEntity: Repository; /** * 智能意向度评分计算引擎 * 根据停留时长、滚动深度、浏览模块数加权综合评分 (0 - 100 分) * * 权重分配: * - 停留时长: 权重 40 分 (满 60 秒拿满分) * - 滚动深度: 权重 30 分 (100% 滚动拿满分) * - 模块浏览: 权重 30 分 (浏览 5 个及以上模块拿满分) * * 等级划分: * - score >= 75: 'hot' (高意向/火热) * - 40 <= score < 75: 'medium' (中意向/温和) * - score < 40: 'low' (低意向/冷淡) */ calculateIntentScore( stayDurationMs = 0, scrollDepth = 0, blockCount = 0 ): IntentScoreResult { const safeStay = Math.max(0, stayDurationMs); const safeScroll = Math.max(0, scrollDepth); const safeBlocks = Math.max(0, blockCount); // 1. 停留时长得分 (0 - 40) const stayScore = Math.min(40, Math.round((safeStay / 60000) * 40)); // 2. 滚动深度得分 (0 - 30) const scrollScore = Math.min(30, Math.round((safeScroll / 100) * 30)); // 3. 模块浏览得分 (0 - 30) const blockScore = Math.min(30, safeBlocks * 6); // 4. 加权总分 const score = Math.min(100, Math.max(0, stayScore + scrollScore + blockScore)); // 5. 自动判定意向度等级 let level: 'low' | 'medium' | 'hot' = 'low'; if (score >= 75) { level = 'hot'; } else if (score >= 40) { level = 'medium'; } return { score, level }; } /** * 潜客互换名片留资沉淀 */ async submitLead(data: SubmitLeadDTO): Promise { if (!data || !data.cardId) { throw new CoolCommException('缺少名片标识 cardId', 400); } const name = data.name ? String(data.name).trim() : ''; if (!name) { throw new CoolCommException('缺少客户姓名 name', 400); } const phone = data.phone ? String(data.phone).trim() : ''; const email = data.email ? String(data.email).trim() : ''; if (!phone && !email) { throw new CoolCommException('请至少填写手机号或电子邮箱等联系方式', 400); } // 计算或采用意向度评分 let intentScore = data.intentScore ?? 0; if ( data.intentScore === undefined && (data.stayDurationMs !== undefined || data.scrollDepth !== undefined || data.blockCount !== undefined) ) { const calculated = this.calculateIntentScore( data.stayDurationMs, data.scrollDepth, data.blockCount ); intentScore = calculated.score; } // 判定初始状态 let status: LeadStatus = data.status || 'new'; if (!data.status && intentScore >= 75) { status = 'hot'; } const lead = new CrmLeadEntity(); lead.cardId = Number(data.cardId); lead.visitorId = data.visitorId || ''; lead.name = name; lead.company = data.company ? String(data.company).trim() : undefined; lead.phone = phone || undefined; lead.email = email || undefined; lead.status = status; lead.intentScore = intentScore; lead.source = data.source || 'link'; lead.notes = data.notes; return await this.crmLeadEntity.save(lead); } /** * 线索跟进状态生命周期流转 */ async updateStatus( id: number | string, status: LeadStatus ): Promise { const validStatuses: LeadStatus[] = [ 'new', 'contacted', 'hot', 'won', 'archived', ]; if (!validStatuses.includes(status)) { throw new CoolCommException('非法状态值', 400); } const lead = await this.crmLeadEntity.findOne({ where: { id: Number(id) }, }); if (!lead) { throw new CoolCommException('潜客线索不存在', 404); } lead.status = status; return await this.crmLeadEntity.save(lead); } }