讲真,我第一次看到“DeskcommCRM”这串字符的时候,第一反应是这名字不像是随手拍脑袋起的。Desk代表桌面办公,Comm是Communication的缩写,合在一起就是“桌面沟通型客户关系管理系统”——这恰恰是我在立项前期最真实的需求定位:把散落在Excel表格、微信聊天记录、邮箱和各类零散小工具里的客户信息,全部收拢到一个统一的桌面工作台上。当时团队里销售、运营和客服各用各的记录方式,客户跟进到哪一步了没人能说清楚,催单、漏单、撞单的情况几乎每周都发生。做这样一套轻量级CRM,不是为了搞一个看起来很酷的中台,而是为了解决实际业务里“信息断层”和“跟进黑洞”这两个真问题。这篇文章会完整还原我从需求拆解到架构选型、从表结构设计到核心功能落地,再到部署上线和问题排查的全过程,希望能给同样在自研CRM系统的朋友提供一份可以直接参考的作业。
1. 项目背景与需求拆解:为什么叫DeskcommCRM
1.1 从一个长期存在的痛点聊起
如果你在一个十人以内的小团队待过,一定经历过这样的混乱:销售A把客户电话记在手机备忘录里,销售B把客户需求写在聊天软件的个人备注里,客服C干脆靠脑子记。到了月底对业绩的时候,三个人对同一家客户的跟进情况各说各话,谁也没有完整的过程记录。即便是用了市面上现成的CRM产品,也常常因为功能太重、配置太繁琐,最后沦落为“领导要求填写的信息录入系统”,一线人员压根不愿意用。
这就是我决定自研DeskcommCRM的起点。我不想做一个大而全的客户管理平台,而是想做一个“让一线同事愿意每天打开、顺手就能记录”的桌面沟通工具。DeskcommCRM的定位很明确:它是一个以客户档案为中心、以跟进记录和任务提醒为纽带、以数据看板为决策辅助的轻量级CRM系统。它解决的核心问题是“客户信息统一沉淀”和“跟进动作有效闭环”。
1.2 核心需求清单与产品边界
在动手写第一行代码之前,我把需求梳理成了四层。第一层是基础客户档案管理,包括客户的新建、编辑、标签、等级和负责人归属,这是整个系统的地基。第二层是跟进记录与协作留痕,每次电话、拜访、消息沟通都能留下结构化的记录,并且记录可以按时间线聚合展示。第三层是任务与提醒机制,销售给自己的下次跟进设置提醒,到点系统自动通知。第四层是统计数据看板,用图表直观展示客户分布、跟进频率和成单转化情况。
这个范围我刻意做了收敛。不做复杂的权限矩阵,不做工作流引擎,不做公海池和复杂的业绩核算。原因很简单:小团队没有那么多管理层级,过度设计只会拖慢开发节奏,也会让产品变得难以理解。把核心链路跑通,让用户形成习惯,比功能堆砌重要得多。
2. 架构设计与技术选型:稳定与效率的平衡
2.1 整体技术栈与部署形态
DeskcommCRM的技术选型我遵循了一个原则:选团队熟悉度高、社区生态成熟、部署门槛低的技术,而不是追逐最新最热门的框架。前端用的是Vue 3 + Element Plus + Pinia + Vue Router,配套Axios做HTTP请求,ECharts做图表渲染。后端使用Spring Boot 2.7作为主框架,搭配MyBatis-Plus做数据访问,Spring Security做登录认证和接口鉴权。数据库选择PostgreSQL 14,缓存和消息推送的辅助存储使用Redis 6.2。
这个组合看起来不惊艳,但它有一个明显的好处:每个环节都有大量现成的解决方案和踩坑文档,遇到问题几乎搜一下就有人遇到过。部署形态上,我采用前后端分离,前端构建后的静态资源放到Nginx里,后端直接以Jar包方式运行在服务器上,用systemd托管进程,数据库和Redis分别独立安装。没有上Kubernetes,也没有做容器编排,因为初期用户量就几十人内,一台4核8G的云主机完全够用,维护成本是最低的。
2.2 关键选型背后的取舍逻辑
为什么选择PostgreSQL而不是MySQL?其实两者都能做,但我更看重PostgreSQL对JSONB类型和丰富索引的原生支持。客户标签、自定义扩展字段这类非结构化数据,在PostgreSQL里可以直接用JSONB存储并建立GIN索引,查询和过滤都非常方便,这比单独建一堆关联表省事得多。
为什么不用WebSocket而是用SSE(Server-Sent Events)做实时通知?我起初也写了WebSocket方案,但实测下来发现,CRM的提醒场景是典型的“服务端单向推送”,比如任务到期提醒、跟进消息通知,客户端不需要频繁向服务端发送消息。SSE基于HTTP协议,天然支持断线重连,部署时无需额外处理WebSocket的代理和心跳问题,Nginx直接就能转发。只要不是做多人在线聊天这种双向实时通信,SSE是更“轻”的选择。
3. 核心功能模块的实现:从表结构到业务闭环
3.1 客户档案模块:标签化与动态字段设计
客户档案是整个系统的数据底座,设计得好不好,直接决定了后续功能的开发效率。在DeskcommCRM里,客户表我用了一张主表加一套扩展属性字段来支撑。主表的核心字段包括客户名称、所属公司、职务、联系方式、客户来源、客户等级、负责人ID、跟进状态、下次跟进时间、最后跟进时间以及备注信息。客户来源我用一个单独的字典表维护,这样后续想加一个“线下活动”渠道,只要在字典里加一条记录,前端的下拉选项也会跟着更新,不需要改代码。
客户等级和跟进状态我也做了标准化。等级分成S/A/B/C四个档位,分别对应高价值重点客户、普通意向客户、潜在培养客户和低优先级联系人。跟进状态则定义了“未跟进”“跟进中”“已成交”“已流失”四个阶段,方便后续看板做漏斗分析。标签字段使用PostgreSQL的JSONB类型存储,比如标签数组可以是["上海","家装行业","预算充足","偏好电话沟通"],每个销售都可以按自己的习惯打标签,系统不会限制标签的格式和层级。
这里我还做了一个比较实用的功能:客户查重。在新建客户的接口里,我用手机号和邮箱作为唯一性维度,先用索引做精确匹配,再用一个简单的相似度算法对客户名称做模糊匹配。如果检测到疑似重复客户,前端会弹窗提示“该手机号已关联客户xxx”,让用户决定是取消还是继续新建。这个小功能上线后,撞单率下降得很明显,算是性价比极高的一笔投入。
3.2 跟进记录与团队协作留痕
跟进记录是DeskcommCRM第二个核心模块,设计目标是“让每一次触达都有迹可循”。每一条跟进记录需要关联客户ID、负责员工ID、跟进方式(电话、微信、邮件、上门拜访、其他)、跟进内容、下次计划以及可选的附件链接。跟进方式同样走字典表,方便以后扩展。
在查询设计上,我采用主从结构:客户详情页的“跟进时间线”接口一次性返回该客户最近30条跟进记录,按时间倒序排列,前端用时间线组件渲染。这里有一个性能细节容易被忽略——如果直接把文本内容全部查出来传输给前端,随着记录增多,响应体越来越臃肿。我在列表接口中只查id、跟进方式、跟进时间和内容摘要,摘要取前50个字符,只有在用户点击展开某一条记录时,才额外请求详情接口获取完整内容。
为了提升协作留痕的价值,我还在跟进记录里增加了“@同事”的功能。销售在记录中可以用@符号选择的同事,系统会自动给该同事发送一条站内通知,同时在被@的客户详情页上显示一个“有人提到了你”的提醒角标。这个功能实现成本不高,却让售前、售后和销售之间的信息同步流畅了很多。
3.3 任务提醒与实时通知的实现
任务模块我做得相对独立,它包含任务标题、关联客户、负责人、优先级、截止时间和提醒时间。创建任务后,系统会启动一个定时调度,每到提醒时间点就触发通知推送。这里用的是Spring Boot自带的@Scheduled注解去扫描满足条件的任务,每分钟跑一次,判断条件有两个:提醒时间在当前时间前后两分钟之内,且任务状态仍是“待办”。
通知推送走了SSE通道。项目启动时建立一条独立的SSE连接,每个用户登录后通过身份认证绑定到自己的事件流通道。后端有一个内存版的NoticeService,负责把通知事件写入每个用户的待推送队列,然后在SSE连接的异步线程里下发。通知内容统一以JSON格式传输,前端收到后判断是“任务提醒”“@提及”还是“客户分配通知”,对应的弹窗、角标和声音提醒各不一样。为了防止服务重启导致队列消息丢失,我加了落库逻辑:所有通知先写入数据库的通知表,SSE推送只是把未读状态同步到前端,用户打开消息中心可以随时回看历史通知。
3.4 销售看板与统计报表
看板是给管理者用来做决策的模块,但我没有把它做成一个巨大的报表中心,而是聚焦在三个视图上。第一个是客户状态分布图,展示当前S/A/B/C四个等级客户的数量以及跟进状态占比。第二个是跟进趋势图,按天统计最近30天每一位销售的跟进记录数量,帮助管理者识别哪些员工近期跟进积极性下降。第三个是转化漏斗图,从“新建客户”到“已成交”按阶段统计转化率,定位主要流失环节。
数据统计在实现上遇到的最大问题是统计口径。比如“转化率”到底是用成交客户数除以新建客户总数,还是除以有效跟进客户数,这两种算法得出的结果差异很大。我在系统配置里把口径做成可配置的,并在看板标题下方用一行小字标明当前的口径定义,避免使用者误读数据。统计的实时性我也做了取舍,热点指标走Redis缓存,每隔5分钟刷新一次,有效降低了数据库的压力。
4. 实操过程与代码层面的关键实现
4.1 数据库表结构设计实战
表结构是项目的骨架,这里我直接给出部分的DDL片段,大家做类似系统时可以在此基础上迭代。
客户主表的关键字段如下:
CREATE TABLE crm_customer ( id BIGSERIAL PRIMARY KEY, uuid VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL, company VARCHAR(256), position VARCHAR(128), phone VARCHAR(32), email VARCHAR(128), source_code VARCHAR(32), level_code VARCHAR(8) DEFAULT 'C', status_code VARCHAR(16) DEFAULT 'NOT_FOLLOWED', owner_id BIGINT NOT NULL, tags JSONB DEFAULT '[]'::jsonb, next_follow_time TIMESTAMP, last_follow_time TIMESTAMP, remark TEXT, is_deleted SMALLINT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_customer_owner ON crm_customer(owner_id); CREATE INDEX idx_customer_phone ON crm_customer(phone); CREATE INDEX idx_customer_tags ON crm_customer USING GIN(tags);跟进记录表:
CREATE TABLE crm_follow_record ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, owner_id BIGINT NOT NULL, follow_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, content_summary VARCHAR(200) NOT NULL, next_plan TEXT, mentioned_user_ids JSONB DEFAULT '[]'::jsonb, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_follow_customer_time ON crm_follow_record(customer_id, created_at DESC);任务表:
CREATE TABLE crm_task ( id BIGSERIAL PRIMARY KEY, task_title VARCHAR(256) NOT NULL, customer_id BIGINT, assignee_id BIGINT NOT NULL, priority SMALLINT DEFAULT 2, status VARCHAR(16) DEFAULT 'TODO', due_time TIMESTAMP, remind_time TIMESTAMP, created_by BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_task_remind ON crm_task(status, remind_time);这个“idx_task_remind”索引是整个定时任务扫描能够保持高效的关键。如果没有它,每分钟全表扫描一次任务表,用户量大了以后数据库CPU会直接飘红。
4.2 后端接口设计中的关键逻辑
后端基于Spring Boot提供RESTful接口,以客户模块为例,核心接口包括:新建客户、更新客户、客户分页查询、客户详情、客户标签更新、客户转派负责人、客户查重检测。为了避免代码膨胀,我把所有基础CRUD放在CustomerController里,通过请求方法区分动作。
对于新建客户这个接口,校验逻辑需要特别注意。手机号格式用正则校验,邮箱格式用Java的EmailValidator,姓名和公司名去除首尾空格。当检查到客户查重命中时,接口不会直接抛出异常,而是返回一个专用的业务状态码,比如“CUSTOMER_DUPLICATE”,前端根据这个状态码弹出确认框。这一点很多团队会忽略,直接在服务端拒绝重复客户,但实际业务中同一个手机号可能是联系人和客户绑定的关系,简单拒绝会导致用户无法录入数据。
客户的标签更新我设计成“全量覆盖”模式:前端把整个标签数组传进来,后端直接替换旧值。这样实现最简单,也避免了频繁的增删单标签请求产生并发覆盖问题。如果标签数量极端多的情况下可以再做numpy形式的增量更新,但对当前场景没有必要。
4.3 前端核心交互:从列表到详情的时间线
前端的客户列表页我用了Element Plus的表格组件,默认支持按客户等级、跟进状态、负责人和标签筛选。首屏加载时只请求第一页20条数据,滚动到底部时自动请求下一页追加。这个无限滚动需要做好防抖,否则滚动速度快的时候会触发大量重复请求。我的处理方式是维护一个isLoading标志位,在请求发出后置为true,响应返回后再置为false,只有isLoading为false时才允许发起新请求。
客户详情页是交互最重的一个页面。顶部展示客户的基本信息和标签,中间是一个Tab切换,包含“跟进记录”“关联任务”“客户动态”。跟进记录用时间线组件渲染,每条时间线节点左侧展示跟进方式图标,右侧展示内容摘要、跟进员工和跟进时间,点击摘要可以展开查看完整内容。客户动态则是一个更细粒度的审计日志,展示客户从创建到现在的所有关键动作,比如“张三修改了客户等级”“李四创建了跟进记录”,这个动态我是在后端用一个统一的EventInterceptor来实现的,任何关键修改都会自动写入操作日志表,前端不需要额外请求额外的展示逻辑。
为了提升录入效率,我在客户详情页提供了一个“快速记录跟进”的浮层入口,按下快捷键Ctrl+Enter就能提交跟进记录。这个小交互很受销售同事欢迎,因为他们接电话时没有时间点开一堆表单。快速记录只要求填写跟进方式和内容,下次跟进时间和任务可以稍后再补,尽可能降低使用门槛。
5. 部署上线与生产环境问题排查
5.1 前后端部署与进程管理实践
DeskcommCRM的部署没有用特别复杂的手段。前端在本地执行构建后,生成dist目录的静态文件,通过scp上传到服务器的Nginx站点目录。Nginx配置里需要注意两个点,一是gzip要开启,JS和CSS体积能减少70%左右;二是对后端API接口的代理要加上超时时间配置,否则一些耗时较长的统计接口容易返回504。
后端用maven打包成可执行Jar包,放到服务器后通过systemd脚本托管。这里分享一个经验,启动JVM时我显式指定了Xms和Xmx为1G,并将垃圾回收器设置为G1,同时开启了GC日志文件输出。刚开始用默认参数跑,没过几天就出现响应变慢的情况,排查发现是默认的新生代太小导致频繁Full GC,调整后系统稳定了很多。数据库连接池使用的是HikariCP,最大连接数设成20,Spring Boot的项目基本不用额外调参数,默认配置已经足够健康。
Redis我用了一个简单的哨兵架构,主从加一个哨兵进程,定期做数据持久化。虽然当前用户量不大,但提前预留高可用能力,后面扩展的时候不用临时重构。Redis里主要存三类数据:登录Token、客户标签热数据和统计缓存。Token过期时间设置为7天,每次登录自动续期,这样销售同事不用频繁重新登录。
5.2 生产环境常见的坑与排查实录
上线后我整理了一份问题清单,挑了几个最有代表性的记录在这里。
第一个是客户查重接口偶发超时。刚开始以为是因为数据量大,后来看慢SQL日志才发现问题出在模糊匹配那段,客户名使用LIKE ‘%xx%’且客户表数据量过万后走全表扫描。修复方式很直接,我对手机号、邮箱这类高区分度字段走精确索引匹配,客户名相似度匹配暂时降级为只对前50个新增客户做实时比对,同时把查重动作从同步调用改成异步任务,前端先展示新建成功,再由后台任务把疑似重复的数据追加到消息中心。这个调整几乎没有影响用户体验,却把接口耗时从2秒降到了200毫秒以内。
第二个是SSE连接在生产环境频繁断开。排查后发现问题出在两处,一处是Nginx的proxy_read_timeout默认60秒,SSE连接长时间没有数据就会断开;另一处是服务器防火墙对空闲连接做空闲超时回收。解决方法是把Nginx的proxy_read_timeout调整为3600秒,并开启SSE响应头的Cache-Control: no-cache,同时让后端每30秒发送一条心跳注释行保持连接活跃。
第三个是任务提醒重复发送。因为定时任务每分钟扫一次,而提醒时间恰好落在扫描周期内时,任务还没被标记为“已提醒”,下一分钟又会被扫到一次。修复方式是在更新任务状态的SQL语句里加上条件判断:UPDATE crm_task SET status = 'REMINDED' WHERE id = ? AND status = 'TODO',利用更新行数是否为1来判断当前任务是否已经被其他调度周期处理过。这是典型的并发更新场景,靠条件更新比先查询再更新安全得多。
第四个是时区问题导致提醒提前一小时触发。我部署的服务器系统时区是UTC,而业务上希望按北京时间提醒。JVM默认取的是系统时区,所以调度任务每分钟扫描时的当前时间和数据库里的提醒时间出现了偏差。修复方式是在启动脚本中显式添加-Duser.timezone=Asia/Shanghai,同时在数据库连接串上设置serverTimezone=Asia/Shanghai,两边统一之后问题消失。
5.3 数据备份与恢复演练
CRM系统的数据是核心资产,备份不能停留在“有备份任务”的层面,必须验证备份能恢复。我在上线初期就写了一个Shell脚本,每天凌晨2点执行,对PostgreSQL做pg_dump全量备份,保留最近7天的备份文件,同时每周将备份文件同步到另一台独立存储设备。脚本里加了备份文件完整性检查,用gzip -t验证压缩包没有损坏,检查通过后才会清理过期文件。
恢复演练我做了两次,第一次在测试环境模拟误删数据,从备份文件恢复后对比关键表的数据行数,确认一致;第二次直接在生产环境的只读备节点上恢复,验证流程通畅。演练后发现一个容易忽略的问题,pg_dump默认备份的是自定义格式而不是纯SQL,恢复时需要用pg_restore指定连接参数。这个细节不实际操作一次很容易踩坑,所以建议每位运维者至少做一次完整的恢复演练。
6. 项目复盘与经验沉淀
6.1 做得比较对的几个决策
回头复盘DeskcommCRM这个项目,有几个决策我觉得是比较关键的。第一个是把产品边界控制住了,凡是和“轻量桌面协同CRM”定位无关的需求,我在第一期全部砍掉,比如财务管理、进销存对接、工单流转。正因为范围小,整个项目从需求确认到上线只用了不到两个月,团队的热情和专注度保持得非常好。
第二个是坚持了“一线使用优先”的设计理念。我用了一个比较笨的办法——让团队里真正的销售和客服全程参与验收,他们提的每一个别扭都当成一个需求来对待。比如他们反馈录入客户跟不上话速,我就做了Ctrl+Enter快速保存;他们反馈客户等级的含义记不住,我就把等级字段做成了带颜色和文字说明的标签组件。这些细节打磨让系统的好感度提升很多,一线人员从“被迫用”变成了“主动用”。
第三个是数据统计的口径设计。从一开始我就把各种统计指标的业务口径单独建表维护,并允许管理员自定义。这样后续业务指标调整时,不用改代码,只需要在后台更新口径描述和计算规则。
6.2 下一阶段的改进方向
一期上线后我列了一个待办池,有几项是近期一定会做的。第一,客户数据的导入导出功能,目前只有手工录入,历史Excel数据迁移起来特别痛苦。第二,操作审计日志的完善和导出,目前同一个Web页面看到一定数量,管理员后续做合规审查时需要更多人查看。第三,移动端适配,当前系统在手机浏览器上能用,但体验不太好,尤其快速记录和地图定位这类场景需要原生App配合。
我还在调研一个增强点——跟着链路自动生成客户摘要。比如把客户最近5条跟进记录和标签汇总成一段简要描述,显示在客户列表的鼠标悬浮框里,方便销售在打电话前快速回顾。这个用规则模板就能做,不一定要上复杂的自然语言处理,但如果后续数据量多了,引入大模型做更智能的摘要也是顺理成章的方向。
6.3 实际使用中的心得体会
DeskcommCRM上线至今快半年,每日活跃用户稳定在四十多人,累计沉淀客户数据接近一万条。我自己感触最深的一点是:一个小团队的自研系统,真正让它活下去的不是技术多先进,而是它是否精准解决了团队当前最痛的几个问题。如果你也打算自研CRM,我建议先仔细盘点业务现状,找到那三到五个最影响效率的痛点,把它们做深做透,比做一个看似完整但处处浅尝辄止的“大平台”要实在得多。
最后再说一个实用的小技巧:任务提醒和通知这类能力不要一开始就追求多端触达,优先做好站内通知就够了。等到用户真正养成每天打开系统看消息的习惯后,再去对接邮件、短信或企业微信机器人,推送渠道才有价值。否则渠道铺得再多,用户不看系统,一切都是空转。