拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零自建通信型CRM:客户管理、呼叫中心与坐席工作台一体化设计实战

从零自建通信型CRM:客户管理、呼叫中心与坐席工作台一体化设计实战 1. 项目定位与整体设计思路DeskcommCRM 这个名字拆开看很有意思Desk 代表桌面坐席Comm 代表通信CRM 则是客户关系管理。说白了这不是一个传统意义上的“纯 CRM”而是一个把沟通入口和客户数据放进同一个工作台的融合型系统。当时做这个项目的直接动因是团队里客服和销售用的工具太割裂了——客户档案在 CRM 里通话记录在呼叫中心系统里线上咨询的聊天记录又散落在 IM 工具里。客户从电话进线聊到一半坐席要切三四个窗口才能把信息凑齐效率低不说还经常出现“上一个坐席答应客户的事下一个坐席完全不知道”的情况。DeskcommCRM 要解决的就是这一整条线的问题电话、在线消息、邮件、工单、客户档案、跟进计划全部收拢到一个界面里。坐席接起一通电话的瞬间右边自动弹出这个客户的历史记录、会员等级、未完成工单客户在网页上发起咨询系统自动识别他的身份把上一次会话的上下文带出来。这个项目适合谁参考一类是有自建客户服务或销售管理系统需求的团队另一类是想把呼叫中心和 CRM 打通的开发者。如果只是需要一套标准 SaaS CRM那直接买现成的可能更划算但如果你需要深度定制、数据完全私有化或者需要跟内部业务系统对接这套自建思路就有很强的参考价值。1.1 项目背景与核心需求拆解先说说当时为什么没有直接采购市面上的成熟产品。其实方案评审会上我们对比过三四家最终劝退的原因主要有三个第一通信能力深度绑定问题市面上大部分 CRM 自带的是标准呼叫中心接口但我们的业务涉及大量外呼任务、号码隐藏、录音质检很多定制需求在标准产品里根本做不了第二客户数据的归属规则非常特殊我们的客户按区域、按等级、按来源分属不同团队坐席权限要精细到“只能看到自己负责的客户”很多 SaaS 产品的权限模型做不到这个粒度第三数据必须私有化部署客户信息和通话记录属于核心资产老板明确要求全部数据必须放在自己机房里。基于这些约束我们把核心需求拆成了四条统一客户视图只管客户不管客户从哪个渠道来电话、网页、小程序进来的联系记录都能自动挂到同一份客户档案下。通信能力内嵌至少要做到软电话WebRTC 拨号、接听、转接、静音和在线聊天网页 IM两种通信方式的深度集成通话录音和聊天记录自动归档。精细化权限与归属数据归属规则可配置支持按坐席、按团队、按数据范围控制可见性客户转移要有完整的操作审计。可量化运营数据每个坐席的接听量、会话量、响应时长、工单解决率管理层能看到实时看板坐席自己也能看到个人绩效。需求定下来之后架构选型就变得很明确。技术栈最终确定为前端 Vue 3 Element Plus后端 Java Spring Boot单体应用但模块化包结构数据库 MySQL 8.0 存业务数据Redis 存会话状态和路由信息通信用 WebSocket 做消息推送呼叫这块采用接入 SIP 软交换的方案前端通过 WebRTC 拨打电话。没有一开始就上微服务原因是团队规模不大业务量预估在千级坐席以内单体架构加上合理的模块拆分开发和运维成本都要小得多出了问题也更好排查。1.2 功能模块划分与边界整体功能模块按“核心流程”和“支撑能力”两个维度来划分。核心流程模块负责打通业务闭环支撑能力模块负责提供通用服务。模块核心功能技术要点客户管理客户档案、联系人、标签、等级、归属关系客户主数据模型设计沟通中心软电话、在线 IM、消息记录、通话录音WebSocket 实时推送、SIP 集成工单中心工单流转、SLA 计时、分配策略状态机建模坐席工作台待办列表、会话面板、快捷回复、知识库前端交互性能优化数据看板坐席绩效、客户转化、服务时效聚合统计 SQL系统管理组织架构、角色权限、数据字典RBAC 权限模型划清模块边界有一个很实际的好处排期可以并行。客户管理和系统管理是地基先做沟通中心是整个系统最复杂的部分投入最多的人力坐席工作台和数据看板依赖前面几个模块的数据放到第二期。当时我们用了一个最朴素的排期方式——先画出核心流程的端到端链路图也就是“客户来电 - 系统识别客户 - 坐席接听 - 记录跟进 - 创建工单 - 客户回访”然后按这条链路来安排开发顺序保证每一步的前置数据都是齐的。2. 核心模块设计与关键技术点这个部分聊聊系统里最核心的几个模块是怎么设计的以及设计背后的考量。我挑几个当时花时间最多、踩坑最多的点来讲包括客户模型怎么建、通信怎么集成、工作台交互怎么设计以及权限模型怎么落地。2.1 客户模型的建立这不是建一张客户表那么简单很多人在设计 CRM 数据库时第一条就把客户表建出来了这没错但远远不够。客户模型最核心的问题不是“客户字段怎么定”而是“一份客户的完整视图靠哪些表支撑”。如果只建一张客户表你会发现后续接入通话记录、聊天记录、跟进记录、工单时要么不停的加字段要么拆出五六张关联表但互相之间没有统一的外键关系查询变得越来越别扭。我们最终落地的客户模型分为四层第一层是客户主表customer存放客户的基本属性和核心维度比如客户名称、行业、来源渠道、等级、当前阶段线索/潜在/成交/流失。第二层是联系人表contact一个客户下可能有多个联系人每个联系人负责不同的事务联系人有自己独立的电话和邮箱。第三层是关联域表customer_rel主要用来描述客户与坐席、客户与团队的归属关系以及客户与客户的关联关系比如母子公司、上下游供应商。第四层是行为数据表通话记录call_log、聊天记录chat_message、跟进记录follow_up、工单work_order都属于行为数据统一通过 customer_id 或 contact_id 关联到客户主数据上。这样的分层设计带来一个直接的好处——行为数据的“挂在客户下”还是“挂在联系人下”是可以动态调整的。比如一个来电号码匹配到了某个联系人那这条通话记录既挂联系人也通过联系人的 customer_id 关联到客户主档坐席打开客户视图时一次查询就能拉出所有行为数据。一个要注意的细节是客户去重。实际业务中同一个客户可能通过不同号码、不同邮箱反复进线如果不做识别合并客户视图就会散成两三份。我们采用了一个折中方案在客户表中增加 unified_key 字段存一个经过规则合并的标识比如“手机号可识别就先用手机号识别不了就用邮箱前缀再不行就 fallback 到客户名”。接听电话时优先按号码查 unified_key命中就直接弹出客户档案。这个方案实现成本低但确实能挡住大部分重复建档问题比上复杂的清洗算法实在得多。2.2 通信集成把电话和数据放进同一个界面通信集成是整个 DeskcommCRM 里最硬的一块也是区分“有通信模块的 CRM”和“真正可用的通信融合工作台”的分水岭。纯 CRM 加一个“呼叫记录”按钮很简单难的是坐席在网页里点击拨号、通话状态实时刷新、通话结束录音自动归档整个过程不能依赖座机也不能每次都跳转到第三方话务台。我们在呼叫链路上的做法是前端嵌入 WebRTC 软电话基于 SIP.js 实现通过 WebSocket 与信令服务器通信媒体流走 WebRTC后端接 SIP 软交换用的是 Asterisk 兼容方案呼入呼出都统一走 SIP 线路同时事件通道AMI把来电、振铃、接听、挂断这些状态实时推送给业务后端业务后端再通过 WebSocket 推送到对应坐席的工作台界面。这里有一个非常关键的设计通话状态机。很多人以为通话只是“接通/挂断”两个状态但做通信集成时必须细化到振铃、接通、保持、转接、外呼拨出、无人接听、拒接等十几个状态。我们在代码里维护了一个严格的状态机只允许特定状态之间流转比如“拨号中”只能到“振铃中”不能直接跳“已接通”。这个状态机的存在避免了一堆并发情况下出现“坐席已经挂断但界面还显示通话中”这种低级却极难排查的 Bug。消息集成这块用的是 WebSocket 加消息队列的方式。前端与服务端建立长连接后新消息通过 Redis 的 Pub/Sub 广播到坐席所在的节点再推送到浏览器。消息落库是异步的通过消息队列削峰避免大量并发消息阻塞主流程。聊天消息的正文、附件、消息类型文本/图片/文件/系统消息分开存储消息表只存通用索引这样后续如果要接机器人或者做自动分类不需要动表结构。一个实际测试中发现的细节消息顺序和会话上下文强相关。客户发消息时服务端必须先把“这个客户到底属于哪个会话”算好再做消息路由。如果每次都重新分配坐席就会出现同一客户在短时间内被转给不同坐席的尴尬情况。我们的做法是给会话维护一个亲和性会话创建时绑定一个坐席只有当坐席主动转接、离线或会话超过 N 分钟未响应时才允许重新分配。这就是常说的会话亲和性策略在消息系统里比“谁空闲分给谁”更容易保证服务体验。2.3 坐席工作台的交互设计细节很多团队做系统时只关注功能能不能跑通不关注坐席每天要在这个界面里花多少小时。坐席工作台如果用起来别扭再强的功能落地也是打折的。DeskcommCRM 的工作台界面采用经典的三栏布局左侧是待办客户列表中间是会话/通话面板右侧是客户详情与工单区。左栏列表要解决的是“优先级引导”问题。纯按时间排序会出问题——一个昨天来过、今天又发消息的客户跟一个一个月没联系的沉睡客户排在同样的位置坐席很容易漏掉真正需要马上响应的会话。我们的做法是动态计算“需处理度”综合会话状态、最近消息时间、工单紧急程度、客户等级几个维度打分然后按分数降序排列让坐席一眼就看出先处理谁。中栏的会话面板是所有操作的焦点。聊天式的通话记录展示听起来简单但真做起来有几个反直觉的坑。比如来电记录要不要跟聊天记录混排电话是语音不是文字混排后中间会多出一堆“本通电话 3 分 12 秒”的条目视觉上非常割裂。我们最后的处理是做了一个时间轴类型的切换按钮默认按会话聚合展示点击某条通话记录时展开该通电话的详情而不是把所有记录平铺在一个消息流里。右栏的客户信息区最有价值的是“客户 360° 快照”。快照不是把客户表所有字段都塞出来而是聚合了最有决策价值的信息客户等级和阶段、最近一次跟进记录、待处理工单数、最近 30 天的通话和消息数、未完成事项提醒。坐席接通电话后扫一眼快照基本就能判断这是新客户要建立信任还是老客户要解决问题还是某个工单到期需要催办。快照数据全部通过一个聚合接口返回前端只调用一次避免大量散接口的多次请求导致页面卡顿。2.4 权限模型数据归属和可见范围怎么设计权限设计是整个系统里被质疑最多的部分。团队管理层要求“每个坐席只能看到自己负责的客户”但同时又要求“团队主管能看到团队内所有客户”而公司的超管还要能看到全部。如果只做角色权限比如坐席、主管、管理员三级数据可见范围根本控制不住。我们采用的方案是“角色 数据范围”的双层控制。角色决定能做什么操作比如新建工单、删除客户、导出数据数据范围决定能看到哪些数据本人、本团队、全部。数据范围不是角色上直接配置的而是挂在组织架构上坐席默认继承所属团队的数据权限主管可以额外配置跨越多个团队的数据范围。查询层通过一个统一的权限过滤器实现——所有查询客户、工单、通话记录的 SQL 都必须带上当前用户可访问的数据范围条件这个条件由后端根据用户的组织归属和角色规则动态拼接前端根本不参与权限判断。这个设计踩了一个很深刻的坑权限规则放在前端会直接被绕过。最初版本我们把权限判断放在前端路由和按钮显隐上数据接口只做了简单的登录校验。结果测试人员用普通坐席账号直接调接口把全量客户数据给拉出来了幸好只是内测阶段发现。后来所有权限下沉到后端接口请求先经过权限过滤器再做业务处理才把这个漏洞堵死。这件事也成了团队“安全底线”的教科书案例后面所有新增接口都必须走同一套权限校验逻辑。3. 实操过程从数据库到功能实现这段我把实现过程中几个关键环节的具体做法和参数写下来尽量给出可以直接复用的方案。数据库表结构怎么设计、核心接口的流程怎么组织、消息怎么保证不重复不丢失、统计看板的数据口径怎么定义这些每一条都是实际调试过的不是纸上谈兵。3.1 数据库设计与初始化先看客户主表和行为表的核心结构设计。当时用的是 MySQL 8.0字符集统一 utf8mb4InnoDB 引擎。客户主表字段比较多这里摘几个比较关键的CREATE TABLE customer ( id bigint NOT NULL AUTO_INCREMENT COMMENT 客户ID, unified_key varchar(64) NOT NULL COMMENT 统一识别key用于匹配客户, name varchar(128) NOT NULL DEFAULT COMMENT 客户名称, industry varchar(32) DEFAULT COMMENT 行业, level tinyint NOT NULL DEFAULT 1 COMMENT 客户等级 1-5, stage varchar(16) NOT NULL DEFAULT LEADS COMMENT 阶段LEADS/OPPORTUNITY/DEAL/CHURN, source varchar(32) DEFAULT COMMENT 来源渠道, owner_id bigint DEFAULT NULL COMMENT 当前归属坐席ID, owner_team_id bigint DEFAULT NULL COMMENT 归属团队ID, next_follow_at datetime DEFAULT NULL COMMENT 下次跟进时间, remark text COMMENT 备注, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除 0正常 1删除, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_unified_key (unified_key), KEY idx_owner_id (owner_id), KEY idx_owner_team_id (owner_team_id), KEY idx_next_follow_at (next_follow_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户主表;两个细节值得说明。一是 unified_key 唯一索引这个字段是整个系统找客户的关键客户去重、来电识别、消息路由都靠它所以必须唯一。二是 next_follow_at 的索引坐席工作台左栏的“待办列表”是按这个字段排序查数据库的没有索引的话客户量一上来查询会非常慢。行为数据表中通话记录和消息记录在设计上有一个共通点必须带上精确的时间戳和来源渠道标识否则后面做数据看板时会发现压根没法统计“哪个渠道的客户量最多”“哪条线路的通话时长最长”。通话记录的关键字段包括主叫号码、被叫号码、通话开始时间、应答时间、结束时间、通话状态、录音文件地址、关联的 customer_id、处理坐席 ID。消息表的关键字段包括会话 ID、发送者类型客户/坐席/系统、消息方向、消息类型、正文内容、时间戳。字段都定好后建表时记得加索引。坐席工作台查询频率最高的是“按坐席查待办”和“按客户查行为”所以 owner_id next_follow_at 的联合索引、customer_id created_at 的联合索引这两类索引必须有。我们后期专门排查过一次慢查询发现 90% 以上的慢 SQL 都集中在缺索引的表上加完索引后整体响应时间从几百毫秒降到了几十毫秒。3.2 后端核心流程实现后端用的 Spring Boot 单体应用按模块分包。我拿“创建跟进记录”和“消息接收落库”两个典型流程来说说实现时容易踩到的细节。创建跟进记录这个接口的完整流程是坐席在工作台勾选客户 - 填写跟进内容 - 选择跟进方式电话/消息/上门/其他- 选择跟进结果有效沟通/未接通/已预约下次/已成交- 设置下次跟进时间 - 提交。后端在保存跟进记录的同时还要做两件事更新客户主表的 next_follow_at 字段保证工作台的待办列表刷新写入操作审计日志记录谁在什么时间对哪个客户做了什么样的跟进。这两步操作必须放在同一个事务里否则会出现“跟进记录已经保存了但待办列表里客户还是排在原来的时间点上”这种数据不一致问题。消息接收落库这个流程是防止消息丢失的关键。客户在网页上发出一条消息消息先到服务端接口服务端生成一条带全局唯一消息 ID 的记录落库再通过 Redis Pub/Sub 推送到坐席工作台的 WebSocket 连接。这时会出现一个并发场景坐席界面可能会因为网络抖动重复收到同一条消息如果前端不做幂等处理界面就会闪出两条一模一样的消息。我们的解决方案是客户端在维护一个本地消息 ID 的 Set收到新消息时先检查 ID 是否已存在存在就丢弃。服务端也做了同样的幂等校验消息表对 message_id 加了唯一索引重复的落库请求直接报错跳过。坐席分配策略在后端用策略模式实现。分配时机有两个客户主动呼入或发起在线咨询时进入型分配以及坐席主动领取客户时主动型分配。进入型分配不是一个简单的“找空闲坐席”我们实际跑了三种策略最后选择的方案是“最少活跃会话数优先同数量时按坐席空闲时长排队”。这个方案的好处是每个坐席的工作量相对均衡不会出现有的坐席堆积了几十个未响应会话有的坐席闲了十分钟的极端情况。分配完成后立即把客户数据写入该坐席的 Redis 待办队列并设置过期时间防止坐席长时间未响应导致客户进线被搁置。3.3 数据统计与看板实现数据看板是一个容易被低估的工作量。表面上就是查几个 count、sum但真正难的是数据口径的统一。团队开会讨论“响应时长”这个指标时产品经理说“客户发消息到坐席回复的间隔”业务主管说“应该是来电振铃到坐席接起来的时长”两个口径得出的数字可能差距巨大。所以看板开发第一步不是写 SQL而是把所有指标口径用文字定义清楚再让开发照着实现。最终定的几个核心指标口径平均响应时长客户发起会话来电或发消息到坐席首次回复/接听的间隔按日聚合取平均值。会话量每天进入系统的会话总数包含电话、在线消息、邮件。工单解决率已解决工单数 ÷ 已关闭工单数按日期统计关闭时间。客户转化率从线索阶段推进到成交阶段的客户数 ÷ 线索阶段客户总数。这些指标在实现上用到聚合 SQL比如按日统计响应时长的核心 SQL 大概是SELECT DATE(created_at) AS stat_date, AVG(TIMESTAMPDIFF(SECOND, created_at, first_reply_at)) AS avg_response_seconds, COUNT(DISTINCT conversation_id) AS total_conversations FROM conversation WHERE created_at 2025-01-01 GROUP BY DATE(created_at) ORDER BY stat_date;这里的 first_reply_at 字段是会话表中专门加的记录坐席首次回复的时间比从消息表里二次统计高效得多。所有看板聚合查询都是在凌晨用定时任务跑批结果存进统计汇总表前端页面直接查汇总表避免每次打开看板都全量扫描明细表导致卡死。4. 常见问题与排查技巧实录这部分把我在开发和上线初期遇到的典型问题整理成一个速查表每个问题都附上排查思路和最终解决方案。这些问题非常有代表性很多自建系统踩的坑都大同小异写出来希望能帮大家节省排查的时间。问题现象根因分析解决方案消息偶发乱序WebSocket 推送与消息确认机制不完善增加客户端 seq 序号校验乱序时请求服务端补拉同一客户短时间内被分给不同坐席会话亲和性策略未生效根据来源渠道和客户统一标识绑定会话坐席坐席转交客户后原坐席仍能看到客户权限过滤条件未更新转移客户时同步更新 owner_id并清理旧坐席的 Redis 待办接口请求慢数据库 CPU 飙升大量列表查询未走索引深分页增加联合索引列表查询改用游标分页通话状态与界面不一致状态机流转边界未完全约束补充状态机校验非法流转直接拒绝并记录日志第一个问题值得单独展开讲讲。消息乱序的根因不在于消息丢失而在于网络传输的到达顺序不确定。客户发的两条消息第一条经验证落库后再推第二条因为网络抖动先到了浏览器界面就会先显示第二条再显示第一条。我们最终在每条消息上加了 seq 序号同会话内递增前端收到消息后先检查 seq 是否连续如果发现跳号说明中间有消息延迟到达前端会主动调用一个补齐接口从服务端拉取缺失的消息重新排序展示。这个机制上线后测试反复制造弱网环境消息乱序问题基本归零。第二个问题是我们上线初期投诉最多的。客户先从官网点击咨询被 A 坐席接待后来客户挂断后再次来电系统却把电话分配给了 B 坐席客户需要重新描述一遍问题体验很差。排查发现消息会话和电话会话各有一套分配逻辑并没有统一检查“这个客户最近是否已经有绑定坐席”。修复方式是增加一个统一的会话亲和性校验进入分配流程前先用客户的 unified_key 查最近 4 小时内是否有未关闭会话有就直接复用绑定坐席没有才进入新会话分配。修改后该场景投诉率下降了 90% 以上。第三个问题属于数据权限和归属一致性问题。客户转移的场景下如果只更新了客户主表的 owner_id而没有同步变更 Redis 待办队列和操作审计日志旧坐席的工作台列表里会残留这个客户的信息点进去还能看到全部数据。我们的解决思路是把客户转移做成一个完整的事务操作不只是改一条数据而是同步更新客户主档、清理旧坐席待办、推送转移通知消息给新旧坐席并写入完整的转移审计日志。这样处理之后数据归属不一致的问题基本消失了。其他两个问题也是典型。深分页的问题经典场景是工作台客户列表翻到几百页时传统的 limit offset 会越来越慢因为数据库要扫描和跳过前面所有的行。我们把所有列表查询改成了游标分页用 id 上一页最大 id 的方式查询性能立即得到改善。通话状态不一致的问题排查下来发现是没有对状态机的所有边界做约束比如坐席在“振铃中”就直接挂断了代码里没定义这个流转路径。补上状态机校验后这类情况会直接拒绝流转并记录日志问题复现概率降到了可忽略的水平。5. 项目复盘与可扩展方向项目上线稳定运行之后回过头来看整个开发过程有几个经验是值得沉淀下来的。第一通信型 CRM 和传统 CRM 最大的差异在于“实时性”要求。传统 CRM 的多数操作是表单提交、列表查询慢半秒用户基本无感但通信场景里电话进来、消息进来如果界面上 1 秒内没有反馈坐席就会觉得系统卡客户体验也会直接受影响。所以在架构设计时一定要考虑好实时链路从信令接入、消息路由、WebSocket 推送到前端渲染整条链路都要提前做压力测试。我们当时犯过的错误是只在功能联调时测了单路通话没有模拟高峰期的并发进线结果上线第一周就出现过消息延迟推送的情况后来加上了消息队列和连接池扩容才解决。第二数据归属规则一定要在设计初期就和业务方达成一致否则越到后期越难改。客户归属、会话归属这两个问题看似简单但业务方往往有非常多的“特殊情况”比如“大客户要归给主管”“某个区域的客户只能由本区域的坐席跟进”“老客户回访时必须找原来的销售”。这些规则如果不提前梳理进权限模型和分配策略后面实现的时候会反复返工。第三统计指标的口径要早定。哪怕是“响应时长”这种看起来很标准的指标团队内部也会有不同的理解。建议在项目启动时就组织业务方和数据开发坐在一起把所有需要上报表的指标一个一个过一遍把口径写成文档作为开发的依据。宁可多花一两天在口径对齐上也不要等看板上线了再让业务方说“这个数据不对”。关于扩展方向我实际在计划中的有三块。第一块是智能化跟进提醒目前待办列表是基于 next_follow_at 的静态提醒后续想加入规则引擎比如“客户超过 7 天未跟进且等级为 4 级以上自动预警”。第二块是消息内容的高级检索现在只能按客户名和电话搜后续打算引入全文索引支持按聊天正文内容搜索历史记录。第三块是移动端兼容虽然工作台是给坐席在电脑上用的但主管希望在手机上也能看到实时看板和工单审批这块涉及一些接口适配和组件库选型优先级排在相对靠后的位置。最后分享一个我个人的体会做一个系统功能清单再长也只是骨架真正让系统好用的是那些不起眼的小细节——坐席接起电话后客户信息 0.5 秒内弹出来、切换客户时不用等接口转圈、转接工单时上下文完整带过去。这些小细节每一个单独看工作量都不大但串起来就是整套系统体验的分水岭。DeskcommCRM 能做到这个程度不是某一个模块做得有多炫而是所有环节的“顺手程度”都在及格线以上。对于后来者我的建议是不要急着堆功能先把一个客户从进线到解决问题的全流程走通走顺了再往上面加花活。
返回列表