自研CRM落地实录:如何把客服聊天记录变成客户资产 DeskcommCRM 从零落地记我把客服聊天记录变成了客户资产做客户管理系统这事我一开始是拒绝的。市面上现成的CRM一大堆付费的、开源的、轻量的、重型的凭什么要自己造轮子直到我们自己的业务跑起来客服工作台、客户资料、跟进记录散落在三个不同的软件里每天光切窗口就要花掉一半时间我才意识到不是工具不够好是没有一个工具能按照我们自己的销售和客服流程长出来。DeskcommCRM 这个名字拆开看就是 Desk Communication桌面端沟通。它不是什么颠覆性的产品而是一个我花了大半年时间从需求梳理到代码落地再从被业务部门吐槽到逐步优化的客户沟通与关系管理系统。这篇文章不是产品发布会纯粹是把我踩过的坑、想明白的道理、以及那些在文档里查不到的细节记录下来。如果你正准备自己动手写一套CRM或者正在为“用现成的还是自研”纠结这篇文章可以给你一个参考。如果你已经有了系统但用得很别扭这里面的很多设计思路和权限模型也许能给你一些改造的灵感。废话不多说直接开始拆解。1. 项目整体设计思路一家“小店”凭什么自研CRM说句实在话大多数团队都不该自研CRM。销售管理、客户管理、工单系统这些都是成熟得不能再成熟的领域用现成方案是性价比最高的选择。但 DeskkcommCRM 的立项动机很特殊我们要的不是一套“管理系统”而是一个和业务强绑定的“沟通中枢”。1.1 核心需求解析客服记录不是数据库里的一行字传统CRM的思路是“管理”客户信息、跟进记录、订单数据一切以“记录”为中心。但DeskcommCRM的设计初衷是“沟通”客服在桌面端和客户通过IM聊天、邮件往来、电话沟通这些对话本身就是最重要的客户资产。我们当时面临的具体问题有三个。第一客服和客户的聊天记录散落在微信、企业QQ、邮件和电话录音里想要回溯一个客户的完整服务历史需要打开四五个工具效率极低。第二销售和客服之间的信息断层严重销售好不容易约来的客户转给客服做售后时客服对前面的沟通背景一无所知客户被迫重复描述问题体验很差。第三管理层想看销售团队的客户跟进情况只能靠销售自己填写的“日报”而那些报表填得有多敷衍做过管理的都懂。所以DeskcommCRM 的第一个核心设计原则就定了一切以会话Conversation为中心客户数据从会话中自动抽取而不是靠人工录入。这一点直接决定了后续所有的数据结构设计。客户、联系人、商机、工单这些传统CRM里的“实体”在DeskcommCRM里都变成了“会话的附属聚合”。客服在聊天过程中系统后台自动把关键信息沉淀到客户档案里。1.2 技术选型与架构抉择为什么最终选了这套组合技术选型这块我踩过最大的坑就是想得太多。最开始规划的是微服务架构客户服务、聊天服务、工单服务、通知服务拆了一堆结果开发到一半发现团队成员连业务逻辑都还没完全理清光服务之间的接口联调就耗掉了大量精力。后来我痛定思痛直接把架构砍成了“模块化单体”。最终技术栈如下后端用 Java 17 Spring Boot 3.x这是我最熟悉的组合生态成熟招人容易出了问题网上一搜一大把解决方案数据库用 PostgreSQL看中的是它对 JSONB 数据类型的原生支持这为后面做动态表单和灵活字段存储提供了极大便利缓存用 Redis主要是扛聊天消息的实时推送和会话状态缓存前端用了 Vue 3 Element Plus桌面端体验和表单渲染都比较顺手。实时通讯这一块我们用了 WebSocket 长连接消息推送走 Redis Pub/Sub 做水平扩展。如果你只是个人项目或小团队内部使用完全用不上微服务一个单体应用加一个 WebSocket 服务就是最稳定的方案。我这套“模块化单体”目前扛住了几百人的内部并发非常稳定。1.3 功能模块边界划分哪些必须做哪些绝对不做自研系统最怕的就是需求蔓延。今天销售说想要一个合同管理明天财务说想要一个发票接口做着做着就变成了一锅粥。DeskcommCRM 在需求阶段就和业务部门达成了共识划定了明确的功能边界。必须做的是这四块客户信息聚合自动抽取会话中的联系方式、公司名、需求关键词、会话管理IM聊天记录全量留存、检索、标签标注、工单协同客服解决不了的问题升级为工单流转给技术部门、以及数据看板管理层看客户跟进情况不再依赖日报。绝对不做的是这些ERP相关的进销存、复杂的财务计费、以及移动端APP。移动端我们只做了一个企业微信的H5简版能看客户资料和待办事项就够了。这个边界划定救了大忙。很多自研系统的失败不是因为代码写不好而是因为目标定太大。能明确说出“我们不做哪些功能”和能说出“我们要做什么”一样重要。2. 核心功能拆解一个支撑全系统的“动态字段”引擎任何一个CRM系统数据模型都是灵魂。但业务部门的需求是永远在变的今天要在客户表单里加一个“行业类型”字段明天要在商机里加一个“预计成交金额”的币种选项如果每次改需求都要去改数据库表结构开发团队就不用干别的了。DeskcommCRM 里我实现了一套基于 PostgreSQL JSONB 的动态字段引擎这套东西是整个系统的地基。它不是最先进的技术但在实际落地过程中确实解决了我和业务部门之间 90% 的字段变更冲突。2.1 客户与联系人数据的重新建模先说最基础的客户模型。传统CRM会把客户分成“个人客户”和“企业客户”然后设计两张表再搞一个关联关系。DeskcommCRM 里没有做这么复杂我把客户Account和联系人Contact统一放在一张表里用一个type字段区分当前记录是“公司”还是“个人”。这样做的好处是客服在创建记录时不需要想“我应该建客户还是建联系人”系统根据聊天上下文自动判断。更关键的是每个记录的“扩展属性”全部放在一个 JSONB 字段里。比如企业客户的年营业额、员工规模、所属行业个人客户的生日、偏好渠道这些统统放进attributes字段里查询的时候直接attributes - industry就能取出来。实际使用中这个设计非常灵活。有一次业务部门说“我们要给企业客户加一个‘是否已经是会员’的标记”这个需求在传统结构里需要加字段、写迁移脚本、改前端表单在我这里只是往 JSONB 配置里加一条定义前端表单动态渲染一个开关按钮15分钟就完成上线。2.2 动态表单渲染数据库字段定义到页面的完整链路动态字段不只影响存储层它必须把“配置”贯穿到前端页面渲染。DeskcommCRM 里的动态表单模块核心是一张form_definition表里面存了表单名、版本号和字段 JSON 配置。字段 JSON 配置的结构大概长这样{ fields: [ { key: industry, label: 所属行业, type: select, options: [制造业, 互联网, 金融, 教育], required: false, placeholder: 请选择客户所属行业 }, { key: annual_revenue, label: 年营业额, type: number, min: 0, unit: 万元 } ] }前端页面加载动态表单时先请求对应的form_definition拿到字段配置后交给渲染组件逐条生成表单项。这样新增字段时后端不需要出任何接口前端也不需要发版只需要在管理后台里配置一下即可。这套流程上线后我们开发组基本从“改表单需求”中解放了出来。当然动态字段也不是银弹。凡是需要参与统计报表、需要建立索引、需要做复杂关联查询的字段我一定会把它抽成独立的数据库列。所有数据都塞进 JSONB 会导致查询性能断崖式下跌这个分寸一定要把握住。2.3 标签系统与自动打标让客户画像自动生长客户的标签化管理在实操中非常有用。销售看完客户聊天记录后会手动给客户打上“高意向”“暂缓”“价格敏感”等标签。这个功能不稀奇稀奇的是我做了“自动打标”规则引擎。规则很简单运营人员在后台配置关键词和条件比如聊天消息中出现“多少钱”“报价”“优惠”系统就自动给这个客户打上“询价”标签出现“合同”“盖章”自动打上“商务推进”标签。这个功能用 PostgreSQL 的存储过程加异步消息队列就能实现成本极低但效果出奇地好。自动打标上线一个月后管理层看报表的维度就完全不一样了。以前只能看“销售跟了多少客户”现在可以直接看“有多少客户被自动标记为询价但还没有销售跟进”这对于销售资源的分配是颠覆性的。技术含量不高但业务价值极高这就是典型的“小而美”需求。3. 会话驱动的客户档案聊天记录怎么变成结构化资产聊完动态字段接着聊 DeskcommCRM 最核心的特色会话驱动。传统CRM是“人录数据”DeskcommCRM 的思路是“对话即数据”。这套设计我参考了现在主流客服系统的思路但结合了内部销售流程做了不少改造。3.1 全渠道会话集成统一收件箱的构建过程客服手里现在同时开着微信、企业QQ、邮件客户端和电话耳机这是很常见的场景。DeskcommCRM做的第一件事就是把多个渠道的会话统一收拢到一个“收件箱”界面里。先说实现难点微信和QQ这样的IM渠道没有官方开放接口给第三方系统直接读写聊天记录可行路径是通过企业微信的会话存档接口或者通过 UI 自动化脚本去截获聊天窗口数据。我们最后用了企业微信的客户联系和会话内容存档接口群把外部联系人聊天记录同步到本地再通过 WebSocket 推送给客服工作台。这个过程里踩过的坑是会话存档接口有一定的调用频次限制拉取全量消息很容易触发限流。我们的解决方案是拉取任务按会话维度和时间窗口分批执行并用本地消息表做增量标记。同步进程写好之后我设置了每5秒拉取一次增量事件配合 Redis 里的游标记录位置就再也没丢过消息。3.2 从会话中自动抽取客户联系信息正则表达式与NLP的取舍会话归档之后真正辛苦的活是“从对话里找到人”。我们在消息内容上做了三层解析。第一层是正则表达式专门抓手机号、邮箱、公司名等固定格式信息。手机号的正则很简单但要注意手机号出现的上下文避免匹配到日期时间。第二层是关键词识别。比如客户说“我是XX公司的王总”系统通过“公司”和“总”这类身份词半自动定位到线索。第三层是人工确认机制系统识别出的候选信息不会直接写入客户档案而是以“待确认”的状态推送给客服客服一键确认后才会正式生效。这个“人工在环”的设计非常关键。有段时间我想直接上 NER 模型自动抽取所有实体后来发现模型推理速度、准确率和标注成本都不理想。对于中小团队来说用正则加关键词把准确率做到 80%再让客服花 5 秒钟点一下确认把准确率拉升到 99%这是性价比最高的一条路。3.3 “360度客户视图”是怎么拼出来的当自动抽取的数据沉淀下来后客户详情页就是整套系统的集大成者。DeskcommCRM 的客户详情页有五个 Tab基本信息、沟通记录、商机阶段、工单列表、操作日志。基本信息页的数据来自客户表 动态字段 JSONB。沟通记录页按时间线展示所有渠道的聊天记录支持全文检索和关键词高亮。商机阶段页展示该客户关联的销售管道从初步接触到成交或流失的每个节点变更都有记录。操作日志页是审计追踪谁在什么时候改了什么字段全量留痕。这个详情页的开发量不大但对用户体验的提升立竿见影。销售打开客户页面5秒钟就能了解过去和这个客户的所有触点接触情况。以前那种“客户重复讲需求、销售一脸懵”的尴尬场景基本绝迹了。我有一条很深的感触客户档案的价值不是录入进去的而是沉淀出来的。每一次会话、每一封邮件、每一次电话系统自动把它们变成时间线里的一条数据客户画像就有血有肉了。4. 权限体系与数据隔离多部门协同的命门所在CRM 系统里藏着一个几乎百分之百会被忽略、但架构设计时必须优先考虑的问题谁能看哪些数据这不仅是安全问题更是销售团队是否愿意持续录入数据的前提。如果销售发现自己辛辛苦苦跟进的客户被别人随意查看甚至抢走整个系统距离被弃用就不远了。4.1 RBAC 之外还需要“数据范围”控制DeskcommCRM 的认证授权用的是行业标准 Spring Security 加 JWT角色权限模型是经典的 RBAC。但只做到这一层是不够的RBAC 控制的是“你能做哪些操作”控制不了“你能看哪些数据”。所以我又加了一层数据权限维度。实现方案是这样的每个客户记录上有一个owner_id所有者、一个team_id所属团队、以及一串share_ids共享用户ID列表。查询时系统根据当前登录用户动态追加过滤条件// 数据权限过滤核心逻辑 Long userId SecurityUtils.getCurrentUserId(); String condition owner_id ? OR team_id IN (SELECT team_id FROM sys_user_team WHERE user_id ?) OR ? ANY(share_ids) ;这个 SQL 条件看着简单却是我在自研 CRM 里做过最正确的一个决策。它天然地实现了“销售只看得到自己的客户销售主管能看全团队的客户公司管理层能通过特殊角色看全量数据”的数据隔离层级。这里还有一个小细节为了让share_ids查得动我在 PostgreSQL 里给这张表的share_ids字段加了 GIN 索引否则到后期数据量大了 ANY(share_ids)的查询会变成全表扫描。4.2 字段级权限动态表单的“隐形”控制在我们实际业务中即便是同一个页面的信息不同角色能看到的字段也可能不同。比如客服人员能看到客户的技术对接人和联系方式但看不到“预计成交金额”这类敏感商务信息。这个需求如果硬编码在页面里后期维护会非常痛苦。DeskcommCRM 的字段级权限是和动态表单定义结合设计的。每个表单字段配置里有一个permission属性{ key: deal_amount, label: 预计成交金额, type: number, permission: { view: [admin, sales_manager], edit: [admin] } }前端渲染表单时先拉取当前用户的角色再根据字段的view权限控制是否渲染根据edit权限控制是否可编辑。后端在返回客户查询接口时同样会根据当前用户角色动态剔除无权限的字段数据。这是一个“双保险”即使有人绕过前端直接调接口也拿不到敏感数据。4.3 手工指定共享与“保护期”机制技术在系统里解决了不少问题但解决不了“人情”问题。销售和销售之间对客户归属的争议是销售管理里常见的问题。有一段时期A销售刚跟进的一个客户被B销售通过共享链接看到了直接打电话过去报价差点把单子搞黄了。为了解决这个纠纷我设计了一套客户“保护期”机制。当一个客户被创建或分配后默认进入 30 天的保护期。保护期内只有所有者本人和上级主管可以查看和编辑。30天到期后如果客户处于“未成交”状态系统自动向全团队开放“只读”权限这个客户可以被其他销售申请转移跟进。这个机制从技术上实现不复杂就是在客户查询条件里根据 protected_until 时间戳动态加过滤但业务价值非常大。它保证了销售在跟单初期有安全感和私密感又防止了长期“占着茅坑不拉屎”的资源浪费。上线后因为客户归属产生的投诉直接降了八成。5. 自动化工作流引擎把销售流程中“催”的活儿交给系统CRM 系统只做到“记录”和“查看”价值就损失了一半。真正让业务运转起来的是“自动化”给未跟进的客户自动发提醒给长时间没动态的商机自动报告主管给超时未处理的工单自动升级。这些功能在成熟CRM里叫“工作流”在 DeskcommCRM 里我实现了一套轻量级的规则引擎。5.1 基于触发器的规则引擎设计设计初衷是不想引入现成的流程引擎因为那个太重了学习成本高、部署复杂。我的做法是建立一张automation_rule规则表里面定义事件类型、触发条件、执行动作和启用状态。事件类型目前有客户创建、客户字段变更、会话结束、工单状态变更、任务超时。触发条件用 JSON 格式存储类似第二章的动态字段配置。执行动作有三种发送站内通知、发送企业微信消息、自动修改字段值。举个例子当客服把一个会话结束时打了“售后问题未解决”的标签系统自动生成一条工单工单的priority字段设置为“高”同时给技术部门的在企业微信值班群里推送一条消息。这一整套动作通过配置规则即可实现不需要写一行代码。5.2 定时任务与超时提醒的实现细节超时提醒是自动化工作流里最常用、也最容易“翻车”的部分。翻车的原因通常不是“没有提醒”而是“提醒太多次”。如果把所有超时客户都推送一遍员工手机一天能收到上百条消息后面大家干脆不看消息了失去了提醒的意义。DeskcommCRM 的超时提醒做了“分层升级”策略客户超过2天没有跟进记录系统提醒销售本人超过5天没有跟进系统直接提醒销售主管超过10天没有跟进系统把客户状态自动标记为“疑似流失”进入团队共享池。这套策略用定时任务Scheduled每半小时跑一次扫描customers表的last_follow_up_at字段和当前时间做差按上述阈值触发不同动作。为了防止同一个客户在同一阶段被重复提醒我在触发动作时都会检查该客户当前follow_up_status状态值处理完才更新。这个小细节让整个提醒体系的体验好了非常多。5.3 无代码规划让业务的人自己改规则很多工作流实际上是业务部门自己的个性化需求比如运营希望每周一上午收到上周新增客户的汇总报表。这需求本身不难但如果每次都让开发写定时任务开发team肯定要累死。于是我把上节提到的规则引擎做成了可视化配置页面让运营和销售主管可以在后台选择事件类型、填条件、选动作。这个配置页面上线前我内心挺忐忑的怕业务同事不会用。结果现实是给运营做了两次培训后他们已经能自己配置简单的提醒规则了。当然复杂的规则他们还是不敢动但至少把80%的零散小需求消化在了业务侧。这也让我看到了一个趋势系统价值的最大化不是把所有需求都实现完而是把实现需求的能力交给业务。6. 常见问题与排查实录这半年我最想删掉的代码系统上线半年运维群里来来去去也积累了不少问题。这里挑几个最有代表性的一个是并发问题一个是数据一致性问题还有一个是消息推送的可靠性问题。这些坑基本属于那种“不在生产环境跑上几个月你根本不会意识到问题存在”的类型。6.1 重复创建客户的并发陷阱有一次两个客服同时和一个客户在不同渠道聊天系统自动抽取客户信息时因为主流程里“先查数据库判断是否存在不存在则插入”的逻辑存在问题同一个客户被创建了两条记录导致后面商机归属乱成一团。这个问题的根子是并发下的竞态条件。一个请求查到“不存在”还没来得及插入另一个请求也查到“不存在”两边同时插入就产生了重复数据。解决方案其实很经典在数据库层面给客户的手机号字段加了唯一索引并对可能的重复请求做了预合并。插入前先尝试捕获唯一索引冲突的异常冲突时再去查已有记录进行关联。生产环境数据量一大“判断后插入”这种模式千万别裸用。6.2 消息推送偶尔丢失WebSocket的隐性坑WebSocket 短连接断线重连时如果客户端恰好正在发送消息服务端可能会因为连接已经断开而把消息丢掉。我们线上出现过一个诡异的现象客服偶尔收不到某条新的客户消息刷新页面后又出现了客户那边看到的却是“已读”状态体验非常糟糕。排查思路是先看消息队列再看消费者日志最后发现 WebSocket 会话对象在服务端已经失效但客户端的连接状态还没感知到。修复方案是做了一个“消息幂等补偿”客户端每次连接成功后主动向服务端拉取最近5分钟的全量增量消息按消息ID去重后合并展示。这样即使实时推送有漏网之鱼也能在秒级被补偿机制拉平。6.3 数据同步性能优化从秒级响应到毫秒级还有一个优化过程值得分享一下。客户列表页一开始查询很慢随着会话数据和客户数据不断增加一个简单的分页接口平均响应时间超过了 3 秒。用EXPLAIN ANALYZE一分析发现customers表和messages表做关联查询时触发了大量顺序扫描。优化的第一步是给外键加了索引。第二步是把“最近一条沟通记录”从原来的子查询方式改成了冗余字段方式即在customers表里加了last_message_at和last_message_preview两个冗余字段在消息写入时同步更新。这样一来客户列表页完全不用关联会话表响应时间直线降到了 150 毫秒以内。在中小型项目里很多时候性能优化不是靠引入组件而是靠改变数据查询方式。7. 一次真实的数据迁移经历从Excel表格到系统档案这套系统真正打动业务团队是在一次大规模数据迁移之后。公司之前用一堆Excel表格管理客户每个销售维护一份私人的“客户台账”字段五花八门格式千奇百怪。公司决定把所有这些历史数据归拢到 DeskcommCRM 时我才发现最难的往往不是“写代码”而是“清洗数据”和“争议取证”。7.1 数据清洗的关键两步手机号规整和客户合并Excel表格里有大量的“脏数据”手机号有的带横杠有的带空格有的缺了一位同一个客户在两张表里被写成了“XX科技有限公司”和“XX科技公司”两个不同的名称还有的客户被两个销售同时存到了各自的表格里都不知道该归给谁。清洗流程是第一步写一个Python脚本读取所有Excel文件把手机号统一格式化成11位数字无法规整的标记为“疑似无效”列入人工复核清单。第二步基于企业名称做模糊匹配用 Python 的fuzzywuzzy库把相似度高于 89 的记录找出来再由运营逐个审核确认是否合并。第三步疑似重复的客户被合并时所有关联的跟进记录和聊天记录都保留只是拥有者归到系统指定的唯一账号下。合并过程全部写入审计日志避免后续纠纷。7.2 去重背后的业务博弈一次合理的合并需要什么合并方案看起来顺理成章但在具体执行时业务部门吵成一团。原因很简单合并后客户归谁直接影响到销售的绩效考核。多个销售都声称这个客户是自己长期跟进的但系统里并没有历史数据可以做支撑。我的处理原则是“证据优先”谁最早发起会话、谁的历史跟进记录最多客户就默认归属谁。如果有争议由销售主管人工裁定但裁定依据必须依据系统里现有的会话和工单记录。整个迁移过程持续了两周前三天是技术和数据清洗剩下大部分时间都是在做沟通和确认。但这次迁移之后客户主数据基本统一销售团队慢慢养成了“有客户先上系统查一下”的习惯真正把系统用了起来。8. 写在最后一些关于构建自家CRM的切身感受项目走到这里回头再看技术上最复杂的其实不是某一个算法或者高并发方案而是如何在“标准化”和“灵活化”之间找到平衡点。CRM 这东西看似是个软件系统本质上是一套管理思想和管理流程的载体。如果流程本身是混乱的系统再强大也只会放大混乱。DeskcommCRM 让我学到的最重要的事情是给业务部门用的系统一定要让他们在使用中觉得“省事”而不是“费事”。所有的信息自动留存、动态字段免改代码、提醒消息分层升级这些设计的核心目的只有一个让正确的人在正确的时机看到正确的客户信息。如果你也想从零做一个类似的系统我建议从小处着手。先别想着一步到位先把“统一会话入口”和“动态客户档案”这两个基本功做扎实再逐步叠加工作流和自动化。权限模型和数据归属规则务必放在系统设计的最前期否则后续改动成本高到你无法想象。这套系统还有很多不完美的地方比如移动端的体验就一直被吐槽自动化规则的可视化编辑器也做得比较粗糙。但至少它解决了一个实际问题我们的客服和销售每天不再需要为了找一个客户的历史信息而在多个软件之间来回切换了。在我看来一个工具能做到这一点就已经值回了它被开发出来的理由。