DeskcommCRM这个项目,严格来说不是提前规划的,而是被我们的Excel表格和微信聊天记录逼出来的。
去年年初,我们销售团队还靠一张共享Excel管理客户:报价单发了几个?跟进到哪一步了?上次答应客户周三给方案,结果周五才想起来?这些问题每天都要在群里问一圈才能凑齐答案。后来实在撑不住了,开始折腾CRM选型,一连试了五六款免费的、收费的,最后还是决定基于RuoYi框架自己搭一套——也就是后来命名为DeskcommCRM的这套系统。
它的定位很简单:一个长期运行在服务器上、浏览器随时能打开的Web端客户管理系统,把客户资料、跟进动作、合同回款、团队权限全部放在同一个环境里。不是为了造轮子,而是因为团队真正需要的很多东西,现成产品很难改。这篇文章把这个项目的来龙去脉、技术选型、模块设计、部署维护和上线后的坑完整写出来,不管你是技术负责人,还是想给公司做信息化的小团队,应该都能找到点参考。
1. 从Excel搬家到CRM:为什么最后自己做了DeskcommCRM
1.1 免费CRM真正卡住团队的地方
试用免费CRM的那段时间,我最大的感受是:功能看着都有,销售就是不用。
大多数销售每天的工作节奏是碎片化的,他们需要的是"我打开一个页面,马上知道今天该联系谁、上次聊到哪了、接下来干什么"。但市面上的免费CRM往往把界面做得非常"重"——左边菜单十几个模块,开个客户详情要等半天,填一条跟进记录要过七八个必填项。销售试了两天就回Excel了,理由是"Excel更快"。
还有一个更深层的问题:免费产品对客户数据的归属和导出限制很多。有的免费版导不出全部客户数据,等于数据被平台"绑定"了;有的免费版有企业账号数上限,团队超过二十人就得升级付费版。这些限制在小团队阶段不明显,人一多就成了巨大的阻力。免费CRM和自建系统之间的区别,本质上是"用别人的规则管自己的业务"和"用自己的规则管自己的业务"的区别。
1.2 自建与租用的成本边界
我知道一定有人问:自建CRM不是要养服务器、养开发吗?成本怎么算都划不来吧?
我的判断标准很简单:如果你的团队人数在10人以内,业务模式稳定,直接买现成的SaaS产品是划算的;但如果你有特殊业务流程(比如客户需要多层审批、合同需要自定义回款计划、数据需要和内部ERP打通),自建反而是节省成本的那条路。
算一笔账:一个能跑起来的CRM系统,后端一台2核4G的服务器,一年大概两三千费用;数据库用云数据库,一年一千多;开发人力是一次性投入。核心链路做完大概一到两个月。而SaaS产品的企业版通常是每人每年几百到上千元,20个人的团队一年就是一两万,而且每年都要续费。两三年下来自建的成本就摊平了,系统还是自己的,想怎么改就怎么改。
当然,前提是你得有一个懂点后端开发的人,哪怕是业余时间搞也行。DeskcommCRM就是在这样的"半业余"状态下做出来的。
1.3 "永久在线"到底是什么意思
"永久在线"这个说法,不是指服务器永不宕机。它的真正含义是:整个团队在任何时间、任何设备上打开浏览器就能用,不需要安装客户端,不需要依赖某个IM工具,更不需要在本地方便地导来导去。
现在很多小团队还在用"Excel + 微信群"的方式管客户,但这种方式有一个天然缺陷:客户资料和沟通记录分散在各个人的手机和电脑里,离职一个人就带走一批数据。DeskcommCRM把数据集中放在服务器上,所有操作都在网页完成,数据带不走也丢不了,在家、在路上、在客户现场,只要有网就能查看客户信息和填写跟进记录。
跟那些必须装App才能用的竞品比,网页端最大的优势是低门槛:一个链接发给新同事,浏览器打开登录就能干活,不需要等待下载安装包,也不需要注册什么手机验证码。
2. 技术选型:为什么吃着RuoYi的"老本"来做CRM
2.1 基于RuoYi-Vue的起点优势
DeskcommCRM的技术底座选了RuoYi-Vue,前后端分离版。前端基于Vue3 + Element Plus,后端基于Spring Boot 3.x,认证方案是Spring Security + JWT,数据库用MySQL 8.0,缓存用Redis。
为什么选它,而不是从零搭一套?
第一,RuoYi把RBAC权限模型做好了。用户、角色、菜单、部门、岗位这些通用体系开箱即用,不需要重复造轮子。CRM虽然是业务系统,但"谁能看哪些数据、谁能操作哪个功能"依然是最核心的底座,RuoYi在这方面的建模非常成熟。
第二,代码生成器省了大量CRUD开发时间。建好表结构之后,可以直接生成前端页面和后端接口代码。客户列表、跟进记录、合同登记这类标准表单页面,生成完再改改业务逻辑就能用,效率比手写高一倍以上。
第三,周边生态完善。RuoYi的文档、示例、问题解决方案非常多,遇到问题搜索一下基本都有答案。做内部系统不是搞技术探索,稳定、快速、能落地才是第一目标。
2.2 DeskcommCRM的核心模块与表设计
整个系统的核心链路是"线索/客户 → 跟进 → 合同 → 回款"这条业务主线。我围绕这条主线把表拆成了几块:
- crm_customer(客户主表):客户名称、企业性质、所属行业、客户来源、当前负责人、客户状态(潜在/跟进中/已成交/流失)、是否进入公海、最后跟进时间、下次跟进时间、扩展字段。
- crm_follow_record(跟进记录表):客户ID、跟进方式(电话/微信/面谈/邮件)、跟进内容、下次跟进时间、创建人。
- crm_contract(合同表):客户ID、合同编号、合同金额、签约日期、开始/结束日期、关联订单明细。
- crm_receipt_plan(回款计划表):合同ID、计划回款日期、计划金额、实际回款日期、实际金额、状态。
- crm_product(产品表)和crm_order_detail(订单明细表):处理报价和合同关联的产品明细。
有两个字段我特意做了索引,一个是客户表的last_follow_time,一个是next_follow_time。后面讲公海自动回收和待办提醒时,这两个字段是核心判断条件,没有索引的话数据量上来之后性能会非常差。
2.3 名字背后的产品定位:Desk + Comm
DeskcommCRM这个名字,取自Desk(桌面办公)和Comm(Communication,沟通)。意思是这套系统不只是把客户数据存下来,还承担着内部协同的职责——每个人登录后看到的首页就是"我今天的待办跟进计划",谁的合同快到期了系统会推送提醒,客户被回收之前管理员能看到预警。
在真正的使用场景里,销售往往需要同时操作客户管理和内部沟通两件事。如果客户管理在一个系统、内部沟通在另一个工具,来回切换的成本非常高。DeskcommCRM把这两件事放在同一个环境里,销售不用频繁切换工具,信息的流转效率就上来了。
3. 核心业务模块落地:从客户池到回款的完整链路
3.1 公海/私海与自动回收:让客户资产流动起来
这是整个CRM里最关键的机制,直接决定了客户资源能不能高效流转。
我的设计是:客户分为私海和公海。私海就是有明确负责人的客户,公海则是所有人都可以领取的客户池。新录入的线索默认进入公海,销售可以随时认领;已分配的客户如果超过30天没有任何跟进记录,系统自动收回公海;但成交客户不受回收限制,因为大客户的成交周期本来就长。
这些阈值不写死在代码里,而是放在系统参数表中,管理员可以在后台自由调整。比如有的团队要求15天不跟进就回收,有的团队核心大客户的回收期放宽到60天,改个配置就行。
自动回收的实现逻辑是每天凌晨1点跑一个定时任务:
SELECT id FROM crm_customer WHERE last_follow_time < DATE_SUB(NOW(), INTERVAL 30 DAY) AND status != '成交' AND owner_id IS NOT NULL找到需要回收的客户后,清空负责人,把客户状态改为"公海",同时在客户状态变更表里写一条日志,记录"谁在什么时候因为超时未跟进被回收"。这个日志很重要,避免销售事后扯皮。
一个特别注意的细节:如果该客户正在走合同审批流程,或者存在未完成的报价单,自动回收任务要跳过这些客户,避免因为系统自动操作打断正在进行的商务流程。
3.2 并发领取:两个销售抢同一个客户的兜底方案
公海客户被多个销售同时看到后,一定会出现并发领取的问题。如果代码逻辑是"先查询这个客户是否无人认领,再执行更新",那在并发场景下一定会有两个销售同时通过检查,然后都更新成功。
上线第一周我就踩了这个坑,两个销售同时抢了同一个客户,然后客户"消失"了——实际上是被第二个人覆盖认领了。排查日志后发现就是典型的"查询再修改"导致的竞态条件。
解决方案是改成一条条件更新SQL:
UPDATE crm_customer SET owner_id = #{userId}, pool_status = 1, claim_time = NOW() WHERE id = #{customerId} AND (owner_id IS NULL OR owner_id = 0)MySQL的UPDATE语句本身是行级锁,两个并发请求同时执行时,只有一个能匹配到owner_id IS NULL的条件,另一个影响行数为0,直接在代码里提示"客户已被他人领取"。加个唯一索引兜底,双保险。
3.3 跟进记录与下次联系提醒:把销售动作变成可跟踪事件
跟进记录设计成独立表而不是客户表里的备注字段,原因是跟进记录是一对多的,而且它本身要支持按时间排序、按销售筛选、甚至在数据看板里统计"本周跟进了多少次"。独立成表才有这些分析价值。
记录每次跟进时,销售需要填写跟进方式、跟进内容,然后选择一个"下次跟进时间"。这个设计是有意为之:它把口头承诺变成系统里的待办事件。销售跟客户聊完说"下周再联系",那就得在系统里选一个具体的下次跟进时间,到期系统提醒,想忘都难。
技术实现上,每天定时扫描next_follow_time在当前范围的记录,生成待办列表,同时在首页顶部的"今日待办"区域展示。当天的待办如果到期未处理,会一直置顶显示,并且随着时间推移颜色从黄色变成红色,心理压迫感直接拉满。
3.4 合同回款与经营看板:从"管客户"到"管经营"
合同和回款这两块,我参考了轻量ERP的思路,但简化了很多。合同表记录客户、合同金额、签约日期、起止日期;回款计划表独立一张,因为一个合同可能分三期回款,每期有自己的预计回款日期和金额。实际回款到账后,更新对应计划的到账时间和状态。
看板是管理层最关心的部分。我做了三个核心指标:客户总量与新增趋势、本周待跟进数量、未来30天预计回款金额。这些指标的SQL聚合并不复杂,真正要命的是性能问题。
主页看板千万别实时去大表group by。客户表、跟进记录表的数据量大了以后,每次打开首页都做几百万行的聚合,页面基本就卡死了。我的做法是每天凌晨定时任务把所有核心指标算好,写入一张汇总统计表,页面直接读这个汇总结果。虽然增加了定时任务这一层,但查询速度从原来的几秒钟降到了几十毫秒,体验完全不一样。
4. 团队协作与权限体系:让整个公司在一个CRM里干活
4.1 员工邀请与部门初始化:从账号到组织架构
很多CRM管理员第一次使用时的第一反应是:怎么把团队拉进来?DeskcommCRM的员工接入流程设计得很直接,管理员在"系统管理→用户管理"里新增成员,填上姓名、手机号、所属部门,系统会自动生成初始密码和专属登录链接,新员工收到后打开链接、登录、强制修改初始密码,就能直接进入系统。
团队人数多的时候,也支持Excel模板批量导入。下载导入模板,填好姓名、部门、手机号,一次性上传,几十个人的账号几分钟就能建好。这一步实现起来并不复杂,但对管理员来说能省掉大量重复操作。
4.2 数据权限:哪些人能看到哪些客户
RuoYi自带的@DataScope注解帮了大忙。通过注解,可以在SQL执行前自动追加数据范围条件,支持五种模式:全部数据权限、自定义数据权限、本部门数据权限、本部门及以下数据权限、仅本人数据权限。
我给不同角色配置了不同范围:
| 角色 | 数据范围 | 备注 |
|---|---|---|
| 普通销售 | 仅本人数据 | 只能看自己名下的客户 |
| 销售主管 | 本部门及以下 | 能看到本部门所有销售的客户 |
| 财务人员 | 仅回款与合同模块 | 不放开客户列表 |
| 老板账运营 | 全部数据 | 用于全局看板和分析 |
这里有一个非常容易踩的坑:RuoYi原版本的DataScope默认基于创建人所属部门过滤,而客户是可能被转交的。当一个销售离职,客户转给另一个部门的人时,客户的创建人部门和当前负责人部门会不一致。如果继续按创建人部门过滤,主管就会看不到原本应该在自己部门名下的客户。
我的做法是在客户表上额外存了一个dept_id字段,每次客户负责人变更时同步更新,数据过滤统一按当前负责人所在部门走。这样主管看到的客户范围始终和当前团队对齐。
4.3 用Webhook把CRM接进钉钉群
销售不是每天都会主动打开CRM看有没有提醒,所以我把关键提醒同步到了钉钉群。实现的方案很简单:系统里配置一个钉钉群机器人的Webhook地址,定时任务扫描到"客户距离上次跟进已超过25天"或者"合同回款计划已到期"时,后端调一次HTTP POST,把提醒内容推送到群里。
消息格式用的是钉钉自定义机器人支持的Markdown类型,会解析成一条带标题的提醒消息。Webhook地址没有写死在代码里,而是放在系统配置表中,后台可以随时更换,避免泄露后被外人恶意调用。
5. 部署与日常运维:让系统真正"永久在线"
5.1 Docker Compose编排:一套命令拉起整个环境
生产环境我用Docker Compose来做编排,包含四个核心服务:
- mysql:8.0,数据目录挂载到宿主机的
/data/mysql,设置character-set-server=utf8mb4,备份时直接打包数据目录。 - redis:6.2,开启AOF持久化,用于缓存和会话管理。
- backend:后端服务,用Maven打出来的jar包构建镜像,环境变量注入数据库地址、Redis地址。
- frontend:Nginx托管Vue打包后的静态文件,配置
/prod-api路径反向代理到后端服务。
version: '3.8' services: mysql: image: mysql:8.0 container_name: crm-mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: deskcomm_crm volumes: - /data/mysql:/var/lib/mysql command: --character-set-server=utf8mb4 redis: image: redis:6.2 container_name: crm-redis restart: always command: redis-server --appendonly yes volumes: - /data/redis:/data backend: image: deskcomm-backend:latest container_name: crm-backend restart: always environment: DB_HOST: mysql DB_PASSWORD: ${MYSQL_ROOT_PASSWORD} REDIS_HOST: redis ports: - "8080:8080" depends_on: - mysql - redis frontend: image: deskcomm-frontend:latest container_name: crm-frontend restart: always ports: - "80:80" depends_on: - backenddocker-compose up -d一条命令,整个环境就起来了。前端Nginx容器对外暴露80端口,用户直接通过域名访问。
5.2 Nginx反向代理与HTTPS自动续期
公网访问必须上HTTPS,否则用户数据在传输过程中可能被窃取。域名解析到服务器后,在Nginx配置里增加一个server块,监听80和443端口,将/prod-api前缀的请求反向代理到后端容器的8080端口。
证书用的是免费自动化签发的方案,首次安装时执行一次签发命令,然后配置一个每天执行的定时任务自动续期。续期后自动重载Nginx配置,整个流程不需要人工干预。这个方案已经跑了半年多,从未出现过证书过期导致网站打不开的情况。
配置完成后,用户输入https://crm.example.com就能打开登录页,浏览器地址栏显示小锁图标,对新用户的信任感建立非常有帮助。
5.3 备份策略与监控告警
数据是CRM系统的核心资产,备份怎么强调都不过分。
我的备份策略是:每天凌晨2点用mysqldump做一次全量备份,备份文件保留最近7天;同时把/data/upload上传的附件目录同步到对象存储做异地备份。恢复演练每季度做一次,确保备份文件真的能恢复成可用数据库——不演练的备份等于没有备份,这是运维中最容易被忽视的教训。
监控告警方面,写了一个简单的shell脚本,每分钟请求一次后端的健康检查接口/prod-api/actuator/health,连续两次失败就通过Webhook通知手机。同时监控服务器磁盘使用率,超过80%会触发告警。这套方案虽然"土",但非常稳定省心。
6. 上线后踩过的坑与让销售愿意用的三个细节
6.1 客户查重不准:同名企业带来的数据合并难题
系统上线第一周,我就发现了一个严重问题:客户列表里同时出现了"深圳市前海XX科技有限公司"和"深圳前海XX科技有限公司"这两个看起来几乎一样的名字,销售录入了两次,后续跟进记录也分散了。
排查之后发现问题出在查重逻辑上。最初的查重就是简单的完全匹配customer_name = ?,只要企业名称里多一个"市"字、缺一个括号,就会被当成不同的客户。而现实中企业名称的写法五花八门,"(上海)"和"(上海)"、全角半角括号、中英文空格,都会导致匹配失败。
解决方法是查重时先对客户名称做标准化处理:去掉所有空格、统一括号为半角、去掉横杠,再做精确匹配。同时对企业客户增加一个更关键的业务唯一键校验统一社会信用代码。前端创建客户时发起异步查重请求,如果命中了相似名称,弹窗提示"是否关联已有客户",从源头上避免数据重复。
6.2 并发抢客户的问题:客户消失的悬案
前面提到并发领取问题是在上线第一周就爆出来的。当时有个销售跑过来跟我说"客户不见了",我登录后台一看,客户确实还在,但负责人已经变成了另一个销售。两个人都说客户是自己先领的,场面一度非常尴尬。
日志排查的结论是:两个请求几乎同时到达,都通过了"无人认领"的查询校验,然后先后执行了更新,后者覆盖了前者的认领结果。这种问题靠"查询再更新"的逻辑永远解决不了,必须用带条件的UPDATE语句保证操作的原子性。改完之后这几个月再没出现过抢客户纠纷。
6.3 销售不录入怎么办:三个验证过的小手段
CRM失败的案例里,十有八九不是技术问题,而是销售根本不往里录数据。系统做得再好看,不录数据就是个空壳。我试了各种方式,最后有三件事是真正有效的:
第一,压缩必填字段。新建客户时只填客户名称、联系电话、来源三个字段,其他信息允许后续慢慢补。销售录一条客户的时间控制在30秒以内,录入意愿会明显提升。
第二,让销售自己定"下一步"。系统不强制规定跟进动作,但每次跟进时让销售填一个"下次跟进时间"。这个字段一旦填了,就会生成待办,到期顶到首页,销售不得不去处理。久而久之,销售反而依赖这个提醒功能,因为确实能帮他们记住重要客户。
第三,周报数据透明化。每周自动生成全员的客户数据贡献排名:新增客户数、跟进次数、回款金额。排名靠后的同事会被团队看到,这种"软压力"比任何强制考核都管用。数据贡献高的销售还会得到系统里的"数据贡献达人"标签,在团队内部形成正向竞争。
如果你也想自建一套CRM系统,我个人的建议是:先别急着写代码,拿张纸把"客户—跟进—合同—回款"这条主链路画出来,想清楚每个环节谁在用、要什么数据、出了什么问题需要提醒。桌面办公和客户沟通本质上是同一批人、同一套数据流,把它们放在一起,CRM才有持续用下去的生命力。DeskcommCRM这套系统的价值,不在于它用了多先进的技术,而在于它准确地融进了团队每天的工作习惯里。