先说个背景。做销售的团队都知道,客户资料到处散落在Excel、微信聊天记录、纸质名片里,跟进到哪一步全凭个人记忆,这种状态撑到几十个客户还行,一旦过了两三百条线索,基本就开始乱了。我接手DeskcommCRM这个项目的时候,团队面临的就是这个局面:线索来源杂、跟进记录断档、工单处理靠吼,所以当时的目标非常明确——做一个轻量、够用、能落地的客户管理系统,不追求大而全,而是先把客户从线索到成交再到售后服务的主链路打通。
整个项目从设计到跑起来大概用了三周,目前已经稳定运行了四个多月,今天把这套系统的搭法、踩过的坑、以及几个关键模块的实现细节完整记录下来,给同样在做CRM选型或自建的朋友一个参考。
1. 项目整体设计与思路拆解
1.1 为什么选择自建轻量级CRM而不是买现成的
当时第一个摆在面前的问题,不是说要不要上CRM,而是买SaaS还是自己搭。
市面上主流的CRM产品我也考察过,功能确实全面,从线索管理到客户画像再到销售预测全都有。但问题也很明显:一是价格不便宜,按坐席收费,团队三四十号人一年下来是笔不小的开销;二是很多功能的颗粒度和我们实际业务流程对不上,比如我们的售后工单要绑定具体的客户联系人,还要关联到产品序列号,这个逻辑在通用CRM里配置起来非常费劲;三是数据都放在别人服务器上,导出和二次开发都受限制。
所以综合考虑之后,决定自己搭一个轻量级CRM。核心思路就一句话——只做业务主链路上必不可少的功能,其余一律砍掉。我们的主链路很清晰:
- 线索进来(官网表单、渠道投放、老客户转介绍)
- 销售跟进(记录沟通内容、更新客户阶段、设置下次跟进时间)
- 客户沉淀(成交后自动转为客户,建立客户档案)
- 售后工单(客户提单、客服处理、结果回写客户档案)
这套流程理顺之后,系统设计就变得非常简单,本质就是一张大宽表加上几个状态机的流转。
1.2 技术选型背后的取舍逻辑
技术栈用的是我比较熟悉的一套组合:前端Vue3 + Element Plus,后端Java Spring Boot,数据库MySQL,缓存用Redis,部署在阿里云一台4核8G的ECS上。
选Java而不是Node.js或者Python,主要是考虑到后续可能会有更复杂的数据统计需求,Java生态里这块的工具链更成熟。前端选Vue3没有太多纠结,因为团队里前端工程师对这个最熟,而且Element Plus的表格和表单组件用来做管理后台非常顺手。
数据库表结构设计是整个项目里最值得花时间的部分,我花了一天半专门理这个。核心表一共就六张:用户表、线索表、客户表、跟进记录表、工单表、工单回复表。很多人做CRM喜欢把线索和客户分成两套完全独立的表,字段各设计一套,但我没有这么做,而是用一张表加一个状态字段(record_type)来区分线索和客户。这么做的好处是减少了跨表查询,客户从线索转化过来的时候,不需要执行Insert,只需要Update一个状态字段,数据的连贯性也更好。
Redis主要用来做两件事:一是登录会话管理,二是跟进提醒的延迟队列。前者不用多说,后者展开说一下——每个销售都可以给客户设置“下次跟进时间”,这个时间到了系统要在工作台提醒。实现这个功能最简单的方案是起一个定时任务每分钟扫一次数据库,但这样对数据库压力比较大。我采用的是把未来24小时内需要提醒的跟进任务缓存到Redis的有序集合里,用时间戳作为分数,定时任务只需要从Redis里取分数小于当前时间的元素即可,只有Redis取不到的时候才去数据库兜底查一次。
1.3 业务权限模型的简化处理
权限这一块,很多CRM会把角色划分得非常细,什么销售主管、销售专员、客服主管、客服专员、运营、管理员,每个角色配一套菜单权限和数据权限。我刚开始也这么设计了,但做完一版发现太复杂了,单单权限中间表就有七八张,后来果断做减法。
最终保留的模型是这样的:角色只有三种——管理员、销售、客服。权限控制分两层:
- 菜单权限:控制能看左侧哪些菜单,在登录的时候一次性查出来放到Redis里
- 数据权限:控制能看到哪些数据,这个不靠按钮级别的细粒度权限,而是靠数据表里的
owner_id字段
比如说,销售只能看自己负责的线索和客户,管理员看全部,客服只能看被分派给自己的工单。因为业务规则足够简单,所以直接在SQL查询层面加条件过滤就行,不需要引入ShardingSphere这类数据权限框架。
提示:自建系统最怕权限模型一开始设计得过重。先跑通主流程,再根据使用反馈加权限维度,这个顺序不要反。
2. 核心功能模块的实操要点
2.1 线索导入与去重机制的实现细节
线索进来的入口有三个:官网表单直接写入、销售手动新增、Excel批量导入。其中Excel批量导入是最容易出问题的地方,我花了比较多精力处理。
批量导入的技术实现本身不复杂,后端用EasyExcel解析上传的文件,逐行校验格式后写入数据库。真正的难点在于去重策略。刚开始团队定的规则是按手机号去重,但实际跑下来发现误伤很严重——同一个客户的手机号可能在系统里已经存在,但是对应的需求描述完全不同,直接合并会把两条独立的需求搞混。
后来我把去重策略改成了两段式:第一段,手机号完全相同的,直接标记为“重复线索”,不进销售私海,而是进入一个待确认列表由管理员处理;第二段,手机号不同但公司名和联系人姓名都相同的,标记为“疑似重复”,允许销售在列表里看到,但需要手动确认合并。这样既避免了误伤,又留了人工判断的空间。
导入这块还有一个容易踩的坑是编码问题。Excel文件虽然有.xlsx后缀,但里面的内容可能来自各种渠道,有的系统导出的CSV文件是GBK编码的,用UTF-8去读全是乱码。我在前端上传之前加了一个文件编码嗅探逻辑,读取文件的前几个字节判断BOM,如果是GBK编码就在前端转换成UTF-8再上传,这个问题就彻底根治了。
2.2 客户详情页设计的核心逻辑
客户详情页是整个系统里最有价值的一个页面,因为销售每天的大部分时间都花在这个页面上。我设计这个页面的原则是:所有和这个客户相关的信息,都在一个页面里展示完,不让销售跳来跳去。
页面从上到下依次是:客户基本信息卡片(名称、行业、规模、来源渠道、负责人);关键字段区(客户阶段、预计金额、下次跟进时间);跟进记录时间线(按照时间倒序展示所有沟通记录);关联工单列表(如果这个客户提过工单,直接展示在下方)。
其中,跟进记录时间线是这个页面的灵魂。每次跟进的内容是什么、下次计划做什么、有没有新的联系人加入,全都以时间线的形式记录下来。销售每天的工作流变成:打开工作台看今日待跟进列表 -> 点进某个客户 -> 看上次跟进记录 -> 添加新的跟进记录 -> 更新下次跟进时间。整个闭环非常顺畅。
技术实现上,客户详情页的数据是分三个接口返回的,基础信息一个、跟进记录一个、关联工单一个,前端并行请求。没有做服务端聚合,因为这三个数据的变化频率不一样,分开查询更灵活,比如跟进记录可以用分页加载,而基础信息只需要查一次。
2.3 工单流转规则的状态机设计
工单模块相对独立,但也比较复杂,因为涉及状态的流转和响应时效的考核。我设计的工单状态机一共有五个状态:待分配、处理中、待客户确认、已解决、已关闭。
流转规则如下:
- 客户提交工单后,状态默认是“待分配”,系统根据工单的产品分类自动匹配到对应客服的待办池
- 客服认领或管理员手动分派后,状态变为“处理中”
- 客服处理完提交解决方案,状态变为“待客户确认”
- 客户确认无问题,状态变为“已解决”
- 超过7天没有新回复的“已解决”工单,定时任务自动置为“已关闭”
每个状态的变更都会记录一条操作日志,方便后续追溯。这里我特别注意了一个细节——状态变更操作全部放在一个事务里,比如从“处理中”变为“待客户确认”的时候,不仅要改工单的状态字段,还要记录操作日志、通知客户(发邮件或者站内信)、更新客服的处理中工单数量,这几步任何一个失败都要回滚,绝不能出现工单状态改了但客户没收到通知的情况。
关于SLA(服务级别协议)报警,我用了一个比较实用的办法。每张工单在分配的时候会根据优先级设置一个“首次响应时限”,比如普通工单4小时,紧急工单1小时。系统每分钟跑一次定时任务,扫描所有处理中且首次响应时间为空的工单,如果当前时间减去工单创建时间超过了响应时限,就向对应的客服和管理员发送一条站内信和邮件提醒。响应时间这个字段是在客服第一次添加工单回复的时候写入的,所以不会重复触发报警。
3. 数据流转与自动化能力的落地
3.1 自定义字段体系的搭建过程
自建的CRM如果字段写死了,后面一定会后悔,因为业务需求经常变。所以我在设计的时候就预留了自定义字段的能力,实现方式不算复杂但很实用。
具体的做法是:在客户表旁边加两张表,一张是custom_field_def存储字段定义,比如字段名称、字段类型(文本/数字/单选/多选/日期)、是否必填;另一张是custom_field_value存储字段值,主键是记录ID加字段ID的联合主键。查询的时候,前端先请求客户基础数据,再请求自定义字段的定义和值,动态渲染到页面上。
这套方案上线之后很快就派上了用场。有一次市场部门说要增加一个“客户预算范围”的字段,用作销售筛选高意向客户,如果当时没有自定义字段机制,就得改表结构、改前端表单、改详情页展示,至少得折腾一天。有了这套机制,市场人员在管理后台自己就能配置好,前端页面自动就显示出来了,前后用了不到十分钟。
当然这个方案也有它的代价:自定义字段没法参与复杂的SQL排序和聚合,如果要按自定义字段筛选,需要先查出匹配的ID列表再回表查询。对于字段总量不大(我们控制在20个以内)、数据量不大(10万条以下)的场景,性能完全不是问题。如果以后数据量上去了,可以考虑用MySQL的JSON字段替代分表方案,但至少现在没有必要。
3.2 跟进提醒的自动化实现方式
跟进提醒这个功能,看起来不起眼,其实对销售的执行力影响特别大。系统上线之前销售经常忘跟进,靠的是主管口头催;系统上线之后,每天上午九点半,销售的工作台就会自动弹出当天需要跟进的客户列表,这个体验上的升级是很明显的。
实现方式在前面提过,核心是Redis的有序集合。具体来说,每次创建或更新跟进记录的时候,后端会把下次跟进时间作为分数写入Redis:zadd remind_queue 下次跟进时间戳 userId:customerId。然后一个后台任务每隔五分钟执行一次:zrangebyscore remind_queue -inf 当前时间戳,取出所有到期任务,按用户维度合并之后推送站内信提醒,同时从有序集合里删除这些已处理的任务。
这里有一个坑需要注意:如果利用Redis有序集合来做延迟队列,任务到期后一定要先删除再处理,不能先处理再删除。因为如果先处理业务逻辑(比如推送消息),然后才删除Redis里的记录,过程中服务一旦崩溃,重启后这个任务会重复执行,客户就会收到两条重复的跟进提醒。反过来,先删除再处理,最多丢失一次提醒,相比之下可接受得多。
3.3 客户阶段转化与数据看板的统计口径
数据看板是最后做的模块,虽然技术上不复杂,但在统计口径上踩了不少坑。
项目刚启动的时候,我用一个简单的SQL统计各阶段的客户数量:SELECT stage, COUNT(*) FROM customers GROUP BY stage。结果跑出来自己都吓了一跳,各阶段客户数量加起来比客户总数多了不少。排查之后发现问题出在客户阶段的历史变动没被处理——同一个客户上周在“初步沟通”阶段,这周变成了“方案报价”,但看板统计的是当前阶段,所以两周的数据对不上。
最终我采用了客户阶段变更流水表的方案:每次客户阶段变更时,在流水表里插入一条记录,包含客户ID、变更前阶段、变更后阶段、变更时间。看板统计的时候,按时间范围去流水表里查每个阶段有哪些客户,再按当前的最新状态归类。这样做之后,数据口径就对上了,而且还可以顺便统计每个阶段的转化时长、各来源渠道的转化率,这些数据对管理者来说特别有参考价值。
看板的实现本身没什么特殊的,就是几张报表页面:今日新增线索数、今日跟进次数、各阶段客户分布、本周成交金额、工单响应时长统计。前端用ECharts画图,后端提供对应的聚合查询接口,缓存策略上是五分钟刷一次,避免频繁查数据库。
4. 实际操作中的部署记录与优化调整
4.1 服务器配置与初期部署方案
服务器用的是阿里云的ECS,配置是4核8G,系统盘40G,数据盘100G。这个配置对于日活一百人左右的内部管理系统来说,可以说是比较宽裕了。实际运行了四个月,CPU使用率基本都在20%以下,内存使用率在50%左右,完全没有性能压力。如果再算上后续的数据增长和可能的并发提高,这台机器撑个两年应该没什么问题。
部署方式用的是最传统的方案:后端打包成Jar包,用systemd注册成系统服务,前端构建之后用Nginx托管静态文件,同时Nginx做了反向代理,把/api开头的请求转发到后端的8080端口。HTTPS证书用的是阿里云的免费证书,配置好自动续期之后基本不用管。
数据库用的阿里云RDS MySQL 5.7,而不是自己装的MySQL,虽然是付费服务,但省去了备份、高可用、监控这一堆事情。对于一个小团队来说,把运维成本降到最低才是正经事,自己折腾主从复制和高可用方案,投入产出比太低。
备份方面,RDS自带自动备份,我额外设置了一个每天凌晨两点的全量备份,保留最近7天。另外每周末手动导出一份SQL文件存到OSS上,作为跨平台的异地备份。这套备份策略在四个月里没真的用到过,但数据库这种基础设施,宁可多备不能漏备。
4.2 连接池、索引与慢查询的初期调优
系统刚上线的那一两周,数据库方面确实发现一些性能问题。最明显的是客户列表页,销售翻页翻到后面几页的时候,接口响应时间从一两百毫秒涨到了一秒以上。
用EXPLAIN分析了一下慢查询日志,发现问题出在客户表的大范围查询上。客户列表页的筛选条件比较多,包括负责人、客户阶段、来源渠道、最近跟进时间,这些字段只有部分建了单列索引,组合查询的时候MySQL只能用到其中一个索引,其他条件只能全表扫描过滤,数据量一旦上去,查询自然就慢了。
解决办法是给最常用的几个查询组合创建联合索引。比如“负责人 + 客户阶段”建一个联合索引,“创建时间 + 来源渠道”建一个联合索引。建完之后,列表页的响应时间降到了两百毫秒以内,效果立竿见影。
连接池方面用的是HikariCP,配置的最大连接数是20,最小空闲连接数是5。这个参数在同规格的机器上跑一百个并发以内的请求完全够用,如果并发上去了,优先考虑加机器而不是调大连接数,因为数据库的连接数并不是越大越好,连接太多反而会把RDS的资源耗尽。
提示:上线初期要养成定期看慢查询日志的习惯。MySQL默认的慢查询阈值是10秒,建议在配置里调低到1秒,这样能更早发现潜在的SQL性能问题。
4.3 搜索功能的演进:从LIKE到全文索引
系统里的全局搜索功能一开始做得比较粗糙,就是客户名称和联系人姓名用LIKE '%关键词%'模糊匹配。客户量在几千的时候没觉得有什么问题,等到了两三万条,搜索接口的响应时间明显变慢了,有时候要三四秒才能出结果。
中间想过接Elasticsearch来做搜索,但考虑到数据量并不大,引入一套分布式搜索引擎有点杀鸡用牛刀,而且要额外维护一套集群,投入产出比不划算。后来翻了一下MySQL的文档,发现5.7版本之后自带全文索引功能,支持中文分词(虽然默认的分词器对中文支持一般,但可以用ngram解析器),就试着用了这个方案。
把客户名称、联系人姓名、联系手机号三个字段建成全文索引,然后用MATCH...AGAINST语法执行搜索,响应时间从三四秒降到了两三百毫秒,效果非常明显。当然前提是数据量在可控范围内,如果以后数据量突破了百万级别,再考虑引入Elasticsearch也不迟。
5. 常见问题与排查技巧实录
5.1 登录会话丢失与Redis配置问题
系统上线后遇到的第一个大问题就是用户反映登录状态经常丢失,有时候上午登录的,下午点开页面就被踢回登录页了。排查了半天才发现不是代码问题,而是Redis的内存淘汰策略导致的。
我部署的Redis实例默认的maxmemory-policy是noeviction,也就是内存满了之后不淘汰任何数据,新的写入直接报错。而系统用Redis存了登录会话、权限信息、跟进提醒队列等一系列数据,内存很快就满了。内存满了之后,新的会话写入失败,用户就被踢出登录状态。
这个问题的解决办法是给Redis设置一个合理的最大内存上限和淘汰策略,比如maxmemory 2gb,淘汰策略用allkeys-lru。同时,给登录会话设置一个合理的过期时间(我用的24小时),再加上滑动续期,这样即使淘汰策略有偏差,用户也不会频繁掉线。
5.2 批量导入数据时的字段映射错误
有一次市场部门导入了两千多条线索,导入完成后抽查数据,发现很多线索的“客户规模”字段是乱的,有的显示“50-100人”,有的显示“100-500人”,跟原始Excel里的内容完全对不上。
排查之后发现是Excel模板的列顺序和前端解析的列映射不一致。市场人员用Excel重新整理模板的时候,不小心插入了一列,导致后面所有列的偏移了一位。前端EasyExcel按索引解析的时候,把“客户规模”这一列的内容解析到了“所在行业”字段上。
这个问题单纯靠代码层面很难完全避免,因为前端没法判断Excel里的每一列到底是不是用户想要的字段。最后我做了两个优化:一是导入之前增加了一个预览步骤,把解析后的前十条数据显示在页面上,让用户确认字段映射正确后再正式导入;二是前端解析Excel的时候使用表头名称进行匹配,而不是用列索引,这样即使列顺序变了也能正确识别。
5.3 工单重复创建与幂等性处理
工单模块上线后,有一个客户反馈说同一个问题提了两次单,我们的系统里也确实出现了两张几乎一模一样(产品相同、问题描述相同)的工单。
排查原因是客户在网络波动的情况下点击了两次“提交”按钮,前端没有做防重复提交的限制,两次请求都到达了后端,各自创建了一条工单记录。这在弱网环境下是个高频问题,尤其是客户那边用的系统网络本身就不太稳定。
解决方法是做接口的幂等性处理:前端提交工单时生成一个唯一的请求ID(UUID),后端在创建工单之前先查一下这个请求ID是否已经存在,存在则直接返回已有的工单信息,不存在再创建。Redis里存储请求ID加过期时间(比如10分钟),这样既解决了重复提交的问题,又不会给Redis带来长期存储负担。同时,前端也在按钮点击后立即置灰,双保险。
5.4 数据看板统计数字对不上的排查思路
看板模块做出来之后,管理员反馈说“本周成交金额”这个数字跟财务那边统计的金额对不上,系统里显示的是580万,财务那边是620万。
这个问题的根源在于成交金额的判定标准不统一。系统里“成交客户”是指客户阶段被修改为“已成交”的客户,但在这个阶段变更之前,客户可能已经付了全款,也可能只付了定金。财务那边的标准是看实际回款到账,比销售阶段变更的时间通常要滞后几天,所以两边统计口径天然就对不上。
解决这个问题的思路是增加一个独立的“成交记录”实体,销售在客户进入“已成交”阶段的时候,需要单独录入一条成交记录,包含成交金额、成交日期、回款方式等字段。看板里的“本周成交金额”统计的是成交日期在本周的记录,而不是客户阶段变更的日期,这样跟财务的口径就一致了。
提示:自建系统里的统计口径问题特别容易忽略。凡是涉及金额、次数、时长的统计,一定要提前明确唯一的判定标准,并且把这个标准固化到代码和文档里,否则后面反复扯皮。
6. 项目上线后的使用反馈与后续规划
系统上线第四周的时候,我做了一个简单的小范围问卷调研,问了一线销售和客服几个关键问题:日常工作效率有没有提升、哪些功能用得最多、还有哪些痛点没解决。
反馈比较集中的有几个点:一是客户详情页的跟进时间线确实是每天用最多的功能,销售觉得比之前翻Excel和聊天记录找上下文要高效得多;二是工单模块的SLA提醒很有用,客服不会漏掉紧急工单了;三是大家普遍希望增加移动端的支持,因为销售在外面拜访客户的时候不方便开电脑。
移动端的支持我目前已经纳入规划了,但不会单独开发App,而是做一套适配手机浏览器的H5版本,只保留最核心的两个功能:查看今日待跟进列表和快速记录跟进内容。考虑到我们用的前端框架是Vue3,可以直接复用大部分代码逻辑,工程量不会太大。
另外一个方向是客户公海和私海的结合。目前线索分配还是管理员手动分配的,销售把线索跟丢或者一直不动的情况没有机制来兜底。后续可以做一个简单的“僵尸线索回收”逻辑:线索超过7天没有跟进记录,自动回到公海池,其他销售可以领取继续跟进,这样能有效提高线索利用率。
最后再分享一个做这类系统的心得:自建CRM最大的优势不是省钱,而是所有功能都能贴着团队的实际业务流程走,没有那些用不上的复杂功能压在头顶上。但前提是你要非常克制,脑子里时刻有一根弦——这个功能真的需要吗?能不能用更简单的方式替代?我见过很多自建系统死在功能膨胀上,做着做着就变成了一个比商用软件还复杂的怪物,最后没人愿意用。保持简单,保持贴近真实业务,是这个项目走到现在最核心的一条经验。