当前位置: 首页 > news >正文

教培SaaS线索分配系统的状态机设计从人工抢单到智能路由的演进

背景

教培机构的招生线索管理是一个典型的状态流转场景。一条线索从进入系统到最终成交或流失,会经历分配、跟进、试听、转化等多个状态。我们最初用status字段加if-else实现,随着业务复杂度增加,代码变成了面条式的条件判断,维护噩梦。后来重构为状态机模式,今天分享这个过程。

一、原始实现的问题

最初的线索模型很简单:

class Lead:

status = models.CharField(max_length=20, default='new')

assigned_to = models.ForeignKey('User', null=True)

last_followup = models.DateTimeField(null=True)

# 分配逻辑散落在各处

def assign_lead(lead, user):

if lead.status == 'new':

lead.assigned_to = user

lead.status = 'assigned'

lead.save()

elif lead.status == 'assigned':

raise Exception('already assigned')

elif lead.status == 'lost':

raise Exception('cannot assign lost lead')

def followup_lead(lead, user, note):

if lead.status == 'assigned' and lead.assigned_to == user:

lead.status = 'following'

lead.last_followup = timezone.now()

lead.save()

elif lead.status == 'following':

lead.last_followup = timezone.now()

lead.save()

else:

raise Exception('invalid state transition')

这种写法的问题很明显:状态流转逻辑散落在十几个函数里,每加一个状态就要改十几处代码。而且没有防止非法状态转换,比如有人把'lost'状态的线索改成'following'也不会报错。

二、状态机建模

首先梳理线索的完整状态流转图:

new -> assigned -> following -> trial_scheduled -> trial_completed -> won/lost

还有几条特殊路径:

assigned -> recycled(超时未跟进回收)

following -> recycled(长时间未推进回收)

trial_completed -> following(试听后继续跟进)

任何状态 -> invalid(线索无效)

用状态机模式重构:

from enum import Enum

from transitions import Machine

class LeadState(Enum):

NEW = 'new'

ASSIGNED = 'assigned'

FOLLOWING = 'following'

TRIAL_SCHEDULED = 'trial_scheduled'

TRIAL_COMPLETED = 'trial_completed'

WON = 'won'

LOST = 'lost'

RECYCLED = 'recycled'

INVALID = 'invalid'

class LeadStateMachine:

transitions = [

# trigger, source, dest, conditions, before, after

['assign', 'new', 'assigned', 'can_assign', 'before_assign', 'after_assign'],

['assign', 'recycled', 'assigned', 'can_assign', 'before_assign', 'after_assign'],

['start_followup', 'assigned', 'following', None, None, 'notify_assignee'],

['schedule_trial', 'following', 'trial_scheduled', None, 'validate_trial_slot', 'notify_parent'],

['complete_trial', 'trial_scheduled', 'trial_completed', None, 'record_trial_feedback', None],

['continue_followup', 'trial_completed', 'following', None, None, 'notify_assignee'],

['win', ['following', 'trial_completed'], 'won', None, 'create_contract', 'notify_manager'],

['lose', ['following', 'trial_completed'], 'lost', None, 'record_loss_reason', None],

['recycle', ['assigned', 'following'], 'recycled', None, 'clear_assignee', 'add_to_pool'],

['invalidate', '*', 'invalid', None, 'record_invalid_reason', None],

]

def __init__(self, lead):

self.lead = lead

self.machine = Machine(

model=self,

states=[s.value for s in LeadState],

transitions=self.transitions,

initial=lead.status,

send_event=True

)

def can_assign(self, event):

# 检查当前用户是否有分配权限

user = event.kwargs.get('user')

return user.has_perm('assign_lead')

def before_assign(self, event):

user = event.kwargs.get('user')

self.lead.assigned_to = user

self.lead.assigned_at = timezone.now()

def after_assign(self, event):

# 发送通知给被分配人

send_notification.delay(

user_id=self.lead.assigned_to_id,

title='新线索分配',

content=f'您收到一条新线索:{self.lead.student_name}'

)

使用transitions库后,状态流转逻辑集中在一处定义,非法转换会被自动拦截,新增状态只需在transitions列表里加一条。

三、智能分配路由

状态机解决了"怎么转"的问题,但没解决"转给谁"的问题。最初是人工抢单,后来客户要求智能分配。

分配策略需要考虑多个因素:销售老师的转化率、当前待跟进线索数、线索来源渠道与销售老师的匹配度、时间段(有些老师晚上效率高)。

class LeadRouter:

def __init__(self, config):

self.weights = {

'conversion_rate': 0.35,

'workload': 0.25,

'channel_match': 0.20,

'response_speed': 0.20

}

self.config = config

async def route(self, lead, candidates):

scores = []

for user in candidates:

score = await self._calculate_score(lead, user)

scores.append((user, score))

scores.sort(key=lambda x: x[1], reverse=True)

# 如果最高分和第二高分差距小于阈值,随机选一个避免总是分给同一个人

if len(scores) >= 2 and (scores[0][1] - scores[1][1]) < 0.05:

top_two = scores[:2]

chosen = random.choice(top_two)[0]

else:

chosen = scores[0][0]

return chosen

async def _calculate_score(self, lead, user):

# 转化率得分

conv_rate = await self._get_conversion_rate(user)

conv_score = min(conv_rate / 0.3, 1.0) # 30%转化率得满分

# 工作量得分(待跟进越少分越高)

pending = await self._get_pending_count(user)

workload_score = max(1 - pending / 30, 0) # 30条待跟进得0分

# 渠道匹配度

channel_match = await self._get_channel_match(user, lead.channel)

# 响应速度

avg_response = await self._get_avg_response_time(user)

response_score = max(1 - avg_response / 3600, 0) # 1小时响应得0分

total = (

self.weights['conversion_rate'] * conv_score +

self.weights['workload'] * workload_score +

self.weights['channel_match'] * channel_match +

self.weights['response_speed'] * response_score

)

return total

这里踩了一个坑:最初把转化率权重设为0.6,结果导致所有高转化率的销售老师线索堆积,而新老师一直分不到线索,能力无法提升。后来把转化率权重降到0.35,加了随机选择机制,线索分配更均衡了。

四、超时回收机制

线索分配后如果销售老师不及时跟进,需要在超时后自动回收。这需要一个定时任务扫描超时线索:

class LeadRecycler:

ASSIGN_TIMEOUT = 2 * 3600 # 分配后2小时未跟进

FOLLOWUP_TIMEOUT = 48 * 3600 # 跟进后48小时未推进

async def run(self):

while True:

await self._check_assignment_timeout()

await self._check_followup_timeout()

await asyncio.sleep(300) # 每5分钟检查一次

async def _check_assignment_timeout(self):

cutoff = timezone.now() - timedelta(seconds=self.ASSIGN_TIMEOUT)

leads = Lead.objects.filter(

status='assigned',

assigned_at__lt=cutoff

)

for lead in leads:

fsm = LeadStateMachine(lead)

try:

fsm.recycle()

lead.status = fsm.state

lead.assigned_to = None

lead.save()

await self._notify_recycle(lead, 'assignment_timeout')

except MachineError:

pass # 状态转换失败,跳过

五、总结

从面条式if-else到状态机加智能路由的演进,核心收获:

1. 状态机模式把散落的状态流转逻辑集中管理,新增状态和转换规则只需改一处配置。

2. transitions库的send_event模式可以在转换前后插入钩子函数,做通知、校验、日志等操作。

3. 智能分配需要多维度加权打分,不能只看单一指标,权重要跟业务方一起调。

4. 超时回收是线索管理容易被忽略的一环,不回收就等于线索浪费。

5. 状态机要有完整的日志记录,每次状态转换都记录who、when、from、to、reason,方便后续分析转化漏斗。

http://www.cnnetsun.cn/news/4011217.html

相关文章:

  • QKeyMapper:打破设备界限,让Windows输入控制随心所欲的终极解决方案
  • 数字孪生智慧水利建设方案:数字孪生水利工程建设、典型项目案例、智慧水利解决方案、 信创与市场机会
  • 网站建设共享ip:中小站长避坑指南与深度解析
  • 大连网站建设仟亿科技:深耕本土数字化服务,以匠心铸就企业品牌数字基石与长远价值
  • 揭秘紫金网站建设背后的那些事儿:从0到1打造企业的数字化门面,为何它不仅是技术更是态度?
  • 深入解析承德建设银行网站功能特色与本地金融服务升级体验指南
  • 先读字段,再开始搜索:科研 Agent 为什么需要 Schema Discovery
  • 基于AlmaLinux的Bash自动化运维:从第一性原理到工程实践
  • 安新建设局网站作为便民窗口,如何打造高效透明的政务服务新标杆?
  • 石家庄网站建设王道下拉棒:深度解析企业官网转型的必由之路与实战避坑指南
  • HR AI智能体协同作战:42个Agent如何接管重复性HR工作
  • 福建漳州网站建设哪家便宜?避坑指南与真实成本解析,帮您在网络时代不被割韭菜
  • 【Android学习-wifi局域网配网开发】
  • 北京企业官网网站建设哪家好:避坑指南与深度解析,教你选对合作伙伴不花冤枉钱
  • Keras ZeroPadding2D层详解:从核心原理到版本降级避坑指南
  • 重庆双福建设开发有限公司网站深度解析:如何解读其背后的城市发展与民生承诺
  • 深圳网站建设搜q479185700专业解答企业数字化转型中的那些坑与雷
  • 揭秘滨州正规网站建设哪家专业:从避坑指南到优质服务商的选择逻辑与深度解析
  • 信号与系统考研复习:从知识到解题的暑假逆袭规划
  • 禅城网站建设价格背后的真相与选择:企业如何避开报价陷阱拿到合理方案
  • 基于Claude Code的Agentic编码:上下文工程驱动复杂软件开发
  • 如何给自己建设的网站设置登陆用户名和密码:从零开始构建安全壁垒的实战指南
  • 为什么很多老板都夸滨州网站建设有实力?揭秘背后那些不为人知的真实故事
  • 漳州微网站建设公司哪家好:避坑指南与选型核心逻辑揭秘
  • 户县建设局网站:深度解析地方城市建设管理与民生服务的全景视窗
  • 数学建模竞赛实战:从路径规划到多约束优化,以“板凳龙”为例
  • 深入了解浙江省建设厅官方网站:获取权威政策解读与办事指南的终极指南
  • 清远城乡住房建设部网站如何助力百姓安居:从政策解读到民生保障的深度解析
  • 手机电子商务网站建设策划书:如何打造高转化率的移动端购物平台实战指南
  • 正则指引——常用语言中正则特性一览