尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

自研轻量级CRM系统实战:从需求拆解到部署上线全记录

自研轻量级CRM系统实战:从需求拆解到部署上线全记录 讲真我第一次看到“DeskcommCRM”这串字符的时候第一反应是这名字不像是随手拍脑袋起的。Desk代表桌面办公Comm是Communication的缩写合在一起就是“桌面沟通型客户关系管理系统”——这恰恰是我在立项前期最真实的需求定位把散落在Excel表格、微信聊天记录、邮箱和各类零散小工具里的客户信息全部收拢到一个统一的桌面工作台上。当时团队里销售、运营和客服各用各的记录方式客户跟进到哪一步了没人能说清楚催单、漏单、撞单的情况几乎每周都发生。做这样一套轻量级CRM不是为了搞一个看起来很酷的中台而是为了解决实际业务里“信息断层”和“跟进黑洞”这两个真问题。这篇文章会完整还原我从需求拆解到架构选型、从表结构设计到核心功能落地再到部署上线和问题排查的全过程希望能给同样在自研CRM系统的朋友提供一份可以直接参考的作业。1. 项目背景与需求拆解为什么叫DeskcommCRM1.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而是用SSEServer-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来实现的任何关键修改都会自动写入操作日志表前端不需要额外请求额外的展示逻辑。为了提升录入效率我在客户详情页提供了一个“快速记录跟进”的浮层入口按下快捷键CtrlEnter就能提交跟进记录。这个小交互很受销售同事欢迎因为他们接电话时没有时间点开一堆表单。快速记录只要求填写跟进方式和内容下次跟进时间和任务可以稍后再补尽可能降低使用门槛。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最大连接数设成20Spring 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.timezoneAsia/Shanghai同时在数据库连接串上设置serverTimezoneAsia/Shanghai两边统一之后问题消失。5.3 数据备份与恢复演练CRM系统的数据是核心资产备份不能停留在“有备份任务”的层面必须验证备份能恢复。我在上线初期就写了一个Shell脚本每天凌晨2点执行对PostgreSQL做pg_dump全量备份保留最近7天的备份文件同时每周将备份文件同步到另一台独立存储设备。脚本里加了备份文件完整性检查用gzip -t验证压缩包没有损坏检查通过后才会清理过期文件。恢复演练我做了两次第一次在测试环境模拟误删数据从备份文件恢复后对比关键表的数据行数确认一致第二次直接在生产环境的只读备节点上恢复验证流程通畅。演练后发现一个容易忽略的问题pg_dump默认备份的是自定义格式而不是纯SQL恢复时需要用pg_restore指定连接参数。这个细节不实际操作一次很容易踩坑所以建议每位运维者至少做一次完整的恢复演练。6. 项目复盘与经验沉淀6.1 做得比较对的几个决策回头复盘DeskcommCRM这个项目有几个决策我觉得是比较关键的。第一个是把产品边界控制住了凡是和“轻量桌面协同CRM”定位无关的需求我在第一期全部砍掉比如财务管理、进销存对接、工单流转。正因为范围小整个项目从需求确认到上线只用了不到两个月团队的热情和专注度保持得非常好。第二个是坚持了“一线使用优先”的设计理念。我用了一个比较笨的办法——让团队里真正的销售和客服全程参与验收他们提的每一个别扭都当成一个需求来对待。比如他们反馈录入客户跟不上话速我就做了CtrlEnter快速保存他们反馈客户等级的含义记不住我就把等级字段做成了带颜色和文字说明的标签组件。这些细节打磨让系统的好感度提升很多一线人员从“被迫用”变成了“主动用”。第三个是数据统计的口径设计。从一开始我就把各种统计指标的业务口径单独建表维护并允许管理员自定义。这样后续业务指标调整时不用改代码只需要在后台更新口径描述和计算规则。6.2 下一阶段的改进方向一期上线后我列了一个待办池有几项是近期一定会做的。第一客户数据的导入导出功能目前只有手工录入历史Excel数据迁移起来特别痛苦。第二操作审计日志的完善和导出目前同一个Web页面看到一定数量管理员后续做合规审查时需要更多人查看。第三移动端适配当前系统在手机浏览器上能用但体验不太好尤其快速记录和地图定位这类场景需要原生App配合。我还在调研一个增强点——跟着链路自动生成客户摘要。比如把客户最近5条跟进记录和标签汇总成一段简要描述显示在客户列表的鼠标悬浮框里方便销售在打电话前快速回顾。这个用规则模板就能做不一定要上复杂的自然语言处理但如果后续数据量多了引入大模型做更智能的摘要也是顺理成章的方向。6.3 实际使用中的心得体会DeskcommCRM上线至今快半年每日活跃用户稳定在四十多人累计沉淀客户数据接近一万条。我自己感触最深的一点是一个小团队的自研系统真正让它活下去的不是技术多先进而是它是否精准解决了团队当前最痛的几个问题。如果你也打算自研CRM我建议先仔细盘点业务现状找到那三到五个最影响效率的痛点把它们做深做透比做一个看似完整但处处浅尝辄止的“大平台”要实在得多。最后再说一个实用的小技巧任务提醒和通知这类能力不要一开始就追求多端触达优先做好站内通知就够了。等到用户真正养成每天打开系统看消息的习惯后再去对接邮件、短信或企业微信机器人推送渠道才有价值。否则渠道铺得再多用户不看系统一切都是空转。
返回列表