
1. 项目定位为什么我们不买现成CRM非要自研一套 DeskcommCRM1.1 从销售痛点出发的原始需求先说结论DeskcommCRM 最初不是老板拍脑袋说要做一个 CRM而是销售团队在客户跟进这件事上已经乱到快要出事故了。我接到这个项目时业务侧反馈的问题非常具体——客户信息散落在 Excel 表格、微信聊天记录、个人印象里一个客户换了两三个人跟进之后连基础的联系人档案都对不上。销售撞单、重复拜访、线索被遗忘、客户移交时口头交接漏信息这些都是每天真实发生的争吵。团队不大问题不小。所以 DeskcommCRM 的定位从一开始就很清楚它不是要做一套功能齐全但没人用的系统而是要先解决“客户记录从无到有、从私有到共享、从混乱到有序”这件事。换句话说我们做的是一个以客户为中心的、销售团队每天必须打开、能真正替代 Excel 和微信的工作台。这个定位决定了后面所有的设计取舍。1.2 自研与外购的成本账、灵活性账、数据账在动手之前团队内部也讨论过到底是买现成的还是自己写。市面上成熟 CRM 确实很多功能模块一个比一个全价格从几百到几千一个账号每年都有。但为什么最后选了自研主要权衡了三点第一是数据账。外购系统数据虽然在云端但字段扩展、接口开放、跟内部业务系统打通这些事往往要等厂商排期严重卡手。销售团队最烦的就是“我要看一个字段但系统里没有”。自研意味着字段模型完全可控今天提需求明天就能改。第二是灵活性账。外购产品为了照顾所有行业客户默认配置复杂、菜单冗余。而中小型销售团队真正高频使用的功能就那几个客户管理、线索跟进、商机推进、简单报表。我们只需要做精做透不做大而全。像“对象自定义字段”“审批流配置”这些通用功能第一版全部砍掉。第三才是成本账。短期看自研人力成本一定高但长期算下来按账号付费的订阅成本加上定制开发的隐性成本通常比自研高不少尤其当团队超过二三十人后差距会拉大。这还没算上数据迁移和切换成本。提示如果你所在的团队业务模式非常标准化、没有太多特殊流程外购确实更划算。自研的前提是——你真的知道自己要什么而且有足够的技术人力把这件事持续维护下去。DeskcommCRM 是符合这个前提的。1.3 功能边界划定第一版只做四件事这里要非常坦诚地说一句第一版我们用了很大力气抵抗“功能蔓延”。产品讨论会上提出来的需求五花八门有要日历同步的有要自动群发邮件的有要做价格审批的最后全部划到了二期。第一版 DeskcommCRM 只围绕四件事展开客户与联系人管理统一存放客户信息解决“客户在哪”的问题。跟进记录每次电话、拜访、微信沟通后的结果沉淀解决“聊了什么”的问题。商机管理把有成交意向的客户放进销售漏斗解决“单子在哪”的问题。团队数据看板让管理者看到每个人的工作量、转化率和线索状态解决“干得怎么样”的问题。这四件事覆盖了销售团队日常最痛的场景。其余功能比如工单、回款、合同等主链路跑通了再逐步加。事实证明这种克制非常有必要——系统上线后销售团队学会的成本极低两周内活跃率就到了九成以上。2. 核心模型设计客户、联系人、商机的底层关系怎么设计才不返工2.1 客户表与联系人表为什么要强行拆开这个点看起来基础但我见过太多小型项目直接把客户和联系人混在一张表里字段塞在一起公司名称、联系人姓名、电话各占一列。当时觉得省事后面想做“一个公司有多个对接人”的时候全部逻辑都要推翻。DeskcommCRM 第一版的数据模型就是这样设计的customer客户主体对应一个公司或组织字段有公司名称、行业、规模、客户来源、所属销售、创建时间。contact联系人归属于某个客户字段有姓名、电话、微信、职位、生日、备注。opportunity商机归属于某个客户可能关联一个或多个联系人字段有名称、金额、预计成交日期、阶段、赢率。follow_record跟进记录关联客户或商机记录沟通方式、内容、下次跟进时间。为什么客户和联系人要拆因为真实业务里一个客户公司里会有多个决策链条使用部门是一个人采购部门是另一个人拍板的是技术总监走流程的又是商务。如果联系人挂在客户表里每加一个联系人就要重复填写一遍公司信息录入成本高而且数据必然会出现大量冗余和不一致。从数据库设计角度说拆开之后客户表是主表联系人是子表通过customer_id外键关联这样查询某个公司的所有联系人只要一条 SQL。而且后面做客户移交、合并、查重也会方便很多。另外一个初创项目容易忽略的点所有业务表统一包含owner_id归属人、created_by创建人、updated_by更新人、deleted_at软删除标记。听起来是老生常谈但很多项目做报表、做权限、做审计的时候才发现没留这些字段只能回头补数据非常痛苦。2.2 商机阶段与跟进记录的数据流转商机管理这部分我们的模型并不是一开始就设计好的踩了两次坑之后才调整成现在这样。第一次版本里商机阶段直接用了单个字段字符串如“初步沟通”“需求确认”“方案报价”“谈判”“赢单/输单”。结果上线两周就发现这个字段无法回答两个问题当前阶段已经停留多久了每一阶段的转化率是多少后来我们重构成了标准漏斗模型每个商机有stage字段但阶段值是受控的字典码配合stage_change_time记录进入当前阶段的时间。再看一个商机在哪个阶段停留的时间直接用当前时间和stage_change_time做差就行。要拉出转化率报表也只需要按历史阶段变更记录去统计而不是靠商机表的当前值。跟进记录的设计同样经历了一轮简化。最开始我们做得太重每个跟进记录可以关联客户、商机、联系人多张表还要支持附件上传结果前端表单复杂销售根本不爱填。后来改成了极简原则跟进记录只干两件事——选关联对象客户/商机填沟通内容。那些“下一步计划”“工作量评估”等额外字段全部去掉。别小看这个简化。跟进记录是 CRM 里使用频率最高的操作每多一个必填字段录入意愿就降低一截。第一批销售用户都是嫌麻烦的真实业务人员不是软件爱好者谁用三天都要加好几个步骤这系统就废了。2.3 公海池与线索回收机制的设计公海池是销售团队最关心、也是老板最看重的机制之一。我不想谈太抽象的概念直接说它的业务价值防止有人占着客户不跟防止客户资源被个人囤积。DeskcommCRM 的公海规则是这样的新线索进入系统后默认放在“公海池”里销售可以主动领取到自己名下领回去之后有 7 天的保护期保护期内如果没有任何跟进记录7 天一到自动退回公海让其他同事有机会领取。对于商机客户保护期放宽到 14 天因为大客户的沟通周期本来就更长。这个机制在技术实现上有两个关键点。一个是定时任务每天凌晨扫描所有销售名下的客户判断“最后一次跟进时间距今是否超过保护期”如果超过就自动变更owner_id并追加一条系统跟进记录“客户因超期未跟进被回收至公海”。另一个是状态机的转换限制客户只能在“公海 → 已领取 → 跟进中 → 回收至公海”这几个状态间流转不允许随意跳转防止有人手动把客户调来调去。注意回收规则一定要提前和业务方确定清楚并且要留出申诉和人工恢复的入口不能全自动一刀切。销售出差一周回来发现客户被回收了如果没有人工申诉通道情绪反弹会非常剧烈。我们在第一版就把“管理员手动恢复”这个功能做进去了后面果然帮了大忙。2.4 字段权限与操作权限的真实用法权限这块很多人一上来就只想到“谁能看这个菜单”。但在实际业务里CRM 权限的颗粒度要比这细得多。DeskcommCRM 的权限分三层第一层是功能权限也就是菜单和按钮的可见性。比如普通销售只能看到自己的客户和公海销售主管能看到团队所有客户管理员能进后台配置。这是最基础的一层用 RBAC 模型角色-权限-用户实现角色表、权限表、角色权限关联表、用户角色关联表四张表搞定。第二层是数据权限也就是“看到哪些数据”。同样一个客户列表销售看到的是自己的主管看到整个团队的老板看到全公司的。这块不能靠前端过滤必须核心在 SQL 层强制带上数据范围条件不然前端一切信息都可能被绕过。第三层是字段权限也就是“看到哪些字段”。销售跟进客户时要看到客户的电话和微信但合同折扣、成本价这类敏感字段只有主管以上才会显示。字段级权限实现起来比前两层更麻烦需要在后端查询时动态组装返回字段。我们第一版只对客户模块加了字段权限控制其他模块暂时没做避免过度设计拖慢开发节奏。这里给一个最重要的经验权限系统不要等上线后再补一定要在写核心业务接口的第一天就留好拦截点。哪怕第一版只做简单的角色判断也要把getUserDataScope()这个方法的位置留出来后面扩展成本会低很多。3. 实操环节DeskcommCRM 从 0 到 1 的关键实现细节3.1 技术选型为什么选了这套组合我在描述 DeskcommCRM 的技术栈时尽量说人话不搞卖弄。因为这类系统最重要的是稳定好维护不是炫耀技术。后端我们用的是 Java Spring Boot配上 MyBatis-Plus 做数据访问。Spring Boot 的生态成熟招人容易遇到问题网上方案也多。MyBatis-Plus 对单表 CRUD 的封装很顺手写业务代码非常快。前端选了 Vue 3 加 Element Plus管理后台常见的表格、表单、弹窗、树形控件都有现成组件开发效率高。数据库用的 MySQL 8.0这个体量的数据量用 MySQL 完全够了没必要一上来就上分布式增加运维复杂度。从部署角度我们用了最简单的单体架构一个 Spring Boot 应用加一个 MySQL 实例部署在云服务器上用 Nginx 做反向代理。没有引入 Redis、没有消息队列、没有微服务。有人可能会觉得这“不够先进”但我的判断标准是CRM 系统的主链路是录入和查询数据量在百万级以内单体架构足够扛住而且排查问题、备份和升级都简单很多。技术选型解决的是业务问题不是满足技术迷恋。3.2 权限系统落地RBAC 加数据范围过滤权限系统听起来繁琐拆开之后其实只有两件事用户是什么角色角色能看什么数据。先说功能权限。我们建了sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu这几张核心表。sys_menu表里不仅是菜单还包括按钮比如“新建商机”“删除跟进记录”“导出客户列表”。登录时根据用户的角色列表查出所有可访问的菜单和按钮编码放到内存缓存里前端渲染菜单时用后端接口校验时也用。前端隐藏按钮解决的是用户体验后端判权限解决的是安全底线。再说数据权限这是最容易出问题的地方。简单说就是用户发起列表查询时MyBatis 的拦截器会在 SQL 上自动追加数据范围条件。具体实现思路是这样如果用户是普通销售追加owner_id {当前用户ID}。如果用户是主管追加owner_id IN (SELECT user_id FROM sys_user WHERE dept_id {主管部门ID})。如果是管理员则不追加额外条件。这套逻辑看起来不复杂但有一个坑很容易踩联表查询时拦截器不知道给哪个表加条件。比如查询商机列表要关联客户表如果直接给 SQL 加了owner_id条件数据库会报“字段不明确”。我们的解决方案是统一约定所有业务表的归属人字段都叫owner_id并且在 SQL 里给主表起别名时固定规则拦截器按别名动态拼接条件。3.3 跟进记录的写入与防重复操作跟进记录是使用频次最高的接口也是并发问题最容易爆发的点。这里分享两个真实遇到的问题。第一个是重复提交。销售在弱网环境下点击“保存跟进记录”前端没有点击后的禁用状态后端的保存接口被同一个请求发出两遍系统里就生成了两条一模一样的跟进记录。销售自己没注意后面做数据统计时发现记录数虚高很难核查。这个问题我们用了两种方式双保险前端在点击保存后立即禁用按钮并加上 loading 状态这个最简单也最有效后端在接收请求时校验用request_id加 Redis 做幂等判断同一个请求 ID 在几秒钟内不允许重复提交。由于当时系统还没有引入 Redis后端的幂等逻辑先用的数据库唯一索引——在跟进记录表上加了一个unique_key字段由用户 ID、关联对象 ID 和请求的时间戳拼接生成重复插入时数据库层就直接拒绝了。第二个问题是并发抢单。两个销售同时看到公海池里同一个优质客户同时点击“领取”数据库操作是先查owner_id的值为空然后更新成自己的 ID。如果接口没有加锁两个请求可能都通过检查然后都执行更新产生脏数据。解决方案也不复杂更新时在 SQL 里带上“如果归属人还没变才更新”的条件。UPDATE customer SET owner_id #{currentUserId}, updated_at NOW() WHERE id #{customerId} AND owner_id IS NULL这样即使两个请求同时到达数据库的行锁会保证只有一个请求更新成功。再配合后端捕获影响行数返回结果为 0 时提示“客户已被其他同事领取”体验就很流畅了。3.4 销售漏斗看板的 SQL 口径问题这块是我很想详细展开的因为很多 CRM 项目做数据看板时最容易翻车的地方就是口径不统一。DeskcommCRM 的看板第一版要展示三个指标新客户数、跟进次数、商机转化率。听起来很简单但从不同部门嘴里理解的“商机转化率”竟然有两个完全不同的含义。销售总监说商机转化率是“赢单商机数除以所有商机数”销售运营说是“赢单商机数除以本月新增商机数”。这两个口径差异巨大如果不统一上线后必然扯皮。我们的做法是在产品层面强制统一看板的默认口径是“本月赢单商机数 ÷ 本月新增商机数 × 100%”并且在看板页面加了一个口径说明的小问号提示点击后能看到具体计算逻辑。虽然这样会让一部分老销售不习惯但至少团队内部吵架能对得上。SQL 实现上漏斗数据不是一张表能解决的。我用了临时表加子查询的方式先把每个阶段的商机数统计出来再逐层计算转化率。经验是这类统计查询尽量在业务低峰期跑或者用缓存把整点结果存下来避免每次打开看板都实时扫全量数据。我们的数据量不大第一版直接实时查询也能接受但为了性能还是加了一小时级别的缓存。上线后看板接口从最初的三秒降到了两百毫秒以内体验完全不一样。4. 上线前后踩过的坑问题排查与优化实录4.1 慢查询客户列表越翻越慢系统刚上线一周时一切正常到了第二周销售开始反馈客户列表页面打开越来越慢有些同事的账号直接卡到十几秒才能加载完。第一反应是看数据库慢查询日志结果发现了一段高频执行的 SQL问题出在列表页的分页查询上。SELECT * FROM customer WHERE owner_id 101 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 20 OFFSET 40这条 SQL 单独执行不慢但它的执行计划里 type 是 ALL也就是全表扫描。原因很简单owner_id和deleted_at没有建联合索引MySQL 要先把所有行拉到内存再排序数据量一上来自然慢。优化方案是在customer表上建了联合索引(owner_id, deleted_at, updated_at)。这样既完成了数据过滤又直接走索引排序之后再看执行计划type 变成了 ref耗时直接下降到毫秒级。后续我们养成了一个习惯所有业务表的分页查询都必须按“过滤字段 软删除字段 排序字段”这组模式提前评估索引不能等线上出问题再补。4.2 并发抢单公海领取差点变成手速大赛公海领取的问题前面提过但我要再补充一个真实事故。上线第三天管理员反馈公海池里一个高意向客户被两个销售同时领取系统里出现了两条归属变更记录后台看这个客户的owner_id也是两个值交替闪跳。排查后发现原因出在 Service 层的写法上我的同事先写了一个updateCustomerOwner()逻辑第 1 步查询客户当前归属第 2 步判断是否为空第 3 步更新归属。三步之间没有事务和锁两个请求并发执行时第 2 步的判断都通过了导致两个更新都执行成功。修复方案很简单就是把前面那条带条件的 UPDATE SQL 作为唯一更新入口同时去掉“先查后改”的模式。后面又加了一层兜底在代码里通过synchronized按客户 ID 加锁确保同一个客户同时只有一个请求进入更新逻辑。这个组合方案上线后再没出现过抢单脏数据。4.3 数据迁移Excel 导入乱码与重复记录系统上线第一天销售团队要批量导入历史客户数据。我们做好了模板销售按要求填写结果导入后乱码一片中文全部显示成问号部分电话号码变成了科学计数法格式。第一个原因是字符集问题导入模板用 Excel 打开另存时默认编码可能变成ANSI而后端读取用的UTF-8解析自然出错。我们在导入接口里加了编码自动检测读取文件内容时判断 BOM 和字符集同时给销售团队发了一个导入规范文档要求模板必须用指定版本填写。第二个问题更隐蔽历史数据没有统一的查重标准同一个客户在 Excel 里被录入了三遍只是联系人电话不同导入后产生了大量重复客户。我们上线前紧急加了一个“客户查重合并”工具先按公司名称的精确匹配和模糊匹配分别做一遍扫描列出疑似重复项由管理员人工确认后合并合并时保留最新的一条跟进记录和所有商机。这个功能虽然开发了三天但避免了后面几个月“每个客户都是重复的”这种灾难。4.4 权限泄漏越权访问是怎么被堵住的这个坑是最惊险的一个。上线一个月后产品经理无意中用一个普通销售账号直接输入了客户详情的 URL比如/customer/10239居然看到了别人的客户信息。问题出在接口层没有做数据归属校验。列表页虽然按owner_id做了数据过滤但详情页接口只校验了用户是否已登录没有校验这条客户记录是否属于当前用户。结果是只要知道客户 ID任何人都能打开任意客户详情。修复方案是在所有详情查询接口里增加checkDataPermission方法如果用户是管理员放行。如果是主管检查客户owner_id是否在本人管理的部门用户范围内。如果是普通销售检查customer.owner_id 当前用户ID。同时后端统一加了一层鉴权切面所有标注了RequiresDataScope的接口都会自动执行权限校验不让业务代码自己决定是否要检查——因为人总会忘但框架不会。这是我把这套逻辑沉淀成公共组件之后后端团队写任何新接口都不用再各自补权限判断安全底线才能真正稳定住。4.5 一个完整的排障案例跟进记录莫名自动消失这个问题当时困扰了我们整整两天。现象是销售在后台记录了一条重要跟进第二天打开发现记录不见了但日志里没有任何人执行删除操作。排查过程从数据库 binlog 开始。我先按时间范围翻查了follow_record表的操作记录发现那条记录的deleted_at字段被置上了时间戳说明走的是软删除。继续追踪找到了一行定时任务的日志——是公海回收任务在搞鬼。公海回收逻辑里有一段“客户回收时自动把名下未完成的跟进任务关闭”的代码本意是客户被回收后之前销售留下的“待处理跟进”要自动过期避免在待办里残留无效任务。但这段代码的过滤条件写宽了把“正常填写完成的跟进记录”也误判成了“待处理任务”全部一起关掉了。修复方案是给跟进任务增加一个独立的task_status字段从数据上区分“跟进历史”和“待办任务”两种业务不再用同一个软删除字段混着表示。这个案例给团队的教训很深刻定时任务里的条件过滤一定要先用小数据量做影子验证不能直接全量跑否则删掉的数据想恢复很难。5. 数据复盘与看板设计的实践心得5.1 转化率指标的口径冲突上面提到了商机转化率的两种口径这里再展开说说这个冲突是怎么收场的。我们做的第一版看板把“本月新增商机数”“本月赢单数”“本月输单数”“赢单金额合计”等指标都展出来了但销售总监看完后当众问了一句你们这个转化率怎么算的我上个月明明成交了 8 单为什么显示只有 5 单症结在于“上个月成交 8 单”指的是“上个月签约的商机”而看板里的“本月赢单数”统计的是“商机的赢单时间落在本月”如果其中 3 张单子的商机创建时间在上上个月那在按赢单时间过滤的统计口径下它们确实不计入“本月新增商机转化率”。这个口径差异在业务上完全合理但在产品上没人解释就会变成“系统数据不准”的负面口碑。后来我们做了两件事解决问题。第一件是给每个 KPI 卡片加上“查看口径”入口点击后弹出一段大白话说明这个数的统计规则。第二件是给销售管理层单独做了一个“商机转化明细”列表可以从看板数字直接下钻到具体商机记录让每个人都能看到这些数字是从哪些记录算出来的再也不存在“数据是黑盒”的质疑。经验CRM 报表做得再花哨如果数字口径不透明业务方就会不信任系统。每次数数据都要能找到它的来处这个信任建立起来之后后续再推数据分析功能会顺利很多。5.2 销售行为分析能发现什么数据量积累到三个月左右我们开始尝试做一些更偏向“行为分析”的看板。一开始只是想看看“谁最活跃”后来发现几个挺有意思的规律。比如把每个人“跟进次数”和“成交金额”做了散点图的对比之后能清楚看到有人的跟进次数虽然很高但成交率反而低另一个同事跟进次数少但成交单子金额都很大。这两类销售的破局方式完全不同前者需要培训沟通中如何有效推进商机后者需要的是多分配线索资源。又比如我们统计了“客户从首次创建到第一次有效跟进的间隔时间”发现超过 60 小时没有跟进的线索最终成交概率明显更低。这个结论直接推动了公海池保护期从 7 天调整到 4 天——因为 4 天还没动手跟进的线索大概率已经凉了不如早点释放给其他销售。这些分析不需要什么高级算法就是 SQL 加简单统计最重要的是先把行为数据完整记下来。这也是为什么整个项目里我始终坚持“跟进记录必须强制填写”的根本原因——没有数据积累后面的一切洞察都是空谈。5.3 后来我们还计划做什么第一版跑稳之后团队给 DeskcommCRM 列的后续方向有三个目前已经完成了一部分另外两个还在推进中。第一个是工单售后一体化。销售签单只是开始后续客户在使用产品过程中提出的问题需要一套标准化工单来承接。我们打算把客户、商机和售后工单打通销售在客户详情页就能直接看到这个客户的售后状态避免销售给老客户打电话前完全不了解近期吐槽记录导致沟通尴尬。第二个是移动端适配。销售在外面跑客户时用手机浏览器访问网页版体验确实一般。我们正在做一套 H5 的轻量版只保留客户管理、跟进记录、公海领取三个核心模块不做复杂报表先满足“在外面也能快速记一笔”这个刚需。第三个是商机预测。当数据积累到能以“月”为单位观察趋势之后尝试用简单的线性回归模型预测下个月的成交金额。这是一个实验性的方向能做成自然好做不成也不会影响核心业务。写在最后这套系统给我带来的几条经验桌面上挂着 DeskcommCRM 的开发文档经常有朋友问我这类自研系统值不值得做。以我个人实践下来的体会说几句一个 CRM 项目能不能成技术只占三成另外七成取决于业务规则是否跑通、销售团队是否愿意用、数据口径是否有人拍板。如果业务规则还在频繁变动团队内部连“什么样的客户算有效客户”都说不清楚那么不管买还是自研落地都会很痛苦。如果一个项目真的走上了自研这条路我在这次实践中最大的收获是这样的小步快跑比贪大求全重要得多。第一版只做客户、联系人、跟进、商机四张表的主链路先把高频操作做顺让销售觉得“用这个比用 Excel 爽”再去谈数据报表和效率分析系统才真的有可能活下去。至于技术层面别迷信微服务和高并发先保证数据模型扎实、权限边界清晰、索引到位、关键 SQL 别太慢这套系统已经能稳稳支撑团队跑上几年了。