从零自建通信型CRM:FreeSWITCH与WebRTC落地实践
DeskcommCRM 这套系统,严格来说是我帮一支销售团队从零攒出来的轻量级客户关系管理系统。名字里的 Deskcomm 是 Desk(坐席桌面)+ Communication(通信)的组合,CRM 负责客户管理,合在一起就是“坐席桌面上的通信型 CRM”。它解决的痛点是很多小团队最常见的那种:客户资料散落在 Excel、微信、手机通讯录里,通话记录更是一笔糊涂账,谁跟过、上次聊到哪、下次什么时候跟进,全靠个人记忆。
如果你们团队也是几十个销售、几个客服坐席的规模,又不想一上来就上重型商业系统,这篇文章里讲的方案会很有参考价值。我从整体架构、技术选型、核心模块拆解到落地配置都过一遍,最后还会把我实际部署过程中踩过的坑和排查思路整理出来,尽量让有开发基础的人能照着这条路自己搭起来,而不是只能看个热闹。
1. 项目定位与总体架构:DeskcommCRM 解决的是哪一类问题
1.1 “Deskcomm”三个片段拆开看
先从这个名字说起。Deskcomm 不是随便拼出来的英文词,它对应着这类系统的两个核心能力:Desk,是说它面向坐席人员,强调“桌面化操作”,销售和客服每天打开浏览器就能干活,不需要额外的硬件;Comm,则点明了这套 CRM 和普通客户管理工具最大的区别——它把通信能力直接做进了业务流程里,来电弹屏、一键外呼、通话录音、通话记录自动归档,这些都是传统 CRM 里需要额外集成甚至根本不支持的功能。
把通信和客户管理放在一起,本质上是在解决一个信息断点问题。传统做法是“通信归通信,客户归客户”:坐席用话机接电话,接完再去 CRM 里手动补一条跟进记录。这个动作一旦多了,漏记、错记、补录不及时就是必然结果。DeskcommCRM 的做法是把两条线拧成一股绳:电话进来,系统先替坐席认出来是谁;电话挂断,CRM 自动多一条通话记录,坐席只需要补两句备注。对管理者来说,这个系统带来的是可核查的沟通过程和可统计的跟进数据;对坐席来说,减少的是反复切换工具带来的心力和时间损耗。
1.2 商业系统用得好好的,为什么要自己搭
可能有人会问,市面上现成的呼叫中心 SaaS、SCRM 工具不是一大堆吗,按坐席数付费,按分钟通话计费,开箱即用,为什么还要自建?这个问题我在做技术选型前也认真对比过。
商业系统最大的问题不是功能不够,而是“定制成本”和“数据归属”两头堵。SaaS 型系统功能是固定的,销售团队想改一个字段、换一种跟进状态流转方式,往往要走工单向服务商提需求,排期几个月是常态。私有化部署的重型系统倒是灵活,但实施周期长、报价高,对小团队来说功能利用率可能连三分之一都不到。更关键的是,客户数据和通话录音都存在别人的平台上,当你想结合自己的业务流程做数据分析和自动化时,拉数据接口、受调用频率限制,都会变成隐性成本。
自建当然也有代价,主要是需要自己养通信线路和维护软交换环境。但如果你有开发人力,业务方又有比较明确的定制需求,自建这条路是可以走通的。我自己做完后的体会是:自建系统最大的价值不是省了软件订阅费,而是你想改什么就能改什么,数据全在自己手里,后续接 BI、接工单、接企业微信,主动权都在自己这边。
1.3 整体架构与主流程
这套系统的架构我按通信和业务两个平面拆开,避免互相拖累。接入层是运营商提供的 SIP 中继线路;通信层用 FreeSWITCH 做软交换,负责注册分机、桥接通话、触发录音;业务层是一个独立的 CRM 服务,负责客户资料、跟进记录、任务和报表;数据层是 MySQL 加文件存储,MySQL 存结构化数据,录音文件落到磁盘或对象存储里。
两条核心流程可以这样理解。来电时:运营商中继把呼叫送进 FreeSWITCH,FreeSWITCH 在通道建立的一瞬间通过 ESL(Event Socket Library)事件把主叫号码推给 CRM 服务,CRM 拿号码去客户表里匹配,匹配成功就把客户姓名、最近跟进记录、待办事项推送到坐席浏览器上完成弹屏,坐席接起通话,挂断后 CDR 数据自动写入库、录音文件归档。去电时:坐席在 CRM 网页上点击呼叫按钮,CRM 下发指令给 FreeSWITCH,FreeSWITCH 先呼坐席分机、坐席接听后系统再外呼客户号码,两路接通后桥接在一起,同时开始录音,结束后自动生成一条通话记录并关联到对应用户。
整体设计里最需要注意的一点是:通信链路和业务链路虽然耦合在流程里,但服务上一定要解耦。FreeSWITCH 挂了不能拖垮 CRM,CRM 升级也不能导致电话打不进来。后面章节我会逐个模块展开,先解决选型问题,再讲实现细节。
2. 技术选型:每个决定都要知道为什么
2.1 通信层:FreeSWITCH 是性价比最高的“总机”
通信层可选方案我认真比较过三个:FreeSWITCH、Asterisk、云通信 API 服务。直接说结论,我最后选了 FreeSWITCH。
| 对比维度 | FreeSWITCH | Asterisk | 云通信 API |
|---|---|---|---|
| 二次开发友好度 | 高,ESL 事件机制清晰 | 中,AGI/AMI 上手陡一些 | 高,但受平台接口限制 |
| 自带功能完备度 | 高,录音、CDR、会议、WebRTC 都成熟 | 中,很多功能要靠模块拼 | 高,但能力边界由平台定 |
| 单机并发能力 | 单机几百路没问题,小团队足够 | 轻量但并发能力弱一些 | 平台弹性高,按量付费 |
| 长期成本 | 一次性运维成本,不按分钟计费 | 同左 | 通话分钟费和 API 调用费会持续累积 |
| 部署维护难度 | 中等,配置项多但文档齐全 | 中等 | 低 |
Asterisk 是更轻的选择,如果只是做几个分机互拨,它完全够用,但到了要跟业务系统做深度事件交互、做复杂拨号计划、跑 WebRTC 软电话的时候,FreeSWITCH 的 ESL 机制明显更顺手,事件推送是长连接全量推送,业务服务只需要订阅关心的事件类型,不用像 AMI 那样频繁轮询。云通信 API 最省心,但按分钟计费的模式在小团队高频外呼场景下成本会很快累积起来,而且外显号码、呼叫规则这些能力对方说了算,定制空间有限。
选 FreeSWITCH 还有一层现实考虑:社区活跃、案例多,遇到奇怪的线路问题,去邮件列表或 GitHub issues 里搜基本都有前人踩过坑的记录。这一点在自建通信项目里非常关键,因为你永远不知道运营商线路会给你出什么幺蛾子。
2.2 业务端:Spring Boot + MySQL 的组合逻辑
业务服务我用了 Spring Boot + MySQL。这个选择没什么高深理由,就是这条技术栈的生态最成熟,招人好招、网上资料多、出问题容易排查。团队里如果 Java 熟手少,你换成 Python FastAPI 或者 Node.js 也完全没问题,这套架构的核心不在业务语言,而在“业务和通信要分开部署”这个原则。
MySQL 负责存客户档案、联系人、跟进记录、通话记录这些结构化数据,初期一张表几万十几万条数据,性能完全不是瓶颈。Redis 我没有在一开始就引入,因为早期版本里缓存需求不强,等做到数据看板、需要缓存热门客户资料的时候再上也不迟。前期带着一个 Redis 依赖反而增加部署复杂度,如果没用好还会引入缓存和数据库的一致性问题。
Spring Boot 这边我按模块分包,客户模块、跟进模块、通信模块、统计模块互相独立。通信模块比较特殊,它内部维护一个与 FreeSWITCH 的长连接(ESL),所有上报的呼叫事件都从这个连接进来,然后转换成业务事件发布给其他模块。这里有个关键经验:ESL 的监听服务一定要和 Web 服务部署在同一个进程里,或者至少保证切换的时候不掉连接,否则 FreeSWITCH 这边事件推不过来,来电弹屏就会“灵异失踪”。
2.3 浏览器软电话:WebRTC + SIP.js 的落地姿势
坐席团队用的是普通 Windows 电脑,我不想给他们额外采购 IP 话机,最合适的方式就是浏览器软电话。技术路线是 SIP.js + WebRTC,浏览器通过 WebSocket 把 SIP 信令传给 FreeSWITCH,媒体流走 WebRTC。坐席登录 CRM 网页后,软电话面板自动注册分机,耳机一戴就能接打电话。
这个方案的部署前提有两个:一是 CRM 站点必须是 HTTPS,浏览器才会授权 getUserMedia 拿麦克风权限,用自签名证书在 Chrome 里会被拦截,所以上线前最好申请一个正式证书;二是 FreeSWITCH 要开放并配置好 WSS 端口,让浏览器能建立安全的 WebSocket 连接。
这里有几个容易被忽略的细节。浏览器取流必须发生在用户主动点击之后,如果页面一加载就自动去拿麦克风,大概率会被浏览器的自动播放策略拦掉,所以我的做法是点击“接听”或“呼叫”按钮时才去初始化媒体流。另一个细节是分机注册状态问题,WebRTC 软电话受网络影响会比实体话机更大,掉线后不会自动重连,前端需要定时检查注册状态,断开时给出明显的 UI 提示并自动重连。如果这两点没做好,坐席用起来会非常折磨。
3. 核心模块拆解:从客户资料到通话闭环
3.1 客户信息中心:资料查得到,跟进才跟得下去
客户信息中心是整个系统的地基,这块没做好,通信模块接得再漂亮都是空中楼阁。我设计的模型是“一个客户、多个联系人、多条跟进记录”。客户主表存公司名、所属行业、来源渠道、标签;联系人表存多个电话、微信号、职位;跟进记录表存每一次沟通的时间、方式、内容摘要和下一步计划。这样既支持“以客户为主体”的视角,也支持“以联系人为主体”的沟通场景。
数据库设计上有个关键经验:电话号码的存储格式必须从一开始就统一。很多团队的数据里同一个客户可能存着 13800138000、013800138000、+86 13800138000 三种格式,直接导致来电匹配时查不到人、名单去重时出现大量重复客户。我的做法是写入时统一归一化,去掉空格和横杠,国内手机号去掉 0 和 +86 前缀,座机统一为区号加号码的纯数字格式。这个工作越早做越省钱,否则后期洗数据会让你怀疑人生。
跟进记录这块我还做了一个小设计:每次电话结束后自动生成一条“系统跟进”,内容包含通话时长、通话方向、关联录音链接,坐席只需要补充沟通结果,不用从头写记录。这个设计极大提高了数据完整性,因为自动产生的内容不会漏,坐席的补录负担又很小。
3.2 来电弹屏:把“等一个电话”变成“接一个已知客户”
来电弹屏是这套系统里最能提体验的功能,也是我做技术方案时花时间最多的地方。它背后的原理不复杂:FreeSWITCH 在接收到来电时会触发一些列事件,ESL 长连接把这些事件实时推给 CRM 服务,CRM 提取主叫号码后去数据库里匹配客户,匹配成功就把客户资料推送到坐席的浏览器页面。
实现上有一个原则我在前面提过:查询永远不能阻塞呼叫。正常流程是主叫进来后 FreeSWITCH 会同时呼叫坐席分机,整个弹屏查询必须在振铃期间完成。如果数据库查询超过几百毫秒,系统会自动放弃弹屏,让电话正常接入,而不是为了弹屏把整通电话卡住。弹屏对坐席来说是加分项,不是通话的前置条件,一旦变成阻塞流程,整个系统就废了。
未匹配的来电也要有兜底策略。号码库里查不到,就归入“新客户”分组,页面弹出一个轻量级表单,只有姓名和备注两个字段,坐席接完电话顺手填一下就能建档。这样既不会因为无法识别而漏掉潜在客户,也不用强迫坐席在通话中做大量输入。
3.3 一键外呼与通话录音:把沟通沉淀成资产
外呼的流程走的是“先内后外”的双呼叫模式:坐席在 CRM 界面点呼叫按钮,CRM 生成一个外呼任务交给 FreeSWITCH,FreeSWITCH 先呼叫坐席分机,坐席接听后再呼出客户号码,两路接通后桥接。这个设计的好处很多:坐席不用知道客户号码的具体格式,系统会统一清洗;外显号码固定为业务线路号码,保护坐席隐私;整个通话过程中通话记录、录音、状态流转都是自动的。
录音的实现需要注意两点。一是录音文件格式,FreeSWITCH 默认可以输出 wav 格式,但 wav 非常占磁盘,我建议录音落盘后交给后台任务转成 mp3,或者从一开始就配置成 mp3 编码。二是录音文件命名和关联,录音文件名必须与 call_log 表里的 call_id 一一对应,否则后期查找录音会非常痛苦。我的命名规则是日期 + 通话唯一 ID,比如 20250612-103000-8899123.mp3,一眼就能看到时间,也能反查到通话记录。
通话结束后,FreeSWITCH 会把 CDR(呼叫详单)数据推给 CRM,CRM 负责写入 call_log 表。这里要注意 CDR 数据可能重复推送,所以 call_log 表里对 call_id 字段要建唯一索引,重复数据直接忽略,避免统计数字虚高。
4. 实操过程:照着做,你也可以跑起来
4.1 环境准备与基础部署
先交代基础环境:一台 4 核 8G 的云服务器足够跑通整套原型,系统用的 Debian/Ubuntu。域名提前准备好,申请好 HTTPS 证书,因为浏览器软电话依赖 WSS,证书是必须的。公网端口要放行 5060/5080(SIP UDP/TCP)和 7443(WSS),这些都是通信层要用的端口。
安装顺序是:先装 FreeSWITCH,再装 MySQL 和 JDK,最后部署 CRM 应用和前端。FreeSWITCH 的安装直接走官方仓库,Debian 系和 CentOS 系都有对应的安装包源,导 key、加源、apt install 三步就行。装完先验证服务能不能起来,systemctl status freeswitch 能看到 active 状态再继续下一步。
这里强调一个安全习惯:MySQL 的 3306 端口绝对不要暴露到公网,CRM 应用和数据库之间走内网连接。SIP 端口要放行是因为运营商中继需要回调,数据库没有任何理由暴露在外。通信类系统一旦被扫到弱口令数据库,客户数据被拖走的后果是非常难收拾的。
4.2 核心配置:中继、分机、拨号计划
FreeSWITCH 的配置核心是三块:SIP 中继、分机、拨号计划。SIP 中继是系统与运营商线路之间的桥,配置好网关后,所有外呼都通过这个网关走;分机是坐席的注册账号,软电话和实体话机都注册到分机上;拨号计划定义呼叫的去向,比如内部分机互拨、外呼号码前缀规则、来电转坐席组等。
我简化后的拨号计划思路大致是这样的:
<extension name="internal">
<condition field="destination_number" expression="^10[0-9][0-9]$">
<action application="bridge" data="user/$1"/>
</condition>
</extension>
<extension name="outbound_trigger">
<condition field="destination_number" expression="^9(\d+)$">
<action application="socket" data="127.0.0.1:8086 async full"/>
</condition>
</extension>
上面这个规则表达了两层意思:以 10 开头的四位号码走内部分机;以 9 开头的号码由 CRM 服务通过 socket 方式接管,CRM 拿到完整被叫号码后,会触发“先内后外”的双呼叫流程。这个“9 前缀触发外呼”的设计是为了和内部短号做区分,坐席在 CRM 界面点呼叫时,前端会把号码自动拼上 9 前缀,用户几乎感知不到这个规则的存在。
ESL 连接配置同样重要。FreeSWITCH 的 event_socket 默认监听 127.0.0.1:8021,CRM 服务所在的本机进程可以直接连接。如果用 external 模式,ACL 一定要限制只允许 CRM 服务器 IP 连接,并且用强密码,因为 ESL 连接等于拿到了软交换的遥控器,暴露出去等于把整个电话系统交给别人。
4.3 数据模型:三张核心表的落地设计
数据模型我先把最核心的三张表设计出来:customer(客户)、contact(联系人)、call_log(通话记录)。跟进记录表 follow_up 也顺带列出来,因为它和通话记录的关系比较紧密。
CREATE TABLE customer (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(128) NOT NULL,
industry VARCHAR(64),
source VARCHAR(32),
tags VARCHAR(255),
owner_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_owner (owner_id)
);
CREATE TABLE contact (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
name VARCHAR(64),
phone VARCHAR(32) NOT NULL,
phone_norm VARCHAR(32) NOT NULL,
position VARCHAR(64),
remark VARCHAR(255),
KEY idx_customer (customer_id),
KEY idx_phone_norm (phone_norm)
);
CREATE TABLE call_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
call_id VARCHAR(64) NOT NULL UNIQUE,
customer_id BIGINT,
contact_id BIGINT,
direction VARCHAR(8),
status VARCHAR(16),
started_at DATETIME,
duration INT,
recording_path VARCHAR(255),
agent_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_customer (customer_id),
KEY idx_started_at (started_at)
);
CREATE TABLE follow_up (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
content TEXT,
next_action VARCHAR(255),
next_time DATETIME,
owner_id BIGINT,
source VARCHAR(16),
call_log_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
几个字段单独解释一下。contact 表里的 phone_norm 是用来做来电快速匹配的,它和 phone 字段并存,业务展示用 phone,查询匹配用 phone_norm,避免归一化逻辑散落在各处代码里。call_log 表里的 call_id 是 FreeSWITCH 通话的唯一标识,加唯一索引就是为了防重。follow_up 表里的 source 字段标记记录来自系统自动生成还是坐席手工填写,统计时可以区分哪些是坐席主动维护的。
早期模型里人容易忽略 customer 和 call_log 之间的关联设计,如果通话发生时客户还没建档,call_log 里的 customer_id 就是 null。这个 null 值不用怕,后期通过 contact 表反查是可以补上的,但前提是 contact 表里有 phone_norm 索引,否则大数据量下一次全表扫描就卡死了。
4.4 前端工作台:登录、客户列表、软电话面板
前端我用的是 Vue 3 + Element Plus。登录后主界面分三块:左侧是菜单和客户列表,中间是客户详情和跟进记录,底部是一条横贯的软电话面板。软电话面板用 SIP.js 注册分机,右侧显示注册状态、通话时长、接听/挂断/拨号盘按钮。
这个布局是经过实际使用反馈调整过的。第一版我把软电话放在右上角的小图标里,坐席反馈要用的时候找不到,后来改成底部固定面板,不管在哪个页面都能看到通话状态,才算是顺手了。前端还有一个细节:页面标题栏显示当前分机注册状态,如果掉线了标题直接变红色“分机未注册”,比页面角落里的小图标醒目得多,坐席不会出现“以为自己在线实际接不了电话”的情况。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题 | 现象 | 排查思路 |
|---|---|---|
| 分机注册不稳定 | 过一会儿软电话显示未注册 | 检查防火墙是否放行 SIP 端口、NAT 环境下的注册间隔配置、FreeSWITCH 日志里有没有 REGISTER 请求 |
| 来电不弹屏 | 手机能打通但界面没反应 | 先查 FreeSWITCH 侧有没有事件推出来,再查 ESL 连接是否断开,最后查主叫号码格式匹配规则 |
| 回音明显 | 客户说听到自己的声音 | 开启回音消除参数,确认线路侧是否重复启用了回声抑制,检查通话链路上有没有多次编解码转换 |
| 录音文件缺失 | 通话结束没有录音文件 | 检查录音目录权限、确认拨号计划里录音动作执行的位置是否正确、查看 CDR 里录音路径是否为空 |
| WebRTC 没有声音 | 拨通了但听不到或对方听不到 | 浏览器是否授予麦克风权限、站点是否 HTTPS、SIP.js 初始化时媒体流是否已获取 |
| 外显号码不对 | 客户看到的号码和预期不一致 | 检查 SIP 中继侧的主叫号码透传配置,确认外呼请求里主叫号码字段是否正确 |
排查这类问题,最忌讳东猜西猜。我的习惯是先看 FreeSWITCH 的日志,找到对应通话的 UUID 或主叫号码,把信令流程和媒体流程完整过一遍,确认问题出在信令还是媒体,再决定动哪边配置。很多自建通信系统的“疑难杂症”,最后定位下来都是简单的配置漏项。
5.2 两个值得展开的排查案例
第一个案例是回音问题。上线后第一个星期就有坐席反馈,客户总说听到自己的回声,体验很差。我一开始以为是浏览器软电话的音频处理问题,折腾了半天 WebRTC 的音频参数,发现没用。后来抓了完整通话日志,才发现问题出在中继链路上:运营商侧已经做了一次回声抑制,FreeSWITCH 这边又默认开了自己的回音消除,两层处理叠加反而引入了新的延迟异常。解决办法是关闭 FreeSWITCH 侧的一些默认回声处理参数,保留线路侧的处理,问题立刻消失。这个案例的教训是:通信链路上的问题不能只看单点,要顺着整个通话路径排查,每一个环节都可能引入意想不到的问题。
第二个案例是外显号码对不上。有一段时间销售反馈客户看到的外呼号码不是我们报备的业务号码,接通率明显下降。排查后发现,问题出在号码格式上:CRM 传给 FreeSWITCH 的号码带了 0 前缀,中继侧在转发时没有做必要的号码清洗,导致运营商识别失败回退成了默认号码。最后我在 CRM 的外呼接口里统一加了号码标准化逻辑,所有号码在进入 FreeSWITCH 之前必须是纯数字无前缀格式,问题才彻底解决。这类问题往往不是单一环节配置错,而是多个环节的“小差异”累积出来的,所以一开始就把号码格式标准定清楚,后面能省掉大量时间。
6. 这半年我踩过的坑和总结的经验
6.1 从“能用”到“好用”的几个扩展方向
这套系统跑顺之后,我陆续加了几个增强功能,对团队使用的帮助很明显。第一个是数据看板,从 call_log 表统计每天的呼入呼出量、平均通话时长、接通率,从 follow_up 表统计跟进覆盖率和转化漏斗。这些指标在没接系统前全靠行政手工数,现在打开页面就能看,管理上的价值远大于功能本身。
第二个是跟进任务提醒。follow_up 表里存的 next_time 字段,每天早晨自动把当天该跟进的客户推送给对应负责人。这个功能不复杂,但实际效果非常好,因为销售的跟进动作一旦有了系统提醒,遗忘率会大幅下降。第三是网页客服通道,把网站上的在线咨询和 CRM 的客户档案打通,访客进入咨询时自动匹配历史客户记录,客服在同一个页面里既能聊天又能看到客户背景。这几个扩展方向的共同特点是复用已有数据,而不是另起炉灶,投入产出比很高。
6.2 关于自建通信系统的几句大实话
回到最初的问题,这套系统适合谁、不适合谁,我这里说几句实在话。如果团队规模特别小、业务流程也没定型,先用现成的 SaaS 工具验证模式,绝对比自建划算。但如果你已经确认业务流程比较固定、数据敏感度高、后续定制需求明显,自建是值得投入的方向。
从我个人的实际操作经验来看,自建的最大门槛不是技术,而是通信线路的稳定性和运维心态。代码出了问题可以改、可以等,但是电话打不通是立刻影响业务的事,所以日志监控和告警一定要尽早做。我这里说的线路合规和客户数据安全,也确实是要认真对待的事,建议在项目启动前就跟相关资源方把资质、外显号码报备、录音告知这些事情确认清楚,不然后面业务量起来了再补会非常被动。
最后再分享一个小技巧:这种系统里,CDR 和录音是比客户资料更值钱的数据资产,很多团队只盯着客户档案,却忽略了通话记录的分析价值。你把 call_log 表里的时长、时段、接通率数据拉出来,跟跟进结果一对比,很快就能看出哪些时段外呼效率最高、哪些话术类型的客户转化率更好。这比任何花哨的功能都实在。
更多推荐

所有评论(0)