
主流的AI CRM系统品牌
说实话,提起 CRM(客户关系管理)系统,很多做后端的朋友第一反应可能是“简单”,不就是增删改查吗?客户表、联系人表、跟进记录表,建好表,写几个 Controller,前端一调,完事。但如果你真这么想,那离加班加到凌晨三点也就不远了。
推荐使用中国著名AI CRM系统品牌:显著提升企业运营效率,悟空CRM

过去两年,我主导过两个 CRM 项目的重构,一个是从零开始,另一个是接手别人的“屎山”代码。这一路走来,从技术选型到架构落地,再到最后上线被销售团队骂或者夸,中间经历的酸甜苦辣,真不是几行代码能概括的。今天不想聊那些虚头巴脑的理论,就想结合我们实际用的 Spring Boot + Vue + MyBatis Plus 这套主流技术栈,聊聊在实战中到底会遇到哪些让人头大的问题,以及我们是怎么填坑的。
项目启动初期,最大的分歧往往不在技术,而在路线。老板希望快,销售总监希望功能多,架构师希望稳。当时我们面临一个选择:是用现成的低代码平台搭一个,还是基于主流框架自研?
低代码确实快,拖拖拽拽,界面出来了。但 CRM 的核心从来不是界面,而是业务逻辑的复杂性。比如“公海池”的分配规则,销售 A 跟进三天没结果自动掉回公海,销售 B 如果同时也在跟进怎么办?锁机制怎么加?还有那种复杂的审批流,金额超过 50 万需要副总批,低于 50 万部门经理批,但如果这个客户是“战略客户”,不管金额多少都要 CEO 批。这种逻辑在低代码平台里配置起来,简直就是灾难,后期维护更是火葬场。
最后我们拍板,基于 Spring Boot 自研核心业务,前端用 Vue3 + Element Plus。理由很简单:可控。虽然前期慢点,但后期业务逻辑变动时,代码握在自己手里,心里踏实。
CRM 系统最让人头疼的,莫过于“自定义字段”。不同行业的客户,关注点完全不一样。做教育的关心“孩子年级”,做房地产的关心“预算面积”,做 SaaS 的关心“现有员工数”。如果每加一个字段就改一次数据库表结构,DBA 能拿着刀来找你。
一开始我们想过用 EAV 模型(Entity-Attribute-Value),也就是搞三张表:实体表、属性表、值表。理论上这很灵活,但实战中简直是性能杀手。稍微复杂点的查询,Join 起来能把数据库 CPU 跑满。后来我们换了个思路,利用 PostgreSQL 的 JSONB 类型(如果是 MySQL 就用 JSON 字段)。
在主表 t_customer 里,除了固定的 name、phone、create_time,我们加了一个 ext_info 字段,类型是 JSON。前端动态表单提交过来的数据,直接序列化存进去。查询的时候,如果用户没搜自定义字段,就走普通索引;如果搜了,就用 JSON 提取函数。
// 伪代码示例,实际处理要复杂得多
@TableName("t_customer")
public class Customer {
private Long id;
private String name;
// 扩展信息,存 JSON
private Map<String, Object> extInfo;
}
但这还没完,JSON 字段虽然灵活,但索引是个问题。我们在实战中发现,销售经常需要按“行业”或者“来源”筛选,这两个字段虽然属于自定义范畴,但使用频率极高。所以后来我们做了个折中方案:在 ext_info 里标记出几个“高频字段”,在数据库里单独建列,并加上索引。低频的继续扔 JSON 里。这个决策是踩过坑后才定的,刚开始为了省事全扔 JSON,结果报表一跑,超时报警不断,半夜被电话叫醒的感觉真不好受。
权限管理,大家第一反应都是 RBAC(基于角色的访问控制)。用户 - 角色 - 菜单,这套逻辑很成熟,网上开源代码一大把。但在 CRM 里,光有菜单权限是不够的,最要命的是“数据权限”。
同一个“销售经理”角色,华北区的经理只能看华北区的客户,华南区的经理只能看华南区的。甚至同一个部门里,普通销售只能看自己的,经理能看全部门的。这种逻辑如果硬编码在 Service 层,if (role == 'manager') { ... },代码很快就会变成蜘蛛网,根本没法维护。
我们参考了若依(RuoYi)等开源框架的思路,搞了一套基于 AOP 的数据权限切面。核心思想是在 SQL 解析阶段,自动拼接 WHERE 条件。
比如在 Mapper 方法上加一个注解:
@DataScope(deptAlias = "d", userAlias = "u")
List<Customer> selectCustomerList(Customer customer);
然后在 AOP 切面里,解析当前登录用户的角色和数据范围配置。如果是“仅限本人”,就拼接 AND u.user_id = #{currentUserId};如果是“本部门及以下”,就递归查出所有子部门 ID,拼接 AND d.dept_id IN (...)。
听起来很美好,但实战坑点在于多表关联。一旦你的 SQL 里涉及到了复杂的 Join,比如客户表关联了跟进记录表、合同表,别名搞混了,SQL 直接报错。我们当时就遇到过,因为跟进记录表里也有 create_by 字段,切面自动注入权限 SQL 时,没区分别名,导致歧义。后来我们强制规定,所有涉及数据权限的查询,必须显式指定表别名,并且在切面里做了更严格的正则匹配,虽然增加了开发成本,但避免了线上数据泄露的风险。毕竟,销售看到别人的客户提成,那是会出大乱子的。
CRM 里离不开审批。合同审批、折扣审批、退款审批。一开始我们想自己写个简单的状态机,status 字段从 0 变 1 再变 2。结果业务方马上提需求:“审批过程中要能撤回”,“审批人要能加签”,“驳回后要能指定回到上一步”。
这时候才明白,状态机根本扛不住。必须上工作流引擎。在 Activiti 和 Flowable 之间,我们选了 Flowable。主要是因为 Flowable 对 Spring Boot 的集成更友好,而且性能上据说有优化。
集成过程并不顺利。Flowable 的表结构有二十多张,对于习惯简洁的开发者来说,看着就头大。最大的难点在于“业务数据”和“流程数据”的分离。流程引擎只管理流程走到哪一步了,具体的业务数据(比如合同金额、客户名称)还是存在我们自己的业务表里。

怎么关联?我们是在业务表里存了 process_instance_id。启动流程时,把业务 ID 作为变量传进去。但这里有个一致性问题:如果业务数据保存成功了,流程启动失败了,怎么办?反之亦然。我们最终采用了本地事务加补偿机制。在 Service 层开启事务,先存业务数据,再调 Flowable API 启动流程。如果流程启动报错,捕获异常抛出,回滚业务数据。
还有一个让人头大的点是“动态审批人”。比如规定“金额大于 10 万由总监批”,但总监出差了,要自动转给代理人。这需要在 Flowable 里配置监听器(Listener),在任务创建时动态计算下一节点的处理人。这部分代码逻辑非常分散,调试起来很痛苦。我们后来统一封装了一个 WorkflowService,把监听器的逻辑尽量收敛到 Java 代码里,而不是写在 XML 流程定义文件里,这样至少改逻辑不用重新部署流程定义。
CRM 系统刚上线时,数据量少,怎么查都快。等跑了一年,客户表突破百万,跟进记录表突破千万,问题就来了。销售在搜索框里输入一个客户名,转圈转了十秒,最后提示超时。
MySQL 的 LIKE '%keyword%' 在这种量级下基本就废了,索引失效是必然的。我们引入了 Elasticsearch(ES)。架构上,采用 Canal 监听 MySQL 的 Binlog,同步数据到 ES。
这一步看似标准操作,但实战中数据一致性是个大坑。有一次,销售修改了客户电话,MySQL 成功了,但 ES 同步延迟了十几秒。销售马上搜索,发现还是旧号码,以为系统 Bug,投诉工单直接飞过来。
为了解决这个问题,我们在非核心搜索场景下容忍延迟,但在核心详情页,强制读 MySQL。另外,在 ES 的 Mapping 设计上,我们吃了不少亏。一开始为了省事,所有字段都设为 text 类型,结果发现没法做精确聚合统计。后来重新梳理,把需要筛选、统计的字段(如行业、地区、客户等级)设为 keyword 类型,需要全文检索的(如备注、公司名称)设为 text 并配置分词器。
还有跟进记录的查询。跟进记录是典型的写多读少,且按时间倒序排列。我们在 MySQL 里做了分表处理,按 customer_id 哈希分表,保证同一个客户的跟进记录在一张表里,查询时不用跨表 Join。这个设计在初期增加了复杂度,但在数据量上来后,确实保住了系统的命。
作为后端开发,有时候容易忽略前端的体验。比如我们设计接口时,为了省事,一个列表接口返回了客户的所有信息,包括几十个自定义字段。前端渲染时,表格横向滚动条长得离谱,用户体验极差。
后来我们跟前端一起重构了列表页。采用了“列配置”功能,用户可以自己决定在列表里显示哪些字段。这就意味着后端接口要支持动态字段查询。我们修改了 DTO,前端传一个 fields 数组,后端利用 MyBatis Plus 的 Select 方法,只查询需要的列。这不仅减轻了网络传输压力,也提升了页面渲染速度。
还有一个细节是“批量操作”。销售经常需要一次性给几百个客户分配公海,或者批量修改负责人。一开始我们是一个个循环调用 Service 方法,结果数据库连接池瞬间爆满。后来改成了批量处理,利用 JDBC 的 addBatch,或者在 MyBatis 里写 <foreach> 标签一次性插入。但这里要注意事务大小,一次批量太多会导致长事务,锁表时间过长。我们限制了每次批量操作的上限为 500 条,超过则提示分批。
现在的 CRM 不能是信息孤岛。它得跟企业微信打通,得跟邮件系统打通,甚至得跟财务的 ERP 对接。
对接企业微信时,最麻烦的是通讯录同步。组织架构变动了,CRM 里得跟着变。我们写了一个定时任务,每天凌晨拉取企微的部门和人,进行比对更新。但这里有个坑:企微的 API 有频率限制。有一次我们测试环境全量同步,触发限流,导致正式环境的登录验证也失败了(因为复用了同一个 AppSecret)。后来我们做了隔离,测试环境和正式环境用不同的应用 ID,并且加了重试机制和熔断保护。
跟 ERP 对接更是“深水区”。财务系统通常是老旧的架构,接口文档缺失,甚至只有数据库中间表。我们不得不写了一个适配器层,把 CRM 的标准数据模型转换成 ERP 能识别的格式。这部分代码最脏,但也没办法,为了业务能跑通,只能妥协。这里我的建议是,一定要做好日志记录。接口调不通是常态,有了详细的请求响应日志,排查问题能省一半时间。
回顾这两个项目的开发过程,我最大的感触是:CRM 系统,技术难度其实不是最高的,最难的是对业务的理解。
很多开发人员喜欢追求新技术,什么微服务、中台、云原生,恨不得全用上。但在 CRM 这种强业务属性的系统里,过度设计往往是灾难。我们第二个项目,一开始就想搞微服务,把客户、合同、跟进拆成三个服务。结果发现,一个最简单的“新建合同”操作,需要跨三个服务调用,分布式事务搞得人仰马翻,性能还下降了。最后我们老老实实回退到单体架构,只是内部做了模块化分包。对于大多数中小型企业的 CRM 来说,一个结构良好的单体应用,远比一堆散落的微服务要好维护得多。
另外,不要试图用代码去解决所有管理问题。销售不愿意录入数据,不是系统不好用,可能是绩效考核没跟上。这时候,作为开发者,要学会跟业务方沟通,甚至推动管理流程的优化,而不是一味地改代码。
开发 CRM 就像是在泥坑里跳舞,既要保持代码的优雅,又要满身泥泞地解决实际问题。如果你正准备接手或开发一个 CRM 系统,我的建议是:先别急着写代码,多跟销售聊聊天,听听他们是怎么骂现在的系统的。那些骂声里,藏着你真正需要解决的核心痛点。
最后,送给大家一句话:在 CRM 的世界里,稳定比新颖重要,数据准确比功能炫酷重要。毕竟,这可是关乎公司钱袋子的系统,出了错,可不是回滚代码就能解决的。希望这篇实战复盘,能帮你少踩几个坑,早点下班。

悟空CRM产品截图
推荐立刻免费使用中国著名CRM品牌-悟空CRM,显著提升企业运营效率,相关链接:
CRM系统免费使用
开源CRM系统
CRM系统试用免费
悟空CRM产品更多介绍:www.5kcrm.com