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

发布时间:2026/10/2 1:55:22

教培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方便后续分析转化漏斗。
延伸阅读

更多相关文章

2026/10/2 1:55:10

从“杀菌”到“养菌”:2026年皮肤管理行业的技术范式转移

引言 2026年,皮肤管理行业正在经历一场技术范式的转移——从传统的 “杀菌”思维转向 “养菌”思维。这场转移的核心,是对皮肤微生态认知的全面升级。 传统范式的局限 传统的皮肤清洁逻辑建立在“杀菌”思维之上——通过物理或化学手段,尽可能…

2026/9/27 15:23:10

口碑好的德州智能晾衣架推荐生产厂家

开篇先说个扎心的事实:很多装修业主在选晾衣架时,面对一堆“承重35kg”“电机质保10年”“智能风干”的参数直接懵圈。更头疼的是,网上虚标严重,同一个配置价格能差出一倍,不同预算到底该选哪个品牌?别急&a…

2026/10/2 1:53:03

快速上手 uv:Python 包与项目管理器的 tldr 速查指南

文档教程知识库 【免费下载链接】tldr Collaborative cheatsheets for console commands 📚. 项目地址: https://gitcode.com/GitHub_Trending/tl/tldr 点击查看 免费下载 导读 uv 是一个以“快”著称的 Python 包管理器与项目管理器,用一条…

2026/10/1 5:21:14

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/10/1 17:09:46

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/1 10:48:55

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/2 0:02:57

PWN入门:从栈溢出原理到ROP链实战

1. 这不是“学PWN”,是重新理解你每天敲的每一行C代码我第一次在CTF赛场上写出能控制程序流的exp时,手抖得连gdb的c命令都输错三次。那道题只有23行C代码,一个gets()调用,一个printf(),一个return——它甚至没开NX&…

2026/10/2 0:02:57

Windows下cudaMallocHost显存占用之谜:WDDM与TCC模式差异及优化方案

1. 一个反直觉的显存占用现象第一次在 Windows 上看到cudaMallocHost把显存吃掉的时候,我的反应是打开任务管理器反复确认了三遍。明明调用的是主机端锁页内存分配,按 CUDA 文档的说法,这块内存应该落在系统 RAM 里,跟 GPU 的显存…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑