做销售系统的人基本都听过一句话市面上CRM产品多到数不清但真正让人用得顺手、不用迁就产品逻辑的没几个。DeskcommCRM 并不是什么大厂出品它是我和团队在两年多前从零开始搭的一套内部客户关系管理系统最初只是为了解决销售团队“客户信息散落在Excel表格和微信聊天记录里”这个老大难问题。项目从前期的需求梳理到上线稳定运行前后花了半年多中间推翻过两版设计方案最后同时服务了公司三个业务部门、两百多个销售和客服账号。这篇文章我想把 DeskcommCRM 从需求拆解、数据库设计、核心模块实现到上线后踩坑的完整过程复盘一遍重点讲讲每个关键决策背后的理由和实际落地时的参数细节。如果你正准备自研一套CRM或者只是在做产品中的客户管理模块这篇文章里的一些思路应该能给你省下不少弯路。1. 内容整体设计与思路拆解1.1 为什么自研CRM采购现成的不香吗很多人听到自研CRM的第一反应都是市场上不是有现成的吗买一个不就行了。这个想法我们当时也有过而且专门对比评测了几套主流的商用CRM和开源方案。最后放弃采购是三个原因叠加的结果。第一是业务定制成本。商用CRM确实功能完整但越是完整的系统配置逻辑越复杂。销售团队对客户字段的定义、跟进流程的节点、数据报表的统计口径都和行业标准的CRM模型有差异。我们尝试过在一套商用CRM里配置自己的销售流程结果发现光是表单字段的关联规则和权限控制就折腾了两周最后系统管理员还在为几个高频操作没法简化而头疼。第二是数据安全的考虑。客户数据是销售团队最核心的资产尤其是联系方式、报价记录、历史沟通内容这些数据放在第三方SaaS平台上从管理角度很难让老板完全放心。自研之后数据落在自己的数据库里权限可以由我们自己控制这一点对于内部推广时的信任成本帮助很大。第三是一体化协作的需求。我们希望客户跟进记录能和内部的工作通知、待办事项关联起来销售每次跟进完客户之后系统要自动创建下一条后续任务同时提醒对应的售后人员。这种跨模块的数据联动在通用CRM产品里要么做不到要么需要额外买API接口或写脚本对接。当然自研也有很大的成本。抛开开发人力不谈最大的隐性成本是需求磨合。销售团队一开始不会轻易信任一个内部系统他们习惯了自己记Excel、自己记本子让他们把客户信息搬进系统需要产品足够好用、字段足够符合习惯。这个问题在后面几节会详细讲。1.2 核心模块划分与产品定位DeskcommCRM 的产品定位很明确不是做一个大而全的CRM全家桶而是聚焦销售线索跟进这一个核心场景。整个系统的模块划分围绕一条主线展开客户从线索进入系统经过销售跟进最终形成成交或者流失所有过程数据自动沉淀。模块划分上我把它拆成了五个部分。客户与线索管理是这个系统的地基负责存储客户的基本信息、来源渠道、归属销售。跟进记录模块记录每一次沟通的内容、时间和结果是后面统计销售动作质量的数据来源。任务提醒模块负责自动生成待办事项包括下一次跟进时间、超时未跟进的预警、售后工单的响应提醒。数据看板模块把线索量、跟进率、转化率、成交周期这些指标用图表展示给管理层。最后是权限管理负责控制不同角色能看到哪些客户、能操作哪些动作。我不想把这个系统做成一个所有功能都平铺的大面板。产品设计的时候销售端入口只放三个核心操作我的客户、今日待跟进、新增客户。管理端入口才放完整的客户列表、数据看板、团队管理和导入导出。不同角色的首页看到的界面完全不同这样做的目的是降低普通销售的使用门槛减少“打开系统不知道点哪里”的情况。1.3 技术选型的取舍技术选型这块我们最终确定的是前后端分离架构前端使用 Vue3 TypeScript Element Plus Pinia后端使用 NestJS TypeORM MySQL 8 Redis 6部署用 Docker Compose 一键启动。前端图表用的是 ECharts因为销售漏斗图和趋势图这类常见的商务图表ECharts 的成熟度和文档完整度比同类的 Chart.js 要高一些。后端选择 NestJS 而不是 Spring Boot 或者 Go核心原因是团队前期的技术栈比较统一前后端都用 TypeScript类型定义和接口联调的成本会低很多。NestJS 的依赖注入和模块化设计在管理这类业务逻辑较重的系统时很顺手加上 TypeORM 做数据库迁移和实体映射都比较成熟整套开发效率明显比用 Java 要高。当时也有人建议用 Python 的 Django 或者 FastAPI带着 Django Admin 的话后端可以省很多事。但考虑到后续要接企业微信消息推送、定时任务、WebSocket 消息通知这些实时性功能Node.js 生态在这些场景下反而更顺手。另外团队里没有专门的 Python 后端开发强行切换语言的成本远大于框架本身的优势。数据库设计上MySQL 8 的窗口函数和 JSON 类型都很好用。客户表里的扩展字段就是用 JSON 类型的字段存储的这样用户自定义字段不需要频繁改表结构。Redis 在这里主要承担两个职责一个是登录会话和接口权限的缓存另一个是数据看板统计结果的缓存。这两个场景对读写性能要求高但对数据一致性要求相对宽松非常适合用缓存。2. 核心细节解析与实操要点2.1 客户与线索的字段设计少即是多客户表是整个 CRM 的核心字段设计的好坏直接决定了销售愿不愿意录入数据。如果字段太多销售觉得烦随便填如果字段太少管理端又拿不到有效的统计维度。我们在第一版设计里犯了所有人都会犯的错列了将近四十个字段什么客户来源、客户行业、公司规模、决策人职位、预算范围、采购时间、竞争品牌……结果上线第一周销售录入的客户数据里接近一半的字段都是空的。后来我们重新梳理了一版字段遵循“录入必填字段不超过8个”的原则。最终定下来的核心字段是客户名称、联系人姓名、联系电话、微信/邮箱、客户来源、所属行业、客户状态、预计成交金额。其他信息例如公司地址、产品偏好、历史报价等都放进“备注”或者自定义 JSON 字段里有则填没有也不强制。字段的设计逻辑其实包含了一个很现实的原因销售人员录入客户信息的行为大多发生在刚拿到一个联系方式的那几秒内比如电话沟通挂断后顺手记一下。这个时候表单越短录入完成的概率越高。等到后面客户跟进深入了再逐步补充详细资料这才是符合实际工作节奏的设计。客户表的具体结构和设计要点大概可以用下面这张表概括字段名类型说明idbigint主键customer_namevarchar(64)客户名称contact_namevarchar(32)联系人姓名contact_phonevarchar(20)联系电话唯一索引contact_wechatvarchar(64)微信/邮箱sourcevarchar(32)客户来源枚举地推/广告/转介绍/线上表单/其他industryvarchar(32)所属行业枚举statustinyint客户状态1-线索、2-跟进中、3-已成单、4-已流失owner_idbigint归属销售 IDexpected_amountdecimal(12,2)预计成交金额metadatajson自定义扩展字段versionint乐观锁版本号created_atdatetime创建时间updated_atdatetime更新时间还有一点值得单独拿出来说联系电话字段必须加唯一索引。客户数据重复是 CRM 系统最头疼的问题之一同一个客户被不同销售录入两遍后面跟进记录就会分裂成两条线统计口径全乱。用手机号做唯一索引再配合导入时的查重逻辑能在很大程度上避免这个问题。2.2 跟进记录的数据模型时间线的核心跟进记录是客户管理系统的灵魂它的核心是回答一个问题这个客户从进来到成交销售一共做了哪些动作每次沟通说了什么效果如何。我见过不少项目把跟进记录设计成客户表里的一个备注大字段这种做法短期看着省事实际上后患无穷——备注一多就变成一坨文字根本没法统计跟进频率和转化效果。DeskcommCRM 采用独立表来存储跟进记录。一张 follow_up_record 表核心字段包括客户 ID、跟进人 ID、跟进类型、沟通内容、下次跟进时间、本次跟进结果。跟进类型我们枚举为电话、微信、拜访、邮件、其他。跟进结果枚举为有意向、暂无意向、待确认、已成交、已流失。每次跟进完成后系统会自动把客户状态和跟进结果做一次联动比如“已成交”之后客户状态直接切到已成交并给销售主管发送一条通知。在实现上有个细节很容易被忽略跟进内容的编辑和删除权限。为了防止销售误删跟进记录或者篡改历史内容我们的设计是跟进记录一旦提交内容不允许编辑只允许补充“补充说明”。这个规则的管理成本很低但避免了很多扯皮问题。销售在系统里留下的是真实可回溯的沟通历史管理层对客户情况的分析才有据可依。每次跟进之后销售需要填一个“下次跟进时间”这个字段会同步生成一条待办任务。如果下次跟进时间超过三天没有记录动作系统会自动给销售推送一条超期提醒。这个机制在后面自动任务一节详细展开。2.3 权限体系与数据隔离CRM 的权限体系比其他内部系统更敏感因为客户数据直接关系销售业绩归属。我们采用的权限模型是三层的功能权限、数据范围权限、字段操作权限。功能权限比较好理解就是用户能访问哪些菜单、能执行哪些操作。我们用角色来管理系统里默认预置了四个角色管理员、销售主管、销售、客服。每个角色勾选自己的菜单权限和按钮权限存储成角色表 JSON 字段登录时加载到内存。数据范围权限决定了一个用户能看到哪些客户的记录。我们用了最经典的“本人数据 / 本部门数据 / 全部数据”三级设计。销售的默认数据范围是“仅本人”也就是只能看到 owner_id 等于自己 ID 的客户。销售主管的数据范围是“本部门”可以看到整个部门销售名下的客户主要目的是为了做团队管理。管理员的默认范围是“全部”同时支持按部门、按销售、按时间段做筛选。实现上数据范围权限是放在 SQL 查询的 where 条件里的。所有查询客户列表的接口后端都会根据当前登录用户的角色动态拼接一个权限过滤条件。这个方案简单直接没有引入复杂的行级安全机制性能上也完全够用。字段操作权限主要控制某些敏感字段的可见性比如客户的预计成交金额普通销售只能看到自己的客户销售主管可以看到部门所有客户的金额汇总但看不到明细管理员无限制。第一版上线的时候权限体系比较粗只有“管理员”和“普通用户”两种角色结果销售主管看不到部门销售客户的动态管理效率很低后来才补上数据范围这套逻辑。这个调整非常值得它让管理端和销售端的体验都变得顺畅了很多。3. 实操过程与核心环节实现3.1 线索流转与状态机的落地线索流转是 CRM 的业务主流程我们把线索状态定义成一棵状态机新建线索、待分配、跟进中、转意向、成交、流失。状态之间不是任意跳转的而是要用代码严格控制。比如一个“已流失”的客户不能被直接改成“已成交”必须先转回“跟进中”并补充新的跟进记录这样做的目的是保证状态变更都有记录可查防止销售为了业绩数据随意改状态。为了实现状态机我单独建了一张 lead_status_log 表专门记录每一次状态变更。字段包括客户 ID、旧状态、新状态、操作人、变更原因、创建时间。这样每次客户状态有跳转都留着完整的审计痕迹。这个表当时看起来像是过度设计后面在排查销售数据争议和做流程优化时帮了大忙。状态变更的核心逻辑写在后端一个统一的 service 方法里大致流程如下先查出当前客户状态判断目标状态是否在允许的转换表中如果允许则更新客户表的状态字段同时插入一条状态变更日志再触发后续的自动任务。如果涉及客户归属变更比如主管把客户从一个销售手里转给另一个销售还需要额外记录一条客户归属变更记录并通知两个销售。客户分配这块我们实现了两种分配方式自动轮流分配和手动指定分配。自动轮流分配的逻辑是当一批新线索通过 Excel 导入或者线上表单进来后系统按销售在当前周期内的未完成任务数量做加权分配任务少的销售优先拿到新线索。这个加权分配规则很实用比简单按编号轮流分要公平得多销售团队对分配结果的接受度也明显更高。3.2 销售漏斗与 Dashboard 统计实现数据看板是管理端最常用的页面也是我们对报表统计口径纠结最多的地方。销售漏斗的核心指标有五个新增线索量、有效跟进量、转意向量、成交量、流失量。这里最容易踩坑的是“统计口径”新增线索量是按创建时间算成交量是按成交时间算跟进率是有效跟进客户数除以总客户数。口径不统一同一个数字在两张报表里对不上管理层的信任会瞬间崩塌。我在看板实现中把所有统计指标的时间口径统一成了“按动作发生时间统计”并且在每一个指标旁边都标注了统计口径说明。比如“成交量”统计的是在选定时间段内发生成交动作的客户数量而不是客户创建时间落在时间段内的成交客户数量。这些细节做的时候多花一点功夫后面省掉无数解释成本。看板数据的前端展示用了 ECharts 漏斗图加趋势折线图。后端统计接口用 SQL 聚合来实现核心逻辑类似下面这样SELECT COUNT(DISTINCT customer_id) AS lead_count, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS deal_count, SUM(CASE WHEN status 4 THEN 1 ELSE 0 END) AS lost_count FROM customer WHERE created_at BETWEEN :startTime AND :endTime;这里注意一点漏斗统计不能只 count 客户表因为一个客户可能在同一个时间段内既有跟进又有成交。正确的做法是先跟跟进记录表关联再去重统计 customer_id否则数据会重复计算。统计接口一开始是每次都实时查数据库很快发现一个问题当跟进记录数据量超过几十万条时看板页面加载需要三四秒根本没法接受。后来我们把统计数据按天、按部门、按销售三个维度做了预聚合表每天晚上凌晨跑定时任务把前一天的统计数据计算好存起来。看板接口直接查预聚合表并加了 Redis 缓存页面加载时间从三秒多降到了半秒以内。这个优化是整个系统上线后收益最高的性能改进。3.3 跟进提醒与自动化任务CRM 系统如果只做数据记录价值会大打折扣真正让销售团队离不开它的是自动提醒和任务分发能力。DeskcommCRM 里最常用的一项自动化是跟进提醒每个客户都有一个“下次跟进时间”一旦这个时间早于当前时间且客户状态不是已成交或已流失就会自动生成一条待办任务推送给对应的销售。技术实现上我们用了一个 Node.js 的定时任务框架每分钟扫描一次客户表。扫描 SQL 的核心条件是 next_follow_time NOW() 并且 status IN (1,2)再按 owner_id 分组统计超期数量。为了避免用户收到重复提醒每个客户加了一个 last_notified_time 字段只有当当前时间距离上次通知超过 12 小时才会再次推送。通知渠道我们在第一版接的是邮件后来发现销售根本不会开着邮箱客户端及时性太差。第二版改成了企业微信应用消息推送打通企业微信之后销售手机能实时收到提醒阅读率从不到 30% 提升到了 90% 以上。这里要提醒一下如果你们的公司用的不是企业微信钉钉或者飞书也有类似的应用消息接口逻辑是一样的。任务提醒还有一种场景是销售主管关心的某个客户超过 7 天没有任何跟进动作。这种情况系统会标记为“沉睡客户”每周一早上自动给对应的销售主管推送一份沉睡客户名单由主管决定是否重新分配或者电话回访。这个功能上线后沉睡客户的激活率提高了不少因为它把原来靠主管人工翻客户列表的低效方式变成了系统化的流程。4. 常见问题与排查技巧实录4.1 客户数据重复导入的时候炸了系统上线第三个月运营同事反馈了一次“客户导入报错”的问题。排查后发现Excel 模板里同一个手机号出现了两次而且手机号的格式有的是 11 位有的是 3 位区号加 8 位号码还有的带了空格和横线。导入逻辑直接按数据库唯一索引去处理结果整个批次全部回滚一条数据都没导进去。这个问题整改后我们沉淀了一套导入规范。第一步是清洗把手机号里的空格、横线、非数字字符全部去掉统一成纯 11 位数字。第二步是预检导入前先把所有手机号做一次去重校验发现重复立即停止导入并返回重复的手机号和所在行号。第三步是匹配策略如果库里已经存在相同手机号的客户默认跳过并记录原因也可以选择“覆盖更新客户名称”这种策略但是绝不静默插入新客户。现在这套规则一直在用再也没有出现导入数据把客户表搞乱的现场。4.2 同一个客户被两个人同时修改覆盖了这个问题是在销售主管尝试批量转交客户时发现的。主管把一个客户从 A 销售转给 B 销售同时 B 销售正在修改这个客户的预计成交金额结果 B 的修改操作把主管的转交操作覆盖了客户归属变回了 A而 B 看到的界面里金额已经改了两边数据对不上。我们看到这个问题后加了不少代码。首先是客户表加了 version 字段每次更新时带上 WHERE version 旧版本号如果影响行数为 0说明数据已经被别人改过后端直接返回冲突错误。其次是把“转交客户归属”和“修改客户基本信息”这两类操作做了事务隔离确保同一时间只有一个写锁在生效。最关键的一步是在更新操作之前增加了一条校验更新客户归属时必须先把客户的最新版本号查出来再执行更新否则直接拒绝操作。这个乐观锁机制实现简单而且非常可靠。4.3 列表页越来越慢索引优化是首要排查项系统运行了半年后客户列表页开始变慢。最直观的感受是筛选“本月新增线索”时接口响应时间从最初的 300 毫秒涨到了 3 秒以上。我们用 EXPLAIN 分析发现慢在 customer 表的 owner_id 和 created_at 这两个字段的查询条件上没有命中索引每次都是全表扫描。优化方案很简单把客户列表最常用的几个查询条件组合在一起建联合索引。我们的查询条件通常是“owner_id status created_at”于是建了一个 (owner_id, status, created_at) 的联合索引。另外一个经验是要注意如果查询里有排序和分页排序字段必须包含在索引里否则数据库还是会用 filesort索引白建。改完后接口响应时间恢复到 350 毫秒左右这个组合索引一直稳定运行到现在。还有个大字段的问题也顺带处理了。客户表里的 metadata JSON 字段存储了不少扩展信息在列表查询时用 SELECT * 会把整段 JSON 都拉出来数据量大时网络传输开销很可观。优化后所有列表查询都改成只选需要的字段详情页才加载完整数据分页性能提升非常明显。4.4 时区与日期相关的几个隐藏坑时区问题是我在做报表统计时踩得最深的一个坑。数据库 MySQL 用的是 UTC 时间存储后端 Node.js 服务也统一用 UTC只有前端展示时转成本地时间。这个方案本身没有问题问题出在统计 SQL 上前端传过来的是本地时间的开始和结束比如 2024-11-01 00:00:00 到 2024-11-30 23:59:59如果后端没有正确处理时区偏移直接拿这个时间去查 UTC 时间的数据库统计出来的结果就会多一天或者少一天。我们最终的解决方案是所有接口统一接收时间戳后端在查询之前把时间戳转成 UTC 的字符串再拼 SQL前端展示统一用 Date 对象的本地时区格式化。只要后端和数据库保持一致的时区处理规则就不会出问题。另外一个和日期相关的坑是“下次跟进时间”的提醒。员工在手机端选择的日期是本地时间比如 11 月 5 号 18:00存储成 UTC 之后如果定时任务按 UTC 的“天”来判断是否逾期就会出现本地已经过了 11 月 5 号晚上但系统还没触发提醒的情况。这个问题的处理方式是所有提醒类判断都以“当前 UTC 时间 存储的 next_follow_time”为准不做天级别的对齐逾期就是逾期简单直接。这里还有个小经验可以分享像 CRM 这种系统所有和用户相关的界面展示都别在服务端做格式化把原始时间戳丢给前端处理。服务端一旦依赖浏览器时区做了字符串格式化全局纠错的成本会非常高。写在最后的一点体会DeskcommCRM 这个项目做下来对我个人来说收获最大的其实不是代码层面的技术方案而是对“业务系统”这四个字的理解。技术选型、数据库设计、接口性能这些都是有标准答案的真正决定一个系统能不能活下去的往往是它有没有贴着使用者的真实工作习惯去设计。如果我只能给后来的人一条建议那就是动手写代码之前把客户的状态流转、字段的必填规则、权限的数据范围这三件事和实际使用者逐条确认清楚哪怕多花两周时间都值得。另外所有的状态变更和关键操作都要留操作日志这个当时觉得可有可无的设计后面查起数据问题来真的能救命。如果你也要做类似的客户管理系统建议第一期就瞄准一个核心场景去做深做透比如“线索跟进提醒”不要一上来就想做全功能。系统上线之后销售们真正依赖上了某个功能再往周边慢慢扩展也不迟。
阅读完成 · 觉得有帮助?