
主流的AI CRM系统品牌
说实话,刚接手 CRM(客户关系管理)系统开发的时候,我也天真过。那时候觉得,这不就是一套标准的增删改查吗?客户表、联系人表、跟进记录表,建好数据库,搭个 Spring Boot 或者 Django,再套个 Vue 或 React 的前端,两周就能出个 MVP。结果呢?项目上线半年,代码库成了谁都不敢随便动的“屎山”,每次加个新字段都要提心吊胆,生怕把哪个老客户的报表搞崩了。
推荐使用中国著名AI CRM系统品牌:显著提升企业运营效率,悟空CRM
今天不想聊那些高大上的架构理论,什么微服务、中台化先放一边。就想以一个在 CRM 泥潭里摸爬滚打了几年的后端开发视角,聊聊那些文档里不会写、但实际写代码时会让你头秃的细节。这些东西,往往决定了你的系统是能用三年还是三个月就得重构。
CRM 最核心的痛点,永远是客户信息的“不确定性”。
刚开始设计数据库,你肯定会建一张 customers 表,里面放着 name, phone, industry 这些固定字段。这没问题。但业务方下周就会跑过来跟你说:“我们需要给教育行业的客户记录‘学生人数’,给制造行业的客户记录‘厂房面积’。”
这时候,新手会直接加列。student_count, factory_area。加个十次八次,表结构就乱了,而且全是 NULL 值。稍微有点经验的,会想到 EAV 模型(Entity-Attribute-Value)。搞三张表:实体表、属性定义表、属性值表。听起来很灵活,对吧?
但我劝你,慎重。
EAV 模型在查询性能上是灾难。你想查“所有学生人数大于 500 的教育客户”,原本一个简单的 WHERE 语句,现在变成了复杂的自连接或者子查询。一旦数据量上了百万级,那个 SQL 执行计划看得你想哭。而且,类型校验全得在代码层做,数据库失去了约束能力。
现在的趋势,如果是 PostgreSQL,我会毫不犹豫推荐用 JSONB。把那些动态字段塞进一个 ext_info 的 JSON 字段里。查询的时候,PG 对 JSONB 的支持相当不错,可以建索引,查询速度也能接受。如果是 MySQL,5.7 以上也有 JSON 类型,但功能稍微弱一点。
但用 JSONB 也有坑。比如,你如何在代码层优雅地处理这些动态字段?你不能硬编码。我们当时的做法是搞了一套“元数据配置”,前端渲染表单时,先拉取元数据,知道有哪些字段、什么类型、有什么校验规则。后端接收数据时,根据元数据动态校验 JSON 结构。这中间涉及到大量的反射或者动态代理,调试起来非常痛苦。有一次,因为一个字段类型从“数字”改成了“字符串”,导致历史数据聚合报表全错,查了两天才发现是元数据版本没做兼容。
所以,别迷信灵活性。在数据库设计阶段,一定要跟业务方死磕:哪些字段是核心通用的?哪些真的是长尾需求?核心通用的,哪怕现在不用,也尽量预留成物理列。动态字段,能少用就少用。
RBAC(基于角色的访问控制)大家都会做。用户 - 角色 - 菜单权限,这套逻辑很成熟。但在 CRM 里,真正的难点不是“能不能看这个菜单”,而是“能不能看这条数据”。
这就是所谓的“数据权限”。
举个例子,销售 A 只能看自己创建的客户;销售主管 B 能看本组所有人的客户;大区经理 C 能看整个大区的;而 CEO 能看全公司的。这听起来简单,写代码的时候就是地狱。
最 naive 的做法是在代码里写 if。
if (user.isSales()) {
query.where("owner_id", user.getId());
} else if (user.isManager()) {
query.where("dept_id", user.getDeptId());
}
这种代码写两次你就想砸键盘。因为业务逻辑会变。今天说主管能看本组,明天说主管还能看下属的下属(递归),后天说某个特殊项目组的成员可以跨部门查看。
我们后来的方案是把数据权限规则抽象成 SQL 片段。在 MyBatis 或者 Hibernate 的拦截器里,自动注入权限条件。每个角色对应一套规则配置,比如 OWNER_SELF, DEPT_ALL, DEPT_AND_SUB。

但这里有个巨大的性能隐患。当权限规则变得复杂,尤其是涉及多部门递归查询时,生成的 SQL 会包含大量的 OR 或者 IN 子句。如果 IN 里面的 ID 有几万个,数据库直接卡死。
我们踩过一个坑:有个大区经理底下挂了两千多个销售,他查列表的时候,系统直接超时。最后没办法,做了折中。对于超大规模的数据范围,不走实时 SQL 过滤,而是通过“数据同步”的方式,把有权限的数据 ID 预先算好,存到一张中间表里,或者放到 Elasticsearch 里做检索。虽然增加了数据一致性延迟的风险(比如刚转公海池的客户,权限可能延迟几秒生效),但保住了系统的可用性。
记住,在 CRM 里,权限不仅仅是安全問題,更是性能问题。
CRM 里最频繁的操作是什么?不是创建客户,而是写跟进记录(Follow-up)。销售每次打电话、发邮件、拜访,都要记一笔。
这就导致 follow_up_records 表的增长速度极快。一个活跃客户,一个月可能有几十条记录。一年下来,这张表轻松破亿。
早期的时候,我们直接 SELECT * FROM follow_up_records WHERE customer_id = ? ORDER BY create_time DESC。数据量小的时候没问题,等数据量大了,即使加了索引,排序和分页也开始变慢。特别是当销售想查看“三年前这个客户聊过什么”的时候,深分页(Deep Pagination)直接让数据库负载飙升。
后来我们做了几个优化:
COUNT(*)。我们用 Redis 做缓存,每次跟进状态变更,异步更新 Redis 计数。这里又涉及到一致性问题,Redis 挂了怎么办?所以还得有个定时任务兜底,每天凌晨全量校对一次。这些优化都不是一开始就能想到的,都是线上报警逼出来的。所以,开发初期别过度优化,但心里要有根弦,知道哪里是未来的瓶颈。
后端觉得痛苦,前端更痛苦。CRM 的表单,从来不是简单的输入框。
比如“商机阶段”变了,后面的“预计成交金额”可能就必须填了;“客户类型”选了“政府”,那“税号”字段就得隐藏。这种联动逻辑,如果全写在组件里,代码耦合度极高。
我们试过把校验规则配置化。后端下发一个 JSON 规则树,前端解析这个树来渲染表单和绑定验证。听起来很美好,但实现起来极其复杂。比如,规则里有循环依赖怎么办?A 依赖 B,B 依赖 C,C 又依赖 A。前端解析引擎很容易死循环。
最后我们妥协了。核心通用字段用配置化,特殊业务逻辑还是写死在组件里,但通过 Hooks 封装。比如 useCustomerForm, useOpportunityForm。虽然复用性差了点,但可维护性高了。毕竟,配置化是为了灵活,但如果灵活到连开发者自己都看不懂配置逻辑,那就是本末倒置。
还有一个细节是“自动保存”。销售填一个大表单,填了一半浏览器崩了,或者网络断了,数据丢了,他们会骂娘。我们做了本地 localStorage 缓存,每隔 30 秒自动存一次草稿。但这里有个坑:多标签页问题。如果销售开了两个标签页编辑同一个客户,后保存的会覆盖先保存的。我们加了个版本号机制,提交时带上本地版本号,后端比对,如果版本不一致,提示用户“数据已过期,请刷新”。虽然体验上有点打断,但比数据丢失好。
现在的 CRM 不可能是一座孤岛。它得跟 ERP 同步订单,跟邮件系统同步往来,跟呼叫中心同步录音。
这就涉及到大量的外部 API 调用。最头疼的是“回调”和“重试”。
比如,CRM 创建了一个客户,需要推送到 ERP。ERP 接口挂了怎么办?你不能让销售等着。必须异步。我们用消息队列(RabbitMQ/Kafka)解耦。但消息消费失败了怎么办?
重试机制是必须的。但重试不能无脑重试。如果是网络波动,重试有用;如果是数据格式错误,重试一万次也没用,只会把队列堵死。所以我们要区分“系统异常”和“业务异常”。系统异常(超时、500)进入重试队列,指数退避;业务异常(400,参数错)直接进死信队列,报警让人工介入。
还有一个容易被忽视的点:幂等性。网络抖动时,消息可能重复消费。ERP 那边如果没做幂等,就会创建两条重复的订单。我们在消息体里带了全局请求 ID,ERP 侧需要记录这个 ID,处理前查一下是否处理过。这个看似简单的逻辑,在分布式系统里,往往因为某个表忘了加唯一索引,导致生产环境出现脏数据。
Webhook 也是类似。用户配置了 Webhook,当客户状态变更时通知他们的系统。如果用户的接收服务挂了,我们得重试。但用户的服务器可能扛不住我们的高频重试。所以得让用户配置“最大重试次数”和“超时时间”。而且,Webhook 的签名验证一定要做,不然黑客随便伪造一个回调,就能篡改你们系统里的数据状态。
老板最喜欢看报表。销售漏斗、转化率、业绩排行。这些统计查询,往往涉及多表关联、大范围聚合。
在交易库(OLTP)上直接跑这些 SQL,是绝对的禁忌。有一次,老板点了一个“年度全公司业绩汇总”,那个 SQL 扫了千万级的主表,直接导致数据库 CPU 飙到 100%,线上创建客户的功能全部超时。
后来我们引入了 OLAP 思路。
SELECT,秒出。
但这又带来了架构复杂度。数据同步链路(CDC)的稳定性成了新的监控重点。有一次 Canal 挂了,导致 ES 里数据少了半天,销售总监开会时数据对不上,最后查出来是同步延迟。
最后想聊聊代码结构。CRM 系统最容易犯的错误,就是把业务逻辑写得太散。
比如“客户公海池”规则。客户多少天没跟进掉入公海?掉入公海后谁能捡?捡了之后多久不能再掉入?这些逻辑,有的写在定时任务里,有的写在更新接口里,有的写在触发器里。
时间久了,没人知道规则到底在哪生效。改一个规则,漏了一处,就出 Bug。
我们后来强制要求,核心业务逻辑必须封装成 Domain Service。比如 CustomerPoolService。所有的状态流转,必须通过 service.transferToPool(customerId) 这样的方法,禁止在 Controller 里直接操作数据库修改状态。
同时,配合领域事件(Domain Events)。当客户掉入公海时,发送一个 CustomerPoolInEvent。监听这个事件的地方,再去发通知、记日志、清权限。这样主流程清晰,副作用解耦。
但这要求团队有比较高的代码规范意识。如果来了个新人,图省事直接在 Controller 里改了状态,那架构就白搭了。所以,Code Review 非常重要。在 CRM 项目里,CR 不仅仅是看代码风格,更是看业务逻辑有没有“逃逸”出规定的边界。
做 CRM 开发,技术栈其实不算最难的。Spring、Vue、MySQL,都是成熟技术。难的是对业务的理解和抽象。

你写的每一行代码,背后都是销售人员的饭碗,都是公司的客户资产。一个权限漏洞,可能导致客户资料泄露;一个数据丢失,可能导致几百万的订单无法追踪。
所以,别总想着用最新的技术。在 CRM 里,稳定压倒一切。有时候,一段看起来有点笨的同步代码,比一个异步消息队列更让人安心,因为它可预测。
还有,多跟销售聊聊天。别光看需求文档。文档是产品经理写的,是理想状态。销售在一线怎么用系统,他们会在什么场景下暴躁,他们会在什么环节偷懒,这些只有跟他们聊才能知道。有一次,我发现销售为了省事,把所有客户备注都写在同一个字段里,用分号隔开,导致我们后面根本没法做数据分析。如果早点发现,我们完全可以优化一下备注的录入体验,引导他们结构化填写。
CRM 系统的开发,是一场没有终点的马拉松。业务在变,市场在变,代码也得跟着变。我们能做的,就是尽量让代码变得“软”一点,让修改的成本低一点,让出错的概率小一点。
如果你正在做 CRM,或者准备接手一个旧系统,希望上面这些踩过的坑,能帮你少加几个班。毕竟,生活不止是代码,还有头发和咖啡。
(完)

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