自研CRM系统实战:从架构设计到部署落地的完整指南
DeskcommCRM这个项目我前前后后从需求梳理到正式上线跑了将近半年。它不是一个挂着CRM名头的客户通讯录而是一套把客户资料、销售跟进、售后服务、工单流转全部串起来的完整业务系统。当时团队就七八个人销售、实施、客服混在一堆Excel和微信记录里客户信息散得厉害老板一催数据大家就加班导表于是才有了自己动手做DeskcommCRM这个想法。如果你也在纠结要不要自研一套CRM或者正在为手上的客户管理系统选型、规划模块这篇内容应该能给你一些实际参考。我会把DeskcommCRM的整体架构、核心数据模型、部署落地过程以及我在里面踩过的坑一次说清楚大部分方案都是可以直接“抄作业”的。1. 项目背景与整体设计思路1.1 为什么选择自研而不是直接买现成的市面上现成的CRM产品不少好用的每年报价按人头算功能倒是齐全但真正用起来总有那么几天让人难受销售要的快速录入太繁琐实施要的工单流程又不够灵活客服希望把通话记录、微信聊天都和客户档案挂钩现成产品基本都做不好。更关键的是我们内部有一套自己定义的客户分级和项目报价规则标准产品要想改到合适得在定制开发上砸一笔不小的钱周期还长。自研DeskcommCRM的核心逻辑只有一个用最小成本做出最贴合业务流的工具。我们不追求大而全而是把客户、商机、工单、跟进记录这四件事做成一个闭环。这个决定的关键在于团队成员本身懂业务又有人能写代码系统就长在每天的实际使用场景里改起来快用起来顺手。项目启动时我和老板明确了一点第一版不做报表大屏、不做移动端、不做复杂工作流引擎只把核心数据管起来让销售能查到客户历史、领导能实时看到商机阶段。这个“砍需求”的决策是整个项目后来没有失控的最重要原因。1.2 系统架构与技术选型DeskcommCRM后端用的是Spring Boot 3.x前端用Vue3配合Element Plus数据库选了PostgreSQL 14。之所以没用MySQL是因为客户信息、跟进记录这类数据天然会有大量关联查询和文本检索PostgreSQL的JSONB字段和全文检索能力在后面帮了很大忙比如把客户的微信ID、邮箱、备注全部塞进JSONB后查询和扩展字段都变得非常灵活。认证方面一开始用的是JWT但后来发现内部工具场景下管理端和移动端钉钉内置浏览器打开的会话策略不一致JWT的登出和刷新处理很别扭。于是在1.2版本切成了Spring Security Session配合Redis做会话缓存内部系统这个量级完全扛得住还省掉一堆JWT过期的问题。前端部署就用Nginx托管静态文件再反代到后端接口一套摸熟之后配置特别快。系统的整体结构分为三层业务模块层负责客户、商机、工单等核心数据公共服务层提供文件上传、消息通知、Excel导入导出底层就是基础组件包括数据库、缓存、对象存储。没有引入微服务所有功能在一个应用里跑因为我们清楚这个系统的并发上限撑死几十个人同时操作过度设计才是最大的风险。1.3 核心业务模型设计DeskcommCRM的数据模型是项目里最先定稿、也最需要花心思的地方。最初我设计的是客户和联系人一对多的经典模型结果业务方提出一个需求同一个联系人可能同时是A客户的售前顾问又是B客户的对接人单纯的主外键关系根本表达不了。后来我引入了客户 联系人独立表 中间关联表的模型中间表上还挂了这个联系人在具体客户下的角色字段售前、售后、商务对接等。这样既能维护一个联系人的完整档案又能表达他参与多个客户的情况查询的时候只需要join中间表就行性能完全没问题。商机和工单是另外两条主线。商机表记录潜在的销售机会关联客户、负责人、金额和阶段阶段字段用varchar直接存名称不搞什么状态机因为销售的阶段不可能是一套严格有向的流程回退很正常。工单表则是售后和客服在用的关联客户、处理人、工单类型、优先级一张表走天下配合一个简单的状态流转逻辑就够用了。2. 核心模块拆解与实操要点2.1 客户信息管理模块客户管理是整个DeskcommCRM的基础模块但越是基础的东西越容易做砸。我的经验是不要把客户表单设计得太长否则销售录入时就会敷衍很多字段瞎填甚至不填。实际落地时客户表只有必填的六个核心字段客户名称、客户编码、所属销售、客户来源、行业分类、状态。其他信息全部放JSONB扩展字段里不同行业类型可以定义不同的扩展结构后期加字段也不需要改表结构。客户列表页是最常用的页面做了三件事搜索、筛选、批量操作。搜索支持客户名称的模糊匹配、编码精确匹配和联系人手机号模糊匹配这三个场景是实际使用中最频繁的。筛选支持按销售、行业、状态、最近跟进时间区间组合过滤。批量操作用得最多的是批量分配和批量导出Excel导出一开始用POI写几万行数据经常内存溢出后来换成EasyExcel流式写入才稳定下来。这里有一个很容易忽略的点客户列表默认不显示全部数据而是按当前用户的数据权限自动过滤。管理员看全部销售只看自己负责的和参与协作的客户这个权限过滤是在SQL层做的不是查出结果再过滤。如果权限规则写在业务代码里循环判断数据量一上来查询就会慢得离谱而且很容易出现越权风险。2.2 销售管道与商机管理商机管理模块我采用的是最常见的管道视图Kanban展示方式把所有商机按阶段横向排列每个阶段是一列卡片拖拽切换阶段就自动更新商机和金额。这个交互在标准CRM里已经很成熟我们实现时只用了简单的拖拽库没有引入重量级看板组件。商机阶段设计成六个初步接洽、需求确认、方案报价、商务谈判、赢单、输单。每个阶段记录当前预计金额和预计成交日期销售在卡片上就能看到金额汇总管理层关心的“管道总额”就是所有未赢单商机的预计金额之和这个数值直接显示在首页看板上。我还在商机上加了“最近跟进状态”的概念如果某个商机超过三天没有新增跟进记录系统会在首页提醒列表里出现。实现方式很简单就是查询时算一下每个商机的最后跟进时间和当前时间差超过阈值就显示。这个功能虽然不起眼但对销售团队的自我驱动作用非常明显。商机的历史变更记录是必须做的我当时因为偷懒一开始没记录阶段变更日志结果销售说某个大商机的赢单日期不对但谁也说不清是哪天改的只能翻聊天记录。后来加了一张商机日志表任何字段更新都写入旧值和新值杜绝了这个扯皮问题。2.3 工单与售后跟踪工单模块最初不在我的计划里是售后同事强烈要求加的。她们原来靠微信群接需求消息一多就漏经常出现客户投诉了才发现工单根本没派下去。DeskcommCRM的工单模块做得比较轻量客户来电或微信反馈问题客服直接建单选择客户和问题类型系统自动分配给自己如果是销售相关的问题则转给对应销售。工单状态分为待处理、处理中、待客户确认、已关闭。这里没有搞复杂的SLA超时自动升级就是简单地在工单列表上标识出超过48小时未关闭的单子高亮提醒。因为团队小人工盯比自动化规则更靠谱不会误报。工单和客户的关联有一个细节工单表直接冗余了一个客户名称字段而不是只存customer_id。主要原因是历史老工单可能关联的客户后来被合并了直接冗余名称至少能保留当时的表现形式。当然这个做法牺牲了一点规范化但在实际查询展示场景下省了很多join值得。状态流转上工单被“转交”的时候必须填写转交意见否则无法提交。这个设计初期被同事吐槽麻烦但真正跑起来后发现转交意见其实就是问题的书面交接单减少了很多“他转给我但我不知道上一个是谁”的情况。2.4 消息通知与跟进记录跟进记录是DeskcommCRM里数据量增长最快的表每天好几百条内容包括电话沟通摘要、微信聊天记录整理、拜访纪要和报价反馈。设计上跟进记录必须关联某个客户可选关联商机或工单形成一条完整的时间线。页面上客户详情里按时间倒序展示所有跟进记录销售打开客户第一件事就是看历史。消息通知模块没有打通企业微信推送这些只做了站内信和邮件提醒。因为大家更多的是在办公室里用电脑钉钉消息推送我试过接口不稳定权限开通也麻烦最终还是用邮件通知关键事件工单分配、商机转交、客户被审批通过等。邮件这块用JavaMail封装了一个公共组件所有的通知都通过一个异步队列发出去避免影响主流程响应速度。我个人的建议是内部工具的通知要克制不是每个操作都要发通知那会变成骚扰。DeskcommCRM只设了四类通知任务分配给我、我负责的数据被修改、工单逾期待处理、客户被成功导入。其余操作一律不通知。3. 部署落地与数据迁移实战3.1 环境准备与依赖安装DeskcommCRM的服务端部署我选了CentOS 7.9的服务器2核4G内存对这套系统来说绰绰有余。部署前先按顺序装好了JDK 17、PostgreSQL 14、Redis 6、Nginx 1.20还有用于附件存储的MinIO。MinIO主要存客户合同附件、产品资料这些文件很少有大文件磁盘用了100G完全够。Java环境的安装有个小坑CentOS自带的yum源里OpenJDK版本太老我直接下载了官方tar包解压到/usr/local/java并通过/etc/profile配置JAVA_HOME。很多人在这步喜欢用yum install java-11-openjdk但Spring Boot 3必须JDK 17版本对不上后面会有一堆莫名奇妙的启动报错。数据库装好后我建了专门的应用账号没有用postgres超级用户跑业务这样即使应用被攻破数据库其他库也不会受影响。Redis没有设密码因为服务器安全组已经限制了只允许内网和公司IP访问这也是一个小技巧与其把宝全押在软件配置上不如从网络层就掐掉风险。3.2 数据库初始化与核心表结构数据库初始化我是用Flyway做的版本化迁移管理每个版本的SQL脚本按序号放在resources/db/migration下应用启动时自动执行。这个习惯建议所有人坚持因为生产环境多个人手动改库表结构不到一个月就会出现“我本地是好的但测试环境表缺字段”的问题。核心客户表的结构设计出来非常简单但字段含义要先和业务确认好CREATE TABLE customer ( id BIGSERIAL PRIMARY KEY, customer_code VARCHAR(32) UNIQUE NOT NULL, customer_name VARCHAR(200) NOT NULL, owner_id BIGINT NOT NULL, source VARCHAR(50), industry VARCHAR(100), status SMALLINT DEFAULT 1, ext_info JSONB, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() );owner_id字段就是数据权限的核心依据所有查询都要带上“owner_id 当前用户 或者 用户参与协作的客户”。不要把权限判断写成一段到处复制的Java代码我当时封装了一个MyBatis拦截器自动给SQL追加权限条件极大减少漏加权限导致的越权隐患。3.3 权限体系与角色分配DeskcommCRM的角色只有三种管理员、销售、客服。管理员拥有全部配置权限销售能查看和管理自己名下的客户、商机、工单客服能查看所有客户但只能管理分配给自己的工单。权限控制粒度没有再细化到字段级因为内部团队信任度高字段级权限不仅增加开发量还会影响操作效率。菜单权限用一张简单的menu表和role_menu关联表实现菜单是前端写死的路由后端在登录接口返回该角色可见的菜单标识列表前端用v-if控制显示。这个方法很土但很可靠不需要动用Spring Security的复杂表达式。数据权限则是在所有Mapper查询里固定拼接一个条件用户只能看到自己创建的或通过团队协作表关联的数据。我在客户表里加了一个team_members JSONB字段记录参与协作的用户ID数组查询时用PostgreSQL的JSONB包含操作符判断比额外开一张关联表的查询快。3.4 数据迁移与清洗从旧的Excel系统切到DeskcommCRM最耗时的是数据清洗。原有的2000多个客户数据里至少有四成存在重复、手机号缺失、负责人离职等脏数据问题。这时候千万别想着用代码全自动清理编排规则再复杂也跑不干净。我的做法是先写一个导入模板要求销售们按固定格式整理自己名下的客户然后通过系统内置的Excel导入功能上传。导入前服务端会做完整性校验比如手机号格式非法或负责人为空直接拦下来并生成错误行提示。这样分人分批导入的好处是销售自己最清楚哪些客户是有效的还能顺便做一次数据认领。迁移过程中有一个容易忽略的小功能导入时按客户名称手机号双重匹配进行去重。如果发现重复系统不会直接拒绝而是给出“已存在同名客户是否合并”的提示。实际用下来真正需要合并的重复客户不到五十条但如果没有这个提示后面客户详情里出现两条一模一样的记录销售会疯掉的。4. 常见问题与排查技巧4.1 问题速查表我把在DeskcommCRM开发和试用阶段遇到的高频问题整理成了速查表方便后面接手的人快速定位。现象描述可能原因排查方式与解决办法页面登录一直转圈后端日志无请求Nginx反代未生效或后端端口未启动先curl后端健康检查接口再用ss -lntp看端口监听状态客户列表按负责人筛选无数据owner_id和用户表主键类型不一致检查用户实体主键是否为Long类型字段类型不匹配时查询结果永远为空工单附件上传后打开文件损坏MinIO存储路径和数据库记录路径不一致检查保存到数据库的是完整访问URL还是相对路径建议统一用相对路径前缀拼接某个页面操作特别慢SQL未走索引或JSONB字段查询未用GIN索引用EXPLAIN ANALYZE分析执行计划重点给外键和时间字段建索引导出Excel时内存溢出POI一次性加载了全部数据写Sheet换成EasyExcel流式阈值设为5万行注意对象复用突然所有销售都看不到客户权限拦截器误判登录用户角色查看用户会话是否过期权限脚本是否被统一应用到了查询上4.2 性能优化经验系统上线第三周客服同事反馈客户详情的打开速度越来越慢最慢的一次要等十几秒。我第一反应是跟客户相关的工单、商机、跟进记录全部一次性查出来了后来查日志发现一个更大的问题客户详情页居然对每个跟进记录都做了单独的权限校验查询一条时间线带十条跟进记录就多出十次数据库查询。优化方案很简单把客户详情页涉及的所有数据放在一个批量查询上下文里查询权限校验统一在客户层面完成一次子数据不再单独校验。这个改动之后详情页接口从平均1.6秒降到了300毫秒左右。核心的思路是权限校验尽量前置到主查询而不是套一层循环检查子对象。另一个性能隐患是客户列表页的搜索模糊查询一开始用LIKE %关键词%客户到一万条的时候已经能看到明显延迟。最后我用PostgreSQL的pg_trgm插件给客户名称字段加了GIN索引配合ILIKE查询效果立竿见影。如果你也用的MySQL那就要考虑es或更复杂的方案或者退而求其次用前缀匹配。4.3 避坑心得与操作建议第一个坑是关于JWT的。我最初图省事用JWT做登录态但内部系统需要实时踢人员工离职必须立刻失效账号JWT做不到服务端主动失效。第一版上线后被同事发现离职员工的账号还能访问这就尴尬了。后来在1.2版本全部改成Session Redis会话缓存才彻底解决问题。第二个坑是数据库连接池配置的问题。Spring Boot默认的HikariCP连接池初始化和最大连接数在小团队场景下需要手动调低不然空闲连接长时间挂着数据库报too many connections。我们的场景十个人同时用最大连接数设成15就够了。第三个建议是关于备份的。DeskcommCRM用定时任务每天凌晨把PostgreSQL数据dump到本地服务器再同步到对象存储保留七天。这个备份任务我建议加一个“验证备份文件大小”的检查防止数据库异常时备份出来的是空文件到时候恢复时才发觉数据丢了就晚了。4.4 后期扩展的规划方向DeskcommCRM第一版的目标已经达成但业务永远不会停下。目前我已经开始梳理第二版的需求主要方向有三个一是销售报表的可视化看板直接从商机表聚合出各阶段金额和转化率不再靠人肉导Excel二是和办公软件的对接比如钉钉审批流结束后自动创建客户或更新商机状态三是工单模块的SLA计时当客户VIP等级高时可以配置更严格的响应时限。做这些扩展有一个前提核心数据模型要稳定不要轻易推翻字段设计。所以我的建议是在开发初期多花时间调研业务方的真实使用场景把扩展字段设计成JSONB这种灵活结构后期加需求就不用动不动改表结构了。我个人在实际操作中的体会是像DeskcommCRM这样的内部工具真正决定成败的不是技术栈有多新而是需求边界划得清不清楚、数据模型设计得合不合理、权限和日志做得够不够细。如果能在需求阶段把“不做什么”想明白这个系统就已经成功了一大半。