教培SaaS线索分配系统的状态机设计从人工抢单到智能路由的演进 背景教培机构的招生线索管理是一个典型的状态流转场景。一条线索从进入系统到最终成交或流失会经历分配、跟进、试听、转化等多个状态。我们最初用status字段加if-else实现随着业务复杂度增加代码变成了面条式的条件判断维护噩梦。后来重构为状态机模式今天分享这个过程。一、原始实现的问题最初的线索模型很简单class Lead:status models.CharField(max_length20, defaultnew)assigned_to models.ForeignKey(User, nullTrue)last_followup models.DateTimeField(nullTrue)# 分配逻辑散落在各处def assign_lead(lead, user):if lead.status new:lead.assigned_to userlead.status assignedlead.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 followinglead.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 Enumfrom transitions import Machineclass LeadState(Enum):NEW newASSIGNED assignedFOLLOWING followingTRIAL_SCHEDULED trial_scheduledTRIAL_COMPLETED trial_completedWON wonLOST lostRECYCLED recycledINVALID invalidclass 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 leadself.machine Machine(modelself,states[s.value for s in LeadState],transitionsself.transitions,initiallead.status,send_eventTrue)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 userself.lead.assigned_at timezone.now()def after_assign(self, event):# 发送通知给被分配人send_notification.delay(user_idself.lead.assigned_to_id,title新线索分配,contentf您收到一条新线索{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 configasync def route(self, lead, candidates):scores []for user in candidates:score await self._calculate_score(lead, user)scores.append((user, score))scores.sort(keylambda x: x[1], reverseTrue)# 如果最高分和第二高分差距小于阈值随机选一个避免总是分给同一个人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 chosenasync 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(secondsself.ASSIGN_TIMEOUT)leads Lead.objects.filter(statusassigned,assigned_at__ltcutoff)for lead in leads:fsm LeadStateMachine(lead)try:fsm.recycle()lead.status fsm.statelead.assigned_to Nonelead.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方便后续分析转化漏斗。

相关新闻

最新新闻

VLM如何学会自主调用视觉工具?VTool-R1强化学习框架解析

VLM如何学会自主调用视觉工具?VTool-R1强化学习框架解析

1. 项目概述:当VLM学会“动手”最近在跟进视觉语言大模型(VLM)的应用落地,一个核心痛点越来越明显:这些模型“看”和“说”的能力很强,但“做”的能力几乎为零。它们能精准描述一张图片里有“一个红色的苹果…

2026/8/14 3:00:37
周三-自动愈合稳定层-工程化实战02

周三-自动愈合稳定层-工程化实战02

别再写 try/except 了——给浏览器自动化加一层"自动愈合"的稳定层「工程化实战」栏目 第 02 篇 与周一《从 IP 到 Browser Context》的关系:周一讲的是环境隔离(L1/L2/L3 指纹),这篇讲的是脚本自身的健壮性——环境隔离…

2026/8/14 3:00:37
深圳精洗专业度看设备流程

深圳精洗专业度看设备流程

在深圳,车主对车辆清洁养护的要求早就不满足于路边随便冲两下了,不少开了三五年以上的老车车主,都愿意花大几十做精洗,精洗也成了很多爱车人的日常选择。不过要说深圳哪家精洗做的专业,很多人都拿不准。其实精洗专不专…

2026/8/14 3:00:37
Windows 10更新失败终极解决方案:覆盖安装原理与实操指南

Windows 10更新失败终极解决方案:覆盖安装原理与实操指南

1. 项目概述:当Windows 10更新“卡死”时,覆盖安装为何是终极方案? 如果你也经历过Windows 10更新时那个令人绝望的“正在准备更新”或“正在下载更新”的无限循环,或者看到过“我们无法完成更新,正在撤销更改”的提示…

2026/8/14 3:00:37
水力发电站智能巡检与运维整体方案

水力发电站智能巡检与运维整体方案

国家能源局数据显示,截至2026年6月底,全国水电累计装机容量达4.54亿千瓦,其中常规水电3.85亿千瓦、抽水蓄能0.69亿千瓦。以一座240万千瓦水电站为例,传统人工巡检需跨越发电机层、母线层、水轮机层和蜗壳层4个作业面,单…

2026/8/14 3:00:37
深圳58同城网站建设一站式指南如何从零开始搭建高效转化的B2B营销平台

深圳58同城网站建设一站式指南如何从零开始搭建高效转化的B2B营销平台

深圳这座城市的节奏,就像被按了快进键。每天早晨,早高峰的地铁里挤满了渴望在这个超大城市扎根的年轻人;深夜的写字楼里,依然闪烁着不肯熄灭的灯光。在这里,机遇与压力并存,焦虑与希望同在。作为一名在深圳深耕互联网营销多年的老炮儿,我见过太多老板因为不懂网络,而让…

2026/8/14 2:55:36