ARTICLE DETAIL

资讯详情

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

自研轻量级桌面CRM:基于Electron与SQLite的实践与避坑指南

自研轻量级桌面CRM:基于Electron与SQLite的实践与避坑指南 做CRM系统这事我一开始是拒绝的。市面上的CRM工具我前后试了不下十款要么功能堆得像瑞士军刀实际用起来三分之二的功能一辈子碰不到要么按坐席收费团队还没扩到两位数账单倒是先膨胀起来最难受的是数据全在别人服务器上想导个自定义报表出来得先学会跟客服扯皮。后来我实在忍不了决定自己动手做一个——这就是DeskcommCRM的由来。DeskcommCRM不是什么颠覆性的大平台它就是一个轻量、桌面端优先、数据完全本地化的客户关系管理工具核心解决三件事把客户资料和跟进过程管起来把日常沟通记录自动归到对应客户名下以及用一套靠谱的任务提醒机制保证你不漏跟单。我用了大概两个月业余时间做完了核心版本现在已经在自己团队里跑了将近半年稳定性和实用性都经住了考验。这篇文章把我的设计思路、技术选型、核心实现和踩过的坑全部摊开讲适合两类人看一类是跟我一样对现有CRM工具不满、想自建一套的人另一类是打算做桌面端业务应用的开发者我的很多选型思路和处理方式可以直接复用。1. 整体设计与思路拆解1.1 为什么要自研CRM而不是继续买工具先说动机。我团队的业务场景是典型的B2B小团队销售客户量级几千个人员不到十个大家需要共享客户信息但不需要复杂的权限层级。市面上的主流CRM在我看来有三个难以调和的矛盾第一是重。主流CRM为了覆盖大企业的复杂流程底层的对象模型、权限体系、审批流、工作流引擎都是必须的。但对小团队来说这些全是负担。光是把客户-联系人-商机-合同-回款这套标准对象关系配明白就要花掉好几天还得忍受字段布局永远不符合自己习惯的憋屈感。第二是费用结构不友好。很多CRM按用户按月收费人一多一年下来真金白银交出去换来的却是一堆用不上的高级功能。这个账怎么算都不划算。第三是数据自主权。客户数据是一个团队最核心的资产之一。放到第三方平台上一旦对方调整产品策略、改字段限制、停止某些接口你就很被动。我更倾向于把数据握在自己手里本地存储、定期备份随时可以完整导出。所以DeskcommCRM的设计目标一开始就定得很朴素数据本地化界面快跟单流程清晰沟通记录自动归档以及尽可能少的学习成本——全团队没有任何人需要翻说明书。1.2 DeskcommCRM的核心定位与功能边界我给它划定的边界是做精不做多核心模块只有四个客户管理客户档案、联系人、所属行业、来源渠道、标签体系。跟进管理跟进记录、下一步计划、任务提醒、商机阶段。沟通集成邮件往来、通话记录、聊天消息的统一归并自动关联到客户。数据导出所有数据一键导出为CSV/JSON随时可以迁移走。刻意不做的东西也有很多不做多用户云端同步不做复杂的审批流不做权限分级不做移动端App。这些不是不重要而是对于当前阶段的我来说稳定跑起来比功能全重要得多。把边界划清楚之后开发的每一个步骤都变得很明确这也直接避免了中途无限加需求的毛病。1.3 技术选型背后的考量技术栈这块我纠结了一段时间。第一版方案用了C# WPF跑起来确实顺滑但界面开发效率太低改个布局要重新编译迭代速度完全跟不上想法更新的速度。后来我推倒重来换了Electron Vue 3 SQLite的组合这个决策基于四个具体原因跨平台。团队里有人用Windows有人用macOSElectron一次性解决。UI迭代快。Vue的热更新让改界面几乎即时可见两周时间就能把界面从丑小鸭变成能看的样子。SQLite天然适合本地单机应用。一个文件就是整个数据库备份就是复制文件零配置、零运维。生态成熟。无论是邮件解析、日历集成、还是Excel导出npm上都能找到成熟的库不需要自己从头造轮子。这些选型我在做技术评审时列过一个对比表核心权衡是开发效率和数据安全之间的平衡。Electron的内存占用确实比原生应用高但我们的使用场景是长驻后台的桌面工具4GB内存的电脑也能流畅跑这个缺点完全可以接受。2. 核心细节解析与实操要点2.1 客户数据模型不搞对象关系只做扁平化管理很多CRM一上来就搞五层对象关系客户、联系点、商机、合同、订单。我这边的经验是对千级客户量、十人以内团队来说扁平化模型远比复杂关系模型好用。DeskcommCRM的核心表只有五张customers客户主表存公司或个人的基本档案。contacts联系人表一个客户下可有多个联系人。interactions互动记录表所有邮件、通话、面谈记录都统一存在这里。tasks任务表跟跟进计划、待办事项相关。tags标签表用于客户分组和筛选。关键设计决策是不单独建商机表和合同表。原因很实际小团队的成交流程往往模糊销售从一个线索到成交中间可能没有正式的合同阶段。硬性建模反而逼着销售去填一堆虚假字段。我把商机阶段做成了customers表里的一个枚举字段initial → contacted → qualified → proposal → won → lost外加一个expected_amount字段这样既能做销售漏斗分析又不会增加额外的录入成本。客户去重是我在第一个月就踩到的大坑。销售从名片和邮件里手工添加客户几天后库里就出现了大量ABC科技和ABC科技有限公司这种重复条目。后来我在写入流程里加了一层去重逻辑新客户进来时先对公司名做标准化处理转小写、去掉有限公司股份有限公司等后缀、去掉空格再查重相似度超过阈值就提示合并。这块逻辑虽然简单但保存下来的数据质量远比后期清洗划算得多。2.2 沟通记录整合把碎片信息统一归到客户名下沟通记录的采集是这个系统里最琐碎也最关键的模块。我实际接入了三类数据源邮件、通话和微信消息通过手动导入对话记录实现。邮件这块的做法是用IMAP协议定时拉取收件箱然后通过发件人域名和联系人邮箱做匹配自动把往来邮件挂到对应用户名下。这里有一个必须处理的细节同一个客户可能有多个邮箱域名比如一个集团下面有好几个子公司。我加了域名别名功能可以在客户档案里配置多个域名拉取邮件时只要发件人域名命中任意一个别名就会自动归到这个客户名下。这个功能设计起来很简单但在实际使用中将邮件精准归档的比例从六成直接拉到了九成以上。通话记录的处理更蛮力一些中国移动和中国联通都有通话详单导出功能导出的Excel或者CSV文件我直接解析后按电话号码反查联系人。号码匹配时有个坑有的人存在通讯录里的格式是138-xxxx-xxxx详单里却是138xxxxxxxx我写了个号码归一化函数把分隔符全部去掉再比较否则匹配率会惨不忍睹。2.3 任务提醒机制用下一次跟进时间驱动销售动作没有任务提醒的CRM只是客户通讯录加了个壳。DeskcommCRM的任务引擎核心是一句话每次录入跟进记录时强制选择下一次跟进时间。这个时间如果不填系统就会弹一个提示允许暂时跳过但记录上会有一个醒目的红色标记。到点之后任务怎么提醒我做了三层递进机制第一层是应用内的待办列表按时间倒序排列第二层是系统通知通过Electron的Notification API实现电脑右下角会弹出提醒第三层是每日摘要邮件每天上午九点把当天到期和已经逾期的所有任务发到销售邮箱。这个机制运行下来效果非常明显。团队里原来总有销售说我记着这个客户呢就是最近太忙没顾上有了系统强制记录下一步动作之后忘了跟这个借口基本消失了。我个人体会是下一步跟进时间是CRM系统里最有价值的一个字段它比任何花哨的商机预测算法都管用。3. 实操过程与核心环节实现3.1 环境搭建和项目初始化DeskcommCRM的实际代码我托管在GitLab私有仓库里基础结构是标准的Electron Vue 3项目。初始化那步我不多废话直接上关键命令npm create vuelatest deskcomm-crm cd deskcomm-crm npm install electron electron-builder better-sqlite3 imapflow dayjs几个依赖库的用途分别说一下electron和electron-builder负责桌面壳和安装包打包better-sqlite3是SQLite的同步驱动比异步驱动写起来爽得多桌面应用的数据量完全不需要异步imapflow是拉取邮件用的协议实现完善文档也清楚dayjs处理时间轻量好用。有一点要特别提醒better-sqlite3是原生模块在Electron里使用需要重新编译否则会出现版本不兼容的报错。我用的解决方式是安装electron-rebuild工具在postinstall阶段自动重新编译npm install --save-dev electron-rebuild # package.json 里加一行 # postinstall: electron-rebuild -f -w better-sqlite3不处理这一步的话大概率会在应用启动时遇到was compiled against a different Node.js version之类的错误这个坑我第一次搞的时候卡了整整一个下午。3.2 核心表结构与建表SQL下面是DeskcommCRM的实际建表SQL我精简掉了次要字段保留最核心的结构方便你直接参考CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, normalized_name TEXT NOT NULL, industry TEXT DEFAULT , source TEXT DEFAULT , stage TEXT DEFAULT initial, expected_amount REAL DEFAULT 0, owner TEXT DEFAULT , notes TEXT DEFAULT , created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), name TEXT NOT NULL, email TEXT DEFAULT , phone TEXT DEFAULT , title TEXT DEFAULT , is_primary INTEGER DEFAULT 0 ); CREATE TABLE interactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), contact_id INTEGER REFERENCES contacts(id), type TEXT NOT NULL, -- email / call / meeting / note direction TEXT DEFAULT in, -- in / out content TEXT DEFAULT , happened_at TEXT NOT NULL ); CREATE TABLE tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), title TEXT NOT NULL, due_at TEXT NOT NULL, done INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE INDEX idx_customers_normalized_name ON customers(normalized_name); CREATE INDEX idx_interactions_customer ON interactions(customer_id); CREATE INDEX idx_tasks_due ON tasks(due_at, done);几个值得说清楚的设计点。首先所有时间字段我都存的本地时间字符串而不是Unix时间戳原因是SQLite的datetime函数处理起来直接而且这个系统不涉及多时区协作没必要引入复杂的时间处理逻辑。其次normalized_name字段是给去重逻辑预留的索引每次插入客户时程序会生成归一化名称并存入这个字段查重时直接索引查询速度飞快。3.3 关键功能代码跟进记录写入与任务生成跟进记录写入是销售每天用的最多的入口。我把它做成一个组合操作销售选择客户、填写跟进内容、选择沟通类型、填写下次跟进时间然后一次提交。后端逻辑保证两件事同时完成——写入互动记录生成对应任务。核心代码简化如下function addInteractionWithNextTask({ customerId, contactId, type, direction, content, nextFollowUpAt }) { const insertInteraction db.prepare( INSERT INTO interactions (customer_id, contact_id, type, direction, content, happened_at) VALUES (?, ?, ?, ?, ?, ?) ); const insertTask db.prepare( INSERT INTO tasks (customer_id, title, due_at, done) VALUES (?, ?, ?, 0) ); const updateCustomer db.prepare( UPDATE customers SET stage ?, updated_at datetime(now, localtime) WHERE id ? ); const now dayjs().format(YYYY-MM-DD HH:mm:ss); const transaction db.transaction(() { insertInteraction.run(customerId, contactId, type, direction, content, now); if (nextFollowUpAt) { insertTask.run(customerId, 跟进${content.slice(0, 30)}, nextFollowUpAt); } // 简单自动推进阶段如果客户还在 initial 状态录入跟进后至少推进到 contacted const customer db.prepare(SELECT stage FROM customers WHERE id ?).get(customerId); if (customer.stage initial) { updateCustomer.run(contacted, customerId); } }); transaction(); }注意我把三张表的写入包在了同一个事务里这是保证数据一致性的关键。如果分开执行万一写入互动记录后、写入任务前程序崩溃就会出现有记录没任务的脏数据。better-sqlite3的事务API是同步的不需要await在事务内部抛异常会自动回滚用起来非常舒服。销售漏斗阶段自动推进这段逻辑是我后期加的。最开始所有阶段都靠销售手工更新结果忙起来根本没人管。后来改成只要录了跟进记录至少把客户从initial推到contacted阶段数据就开始变得可信了。当然这只解决了最小推进问题更精细的逻辑比如发了报价单就自动标记为proposal我留到了后续版本。3.4 数据导出与备份给自己留好退路数据安全这块我没有搞花哨的远程灾备而是坚持了三个朴素原则每日全量备份、每周测试恢复、导出格式永远保持通用。备份功能我写了一个定时脚本每天凌晨三点调用better-sqlite3的backup方法把数据库文件复制一份到本地备份目录并保留最近三十天的版本db.backup(/path/to/backup/deskcomm- dayjs().format(YYYYMMDD) .db) .then(() console.log(backup done)) .catch((err) console.error(backup failed, err));SQLite的backup API是官方推荐的方式它能保证即使在备份过程中有新的写入也不会产生损坏的备份文件。这比我最初的方案——直接用文件复制命令——靠谱得多。我实际测试过在数据库正在被写入时直接复制文件有一定概率复制到损坏的中间状态恢复的时候会报database disk image is malformed到那时候心态就真的崩了。数据导出方面DeskcommCRM在设置页里提供了两个按钮导出CSV和导出JSON。CSV用于Excel打开做常规分析JSON用于完整的无损迁移。导出逻辑很简单就是遍历所有表并序列化但有一个细节值得提SQLite的TEXT字段内容里可能包含换行符和逗号直接拼CSV会把列弄乱。我用了npm上的csv-stringify库来生成标准格式的CSV而不是自己手写字符串拼接。这个看起来不起眼的决定省下了我在Excel里对着乱掉的表格骂街的时间。4. 常见问题与排查技巧实录4.1 问题速查表把我在开发和实际使用中最常遇见的七个问题整理成了一张表你可以直接对照排查问题现象根本原因解决办法启动报错was compiled against a different Node.js versionbetter-sqlite3 未重新编译运行npx electron-rebuild -f -w better-sqlite3邮件拉取超时或一直转圈IMAP连接未处理空闲超时检查网络给imapflow设置connectionTimeout参数数据库文件越来越大半年涨到2GBinteractions表无限增长且无归档策略三个月前已关闭客户的记录做归档单独存到archive表任务列表卡顿一次渲染几千条任务DOM节点改为虚拟滚动只用列表项的索引数据拉取子项邮件重复归因到两个客户两个客户配置了相同的域名别名写入前增加唯一性校验别名全局不可重复客户列表搜索慢没有给name字段建索引执行CREATE INDEX idx_customers_name ON customers(name)备份文件恢复时报格式错误备份时直接复制数据库文件而非使用backup API改用db.backup()或先做SQLite Online Backup4.2 一个典型的排查过程任务提醒消失有一次团队成员反馈某几条任务的系统提醒没弹出来但应用内的待办列表里能看到。我排查的思路分享给你这类问题在Electron应用里很典型。第一步先确认时间字段是否异常。查数据库后发现任务的due_at字段是正常存储的没有为空也没有乱码。第二步怀疑系统通知权限。Electron的Notification在Windows上需要应用具备发送通知的权限如果用户在系统设置里关掉了该应用的通知权限API调用不会报错通知会静默失败。我检查了系统设置确实如此。第三步我发现另一个更隐蔽的问题任务提醒模块只在启动后的前十分钟执行了一次扫描之后就没有再扫描了。也就是说如果应用持续开着新产生的任务永远不会触发通知。这是一个低级但很难发现的逻辑错误——因为开发时我一直在重启应用每次重启都会扫一次到期的任务所以从来没见过这个bug。修复方式是把定时扫描改成每五分钟检查一次同时每次新增任务后主动触发一次检查。这个案例的教训是本地应用的定时任务一定要明确测试长时间运行的场景不能只测短时间的。4.3 避坑经验接口和界面的先后顺序最后分享一个在架构层面的避坑经验。DeskcommCRM刚开始开发时我直接把界面组件和数据访问逻辑写在一起按钮点击事件里直接操作数据库。在只有十几个页面的阶段这种写法确实挺爽写起来快也不需要维护接口层。但是当我开始做邮件自动归类时问题来了邮件拉取是一个后台常驻进程它也需要访问数据库而且需要和界面进程共享同一份数据库连接。我把代码拆成了多个模块后发现每个模块里都散落着SQL语句逻辑重复严重。后来我花了一个周末做了重构把所有数据库访问收敛到一个repository层所有页面和后台进程都只调用repository暴露的方法不直接操作数据库。这个重构让代码清晰度提升了好几个量级也让后续新功能的开发速度快了很多。我的建议是哪怕做一个内部工具也值得在前期就做好分层。不一定要用复杂的设计模式但界面不碰SQL、后台只有业务逻辑这个原则越早遵守越省钱。5. 数据统计与使用效果复盘5.1 从数据看团队使用情况DeskcommCRM跑了大半年之后我导出了一份使用数据来复盘。以下是我们团队的真实数字供你参考这个系统的实际效果客户总数从起步时的800多个增长到2600多个重复率始终控制在3%以内这归功于建表时的归一化去重逻辑。销售录入跟进记录的习惯逐步养成。第一个月人均每天录入不到3条第五个月稳定在8到10条。我的判断是这个增长主要来源不是考核压力而是任务提醒机制让录记录这件事有了即时回报——录完就能生成下一步任务不录就等着挨系统提醒。邮件自动归档的命中率在配置域名别名后稳定在92%左右。剩下8%大多是营销邮件和陌生联系人的首次来信会被放进一个未关联收件箱由销售决定挂到哪个客户名下。从stage分布来看client桶里proposal阶段的比例从最初的15%提升到了27%。原因是销售不再忘记给客户发完报价单后继续推进系统会在跟进提醒里明确提示这个客户在proposal阶段已经待了很久。我没有在这套系统里做过复杂的收入归因分析但一个直观的结果是团队月均在建客户数比之前用共享Excel表格管理时期增加大约四成。这个提升我不敢全部归功于DeskcommCRM但至少可以确认它没有拖后腿。5.2 这套系统适合什么样的人参考如果看完上面的内容你也动了要搞一个同类系统的念头我建议先冷静评估一下自己的场景。DeskcommCRM这套方案最适配的特征是客户量在五千以内、团队人数不超过二十、业务以线下沟通和邮件沟通为主、不需要跨地域实时协作。如果你的业务是需要多人同时对同一批客户发起协作、有严格的权限管控需求、或者需要和公司现有ERP系统深度对接那我还是推荐先看看市面上的成熟产品。自研工具的甜区是自己最懂自己的流程但甜区之外的成本很容易失控。另外要说清楚的是我这个版本的DeskcommCRM没有做局域网多人共享每个销售的数据存在自己的电脑上通过定期导出合并来汇总。这个模式对一人一摊业务、客户不重叠的场景够用但如果团队有共同跟进一个客户的强需求就必须引入一个中心化的服务端了。那块工作量和本地版完全不是一个量级我目前还没有动手但这确实是下一步最该做的事。6. 后续可以扩展的方向DeskcommCRM在我脑海里的路线图里至少还有四个值得做但还没做的方向第一个是数据看板。现在我只能通过SQL查询去手工统计分析比如跟进频率、阶段分布、成交周期这些指标。做一个可视化看板挂在侧边栏让销售和管理者一眼看到当前客户池的健康度这件事投入产出比很高。第二个是插件机制。现在每次要加一个新功能或者接一个数据源都得改主程序代码。如果能做成插件模式让不同的团队可以只加载自己需要的模块系统的适用范围会大很多。第三个是移动端场景。我自己最常用的一个场景是拜访客户后在回去的路上顺手记录沟通要点这时候没有移动端就只能靠记忆撑到回办公室。用一个轻量的移动配套应用解决这个场景会显著提升录入的及时性和数据质量。第四个是AI辅助。这里我指的不是什么颠覆性的功能而是很基础的实用场景自动总结一段沟通记录的核心要点生成下一步跟进建议或者根据历史跟进模式提醒你可能要流失的客户。这些功能不大但能实打实减少销售的重复劳动。这些方向里我目前最看好第四个因为对销售的体验提升最直接。不过考虑到团队规模短期内我还是会把精力放在稳定性和已有功能的打磨上把地基打牢比不断加盖楼层更重要。做了这么久我最深的一点体会是工具的价值不在于功能有多少而在于它是不是真的贴合使用者的习惯和流程。DeskcommCRM谈不上精致甚至代码里还有不少可以优化的地方但它是我团队每天真正在用的东西是陪着我们从一张Excel表格一路走到两千多个客户的老伙计。如果你也在跟CRM工具的别扭感作斗争希望这篇文章能帮你少走一些弯路不管是自建还是选购想清楚自己的需求边界永远是最值钱的一步。
返回列表