
DeskcommCRM这个名字是我们内部对一个客户管理系统的代称。做成它之前销售团队手里的客户信息散落在Excel、微信聊天记录和个人笔记本里撞单、漏跟、离职带走客户三件事轮番上演。后来换了市面上主流的CRM录数据的人却越来越少原因很简单系统里的流程是厂商定义的跟我们的签单节奏根本对不上。这促使我决定自己动手把DeskcommCRM做成一套贴合销售、客服、管理三条线的客户运营系统。这篇文章把从需求梳理、数据建模、权限设计到对接第三方系统、上线踩坑的过程完整写一遍适合正在做CRM、工单系统或销售中台的同学参考也适合准备自研内部系统但又担心工期失控的团队。1. 为什么自研DeskcommCRM现成方案解决不了的那几件小事很多人听到“自研CRM”第一反应就是“重复造轮子”。我一开始也这么想毕竟市面上的客户管理工具已经很成熟从线索到回款都有现成方案。但真正深入业务之后会发现一个公司内部的客户管理逻辑往往隐藏着大量只有自家团队才懂的细节这些细节才是决定系统能不能用起来的生死线。1.1 线索分配乱销售觉得“这单不该我管”最典型的是线索分配。过去我们把市场部收集来的线索用Excel发到群里谁先回复谁跟进。表面上是公平竞争实际上一单往往被两三个人同时跟客户被反复打扰最后不管成交在谁名下另外一位就觉得“这明明是我的客户”。团队内部为了归属问题吵架严重消耗信任。这个问题的本质是“客户所有权”不明确。DeskcommCRM做的第一件事就是给每条线索定义一个明确的负责人并且把负责人变更做成了只有主管才能操作的流程。谁录入、谁认领、谁转移每一步都有时间戳和操作人。上线当天两个销售为一个客户来争论的场面真的消失了——不是人变了是规则终于落到系统里了。1.2 厂商系统里的流程跟我们的签单节奏对不上用现成CRM时我们发现一个很尴尬的情况系统自带的销售阶段只有“初步接触、需求挖掘、方案报价、谈判、赢单”但我们的业务里还有“客户已立项、等待预算审批、客户内部试用中”这些状态。厂商标准字段不能改我们就只能把这些信息写在备注里时间一长报表统计出来的转化率根本没参考价值。这个问题靠配置是解决不了的。客户生命周期、阶段推进规则、每个阶段要完成的关键动作这些必须由公司内部梳理清楚再固化到系统里。自研最大的价值不是代码写得多漂亮而是你可以完全按自己的业务场景建模而不是反过来迁就一套通用模板。DeskcommCRM的销售阶段是我们走访了三条业务线负责人以后提炼出来的每个阶段还配了“需要上传什么材料”把销售动作标准化了。1.3 自研的边界只做核心链路不碰通用能力我不是那种“什么都要自己写”的技术派。DeskcommCRM从一开始就划清楚了一条线客户数据、跟进记录、商机流程、权限规则、业绩看板必须自己建模但文件存储、消息推送、短信发送、OCR识别这些成熟能力直接用云服务和第三方组件绝不重复实现。定下这个边界有几个好处开发周期可控出问题的面小团队只需要聚焦在“客户信息和销售流程”这一个核心主线上不会陷入通用组件的泥潭。这也是项目能在三个月内从零到上线、而不是无限延期的关键原因。2. 核心领域模型客户、联系人、商机、工单怎么串起来CRM系统做得好不好第一步看数据模型。很多团队失败不是前端不好看而是字段关系从一开始就是乱的把公司名称写在联系人的姓名栏里把确定要买的客户和只是留过资料的客户放在同一个列表里最后查出来全是脏数据。2.1 Account与Contact为什么要拆开DeskcommCRM第一个模型设计原则就是把“客户”和“联系人”彻底拆成两张表。客户(Account)代表一个企业属性是公司名称、行业、规模、客户等级联系人(Contact)代表企业里的具体个人属性是姓名、职位、电话、微信、决策链角色。一个客户下面可以有多个联系人联系人可以归属不同客户但一对一场景居多。为什么要这样拆销售业务里最典型的情况是你今天跟金华某制造企业的采购经理聊完产品明天他们的技术总监也来咨询如果不能在一个客户档案下挂多个联系人你就只能新录一条客户记录第二天看列表的时候还以为这家公司有两个同名客户客户数据越滚越乱。把Account和Contact分开之后企业维度能够独立统计客户数、客户等级、行业分布个人维度能够记录每一段对话细节。这种主数据拆分是整个系统不乱的地基。2.2 商机与工单销售和售后共用一份底账商机(Opportunity)是销售链路的核心。DeskcommCRM里每条商机都关联一个Account标明了产品、预计金额、签约概率、阶段、预计结单时间和所有者。商机阶段是公司业务团队一起梳理出来的五个状态每个状态流转都要留记录方便后面算漏斗转化率。工单(Ticket)则是售后团队使用的模型。客户用产品出问题后售后同事要把问题登记成工单记录影响范围、状态、处理人和解决方案。设计最初我们差点把工单做成独立系统后来发现这会让“客户底账”断裂——销售只看到客户买过什么看不到客户用过产品之后遇到了什么。最后DeskcommCRM把工单也挂在Account下面销售打开客户详情页第一屏能看到客户的基本信息、联系人、进行中的商机、历史订单和未关闭的工单。这个页面就是客户完整的生命周期视图做续费和增购的时候能少打很多瞎猫撞死耗子的电话。2.3 Activity时间线用一张事件流还原客户旅程一个客户从线索变成付款中间会产生大量沟通记录电话打了半小时微信里聊过报价客户来看过系统演示现场培训做了两场这些信息如果分散在不同的模块里销售换个人跟进就完全断片。DeskcommCRM用了一张Activity表来记录这些动态每条记录都有类型电话、拜访、邮件、微信沟通、系统演示、报价有开始时间、持续时长、关联联系人和内容纪要。这里有个很值得细讲的设计我们一开始想给每种活动建一张单独的明细表比如电话记录表、拜访记录表、邮件记录表但这种设计让前端列表查询变得非常痛苦——要看一个客户的完整动态得把五张表拼在一起。后来我们回归到最简单的一张事件表多出来的属性统一用JSON存在扩展字段里。查询一个客户的动态只需要一次索引扫描排序和分页都简单。这个取舍在数据量没有大到爆发之前是杠杆率最高的设计。模型骨架大致长这样class Account(models.Model): name models.CharField(max_length255, db_indexTrue) industry models.CharField(max_length100, blankTrue) level models.CharField(max_length20, choicesLEVEL_CHOICES, defaultB) owner models.ForeignKey(User, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) class Contact(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, related_namecontacts) name models.CharField(max_length100) title models.CharField(max_length100, blankTrue) phone models.CharField(max_length30, blankTrue) class Opportunity(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, related_nameopportunities) product models.CharField(max_length100) amount models.DecimalField(max_digits12, decimal_places2) stage models.CharField(max_length20, choicesSTAGE_CHOICES) owner models.ForeignKey(User, on_deletemodels.PROTECT) class Ticket(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, related_nametickets) title models.CharField(max_length255) status models.CharField(max_length20, choicesTICKET_STATUS) assignee models.ForeignKey(User, on_deletemodels.PROTECT, nullTrue) class Activity(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, related_nameactivities) contact models.ForeignKey(Contact, on_deletemodels.SET_NULL, nullTrue, blankTrue) type models.CharField(max_length30) content models.TextField() occurred_at models.DateTimeField(db_indexTrue)这套模型跑了一年多没有大面积改动过。核心原因就是表的职责划分清楚后续需求大多是在扩展字段和状态枚举上做文章而不是推翻关系。3. 权限模型与数据隔离让销售看到该看的又不把底牌全亮出来客户数据是公司最敏感的资产权限设计做不好轻则员工互抢客户重则核心数据被轻易导出。DeskcommCRM的权限体系分成两部分一是功能权限什么角色能进什么菜单这部分用标准的RBAC就能解决二是数据范围同一个角色能看哪些客户、哪些商机这才是真正考验设计功力的地方。3.1 数据范围不能只看角色要看归属和协作关系我们的数据范围规则刚开始只有三档本人只能看到自己名下或自己创建的客户、商机、工单。全部管理员和部门主管可以看组织内的所有数据。部门部门主管能看本部门所有人的数据。这套规则上线后很快暴露问题大客户的成交周期长需要销售经理和售前顾问一起参与客户负责人虽然是销售A但A的经理接手谈判后临时需要在商机上帮A修改报价方案系统却因为“数据范围本人”把经理挡在门外只能让A改完再拍照发到群里。这个问题很实际数据隔离既要防撞单又不能把协作链路上的人全部隔绝。3.2 共享规则和团队协作不是越开放越好后续我们补了一个“共享规则”机制客户负责人可以把自己名下的某一个客户或某一批商机共享给指定同事或指定团队共享权限分为只读、可编辑和可转移三级。比如招标项目里销售可以把技术负责人加为商机的协办人协办人只能看方案相关的字段不能修改合同金额项目验收阶段销售再把售后主管加进客户详情方便直接开工单。这里要特别提醒一个经验共享规则的默认值必须保守。我们最早把共享的默认权限设为“可编辑”结果同事之间互相乱改客户备注不到两周就出现了把客户等级从A改成C的情况。后来改成默认只读、按需提升编辑权限这类问题才消停下来。权限规则宁可一开始紧一点再用运营手段放宽也不要先松了再收收紧的时候一定会得罪人。3.3 敏感字段和操作审计每次导出都要有痕迹除了范围控制敏感字段还需要单独保护。客户的联系电话、微信、地址这些字段默认对协作同事脱敏展示只有客户负责人和直属主管能看到完整信息。售后要是需要通过电话联系客户系统里可以拨号但看不到完整号码号码在界面上自动打星号。另一个容易被忽略的点是“导出审计”。用一个Excel导出全部客户资料这个操作如果无痕那前面所有权限控制都形同虚设。DeskcommCRM把导出行为全部落审计日志记录谁在什么时间导出了哪一批客户并且在管理后台做了图表化展示。有一次我们发现有位同事一夜之间导出了3000名客户审计日志帮我们及时发现了这个异常避免了一次大面积数据外泄。做CRM系统千万别把防数据泄露这件事想得太复杂但一定要把审计日志做全。4. 与ERP、企业IM、外呼系统对接接口稳定性的几个工程细节光有CRM内部系统还不够DeskcommCRM要真正成为业务中台必须跟周边的ERP、企业IM、外呼系统打通。这一步踩过的坑比自研核心功能还要多。最大的教训是对接外部系统方案设计上要默认“对方随时可能挂”。4.1 用事件消息而不是同步调用第一版同步客户数据时我们直接在保存客户后调用ERP接口把客户档案推过去。线上运行一个月问题很快就来了ERP系统每个季度末做大促数据库压力大接口响应经常超过30秒CRM这里保存一个客户要等半分钟销售体验极差更糟糕的是ERP偶尔返回超时但其实已经落库CRM这边却报错销售以为没同步成功又点了一次结果ERP里出现两条重复数据。后来的改造思路很简单也很有效CRM先写自己的库提交成功后把“客户已创建”这个事件写到Redis队列或RocketMQ里再由一个独立的同步任务去消费事件、调用ERP接口。CRM的接口响应时间从500毫秒下降到80毫秒ERP慢不慢不再影响销售操作。外部系统调用失败时任务会进入重试队列等系统恢复后它自然会重新执行。4.2 接口幂等与重试一次发货不能重复同步重试机制的前提是接口必须支持幂等否则重试就是灾难。举个例子我们在对接ERP订单同步时最开始用“创建订单”接口没有传业务单号做唯一标识。结果网络抖动导致任务重试用户被扣了两次款虽然最后把钱退了但这提醒我们必须把幂等设计放在对接第一位。现在的做法是每个外部接口的请求都带一个全局唯一的业务ID比如CRM生成的订单号或客户UUIDERP收到请求后先用这个ID查重如果已经处理过就直接返回成功不再重复创建。同时我们在本地也维护了一张“外部同步记录表”记录每次请求的业务类型、目标系统、业务ID、状态和返回结果。排查问题的时候这张表基本上是救命稻草。4.3 回调地址安全验证数字签名不能省DeskcommCRM和企业IM工具对接时需要接收IM服务端推送的消息回调。最初我们为了图省事回调接口只有路径级别的token校验没有对请求体做签名验证。后来有一次我们在后台日志里看到大量伪造的推送数据虽然业务逻辑没跑坏但这个风险非常大——恶意构造的消息可以直接触发系统里的工单自动创建相当于给任何人开了一个创建工单的后门。正确做法是外部平台推送的数据必须包含按约定规则生成的签名我们收到后重新对关键参数签名并比对不一致就拒绝处置。同时回调接口要做频率限制同一事件源每分钟最多处理N次超过就报警。这块内容虽然不复杂但直接影响系统安全边界没有任何偷懒余地。4.4 对接失败后的数据修补流程即使做了重试和幂等还是会有极端情况消息队列本身出了问题、网络连续几天被防火墙拦截、ERP改了字段枚举导致解析报错。DeskcommCRM专门做了一个“同步任务中心”页面运营和管理员可以看到每一类同步任务的积压量、失败原因和最后一次成功时间并支持手动触发单条补传。这个落地页看起来不起眼但在系统对接初期几乎是每天都要用的它是整个同步方案的兜底。5. 销售漏斗与业绩看板指标口径比图表美观更重要CRM到了中后期大家最关心的只有一个问题这个月业绩到底怎么样销售漏斗和业绩看板是DeskcommCRM里使用频率最高的页面。但做这块的时候我最大的体会是技术不是难点难的是把指标口径跟管理层对齐。5.1 指标口径成交归属认哪个时间点“本月成交金额”这句话至少有两种理解一是本月创建的商机中最终成交的金额二是本月实际收到回款的金额。这两者差了可能几个月的周期。如果我们不在系统里定义清楚销售看板和管理层月度经营会的数字就对不上复盘会议开成了数字辩论赛。DeskcommCRM里的指标口径我们按业务阶段拆成两类指标口径说明数据来源签约金额商机状态变为“赢单”当日的预计金额商机表回款金额订单触发回款事件当日的实际到账金额回款记录表新增客户数首次创建Account的日期归属统计客户表跟进次数当日产生的所有Activity数量活动表5.2 阶段变更历史漏斗转化率不能只看当前阶段漏斗图要反映的是“每个阶段到下一个阶段的转化率”。如果只看商机表最终的当前阶段就永远看不到那些已经在“方案报价”阶段流失、最后被标记为“赢单失败”的商机漏斗就变成了幸存者偏差。解决方法是加一张“商机阶段变更历史表”每次阶段变更都插入一条记录包含商机ID、原阶段、现阶段、变更时间、变更人。计算转化率时要按人、按团队、按时间段对这张历史表做统计统计“有多少商机进入过阶段A并且之后某个时间进入过阶段B”。有了这张表不光能算转化率还能知道每个阶段的平均停留时间——如果客户在“方案报价”阶段停留超过20天大概率是方案价格没对齐销售应该尽早介入而不是一直等。5.3 图表选型组件库省时间但大屏还是要自己调图表这块我建议不要一上来就自研。DeskcommCRM的第一版数字看板换过两套方案第一次用的是某开源BI系统接入我们自己的数据库结果权限体系跟CRM完全割裂每个销售都能在BI里自主查询全公司的客户明细风险很高后来我们换成了前端图表组件库直接读CRM内置聚合接口权限和业务逻辑都在服务端统一控制虽然开发量大一些但数据安全始终掌握在自己手里。组件库选型上我只说一个心得不要被好看的大屏模板带偏实际使用中大家看得最多的还是列表型月度业绩排名、漏斗图和折线趋势图。先跑通这三个核心图表再慢慢补其他分析维度。5.4 聚合查询优化从2秒到80毫秒的索引调整数据量上来之后看板接口开始变慢。最初客户表到了20万条商机记录表到了100多万条按团队维度统计月度业绩时一条SQL要扫全表经常超过2秒。一次老板在例会上现场刷新页面转了快3秒这个尴尬我记了很久。排查下来问题出在两处一是统计SQL里用到了多个JOIN关联字段没有全部建索引二是时间范围过滤用了created_at字段但创建时间和业务归属时间在部分历史数据里不一致统计结果也有偏差。解决方案是先修正了“业绩归属日期”的时间戳字段确保查询和统计使用同一个时间源然后对商机表的“负责人阶段业绩归属日期”建了联合索引。调整之后最核心的月度业绩接口稳定在80毫秒以内。这里想提醒大家CRM看板性能优化永远要先找慢SQL再建索引不要盲目换数据库。6. 上线前后的真实踩坑清单Excel迁移、时区、软删除和并发更新最后这部分挑几个上线前后真实踩过的坑写出来每一个都是花过加班时间换来的经验。6.1 从Excel迁移客户数据差点把编码搞崩上线前最痛苦的一件事是从各个销售的Excel里把历史客户数据导进DeskcommCRM。我们统一格式后用了Python脚本批量导入第一个版本跑完发现大量客户名称变成乱码尤其是中文和特殊符号。排查到最后的原因很简单销售的Excel文件有部分是xlsx有部分是老的xls编码格式还混着GBK和UTF-8导入脚本没有统一处理编码。我的建议是任何历史数据迁移刚开始不要追求一次性导入全量。先拿一个团队的20条真实客户数据做完整性验证把字段映射、编码、去重规则全部跑通再分批次导入。单批次不要超过5000条每批次记录成功和失败明细失败数据单独生成报告方便业务同事手工修正。直接一把梭导10万条数据的项目我见过太多最后导出来的数据连自己的销售都不敢认。6.2 时区问题销售记的“今天”和报表统计的“今天”不是同一天DeskcommCRM服务端存的标准UTC时间前端展示转成了北京时间。一开始没觉得这个设计有什么问题直到上线第二周销售反馈说“昨天晚上21点打的跟进电话在活动列表里变成了第二天”。原因出在我们统计“今日跟进”的时候直接用数据库时间字段和服务器本地时间比较而服务器时区配置的是UTC晚上21点是UTC当天的13点统计归到前一天去了。最后统一了规范服务端所有时间字段存UTC但业务统计时一律在应用层把UTC先转成北京时间再计算日期边界统计SQL里不要依赖数据库时区。这个坑排查过程不难但它说明CRM这种业务系统时间口径不统一会造成用户对系统极不信任一定要在一开始就把时间边界定义清楚。6.3 软删除与唯一约束删除客户的时候怎么处理手机号销售在CRM里删除客户我们默认不会物理删除而是一张is_deleted标记置为True。但客户的手机号在表里是唯一索引销售A删除了一条客户记录第二天销售B想新增同一个客户时系统提示“客户已存在”因为那条软删除记录还占着手机号唯一索引的位置导致新客户加不进来。这个问题的处理方式有两种一种是把唯一索引改成“手机号is_deleted”的联合唯一索引但软删除记录只能存在一条同一个手机号被不同销售删除两次还是冲突另一种是数据删除时直接把核心字段做脱敏处理比如手机号后四位改成随机数这样软删除记录不再具备业务标识新的客户可以正常创建。我们最终用的是第二种方案因为在真实业务里一个删除掉的客户不应该再占用资源也不应该再被查出来成为脏数据。6.4 并发更新覆盖乐观锁版本号保护商机金额最后一个坑是真金白银的教训。有两个销售负责同一个大客户市场部在后台批量更新客户行业标签销售在编辑商机金额两边同时把商机记录读进内存销售改完保存市场部也保存了最后数据库里留下的是市场部写回去的那条旧数据销售改好的商机金额被覆盖。后来我们在商机表里加了version字段更新商机时先检查当前数据库的version和请求传入的version是否一致不一致就返回“该记录已在其他页面被修改请刷新后重试”一致则把version加1再提交。这段逻辑看起来是常规操作但它在CRM这类多人高频协作场景里极其重要。没有乐观锁之前销售在群里投诉“我明明改了金额怎么又变回去了”的工单每个月至少有十几条加上版本号控制之后这类问题基本归零。最后再分享一个真实体会做CRM系统最难的不是技术选型也不是某个字段怎么设计而是让销售愿意每天都打开它。DeskcommCRM上线第一个月登录率只有六成后来我们把“强制录入”改成了“顺手记录”又把每个销售最关心的今日待办放到首页系统使用率才慢慢起来。系统做得再复杂回到原点还是那句话它是帮人干活儿的工具不是考核人的KPI。这一点想明白了很多设计上的纠结自然就解开了。