ARTICLE DETAIL

资讯详情

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

自研桌面通讯CRM:用Tauri+Rust把沟通变成客户资产

自研桌面通讯CRM:用Tauri+Rust把沟通变成客户资产 做 DeskcommCRM 之前我一直觉得“客户关系管理”是个伪需求。市面上的 CRM 我基本都摸过一遍从早期用表格管客户到后来各种成熟的商业化产品说实话在联系人管理、商机漏斗、合同回款这些维度它们已经做得足够专业。但真正让我决定动手自研的是一次特别普通的丢单复盘客户已经明确说了“价格再降 5% 今天就签”负责跟进的销售当时在聊天工具里回复“我去申请一下”然后这条消息就彻底留在销售自己的聊天窗口里了。三天后没人跟进客户签了别家。那一刻我才意识到传统 CRM 离真正的“沟通现场”太远了。DeskcommCRM 这个名字里的 Desk桌面和 CommCommunication从一开始就不是指“在桌面上做个客户管理软件”而是要解决一个更具体的问题——让每一段真实发生在企业微信、飞书、钉钉、电话、邮件里的客户对话自动成为可检索、可跟进、可分析的客户资产。这篇内容适合正在做客户沟通类产品、打算自研 CRM 系统、或者因为现有工具追不到“沟通最后一公里”而头疼的人。我会把整个项目的设计逻辑、架构选型、通讯接入踩坑、数据模型、性能优化和范围控制都摊开来讲尽量说人话不谈虚的。1. 为什么我放着现成 CRM 不用非要写一个桌面通讯 CRM1.1 那场丢单复盘暴露了一个黑洞那次复盘其实很尴尬。我们查了所有能查的系统合同系统里有这个客户两年前的历史订单市场部系统里有他们参加过线下活动留下的线索记录CRM 里也有联系人档案。可所有人对“这个客户最近到底聊了什么”都只能靠回忆。销售说他记得客户提过预算收紧客服说他印象里客户对一个功能抱怨过好几次但没有任何一个系统能告诉我们“客户最后的真实意图”是什么。丢单的直接原因很明确关键的承诺和条件都发生在聊天工具里而聊天工具天然是个人私有数据。企业里的聊天消息默认只有当事人能看同事不知道主管不知道系统更不知道。传统 CRM 管的是“结果”比如商机金额、状态、阶段转换时间但形成这些结果的“过程”比如客户在什么语境下说了那句“降价 5% 就签”恰恰是空白。这个空白就是我当时想用软件补上的黑洞。1.2 DeskcommCRM 到底想解决什么问题想清楚这个黑洞之后我列了一个非常明确的问题清单“客户给我们发过哪些消息谁回复的回复前有没有查过客户历史客户最近的沟通情绪是正常还是有抱怨哪些会话超过 24 小时没人跟进哪些客户在多个渠道都有未读消息” 这些问题的共同特征是它们都围绕沟通事件展开而不是围绕客户档案展开。所以 DeskcommCRM 的产品定位和其他 CRM 有明显的差异。传统产品用“客户”作为第一实体我的数据核心是“沟通事件”。客户档案当然要有但档案不是静态的表格而是从各个通信通道实时流进来的消息、电话、邮件、会话动态汇聚成的。每次打开系统看到的不是一堆字段而是“这个客户今天说过什么、我们回没回、接下来该怎么办”。这个定位直接影响了后续所有技术选型和数据结构设计。1.3 名字里的三个词就是我的产品边界我给项目取名 DeskcommCRM拆开看就是 Desk桌面、Comm通讯、CRM客户关系管理。这三个词不是我为了好听硬凑的它们正好划定了产品的三条边界Desk强调的是使用场景。销售、客服、跟单人员的日常工作终端就是桌面电脑消息、电话、邮件都在桌面端循环。桌面端意味着要原生级体验比如系统级通知、开机能常驻、跨应用全局搜索而不是浏览器里多开一个标签页。Comm强调的是数据来源。核心数据源必须是实时通讯不是手动录入。所以系统要完整接收企业微信、飞书、钉钉、邮件、SIP 电话等渠道的消息事件。CRM强调的是业务目标。它最终还是要支撑客户管理、跟进计划、团队协作、销售决策不是另一个聊天工具更不是简单的消息记录归档。这条边界帮我挡掉了无数需求。有人让我加合同管理、加报销审批我全都推掉了。因为一旦把边界模糊掉这个项目就会变成又一个什么都想做、什么都做不精的怪物。2. 桌面端通讯架构Tauri 2.0 加 Rust 网关的组合2.1 为什么桌面端而不是纯 Web项目最开始我其实想过直接做纯 Web 版毕竟团队里的人都会写 JavaScript部署也简单。但认真梳理使用场景之后我放弃了。原因很直接一线使用人员的工作环境普遍是 Windows 主机屏幕不大浏览器标签一堆如果 CRM 只是其中一个标签页消息通知被浏览器折叠切换成本高非常容易被遗忘。桌面端的优势在通讯场景里是决定性的系统原生通知即使 CRM 不是前台窗口也能看到新消息不会被浏览器标签页的功耗问题压掉。开机自启动常驻内存关闭主窗口后仍能接收消息和弹屏提醒。本地能力完整比如读取条码、访问 COM 接口、与本地软电话 SDK 联动Web 里做这些事都很别扭。消息体验更快不用网络加载整个应用本地缓存多秒开。所以最终我确定的形态是“桌面端为主消息网关和 API 独立未来可扩展 Web 端”。桌面端只是一个壳和交互层真正的业务逻辑全部在服务端这样将来想加 Web 端只需要照着 API 再写一层界面。2.2 Tauri 与 Electron 之间的取舍桌面端技术栈我认真比较过 Electron 和 Tauri也做过一个 200 行的 Demo 来测实际体感。结果如下对比项ElectronTauri 2.0内存占用空窗口常驻 300MB 以上约 80–120MB安装包大小80–120MB8–20MB原生能力调用需要 Node 桥接Rust 命令直接暴露Web 技术栈完全友好前端照常用 Vue/React后端逻辑用 Rust系统通知/托盘成熟较成熟但个别平台有坑多窗口管理很成熟需要自己实现逻辑团队上手难度低需要有人会 Rust 或愿意学我最终选了 Tauri 2.0。原因是这个项目在桌面端要做的事很重处理 WebSocket 长连接、消息本地缓存、系统托盘、全局快捷键、剪贴板检测、与 SIP 软电话通信这些场景恰恰是 Rust 的强项比 Node 更稳、更快、更不容易出内存泄漏。更重要的是Tauri 的前端资源可以完全走 WebView没有把整个 Chromium 打包进来更新包很小对一线用户来说“安装一个 90MB 的 CRM 客户端”远比“安装一个 300MB 的客户端”心理门槛低。当然代价就是团队里必须有一个人扛起 Rust 侧开发如果你们团队全是纯前端没有精力碰 Rust那还是老老实实用 Electron。工具成熟度比软件洁癖重要得多。2.3 通讯接入层的整体数据流DeskcommCRM 的通讯接入层设计成了一条单向、清晰的数据管道。外部渠道消息进来以后统一走这七步外部渠道企业微信/飞书/钉钉/邮件/SIP 电话 → 接收适配器 → 标准化消息体 → 鉴权和幂等校验 → Redis 写入待确认队列 → PostgreSQL 落库 → WebSocket 推送到桌面端 → 前端本地缓存并渲染为了不把消息结构锁死在某个渠道上我定义了一套标准消息体。所有渠道适配器在进入核心系统之前都必须把原始消息转换成这个结构{ msg_id: feishu_oc_abc123_msg_456, channel: feishu, channel_thread_id: oc_abc123, direction: inbound, from: customer_uid_001, to: agent_uid_002, content_type: text, content: 这个方案我们还要再内部评估下, timestamp: 2025-01-01T10:30:00Z }这里有个很关键的设计决策msg_id不是我自己生成的 UUID而是由“渠道标识 渠道自己的消息 ID”组合出来的。这样做的原因很简单同一个渠道的同一个消息无论通过 webhook 推送、轮询拉取还是手动补录到达都会生成完全一样的msg_id下游做幂等去重时才有的放矢。如果自己生成 UUID同一消息来两次就会变成两条记录直接污染会话历史。通道层的整体逻辑并不复杂但做好这层“翻译”之后上层业务就再也不用关心消息到底是从企业微信来的还是邮件来的。给客户打标签、生成跟进任务、做情绪分析面对的都是同一套结构。这个抽象带来的收益在后来的功能迭代里被不断放大。3. 通讯接入的三个硬骨头幂等、弹屏与离线补偿3.1 多通道消息接入先解决重复和数据割裂第一版我天真地以为接入 webhook 就完事了。结果上线第一天就被打脸客户在企业微信里发了一条消息系统里显示了七遍。原因是每个消息源都独立给我推送了一遍企业微信开放平台的实时消息回调推了一次我们自己的同步脚本轮询拉了一次同事测试时又手动手动录了一次。三个数据源没有一道统一的幂等屏障。后来我在消息表上加了msg_id的唯一索引所有通道在落库前先按msg_id查重重复消息直接丢弃。同时配合 Redis 里存最近 10 万条消息 ID 作为布隆过滤器避免每次重复消息都打到数据库。这样处理后重复消息的比例从第一天的几十条降到零。但另一个问题比重复更隐蔽那就是“同一个人不同渠道怎么识别成同一个客户”。同一个客户可能上午在你的企业微信群里提问下午发邮件第二天打电话。如果不对联系人做统一身份识别360 度视图就是空中楼阁。我用的方案是 ID 映射表每个外部联系人档案里维护一个customer_link字段通过手机号、邮箱、企业微信 userid、飞书 open_id 等多个唯一标识接入时按优先级自动关联。关联不上就创建一个“待合并客户”由人工审核合并绝不自动乱并。3.2 来电弹屏和通话录音比想象中更麻烦语音通话是 Comm 里绕不开的环节。一开始我用 SIP 软电话的方案通过 SIP 信令里的主叫号码去查客户命中后弹屏显示客户资料和最近会话。听起来很简单实际踩坑集中在两个地方第一个坑是事件状态机。SIP 的振铃事件、应答事件、挂断事件是异步到达的如果只是简单地在“来电事件”里弹屏就会出现“客户还没接电话弹屏先出来了”的尴尬甚至未接来电也弹。我后来处理得很朴素也很有用在内存里维护一个呼叫状态机ringing - answered - ended只有状态流转到answered且主叫号码在客户库里匹配到联系人时才弹屏。号码匹配不到的先弹一个“未知号码”的侧边条同时自动抓取号码归属地做参考。第二个坑是录音文件。通话录音不能等服务端收到完整文件再落盘一个小时的通话录音可能几百 MB很容易把内存打崩。我用的方案是服务端接收流式上传边录边传文件分片写在临时目录通话结束之后按call_id关联录音文件和话单记录。同时设置一个定时任务每天凌晨把超过 30 天的原始录音归档到对象存储本地只保留索引和缩略播放地址。这些细节不处理后面跑量的时候一定会炸。3.3 离线消息补偿机制不能靠“重连后全量拉取”桌面端在职场的网络环境非常不稳定会议室 Wi-Fi、客户现场临时网络、电脑休眠唤醒都会导致 WebSocket 断线。断线重连之后最原始的做法是让客户端重新拉取某个时间段内的全部消息但这样做非常容易把消息顺序搞乱重试期间新老消息混在一起前端渲染容易错乱。我最终参考了消息队列里的消费位点机制。每条消息落库时都会拿到一个全局自增的seq字段桌面端在本地保存最后消费的seq。重连时客户端带上since_seq服务端返回从这个位点之后的增量消息客户端把这些消息插入本地缓存再提交新的消费位点。这里有个和直觉相反的点消息排序的绝对基准是服务端的seq而不是客户端时间戳。因为客户端时间不可靠有人电脑时间慢五分钟有人快两分钟如果按客户端时间排同一个会话的聊天顺序就会乱。所以即使消息自带的业务时间在界面上展示底层排序也永远用seq。这个细节后来帮我们避免了好几次“看起来消息顺序错了”的返工。4. 客户 360 度视图是怎么攒出来的4.1 数据模型客户、联系人、会话、工单怎么串很多 CRM 的数据库设计喜欢把“客户”设计成一张巨无霸表几百个字段往里堆信息。我做 DeskcommCRM 的时候刻意反着来客户档案只保留核心信息其他全部通过关系表关联。核心表一共六张customers客户主体可以是公司也可以是个人只存名称、行业、等级、所有者、创建时间。contacts联系人归属某个客户保存手机号、邮箱、企业微信 ID、飞书 ID 等外部标识。channels渠道接入配置记录这个客户/联系人在哪个渠道、哪个会话里出现过。conversations会话一次持续的沟通过程可能包含很多条消息通常按会话线程 ID比如企业微信群的chat_id、邮件的thread_id聚合。messages单条消息上面讲过是全部沟通数据的最小单元。activities跟进活动包括日程、任务、提醒、备注可以关联到客户、商机或某一通会话。这套模型最核心的设计思路是从“事件”出发而不是从“字段”出发。传统 CRM 里客户信息靠人录DeskcommCRM 里客户信息靠每一次沟通自动累积联系人从原始聊天记录里自动解构出来会话历史就是客户的真实行为轨迹。看起来很绕但实际查询效率比想象中高。因为所有高频查询都是“按客户 ID 查最近会话”或“按会话 ID 查消息列表”这两个查询模式都可以走二级索引不需要做复杂的关联扫描。等数据量上来之后我再按customer_id做分区把单个分区的数据量控制在一个合理的量级查询基本都能稳定在几十毫秒内。4.2 从“会话摘要”到“客户意图”NLP 落地的一点经验聊天数据和结构化数据最大的区别就是“难检索”消息是自然语言文本很难直接变成可用的业务指标。所以我在第一版里做了会话摘要功能原理非常朴素每天凌晨把每个客户当天所有会话消息聚合起来先做关键词提取再按“客户诉求、我方承诺、风险点、下一步动作”四个维度生成一段总结。这里有个非常值得说的经验先跑离线批量不要一上来就追求在线实时。离线批量一天只跑一次拿到结果后存在数据库里白天的页面和报表直接查结果。这样做的好处有两个一是成本可控大模型或 NLP 接口不会因为白天实时调用而烧钱二是效果稳定如果哪天的摘要生成失败可以重跑不影响线上功能。后来又做了情绪打标和意图识别。情绪打标其实就是让模型判断当前会话的整体情绪是正面、中性还是负面出现负面情绪时自动给销售端弹一个提醒意图识别则是识别客户是否表达出了“要报价、要试用、要延期、要投诉”等具体动作。这两个功能刚上的时候有几次误判客户说了句“这东西还不错”被标成正面但其实是反讽。后来我把上下文窗口扩大到当前会话最近 20 条消息并且要求模型在不确定时输出“未知”而不是硬猜指标才变得可用。4.3 权限模型谁能看哪个客户的哪段对话通讯数据比普通客户档案更敏感因为消息里可能包含价格、隐私、内部决策讨论。所以权限模型从一开始就按“谁能看到哪一段对话”来设计而不是按“谁能看到哪一张表”。具体实现上我用了经典的 RBAC 加数据范围双层模型。用户被分配角色管理员、销售主管、销售、客服角色决定操作权限数据范围则决定能访问哪些客户的数据。数据范围分三档本人只能看分配给自己的客户和会话属于默认档。团队能看到本团队的客户数据团队成员可以互相参考历史沟通但跨团队默认不可见。全部仅管理员和部分角色拥有可以看整个组织的客户数据用于管理报表、质检分析。会话级别也有一个不可越过的限制无论数据范围是团队还是全部普通用户都看不到其他团队名下客户的消息详情除非通过“交接”或“协作”机制显式授权。这个设计当时多花了一周时间但我认为非常值。CRM 这类系统数据权限一旦出问题信任成本特别高。前端隐藏菜单只是遮挡真正的权限边界一定是在 API 层和数据库查询层做控制。5. 跑量之后的性能优化与踩坑清单5.1 消息量上来后卡点全在数据库系统刚上线时只有几千个客户、每天几百条消息感受不到任何压力。等到了每天十万条消息的规模时问题开始冒头会话列表加载越来越慢消息发送后要等两三秒才在界面上出现后台工单倒是不慢但客户那边体感已经明显不行了。我首先做的优化是“读写分离思想”高频查询走 Redis 缓存低频聚合走 PostgreSQL。会话列表、未读数、最近消息这些高频数据全部在 Redis 里维护数据库只做最终落盘和后台分析。每条消息接收时同步更新 Redis 里的会话摘要、未读数、最后一条消息内容前端拉取会话列表直接命中 Redis速度从两秒降到几十毫秒。第二步是清理历史数据。Hot 数据只保留最近 90 天超过 90 天的聊天记录自动归档到冷表或对象存储。不是因为查 90 天前的数据没价值而是因为绝大多数一线场景“现在需要调出来”的会话都在最近几天。顾好热数据就成了体验。想查历史数据时再走归档检索通道接受秒级延迟即可。第三步是数据库索引调优。消息表上最终留下的核心索引只有三个(conversation_id, seq)用于拉取会话消息、(customer_id, created_at)用于客户维度聚合、(msg_id)用于幂等去重。有一段时间我加过很多单列索引结果每次写入都要更新几十个索引反而拖慢写入速度后来把用不上的索引全部砍掉写入性能提升了一个档次。5.2 我踩过的几个值得记录的坑这里挑几个我印象最深的坑每一个都花了我至少半天时间排查消息排序错乱现象是同一通会话里旧消息跑到新消息上面。排查到最后发现是前端渲染时用了消息里的created_at做排序而有的渠道消息里这个字段是客户端本地时间有的渠道是服务端时间。修复方式是统一用服务端分配的seq做排序字段客户端时间只做展示。通知重复轰炸企业微信收到一条新消息系统通知弹了一次页面内 toast 又弹了一次如果恰好在多个窗口里打开客户端还能弹三次。后来统一维护一个“已通知消息 ID”集合保证同一条消息最多触发一次系统通知。桌面端休眠唤醒后假死Tauri 的 WebView 在系统休眠唤醒后偶尔会出现界面还在但网络层断掉的情况。解决办法是监听系统睡眠/唤醒事件唤醒后主动检查 WebSocket 连接状态断线则重新建立连接并刷新未读消息位点白屏问题用本地缓存的 sessionStorage 做快速恢复。Windows 上系统通知堆积用户两天没开机一开机消息通知像雪崩一样弹出来严重干扰工作。后来做了个约定每次唤醒后超过 5 分钟前的未读消息不再逐条弹通知只弹一条“您有 N 条新消息”的摘要通知。5.3 桌面端体验优化的细节桌面端体验不是靠大动作堆出来的而是由大量小细节积出来的。我最满意的一个功能是“全局搜索”。用户按Ctrl Shift F直接弹出搜索框可以搜客户名称、联系人电话、消息正文、邮件主题搜索结果按相关度排序点击任意一条直接跳转到对应会话并定位到具体消息。大量销售反馈这个功能把他们从“翻聊天记录”里救出来了比什么 AI 助手都实用。另一个体验点是“未读会话红点”的设计。系统在托盘图标上常驻一个未读数有新客户进来时显示红色角标点开托盘菜单可以直接跳转到第一个未读会话。一开始我做了每条消息都弹系统通知后来发现过度打扰导致很多人直接关掉了通知反而误了正事。最后调成“只对客户首次会话、未分配会话、超过 4 小时未回复的会话”弹强提醒普通会话只在托盘角标累积效率反而更高。6. 如果你也想自己搭一套 CRM我建议你从这里开始6.1 最小可用版本的范围控制被问得最多的问题是“我也想做一个 CRM从哪里入手”。我的答案始终是先把单渠道做透。第一个版本不要接企业微信、飞书、钉钉、邮件、电话五个渠道只接一个最高频的渠道把“消息接收 → 标准化入库 → 会话列表 → 联系人 360 度视图 → 跟进任务 → 消息搜索”这条链路跑通。选择单渠道有非常现实的原因。跨渠道消息接入的坑大同小异但第一个渠道踩坑时能快速定位如果同时接五个渠道一个问题会叠加成五倍复杂度最后连日志都难查。而且单渠道版本可以让你尽早拿到真实用户在真实业务里的反馈判断产品方向对不对。方向错了迭代再多功能也是白费。我的第一个可发布版本大概是四周做出来的只接了企业微信消息前端做了会话列表、聊天窗口、客户资料侧栏后端做了消息落库和基础搜索。就是这版已经能支撑一个小团队日常跟进客户了。后来每增加一个新渠道大约要用一到两周时间因为要处理渠道自己的 webhook、回调字段映射、消息去重、联系人身份关联这些接入细节。6.2 别急着做智能化先把这三个基础能力做稳很多朋友一上来就想做大模型摘要、智能评分、自动跟进提醒我特别理解因为这些功能听起来确实酷。但从真实使用角度看地基没打好的智能化功能最终只会变成没人看的展示页。我建议先把三块基础能力做到让用户离不开首先是全文搜索。客户的每一句话、你回复的每一句话、邮件正文、通话摘要都必须能搜得到。搜索体验要做到“输入关键词一秒钟出结果点进去就是上下文”。这个能力越早做越好因为历史数据越攒越多往后补索引的代价只会更高。其次是可靠的未读与提醒。一线使用者对软件信任的起点就是“该响的时候一定响不该响的时候不乱响”。未读消息的计数要准强提醒和弱提醒的规则要能配置桌面端断线重连后的消息补发不能丢。这件事看起来简单但真正做到位需要花不少心思设计幂等和位点机制。最后是权限边界。上面讲过通讯数据敏感权限设计不能靠前端藏按钮。服务端每一层查询都要带上数据范围过滤条件保证越权查询在数据层就被挡住。这不是为了应付合规而是为了团队成员敢放心把真实沟通记录放进来。没有真实数据整个系统就是空壳。6.3 后续可以扩展的方向DeskcommCRM 做到目前这个阶段我认为可以继续扩展的方向有两个比较值得尝试。第一个是“跨渠道客户旅程合并”也就是把同一个客户在官网留资、企业微信咨询、邮件往来、电话沟通这几条线串成一个完整时间轴让销售在跟单前就能看到客户和公司之间发生的所有触点。这个方向对数据关联能力要求很高但价值非常直接。第二个是“团队智能质检”不搞复杂的规则引擎而是让系统根据会话情绪、响应时长、是否提到竞品等信号自动挑出高风险会话给主管抽查减少质检抽样的盲目性。如果团队有精力还可以考虑把桌面端消息网关做一个公开的本地 Socket 服务让用户可以开发浏览器插件或者手机端小程序来复用这一套消息数据。只要服务端 API 独立桌面端永远只是一个客户端未来要扩展到 Web、移动端都只是工作量问题而不是架构问题。我自己走到现在最大的体会是CRM 的核心不是“把客户信息记下来”而是“让下一次沟通有据可依”。DeskcommCRM 这个名字里藏的那个目标——让桌面上发生的每一句客户对话都变成可持续运营的资产——比任何单一功能都更重要。以后如果你们也在做客户数据相关的产品可以沿着这个思路试试看先把沟通数据接进来再去想怎么让数据变得聪明。等哪一天客户问起某个细节你能在系统里一秒翻出当时的对话这个系统就真正立住了。
返回列表