自研桌面CRM的技术实践:本地优先架构与通信数据归集
发布时间:2026/10/18 4:13:27 锦皓数字建站

1. 项目诞生背景为什么放着现成CRM不用偏要自己写一个1.1 团队的痛点不是“没有CRM”而是“没有合适的CRM”我们这个团队做的是B2B项目型销售客户数量不算夸张大概几百个但每个客户的生命周期特别长从初次接触到最终签约往往要跨三四个月中间涉及产品演示、报价、法务对接、合同审批、交付对接好几个环节。最早大家用共享表格管理客户后面客户一多、参与的人一多表格就开始失控了同一个客户在不同的Sheet里有三个版本的联系方式谁也不知道最新的报价是哪一版。这时候想起了上CRM。但是市面上的SaaS CRM我们基本都试了一圈最后都停在同一个问题上数据不在自己手里。销售数据是我们最核心的资产把客户联系人、报价记录、沟通历史全部放在别人的服务器上过不了我们内部信息安全那一关。更麻烦的是销售经常在外网环境或者客户现场网络时好时坏但SaaS CRM一旦离线就基本等于白板连查个客户历史沟通记录都做不到。1.2 现有工具的共性短板重、贵、定制难我梳理了一下我们试用过的CRM工具问题集中在三块痛点类型具体表现对我们团队的影响定制成本高想加一个“项目阶段”字段或者改一下销售漏斗的看板逻辑往往需要买更贵的版本甚至是提工单等排期流程变通慢销售等不起离线能力弱网页版依赖网络桌面版也需要实时联网校验权限和拉取数据出差、客户现场几乎不能用通信数据割裂邮件、电话记录、会议纪要都以“附件”形式存在没有一个统一的视图复盘客户关系时靠人脑回忆一开始我们想过用那种开源CRM改一改最常被提到的SuiteCRM、EspoCRM、Twenty都算了一圈。它们功能确实全但是代码量很大数据库结构也被对方定义死了。我们需要的不是一套完整的CRM系统我们需要的是一个“销售工作台”客户资料、沟通记录、待办任务、项目阶段这四样东西做一个干净好用的桌面端工具不搞复杂的权限体系不搞自动化营销不搞一堆没人用的报表。1.3 定下边界不是重构CRM而是做一个“桌面通信 客户管理”的垂直工具这里必须先说清楚一个判断。任何项目在启动前都要明确“你不做什么”比“你做什么”还重要。我们的目标不是做一个又大又全的通用CRM去跟市面产品对标而是做一个贴合自己销售流程的桌面工具。所以名字也很有意思DeskcommCRM。Desk表示桌面端Comm是Communication通信的缩写组合在一起就是“带通信能力的桌面客户管理工具”。我们给自己的边界定义是客户数据和跟进记录必须完全本地存储同步只是一个备份和跨设备手段通信信息邮件、通话记录能做到自动归集但不做强实时聊天不重复造轮子日历、邮件收发、文件预览这些能力能用成熟组件就用成熟组件部署形态以团队内网自托管为主单机可用不上云这个边界定下来之后整个项目的技术选型就好做多了。2. DeskcommCRM的技术栈选型与整体架构2.1 客户端框架为什么锁定 Electron Vue 3 TypeScript桌面端方案当时考虑过三个Electron、Tauri、Qt。我们团队的技术背景是Web全栈对前端生态最熟Qt直接排除了因为它意味着要引入一套新的C技术栈团队的学习成本太高。Tauri是后来认真评估过的方案。它的安装包确实小打包出来只有几MB内存占用也比Electron低一个量级。但当时Tauri的生态还不够成熟我们需要的几个原生能力——比如系统托盘集成、本地SQLite访问、开机自启动、自动更新——在Tauri里都需要折腾Rust插件很多社区库还处于早期阶段。考虑到DeskcommCRM是给销售团队用的生产工具稳定性优先我们最终选了Electron。技术栈的具体选择Electron 28 electron-builder桌面壳和打包发布Vue 3 TypeScript界面层Composition API组织业务逻辑Pinia客户端状态管理better-sqlite3本地数据库同步API、性能好Fastify本地内置HTTP服务用于处理一些渲染进程不方便直接做的文件操作Tailwind CSS Headless UI界面样式主打简单清晰Electron的方案虽然“重”了一点但是生态成熟我们需要解决的问题基本都有现成答案。事实也证明这个决定节省了大量时间后面遇到的所有兼容性问题基本都能在社区找到类似案例。2.2 服务端与存储本地优先内网同步DeskcommCRM的数据存储采用“本地优先”架构。核心数据落在每台电脑的SQLite数据库里然后通过一个轻量级同步服务把数据增量同步到内网的PostgreSQL数据库中。销售出差时没有网络也能正常查看和录入客户信息等网络恢复后自动把本地变更推送到服务端。之所以用PostgreSQL而不是MySQL原因很简单我们服务端有一个字段是JSONB类型用来存客户资料的扩展属性。PostgreSQL的JSONB支持索引和高级查询这在后期做自定义字段、客户分群时非常方便。MySQL虽然也有JSON类型但灵活性和查询能力比JSONB差一些。服务端是一个Node.js Fastify的小服务部署在内网一台4核8G的小主机上。它做的事不多身份认证、数据同步接口、邮件转发可选、审计日志存储。不做复杂业务逻辑核心业务逻辑全部在客户端。这样设计的好处是即便服务端挂了客户端依然可以正常使用不会影响销售干活。2.3 通信模块的接入方式邮件归集是第一个硬骨头DeskcommCRM里的“Comm”不是简单的联系人通信录而是要自动把往来邮件、通话记录归集到对应的客户下形成一条完整的互动时间线。邮件这块我们用了标准协议IMAP和SMTP。客户端直接连接企业邮箱的IMAP服务器拉取指定文件夹比如“已发送”“销售收件箱”的邮件通过解析发件人域名和收件人地址自动关联到本地客户记录上。后期我们加了POP3的支持但IMAP是主流因为IMAP支持服务端文件夹同步客户端可以记住已拉取的邮件UID避免重复导入。电话记录这块比较朴素从华为、思科等电话交换机系统导出通话记录CSVDeskcommCRM提供一个导入向导按被叫号码匹配客户联系人。如果匹配到就挂到该客户的时间线下面没匹配到的进“未关联通话”池子可以手动归类。当时很多朋友问我为什么不做WebRTC直接集成网络电话。说实话企业销售团队的核心通信还是邮件和手机而且电话系统每家用的都不一样直接做软电话的集成性价比太低。做得浅没意义做得深又需要和不同运营商对接不是一个小团队能做完的事。所以“导入 归集”是投入产出比最高的方案。2.4 前端目录结构与模块划分代码组织这块我花了一些心思。DeskcommCRM本质上是一个“重客户端”应用业务逻辑大部分在本地因此前端不能只按页面组织目录必须按领域模块来划分。src/ ├── main/ # Electron主进程 │ ├── db/ # SQLite初始化、数据访问层 │ ├── sync/ # 同步引擎 │ ├── mail/ # IMAP/SMTP封装 │ └── ipc/ # 主进程与渲染进程通信的IPC handler ├── renderer/ │ ├── components/ # 通用UI组件 │ ├── views/ # 页面级组件 │ ├── stores/ # Pinia状态 │ └── utils/ # 工具函数 ├── shared/ │ ├── types/ # 跨进程共享的TypeScript类型定义 │ └── constants/ # 枚举和常量 └── resources/ # 图标、安装包配置等静态资源这个目录结构最大的好处是“主进程代码”和“渲染进程代码”严格分离。Electron开发最常见的坑就是把所有逻辑都堆在renderer里结果一旦涉及原生模块调用就得用IPC绕来绕去代码混乱不堪。我们从一开始就把数据访问全部放在main进程renderer只通过IPC调用界面层完全不知道数据是怎么存的。3. 核心功能模块的实现细节不是堆功能而是打通流程3.1 客户资料管理的字段设计客户资料是CRM的地基字段设计直接决定后续功能的灵活程度。我们一开始没有像传统CRM那样设计几十个字段而是遵循一个原则核心字段固定化业务字段扩展化。固定字段用数据库表列实现字段名类型说明idTEXTUUID主键nameTEXT客户名称industryTEXT所属行业levelINTEGER客户等级1-5owner_idTEXT负责人IDsourceTEXT客户来源created_atTEXT创建时间updated_atTEXT更新时间业务扩展字段存在一个叫custom_fields的JSONB列中。这样设计的好处是不同行业的销售团队可以根据自己的业务特点配置字段而不用改数据库结构。比如做软件服务的团队可能想知道“客户用的什么技术栈”做制造业的可能关心“设备台数”这些都可以通过界面配置加入自定义字段而不需要写代码。UI上我们采用了“分组折叠”的展示方式基本信息、联系信息、业务信息三个分组。默认折叠业务信息因为大多数情况下销售打开客户详情页是想快速找到联系人电话和最近跟进情况而不是看一堆自定义字段的填表信息。3.2 跟进记录“时间线”的数据模型时间线是DeskcommCRM中最常用、也是我最有心得的一个模块。它本质上就是一个按时间排序的流把不同类型的事件统一展示新建客户、发邮件、收邮件、打电话、线下拜访、报价、签约。数据模型上用了单表继承Single Table Inheritance的思路CREATE TABLE activities ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL, type TEXT NOT NULL, -- email_sent, email_received, call, meeting, deal happened_at TEXT NOT NULL, -- 业务时间不是录入时间 summary TEXT NOT NULL, -- 一句话摘要 detail_json TEXT, -- 详细内容按type解析 created_by TEXT, created_at TEXT NOT NULL );关键点在于summary字段。我们在所有类型的事件写入时都强制生成一句摘要比如“向张总发送了产品报价V3金额28万”。这么做的原因是时间线上如果只显示邮件标题和电话时长用户必须点开才能看到内容而有了摘要用户扫一眼就能知道发生了什么。这是我们自己用了一段时间才总结出来的优化信息获取效率提升非常明显。detail_json字段存储结构化详情取值根据type不同而不同。比如邮箱类型就存发件人、收件人、邮件正文全文电话类型就存通话方向、时长、对方号码会议类型就存会议参与者、地点、纪要。查询历史时只需要查到活动记录这一行按type解析JSON即可不用做多表JOIN。3.3 邮件自动归集的实现细节邮件自动归集是整个DeskcommCRM里最容易出错但最提效的模块。销售再也不用去邮箱里翻历史邮件打开客户详情页就能看到所有往来记录。实现逻辑分几步配置邮箱账号IMAP服务器、账号、密码或授权码密码通过Electron的safeStorage加密存储在本地后台定时轮询默认每5分钟新邮件对每一封新邮件解析发件人地址和邮件主题在本地客户库中查找匹配的客户匹配成功则写入activities表并关联到客户匹配失败则进入“未关联邮件”列表由用户手动关联匹配规则是重点。一开始我们只按发件人域名匹配比如发件人是zhangsancompany.com就在客户资料里找邮箱域名含company.com的记录。但是很多客户的邮箱域名可以不对应比如客户用qq.com个人邮箱联系。后来增加了两个辅助规则邮件正文里出现的公司名称关键词匹配比如邮件签名里有公司全称按联系人邮箱精确匹配客户下的联系人表里有具体邮箱地址匹配过程会产生误匹配所以我们对“自动关联”和“手动确认”做了区分。自动关联的邮件在界面上显示一个小标签“AI-匹配”用户可以一键取消关联。这样可以纠错同时不至于太打扰。3.4 任务提醒与销售漏斗的闭环CRM系统如果没有任务漏斗本质上就是一个客户通讯录。DeskcommCRM的任务模块围绕两条主线待办任务线每一条活动都可以“生成待办”。比如你给客户发了报价单系统会提示“是否需要安排一周后跟进”点击确认后就在任务列表里生成一条跟进任务设置截止时间和提醒。为了避免打扰提醒默认只在桌面通知一次不在系统托盘持续闪烁。销售养成每天上班第一件事看当日待办的习惯比任何激励都管用。销售漏斗线我们将销售阶段定义为七个线索 → 初步沟通 → 产品演示 → 方案报价 → 商务谈判 → 合同审批 → 赢单。每个客户只有一个当前阶段阶段变更会记录到时间线上。漏斗视图是一个简单的横向看板按阶段分列展示客户卡片。但这里我们没有做拖拽调阶段而是要求用户进入客户详情页点击阶段变更按钮。这样每一步阶段变更都会强制填写备注并记录到时间线避免误操作和“不知道谁把客户挪走了”的扯皮问题。4. 数据库设计与多端同步冲突处理4.1 本地优先架构为什么选择SQLite 增量同步DeskcommCRM的同步架构我单独拿出来讲因为这是整个项目里技术含量最高、也最容易被低估的部分。选型方面我们用了SQLite存本地数据同步服务端用PostgreSQL。两个库不需要表结构完全一样本地库可以多一些临时状态字段服务端主要负责合并和分发。每一条业务数据客户、活动、任务都有两个关键字段updated_at业务数据的修改时间updated_uuid每次修改生成一个新的UUID同步引擎的做法是每次启动或手动刷新时客户端向服务端请求last_sync_time之后发生变更的数据同时把本地updated_at大于上次同步时间的变更推上去。两边都维护一个sync_clock记录最近一次成功同步的时间点。这套机制看起来很像增量同步但真正写起来会发现很多细节容易踩坑。比如有些表的记录会物理删除比如用户删了一条误建的客户同步时需要把删除动作也传播到服务端。我们的做法是引入“软删除”所有业务表都增加deleted_at字段删除时只更新deleted_at和updated_at不物理DELET。同步时服务端根据deleted_at分发删除信息其他客户端收到后也执行软删除。4.2 冲突策略最后写入胜出还是版本向量多端同步中冲突是避不开的问题。在DeskcommCRM的典型场景下虽然大多数客户只有一个负责人但偶尔也会出现两个人在同一时间段先后修改同一个客户资料的情况。我们采用的策略是“逐字段最后写入胜出Last Write Wins”但不是简单的整行覆盖。举例来说销售A修改了客户的联系电话销售B在同一时间段修改了客户的地址。如果整行覆盖后提交的人会把先提交的人的修改也盖掉这就产生了数据丢失。我们的做法是把更新粒度降到字段级别客户端在更新一条记录时把发生变更的字段名和值打包成一个changes对象同步时服务端和本地都按字段级别合并同一个字段两个时间版本不同保留updated_at较新的值不同字段互不影响这样修改冲突被控制在字段级别绝大多数情况下两个销售的修改能天然合并不需要人工干预。只有极少数情况下比如两个人都修改了“客户等级”这个字段才会出现“最后提交者胜出”的直接覆盖。我们接受这个策略因为在一个销售团队里同一个客户的同一个字段被两个人同时改本身就是管理流程问题而不是技术问题。4.3 同步引擎的幂等设计与断点续传同步引擎最怕的事情是重复执行。网络传输过程中一个请求可能已经到服务端了客户端因为超时没有收到响应于是重试发送。如果服务端不做好幂等就会产生重复数据。DeskcommCRM的同步接口设计遵循一个原则客户端发来的每一条变更都自带updated_uuidUUID是每次修改时生成的唯一标识。服务端在接收变更时先查询这个UUID是否已经存在存在则直接返回成功不再处理。这样即使客户端重试100次服务端也不会产生两条重复记录。断点续传的逻辑也很简单客户端把待同步的变更按updated_at排序每次最多传200条。服务端处理完返回批次内最大的updated_at客户端用这个值更新本地 sync_clock后继续推下一批。如果网络中途断开客户端保留未确认的批次下次重连继续从last_sync_time开始。整个过程不需要复杂的游标利用业务数据的updated_at排序天然实现了批处理。5. 桌面端体验调优与实际部署中的坑5.1 Electron打包体积与启动速度优化Electron应用被打包工具折腾过的人都有体会安装包动辄两三百MB用户下载安装的体验很差。我们的安装包一开始有180MB左右主要体积来自Electron框架本身、Chromium内核、以及Node原生模块。优化动作分了几步使用electron-builder的asar模式压缩应用代码这一步能省一点点但压缩率有限按平台裁剪Electron二进制Windows下用winCodeSign和nsis配置减小体积对原生依赖better-sqlite3做了electron版本配套的重编译。这一步是个大坑直接安装的better-sqlite3是给Node用的拿到Electron里跑会报“NODE_MODULE_VERSION不匹配”必须用electron-rebuild重新编译原生模块图标资源和示例数据不打包进安装包首次启动时通过内置脚本生成最终安装包从180MB降到了110MB左右虽然和Tauri那种10MB级别的安装包还是没法比但在Electron生态里已经算比较收敛了。启动速度方面销售最不能忍的是双击图标后要等8秒才看到界面。我们用了两个优化开启V8的代码缓存让Vue框架代码在第二次启动时能更快解析把本地数据库的初始化放到主进程的异步任务里先渲染UI骨架再加载真实数据。UI骨架先出用户在500毫秒内就能看到界面数据大概在1秒左右填充完成5.2 离线可用与弱网环境的同步重试销售使用DeskcommCRM最频繁的场景之一就是客户现场。会议室网络质量很差经常是连上了Wi-Fi但丢包率极高。这种弱网环境下同步必然失败此时不能报错就完事必须设计自动重试机制。我们的处理方案是同步引擎维护一个待同步队列当服务端请求失败时按指数退避策略自动重试延迟分别是15秒、30秒、60秒、120秒、最长5分钟一次。重试期间用户操作不受影响照常录入数据。只有当队列积压超过200条时才在界面上提示“本地待同步数据量较大”。离线期间生成的修改在界面上用一个小图标标记为“待同步”用户能直观看到哪些数据还没传到服务端。这点很重要因为销售会担心“我记的客户信息是不是没有保存成功”看到状态标记心里就有底了。5.3 权限与审计销售数据安全的边界自托管CRM最容易被忽视的是权限。很多团队用自研系统“反正只有我们内部用权限不用做太复杂”。但我们内部信息安全管理要求所有涉及客户敏感信息的操作必须有迹可循。DeskcommCRM做了一个轻量的审计日志谁在什么时间查看了哪个客户详情谁导出了客户数据谁修改了报价金额谁删除了一条跟进记录软删除也留痕审计日志本身只写入本地通过同步任务上传到服务端的独立审计库客户端无法修改服务端审计日志。这样即使销售在本地手工改数据库服务端的审计记录也改不掉。权限模型没有做复杂的RBAC只有两个角色普通销售、管理员。管理员拥有全部权限可以查看所有客户数据普通销售只能看自己负责的客户。信息安全管理要求我们最小化权限这个两个角色审计的组合目前够用。6. 上线后的数据复盘与后续演进6.1 团队实际使用情况DeskcommCRM上线到现在已经跑了将近8个月。团队12名销售加4名售前工程师总共16人日常使用。客户数据大约有1200多条关联的邮件活动记录2.6万条整个PostgreSQL数据库占用不到2GB。本地SQLite单机数据量最大的用户有300MB左右查询时间线没有明显卡顿。使用反馈最好的模块是邮件归集和时间线。过去销售要回忆“上个月跟这个客户聊了什么”现在打开客户详情页一目了然。有销售同事原话说“这个功能让我的周报好写了太多”。周报不再是凭记忆拼凑直接把本周跟进记录导出就行。6.2 后续迭代方向计划中的迭代方向有三个邮件摘要通过调用大模型对客户邮件线程自动生成摘要提炼关键决策点。这个功能原型已经跑通但考虑到成本和数据安全还在内部评估移动端只读视图销售在外面想快速查一个客户的联系人和最近动态目前只能开电脑太不方便。计划用Flutter做一个只读的移动端同步走同一套API自定义报表销售负责人想要按行业、来源、阶段维度统计客户分布虽然PostgreSQL查起来很简单但界面还没做6.3 给其他想自研CRM团队的建议最后一条心得送给看到这里的朋友。如果你们团队也在纠结自己写CRM我建议先做一次严肃的现状评估。如果客户量只有几百个、对数据安全要求高、通信数据分散自研一个轻量级桌面CRM完全可行。如果团队已经有几千上万个客户涉及大量跨部门协作和复杂权限建议还是选成熟产品别在业务系统上重复造轮子。自研系统最大的成本不是开发而是维护和迭代。DeskcommCRM能活下来一是因为它的功能边界非常克制二是因为我们真的每天都在用它被自己的工具绑架了所以迭代动力拉满。如果只是“做个系统给别人用”大概率做到一半就凉了。这个项目给我最大的收获不是技术本身而是让我重新理解了“工具为人服务”的含义CRM的核心不是系统而是让销售更有效地跟进客户这件事本身。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。