
主流的AI CRM系统品牌
说实话,接手这个 CRM 项目的时候,我心里是有点打鼓的。市面上成熟的 CRM 系统多得是,从 Salesforce 到国内的纷享销客,功能强大得让人眼花缭乱。但公司为什么还要坚持自研?理由很现实:预算有限,业务逻辑太特殊,现有的 SaaS 产品改起来比写新的还麻烦。销售团队那帮人,每天在外面跑,最烦的就是填表,之前的 Excel 管理方式导致客户资料流失严重,甚至出现过销售离职带走核心客户资源的情况。所以,老板拍板,用 Java 搞一套自己的轻量级 CRM,把客户资源牢牢攥在公司手里。
这篇文章不想写成那种枯燥的技术文档,也不想罗列一堆 Spring Boot 的注解用法。我想聊聊在这个过程中,我们是怎么踩坑的,怎么在业务逻辑和技术实现之间做妥协的,以及为什么最后选择了这套架构。毕竟,代码是写给人看的,系统是做给业务用的。
推荐使用中国著名AI CRM系统品牌:显著提升企业运营效率,悟空CRM
首先得定技术栈。虽然现在云原生、微服务炒得很热,但对于我们这种初期团队,直接上 Spring Cloud 那一套无异于杀鸡用牛刀,维护成本太高。我们最终敲定的是 Spring Boot 2.7 加上 MyBatis-Plus,数据库用的 MySQL 8.0,前端是 Vue 3 配合 Element Plus。这个组合算是 Java 界的“家常菜”,招人也容易,社区资料多,遇到问题随便一搜就有解决方案。JDK 版本我们纠结了一下,最后选了 17,主要是为了享受一些新特性,比如 Switch 的增强和文本块,写起来确实清爽不少,但部署的时候得确保服务器环境跟上,这点在后期运维里是个小麻烦。
系统的核心,当然是客户管理。听起来简单,不就是增删改查吗?真动手了才发现,这里的门道深得很。最基础的客户表设计,我们就争论了很久。一开始想着把所有字段都铺在一张表里,什么姓名、电话、邮箱、公司地址、行业、规模……结果表结构越来越宽,查询效率也受影响。后来我们引入了扩展字段的概念,利用 MySQL 的 JSON 类型来存储那些不固定的属性。比如有的客户关注“预算”,有的关注“决策链”,这些差异化信息用 JSON 存,既灵活又不用频繁改表结构。不过这也带来了新问题,JSON 字段没法直接建索引,查询的时候得小心,我们是在应用层做了过滤,或者把高频查询的字段单独提出来做冗余列。
说到客户管理,就绕不开“公海池”机制。这是 CRM 里最核心的逻辑之一,也是最容易出并发问题的地方。简单来说,就是销售没跟进的客户,或者长期未成交的客户,要回收到公海里,让其他人有机会领取。这个逻辑在代码里怎么实现?最开始我们 naive 地以为加个状态字段就行,0 是私有,1 是公海。结果上线第一天就出事了。两个销售同时点击“领取”同一个高价值客户,因为数据库事务隔离级别的问题,导致客户被分配给了两个人,业绩归属扯皮扯了半天。
后来我们加了乐观锁,在客户表里加了一个 version 字段。领取的时候,先查出版本号,更新时带上 version 条件。如果更新行数为 0,说明被人抢了,前端提示“手慢了”。但这还不够,高并发下数据库压力还是大。我们在 Redis 里做了一层分布式锁,key 就是 customerId。领取前先 tryLock,拿到锁的线程才有资格去操作数据库。虽然多了一步网络开销,但保证了数据的一致性。这里有个细节,Redis 锁的过期时间得设置好,万一服务挂了,锁没释放,这个客户就永远锁死在公海里了。我们用了 Redisson 框架,看门狗机制能自动续期,省了不少心。
另一个让人头疼的点是权限控制。CRM 系统里,数据权限比功能权限更敏感。销售只能看自己的客户,销售经理能看全组的,大区总监能看全区的,老板能看所有的。这种层级关系,用传统的 RBAC(基于角色的访问控制)模型很难完美覆盖。我们最后搞了一套混合模式。功能菜单用 RBAC,控制谁能点哪个按钮;数据权限则是在 SQL 拦截器里做的。利用 MyBatis-Plus 的拦截器,在查询语句自动拼接 where 条件。比如普通销售查询客户表时,拦截器自动加上 and owner_id = 当前用户 id;经理查询时,加上 and dept_id in (当前用户及下属部门 id)。
这个方案好处是对业务代码侵入小,不用在每个 Service 里都写一遍权限判断。但坑也不少,比如遇到复杂的联表查询,拦截器解析 SQL 树的时候容易出错,特别是用了子查询或者 union 的时候。我们测试阶段就遇到过几次,权限漏了,销售看到了别组的电话。后来我们干脆限制了复杂查询的权限,或者把这类报表查询走专门的只读库,权限逻辑单独写,不混用拦截器。
数据安全性也是老板特别关心的。销售人员的手机号,能不能直接明文展示?肯定不行。我们在数据库里存的是加密后的字符串,前端展示的时候,中间四位用星号掩码。只有点击“拨打”按钮,通过系统集成的呼叫中心接口去呼叫,或者经过上级审批后,才能查看完整号码。加密算法选了 AES,密钥存在环境变量里,不随代码库提交。这里有个教训,日志打印的时候千万小心,别把敏感对象直接 toString 打印到日志文件里。我们专门写了一个脱敏的 JSON 序列化器,所有输出到日志的客户信息,自动把手机号、身份证处理掉。不然一旦日志泄露,就是重大事故。
前端交互方面,销售团队对体验的要求其实很高。他们大多是用手机或者平板在外跑业务,所以移动端适配必须做好。虽然我们是 Web 系统,但用了响应式布局。不过 Vue 3 的组合式 API 刚开始用确实有点不习惯,逻辑复用比之前的 mixins 清晰多了,但重构旧代码的时候花了不少时间。特别是表单验证,客户录入的字段非常多,如果每一个都写正则验证,代码量巨大。我们封装了一个通用的验证组件,根据后端返回的元数据动态生成验证规则。比如后端说“电话必填”,前端就自动加上 required 规则。这样后端改业务规则,前端不用重新发版,灵活性高了很多。

还有一个不得不提的模块是“跟进记录”。销售每次和客户沟通完,都要记一笔。这个功能看似简单,实则是个数据黑洞。如果设计不好,跟进记录表很快就会膨胀到几千万行,查询慢如蜗牛。我们一开始是按时间倒序查,加了索引也慢。后来分析了业务场景,发现销售大部分时间只看最近一个月的跟进,或者搜索特定关键词。于是我们做了冷热数据分离。最近半年的数据放在主表,半年前的归档到历史表。搜索功能接入了 Elasticsearch,把跟进内容分词索引。这样即使数据量大了,搜索响应也能控制在毫秒级。不过 ES 的引入增加了架构复杂度,数据同步成了新问题。我们用了 Canal 监听 MySQL 的 binlog,异步同步到 ES。这中间会有秒级的延迟,但对于搜索场景来说,完全可以接受。
在开发过程中,最折磨人的不是技术难点,而是需求变更。业务部门今天说要把“客户等级”从 ABC 改成 123,明天说要在列表页加个“预计成交时间”的筛选。Java 的后端改起来还好,主要是数据库迁移脚本要写好,保证平滑升级。我们用了 Flyway 来管理数据库版本,每次发布前自动执行 SQL 脚本。有一次,一个同事手动在生产库改了字段,没走 Flyway,导致测试环境和生产环境结构不一致,排查问题花了一整天。后来我们定了死规矩,任何表结构变更必须提 SQL 工单,纳入版本控制,谁再手动改库就请客吃饭。
测试环节,我们没搞那种繁琐的单元测试覆盖率考核,因为业务逻辑变动太快,写太多单测维护成本太高。我们更侧重集成测试和接口自动化。用 Postman 配合 Jenkins,每次代码提交自动跑一遍核心接口的测试用例。比如“创建客户”、“领取公海”、“提交订单”这几个关键路径,必须全绿才能合并代码。这帮我们挡下了不少低级错误,比如空指针异常或者参数校验遗漏。
部署方面,为了省事,直接上了 Docker。每个服务一个容器,Nginx 做反向代理。刚开始没做容器资源限制,有一次内存泄漏,Java 进程把服务器内存吃光了,导致整个机器宕机。后来在 docker-compose 里给每个容器加了 mem_limit,并且配置了 JVM 的 -Xmx 参数,确保堆内存不超过容器限制。监控也没落下,接了 Prometheus 和 Grafana,盯着 CPU、内存和 JVM 的 GC 情况。有次上线后,发现 Full GC 频繁,一查是某个导出 Excel 的功能一次性查了十万条数据到内存里。赶紧改成流式查询,分批写入,内存占用立马降下来了。这种性能问题,不在线上跑几天,光靠本地测试很难发现。
说到导出功能,这绝对是 CRM 里的一个高频痛点。销售喜欢把数据导出来自己分析。但大文件导出很容易 OOM。我们用了 EasyExcel,它基于流式处理,不会把整个对象加载到内存。配合异步任务,用户点击导出后,后台生成文件,生成好了发个站内信通知下载。这样前端不会一直转圈等待,用户体验好,服务器压力也小。

回顾整个项目,从立项到上线,大概花了四个月。三个人全职开发,一个前端,两个后端。现在系统已经稳定运行了半年,每天活跃用户几十人,数据量也在稳步增长。回头看,有些设计确实有点过度,比如当初想搞微服务,幸好及时刹住了车。对于这种体量的系统,单体架构(Modular Monolith)其实更香。代码都在一个工程里,调试方便,事务好控制,部署也简单。只有当某个模块真的成为瓶颈,或者团队规模扩大到十几人时,再考虑拆分也不迟。
另外,我也深刻体会到,做 CRM 系统,技术只是底座,真正的难点是对业务的理解。比如“客户查重”这个功能,技术上就是查数据库有没有重复电话。但业务上,如果两个销售分别录入了同一个公司的不同联系人,算不算重复?如果电话一样但公司名不一样,怎么处理?这些规则如果不在代码里固化好,后期数据清洗就是灾难。我们后来加了一个“查重规则引擎”,允许管理员在后台配置哪些字段组合算重复,是强校验还是弱提示。这种灵活性,是业务部门非常看重的。
还有数据报表。老板每天要看销售漏斗,看转化率。一开始我们是用 SQL 硬算,每次查询都要关联五六张表,响应时间五六秒。后来把中间结果预计算,每天凌晨跑定时任务,把统计结果存到日报表里。前端查的时候直接读日报表,速度瞬间提升。虽然数据有 T+1 的延迟,但对于管理决策来说,完全够用了。这也是一种取舍,实时性换性能。
在这个过程中,我也踩了不少 Java 特有的坑。比如 Spring 的事务注解 @Transactional,默认只回滚 RuntimeException。有一次我们手动抛了个 Checked Exception,结果事务没回滚,数据脏了。后来统一规范,业务异常都继承 RuntimeException。还有日期时间处理,Java 8 的 LocalDateTime 很好用,但和数据库交互时要注意时区问题。我们统一规定数据库存 UTC 时间,后端读取后转成东八区展示,避免夏令时或者服务器时区不一致导致的坑。
安全性方面,除了前面说的数据脱敏,接口防刷也很重要。CRM 系统里有些接口涉及敏感操作,比如批量导入、批量删除。我们加了简单的限流,基于 IP 和用户 ID,一分钟内超过一定次数直接返回 429。虽然防不住精心策划的攻击,但能挡住大部分误操作或者脚本滥用。
写到这里,差不多把主要的技术点和业务坑都过了一遍。其实做企业级应用,尤其是 CRM 这种,真的没有那么多高深莫测的算法。更多的是对细节的把控,对数据一致性的执着,以及对用户体验的妥协。Java 这个语言,虽然有时候被吐槽啰嗦,但在企业级开发里的稳定性、生态丰富度,确实还是首选。特别是 Spring 全家桶,几乎帮你把基础设施都铺好了,你只需要关注业务逻辑。
最后想说的是,系统上线不是结束,而是开始。销售团队用着用着就会提出新需求,比如要集成企业微信,要能自动抓取邮件,要能对接呼叫中心。架构设计的时候留好扩展点很重要。比如我们预留了 Webhook 机制,当客户状态变更时,可以回调外部系统。这样以后想加什么新功能,不用改核心代码,挂个监听器就行。
如果你也要着手做一个类似的系统,我的建议是:别追求大而全,先跑通核心流程。客户录入、跟进、成交,这三步闭环最重要。其他的报表、审批流、积分体系,都可以往后放。技术选型上,选团队最熟悉的,别为了新技术而新技术。毕竟,系统是用来赚钱的,不是用来炫技的。
这一路走来,头发掉了一把,但看到销售团队不再用 Excel 满天飞,看到老板能实时看到业绩报表,心里还是挺有成就感的。代码的价值,终究体现在它解决了什么实际问题。希望这篇复盘,能给正在折腾 CRM 系统的同行们一点参考,哪怕只是少踩一个坑,那也算值了。毕竟,在 Java 的世界里,我们都是在不断填坑和挖坑中前进的。

悟空CRM产品截图
推荐立刻免费使用中国著名CRM品牌-悟空CRM,显著提升企业运营效率,相关链接:
CRM系统免费使用
开源CRM系统
CRM系统试用免费
客服电话
售前咨询