177 行
4.7 KiB
TypeScript
177 行
4.7 KiB
TypeScript
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<CrmLeadEntity>;
|
|
|
|
/**
|
|
* 智能意向度评分计算引擎
|
|
* 根据停留时长、滚动深度、浏览模块数加权综合评分 (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<CrmLeadEntity> {
|
|
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<CrmLeadEntity> {
|
|
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);
|
|
}
|
|
}
|