
做客户管理工具这件事我前前后后折腾过不少轮。前几年团队小我用过在线表格、共享文档甚至直接靠聊天记录翻历史客户一多立刻就乱了。后来换过几套开源 CRM配置重、字段死、界面臃肿销售顾问光是录信息就怨声载道。真正让我下定决心自己动手的是因为业务场景里大量沟通发生在电话和桌面客服端但市面上的 CRM 对这块支持要么太浅要么收费离谱。于是就有了 DeskcommCRM——一个面向桌面办公场景、以沟通记录为核心的轻量级客户关系管理工具。这篇文章就从头讲讲这个项目是怎么设计、怎么落地、踩了哪些坑的给同样在纠结自建 CRM 的朋友一个参考。DeskcommCRM 这个名字是 Desk Communication CRM 的组合。我的定位很直接它不是一个包罗万象的销售全家桶而是把“客户资料、沟通历史、跟进任务”这三样东西收拢到一块让坐席和销售随手就能看到完整上下文。项目适合 5 到 50 人左右的小团队尤其是客服坐席、电话销售、渠道运营这类每天要高频联系客户的场景。如果你也受够了“客户信息在一个系统里聊天记录在另一个软件里通话又要去话单系统里查”这种碎片状态这个项目思路大概率能帮上忙。1. 项目概述与整体设计思路1.1 为什么做 DeskcommCRM先搞清楚到底痛在哪自建系统最忌讳一开始就奔着“功能多”去。我最初只是想把三件事打通客户资料、沟通记录、跟进提醒。先说客户资料。团队之前用一张在线表格里面几十列字段业务员填法五花八门同一个客户可能是“某公司王总”也可能是“王总-某公司”Excel 合并单元格一多筛选直接卡死更别提按行业、按地区做统计了。这是最底层的痛点。再说沟通记录。公司有座机、有手机、有微信很多沟通发生完就散落在各个平台里。销售月初想复盘上个月的客户通话得去通话记录后台导出 Excel再手动对到客户资料上两个人一天能完成算快的。这种重复劳动严重消耗耐心。最后是跟进提醒。业务员把客户跟丢了往往不是不勤奋而是完全忘了上次聊到哪。没有系统化的“下次跟进时间”和“待办事项”就只能靠每个人自己的备忘录管理人员根本没法全局掌握。DeskcommCRM 的所有设计都围绕这三个问题展开。说白了首先要做的是“信息聚合”而不是“销售赋能”这种虚词。我宁可少做一个花哨的仪表盘也要把“这个客户上次什么时候联系的、说了什么、接下来该干什么”这组问题回答得又快又准。1.2 功能边界与核心模块拆解项目功能不贪多核心模块就四块客户档案、通讯记录、跟进任务、统计看板。客户档案模块负责统一存储客户基本信息包括公司、联系人、电话、来源渠道、标签、自定义字段。这里的关键设计是“主联系人 关联人”结构一个公司账号下可以有多个联系人避免同一个客户被重复建立多个档案。通讯记录模块是 DeskcommCRM 的差异化所在。它聚合了两种来源的沟通数据一类是软电话SIP 话机/网络电话产生的通话记录包括来电弹屏、通话录音、通话时长另一类是手动补充的沟通记录比如微信语音后的文字纪要、线下拜访后的记录。每条记录都会自动挂到对应的客户档案下面形成时间线。跟进任务模块解决“今天该干嘛”的问题。销售可以为客户创建待跟进任务系统会按时间排序超时未完成的任务自动置顶并标黄。这个模块还支持简单的提醒设置到点后桌面弹窗提醒。统计看板则是把上述数据汇总成几张对管理有用的图包括每日通话量、跟进完成率、客户阶段分布。我不做复杂的预测模型数据准、刷新快是第一位的。技术选型上我采用了 Electron Vue 3 TypeScript SQLite 的桌面应用方案原因后面会展开讲。总之这个项目从数据模型到界面交互都围绕“快”——录入快、查询快、上下文呈现快。1.3 技术选型为什么是桌面应用而不是 Web 应用我知道有人会问现在随便一个在线 CRM 都比自建方便为什么还要自己写这里我得替自建说句公道话。一是数据隐私。客户资料是公司的核心资产很多团队不愿意把完整客户库放到云端 SaaS 上。本地优先的桌面应用主数据都在自己电脑上是否同步、同步到哪里自己说了算。二是沟通集成。团队用的软电话系统、话单服务器都跑在内网桌面应用直接通过局域网接入延迟低、稳定性好。Web 应用要跨域访问内网设备麻烦得多容易撞上浏览器安全策略。三是使用习惯。坐席和销售整天在电脑前工作桌面端常驻任务栏来电话弹屏、到点提醒都是比浏览器更自然的交互方式。浏览器标签页一多CRM 页面被盖住是常事漏接电话就是这么来的。技术堆栈的具体组合是Electron 负责外壳主进程跑 SQLite通过 better-sqlite3存主数据渲染进程用 Vue 3 做界面本地服务通过 IPC 通信调用。软电话对接走 SIP over WebSocket配合一个自写的电话状态机。实际开发下来Electron 确实有包体大、内存占用高的小毛病但对内部工具来说完全可接受。相比之下它带来的跨平台能力和本地资源调度便利非常值。如果团队全是 Windows 办公环境配一款轻量桌面壳方案也够用Electron 只是我的个人偏好。2. 核心细节解析与实操要点2.1 数据模型设计客户表到底怎么建才不返工数据模型是 CRM 的灵魂。我第一版设计图省事把所有字段都堆在 customer 表里公司名、联系人、电话、手机、微信、地址全塞一行。结果入职新同事想新增一个“客户来源”字段直接改表结构迁移脚本写了一堆还容易出错。后期重构成这样核心表拆成五张表名用途关键字段company客户公司主体id, name, industry, region, source, created_atcontact联系人可多个id, company_id, name, phone, email, wechat, roleinteraction沟通记录通话/纪要id, contact_id, company_id, type, content, started_at, duration, recording_pathtask跟进任务id, contact_id, content, due_at, done, done_attag标签表id, name, colorcustomer_tag客户与标签关联customer_id(company_id), tag_id这样设计的最大好处是一个客户可以有多个联系人每个联系人可以有独立的沟通历史查询时通过 company_id 聚合视图上呈现“公司 联系人 时间线”的层级结构。统计报表也容易写了比如“按公司统计通话次数”直接对 interaction 表按 company_id 做 group by。建表时还有一个容易忽略的细节所有时间字段统一用整数存储毫秒级时间戳不直接存 ISO 字符串。这么做跨时区不会乱排序比字符串也快很多。显示层再格式化成「YYYY-MM-DD HH:mm」对销售做报表非常友好。索引方面customer 表的 name 字段要建普通索引或者前缀索引interaction 表要在 company_id 和 started_at 上建立联合索引。没有索引的时候客户量过万按公司查沟通记录能明显感觉到卡顿加了联合索引之后基本毫秒级返回。2.2 通讯集成来电弹屏是怎么做到的最初接到需求客户来电系统要自动识别号码、弹出客户资料。这个功能听着简单实现起来要跨几道门槛。电话接入层我们用的是办公室已有的 SIP 软交换(软电话网关)。PC 客户端注册成 SIP 分机通过 WebSocket 与 Asterisk/Freeswitch 通信。来电时 SIP 信令里带了主叫号码客户端收到 INVITE 请求后先解析出号码再到本地库里模糊匹配联系人。号码匹配不能做简单等值匹配因为客户可能用手机、座机、分机不同号码打过来。我在 contact 表上建了 phone_number_index 表把客户所有可能的号码都拆成 E.164 格式存进去保证来电号码统一加区号后再匹配。匹配命中后触发弹屏窗口显示客户公司、联系人、最近五次沟通记录同时自动打开软电话接听控件。这里有几个技术细节值得单独说来电弹屏窗口采用白名单机制只在系统托盘常驻一个监听进程收到来电事件才拉起渲染界面否则弹窗会被 Windows 定义为垃圾广告直接拦截。来电响铃到弹屏完成要求 800 毫秒内完成。实测 better-sqlite3 在本地库查十万级数据稳定在几十毫秒瓶颈反而是 Electron 窗口创建。我的做法是预创建一个隐藏窗口来电时直接把数据填进去再 show速度明显改善。通话结束时状态机要捕获 hangup 事件自动把通话时长、录音文件路径写进 interaction 表同时弹出“是否补充纪要点”的小窗口做到通话零手工记录。这一步是整个 DeskcommCRM 里最能体现“通信 客户管理”融合的地方。做完之后销售再也不用自己记通话了回访时打开客户详情页所有上下文一目了然。2.3 本地优先与多端同步数据不会丢的底层逻辑本地优先听起来简单真正难的是数据备份和同步。团队几个人共用一套资料库不可能每个人都单独维护自己那一份所以数据一定要有汇聚和分支机制。我的方案是“单机 SQLite 文件级定时备份 局域网主从同步”。具体做法是每位同事本地各有一份 SQLite 数据库文件。每 5 分钟客户端将本地“操作日志表”里新增的数据通过局域网同步到一台主服务器。主服务器上的 SQLite 作为权威库接收各端增量数据做简单冲突处理。新增记录采用 UUID 作为主键避免从库生成自增 ID 导致主键冲突。同步模块的核心不是传整个数据库文件而是传增量操作日志Event Sourcing 的简化版。每条记录有一个 op_log 表字段包括操作类型(INSERT/UPDATE)、表名、记录主键、变更后的内容快照、操作时间戳。客户端启动时向服务器拉取比自己记录水位线更新的日志再依次应用到本地库。冲突处理策略比较简单更新同一记录时以最后修改时间为准不做复杂的三路合并。这个策略对 CRM 场景够用了——毕竟大部分冲突是客户联系方式变更保留最新的就行。离线场景也考虑到了客户端未联网时照常工作所有操作写本地 op_log联网后自动补传。桌面上会有一个小圆点显示同步状态绿色代表已同步橙色代表有未上传的变更防止同事以为数据传上去了其实没有。这个架构的好处是省掉了部署一套云数据库的复杂度坏处是同步实时性有限多人同时改同一条记录时可能产生覆盖。但对我们这种小型内部使用场景稳定性和开发成本之间已经找到了平衡点。3. 实操过程与核心环节实现3.1 从零搭建项目骨架环境准备与目录设计整个项目的主仓库用 monorepo 结构分成 main(主进程)、renderer(渲染进程)、shared(公共类型和工具) 三部分。开发环境是 Node.js 20 LTS包管理器用 pnpmElectron 用 28 版本。初始化命令大概是这样的mkdir deskcomm-crm cd deskcomm-crm pnpm init pnpm add electron better-sqlite3 pnpm add -D typescript vue-tsc vite electron-builderElectron 主进程和渲染进程分离用 Vite 支持热更新开发时用 concurrently 同时启动主进程和渲染进程测试起来很舒服。目录结构建议这样划分src/main主进程代码负责数据库、软电话集成、IPC 暴露、同步任务调度。src/rendererVue 3 组件客户列表、详情页、任务面板、统计图。src/shared主进程和渲染进程共用的类型定义比如 Customer、Interaction、Task 接口。scripts数据库迁移脚本、软电话信令测试脚本。main 进程里最关键的是 IPC 通信设计。我用 contextBridge 暴露固定的 API 给渲染进程没有直接把 better-sqlite3 的能力直接暴露给前端是为了防止 XSS 导致数据库被删。渲染进程只能通过 window.desktopApi.getCustomer(id) 这类方法向主进程请求数据所有数据库操作统一收敛在主进程。3.2 关键模块实现客户视图与时间线聚合客户详情页是使用频率最高的页面我需要做到“一屏看到这个人的全部上下文”。界面分为左、中、右三栏左边是客户基本信息中间是沟通时间线右边是跟进任务和标签。时间线聚合的核心 SQL 是这样写的SELECT type, content, started_at, duration, recording_path, created_by FROM interaction WHERE company_id ? ORDER BY started_at DESC LIMIT 100;这个查询简单但要在界面上展示不同 icon 和文案所以我在前端定义了一层适配器把 interaction.type 映射成「来电」「去电」「拜访纪要」「线上沟通」。时间线默认折叠超过三个月的记录只有点击“展开完整历史”才加载更早的数据避免首次渲染卡顿。保存沟通记录时做了“保存并创建下一步任务”的联动既然刚打完电话大概率会有后续事项所以界面设计上把「记录内容」和「下次跟进时间」放在同一个表单里。填完提交一条 INSERT 同时写入 interaction 表和 task 表这样销售不会忘记安排下一步动作。这个模块开发时踩过最大的坑是录音文件的管理。录音文件如果直接存数据库 blob库文件会迅速膨胀存路径又容易因为同事换了电脑导致路径失效。最终采用相对路径 集中录存储目录的方式客户端把录音文件复制到团队共享盘或同步盘的voicemail/目录数据库里只记相对路径。这样备份、迁移都不乱。3.3 统计报表实现销售漏斗与跟进完成率报表不是 CRM 的核心但是老板要看所以还是要做。我没有选重型 BI 组件就是用 ECharts 封装了两个图表组件数据直接从主进程的只读接口拿。第一张表是“通话与跟进趋势”按天展示呼出电话量、接通量、跟进任务完成量。SQL 很直接SELECT date(started_at / 1000, unixepoch, localtime) AS day, COUNT(*) AS total_calls, SUM(CASE WHEN call_status answered THEN 1 ELSE 0 END) AS answered_calls FROM interaction WHERE type call AND started_at ? GROUP BY day ORDER BY day;第二张表是“客户阶段分布”用圆环图展示客户在“初次接触/意向确认/报价中/成交/流失”几个阶段的占比。这个数据不完全依赖 interaction 表需要配合 customer 表的 stage 字段做统计。报表模块有几个容易出问题的点统计时间范围要统一用“自然日”划分否则月末盘点时跨月数据对不齐。我统一用起始时间戳的天初为分组口径。图表刷新采用 5 分钟定时器加“手动刷新”按钮不做实时推送因为本地应用没必要那么重。导出的 Excel 用前端表格组件导出 CSV 文件即可避免引入后端打印服务。做完这三个模块后系统的闭环已经出来了客户资料进库沟通记录自动归集任务自动生成报表自动统计。数据流转非常顺。4. 常见问题与排查技巧实录4.1 软电话插件在 Windows 下无法挂载这个坑我卡了一整天才解出来。现象是 Electron 启动时主进程加载软电话动态库之后进程一直报 “LoadLibrary failed”但只要手动开调试模式就正常。后来发现原因是Electron 生产环境下主进程默认不跟随系统 PATH 环境变量加载 DLL需要把电话 SDK 依赖的目录显式加入dirname路径里。排查方式是用process.env.PATH打印出实际环境变量对比开发模式和打包模式的差异。结论很清晰打包后的应用工作目录在 resources 目录下SDK 找不着它的运行依赖。解决方法是把整套软电话 SDK 运行库拷贝到resources/phone-sdk/并显式调用process.chdir或使用绝对路径加载const path require(path); const sdkPath path.join(process.resourcesPath, phone-sdk, wrapper.dll); // 通过这个绝对路径初始化软电话封装层经验是遇到 “not found” 类错误先检查工作目录和 PATH不要急着改业务代码。4.2 本地数据库在同步时被覆盖这个 bug 比较严肃属于设计失误。最初实现同步是“拉取最新日志后将本地数据库整体替换为服务器库”结果同事在断网状态录入了一组新客户联网后同步因为服务器端没有这批新日志客户端就把本地数据给覆盖了客户资料凭空消失。好在有 op_log 兜底通过把服务器库和本地 op_log 里的数据逐条重放才找回来。这也逼我把同步模块改成“基础库 增量日志”模式同步时只拉取水位线之后的日志应用日志到本地不再整个替换数据库。给同样做本地优先架构的朋友一个建议绝对不要在同步逻辑里执行“先删本地再拉全量”的操作。要么以增量日志为主要么先导出本地 op_log 再合并。数据安全永远排在“同步一致性”之前。4.3 全屏弹屏导致焦点抢占来电弹屏设计成“全屏无可关闭”的窗口后出现了一个尴尬问题如果用户正在编辑其他文档弹屏会把焦点抢走客户都接完电话了用户切回来发现刚才打的字丢了。解决方法是把弹屏改成“中等尺寸置顶窗口”默认出现在屏幕右上角不抢焦点。只有用户点击“接听”按钮后才开始录屏或记录。这样既保留提醒效果又不干扰正常工作。从产品体验角度说CRM 工具应该尽量“配合”用户的工作流而不是“强迫”用户切换到自己的工作流。任何弹窗都以不打断输入为优先这个小原则后来延伸到所有通知设计中。4.4 数据量上来后搜索卡顿与索引优化随着客户量到了两万条级别列表页的模糊搜索变得很慢输入一个字要等两秒。先怀疑是不是前端没防抖加了防抖后改善有限。后来用EXPLAIN QUERY PLAN查看执行计划发现contact.name LIKE %关键词%这种写法无法命中索引全表扫描必然慢。解决办法不是直接改用全文搜索SQLite 自带的 FTS5 适合大文本检索但对手机号码这种片段匹配不太讨好而是做两层策略常用查询字段比如姓名、手机号、公司名的前缀搜索用LIKE 关键词%命中索引后缀搜索折叠到另一列search_reverse解决“输入后几位手机号找人”的场景。其他模糊搜索改成“搜索框触发后先加载 200 条 延迟加载更多”不让一次性渲染所有结果。实测客户量五万以内搜索基本都能在 100 毫秒内完成。向后索引这一招对 CRM 场景很实用大家可以抄作业。5. 实际使用心得与后续扩展DeskcommCRM 在团队里跑了两三个月最直观的收益是“找信息”的时间大大减少了。以前销售要切三四个窗口才能确认客户上次说过什么现在打开客户详情页时间线里清清楚楚。新同事入职培训也不再需要背一堆零散流程只要学会看系统里的任务列表就能上手。同步机制稳定运行后我们甚至把一些简单的共享知识库也搬了进来比如把客户常问问题的答复模板挂在客户资料的备注区。这块虽然不属于 CRM 标准功能但对一线坐席来说非常实用。后面我打算做的扩展有三个方向一是接入企业微信或自定义聊天工具的消息记录让微信语音、文字、文件也能自动归档到客户时间线。这需要做消息钩子目前还在调研方案。 二是把跟进任务升级成复购提醒模型根据客户的购买周期自动生成“该回访了”的任务而不是全靠人工判断。 三是多端同步升级为可选的中心服务器 SQLite 甚至 PostgreSQL方便远程办公场景下依然能保持数据同步。如果你也在做自己的 CRM最重要的一条建议是先理清数据关系再写界面。字段的设计决定了后续所有功能的上限与其急着做精美的按钮不如花几天把“客户、联系人、沟通记录、任务”这四张表的关系设计和索引方案想透。最后再分享一个小技巧给客户表加一个custom_data的 JSON 字段里面放团队自己定义的可选属性。业务变化时不需要频繁改表结构界面层按 key 渲染自定义表单就行。这个设计让我少加了很多次表字段强烈推荐。