
1. 项目背景与定位思考1.1 为什么需要一个桌面端CRM先说说DeskcommCRM从哪来。团队之前用过好几款在线CRM功能确实多但实际用起来总觉得隔着一层。最典型的痛点有三个一是所有操作都得在浏览器里完成开几十个标签页切来切去客户信息跟着找半天二是数据全在云端公司内部网络一波动整个客户管理流程就卡住了三是权限和自定义字段往往被平台固化想按自己团队的销售节奏调整一下字段还得联系客服走工单流程。这其实不是个别现象。我们团队做完一段时间的客户运营之后发现手头的客户数据分散在Excel、邮箱、聊天记录、电话记录好几个地方真正需要对着客户说话的时候反而没有一个统一的桌面窗口。于是就有了DeskcommCRM——一个以桌面端为主形态、把客户通讯和客户管理结合起来的轻量级CRM系统。这个项目的定位非常明确不做大而全的通用客户管理平台而是做销售和客服团队日常真正会打开、愿意把信息填进去的管理工具。它的核心使用场景是销售人员每天打开电脑第一件事就是看今天要跟进的客户列表直接在桌面窗口里完成客户资料查看、沟通记录录入、任务提醒、数据检索不需要跳浏览器。1.2 适用场景与目标用户从实际需求倒推DeskcommCRM最适合下面几类用户小微销售团队人数在5到30人之间不需要复杂的组织架构和跨部门审批流需要的是快速记录和跟进客户。客户量大、信息维度多的个人从业者比如保险经纪、房产顾问、外贸业务员他们的核心诉求是凡是见过面的客户都要能快速查得到、跟得上。对数据隐私要求高的团队客户资料属于核心资产不希望全部放在第三方平台上更倾向于本地化存储或者私有化部署。已经用过在线CRM但觉得太重的团队管理后台比业务功能还复杂销售根本不想登录结果客户信息还是躺在Excel里。至于为什么选择桌面端而不是Web端后面我会专门讲讲这个选型思考。这里先给结论对于高频、多窗口、需要持久在线和跨系统复用的场景来说桌面端反而比Web端更轻——因为不用打开浏览器不用担心一个标签页崩溃把所有工作都带走。1.3 项目完成形态概览当前DeskcommCRM已经跑通了一个可用的版本核心模块包括客户资料统一管理支持自定义字段、标签体系、客户分组、生命周期状态。跟进记录时间线每次沟通、每次会议、每次邮件往来都能沉淀到客户档案中。任务与提醒以待跟进为维度按时间窗口列出当天应该联系的客户并支持自定义周期提醒。客户检索与筛选支持关键字全文检索以及按标签、状态、负责人、时间范围做组合筛选。数据看板很简化的一个销售漏斗但够用能看出每个阶段有多少客户、总体的转化率大概是什么水平。本机数据备份与导入导出所有数据默认存在本地数据库可以通过脚本定期备份也能从Excel批量导入客户资料。这个项目不算大但在实际使用中确实把散落的客户信息收拢到了一个窗口里。下面把开发过程中的思路、架构、核心实现和遇到的问题都摊开来说。2. 技术选型与架构设计2.1 客户端框架为什么选Electron这里先统一说清楚桌面端是什么意思。在这个项目里桌面端是指用户安装一个客户端程序双击图标就能打开不需要浏览器。桌面端的框架主流选择有三个Electron、Tauri各自的生态还有QT、WPF这类原生方案。选Electron的原因很直接团队的技术栈主要在JavaScript/TypeScript这一系Electron可以用一套代码同时覆盖桌面端和后续可能的Web端开发效率最高。更重要的是Electron生态里有很多现成的UI组件和Node.js模块可以直接用做CRM这种表单密集、列表密集的应用省非常多时间。当然Electron也有明显的缺点比如打包体积大、内存占用高。但放在CRM这个场景里用户基本是办公电脑内存普遍在16G以上这点开销完全能接受。相比之下如果投入时间用QT或WPF来做界面组件的开发量和UI美化成本会高出好几个量级对于一个小团队来说不划算。还有一条重要的理由后续项目如果要做多端复用——比如把客户数据同步到一个移动端查看——基于Web技术栈的Electron可以直接复用大量逻辑代码不用推倒重来。2.2 界面与交互React Ant Design界面层选了React组件库选了Ant Design。这不是什么新鲜的组合但这个组合在CRM场景里非常合适因为Ant Design本身覆盖了大量中后台场景的组件表格、表单、级联选择、日期选择、弹窗、抽屉、步骤条基本不用自己造轮子。在具体的界面规划上DeskcommCRM的布局分三块左侧是导航栏放客户列表、今日跟进、任务中心、数据看板几个入口。中间是列表区展示客户概要信息支持勾选、标签展示、排序。右侧是详情抽屉点击列表中的某一客户右侧滑出该客户的完整档案包括基本信息、跟进时间线、下次跟进提醒。这个布局参考了邮件客户端的交互习惯——左边目录、中间列表、右边详情——用户不需要学习成本打开就知道怎么看。跟客户通讯相关的操作都集中在详情抽屉里记录一通电话、记一次微信沟通、写一封邮件草稿并标记发送状态都在同一个界面内完成不需要切换到另一个窗口。2.3 数据存储SQLite为什么够用这是DeskcommCRM和很多在线CRM的一个核心区别数据默认存本地用的数据库是SQLite。很多人在设计应用时一上来就考虑MySQL或者PostgreSQL但对一个单机为主、最多局域网内共享数据的CRM来说这其实是想多了。SQLite嵌入在应用程序内部不需要单独启动数据库服务数据直接保存在一个文件里。对于几百MB以内的数据量SQLite的查询性能和稳定性完全够用而且备份只需要复制一个文件特别适合做本地优先的桌面工具。当然纯本地存储会带来一个天然的短板——换一台电脑数据怎么带过去我在DeskcommCRM里的方案是做两件事在设置界面里提供数据导出功能一次导出一份完整的JSON或CSV。把数据文件本身放到一个用户指定的目录比如NAS同步目录或网盘目录这样数据库文件天然会被同步工具备份到别的地方相当于实现了一种半自动的云同步。这个方案不是什么高技术含量但对内部工具来说非常灵活。团队里谁愿意用云同步谁就自己开同步不愿意的数据就老老实实待在本地硬盘上。2.4 桌面与业务逻辑的通信IPC设计与数据流Electron开发里有一个关键概念必须正确处理主进程和渲染进程。简单说渲染进程就是用户看到的界面跑的是前端代码主进程是Node.js环境可以操作文件、数据库、系统能力。两者之间不能直接调变量必须通过IPC进程间通信来传消息。DeskcommCRM的IPC设计走了统一事件通道的路线。我没有为每一个数据库操作单独注册一个IPC事件而是定义了一套标准化的接口格式// 在主进程中注册一个通用的数据操作处理器 ipcMain.handle(db:call, async (event, payload) { const { operation, table, data, condition } payload; // operation 取值: insert | update | delete | query | queryOne const result await databaseHandler(operation, table, data, condition); return result; });渲染进程调用时统一走const result await window.desktop.db.call({ operation: query, table: customers, condition: { status: 跟进中 } });这样做的最大好处是渲染进程不需要关心数据库SQL怎么写主进程内部把SQL语句统一管理起来同时前端在数据库结构变更时可以做到不改调用方式只改参数。在实际开发中Electron的IPC有一个很坑的细节传输的数据必须是可以被JSON序列化的比如Date对象经过IPC传输后会变成字符串。这个问题我踩过一次实打实的坑后面在常见问题部分详细展开。3. 数据模型与核心表设计3.1 客户表基础信息的字段取舍DeskcommCRM的客户表设计走了最少必要字段 自定义扩展的路线。内置字段只保留最核心的几个客户名称、客户类型企业/个人、归属销售、创建时间、更新时间、状态、标签、备注。为什么不做一堆细分字段我在早期版本里设计过30多个字段比如客户来源、行业、规模、区域、预算等等结果实际录入时发现很尴尬——销售人员根本没耐心填那么多信息字段越多录进去的数据质量越差。后来果断砍掉大部分字段把需求放到自定义字段来解决。自定义字段是一个很实用的设计用户在界面上可以自己添加文本、数字、日期、下拉选项类型的扩展字段存到一张独立的custom_fields表里和内置字段分开管理。存储结构简单粗暴CREATE TABLE custom_fields ( id INTEGER PRIMARY KEY AUTOINCREMENT, field_name TEXT NOT NULL, field_type TEXT NOT NULL, -- text | number | date | select field_options TEXT, -- 下拉选项JSON 数组格式 is_active INTEGER DEFAULT 1 ); CREATE TABLE custom_field_values ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, field_id INTEGER NOT NULL, field_value TEXT );这种设计的核心思想是把字段结构和字段值分开。这样用户怎么改字段结构都不需要动数据库表结构灵活性极高。缺点也很明显——查询自定义字段时需要多一次关联查询但对数据量不大的CRM来说这个性能损失微乎其微。3.2 跟进记录时间线一张表撑起整个客户历史跟进记录是CRM系统里最核心的数据它记录了这个客户跟我之间的每一次互动。DeskcommCRM的做法是建一张interactions表字段包含客户ID、跟进方式电话/微信/邮件/面谈、跟进内容、下次跟进时间、创建人、创建时间。关键设计在于这张表同时承载了两种查询需求——查看某个客户的全部跟进历史时间线以及查看今天所有需要跟进的客户任务提醒。CREATE TABLE interactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, interaction_type TEXT NOT NULL, content TEXT, next_follow_up_time DATETIME, owner_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_interactions_customer_time ON interactions(customer_id, created_at DESC); CREATE INDEX idx_interactions_next_follow_up ON interactions(next_follow_up_time);第二个索引很关键。每天打开应用首页的今日跟进就是从这张表里查next_follow_up_time在当天的记录然后再关联客户表拿客户信息。一张表同时支持了记录历史和待办提醒避免了一套数据拆到两张表、同步麻烦的问题。3.3 任务与提醒基于事件的动态计算关于任务我一开始的想法是建一张独立的任务表用定时任务去扫描。后来发现这有点过度设计。CRM里的任务大多数不是独立创建的而是跟进一个客户之后顺带产生的下次计划。所以任务本质上就是一条还没发生的跟进记录。DeskcommCRM的做法是不单独建任务表而是在interactions表里用next_follow_up_time字段来表示未来的跟进计划。查询时只需要一条SQLSELECT i.*, c.name AS customer_name, c.phone AS customer_phone FROM interactions i JOIN customers c ON i.customer_id c.id WHERE i.next_follow_up_time IS NOT NULL AND date(i.next_follow_up_time) date(now, localtime) AND i.done_flag 0 ORDER BY i.next_follow_up_time ASC;当这条未来的跟进记录被完成时只需更新它的content字段并新增一条新的记录原有的记录变成历史新的记录带着下个节点。这样做的好处是整个客户的跟进链路是一条连续的时间线不会出现任务和记录对不上号的情况。3.4 标签与分组用空间换时间标签体系在整个CRM里属于加分项但如果设计不好会变成负担。DeskcommCRM里的标签设计遵循一个原则标签是枚举值而不是自由文本。所有标签在后台先维护好用户打标签时从预设列表里选而不是随意输入。这样做的直接好处是筛选时不会出现A类客户和a类客户、重点客户和重要客户并存的情况。实现上用了标签表和客户标签关联表CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, color TEXT DEFAULT #1677ff ); CREATE TABLE customer_tags ( customer_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (customer_id, tag_id) );查询某个标签下的客户时SELECT c.* FROM customers c JOIN customer_tags ct ON c.id ct.customer_id JOIN tags t ON ct.tag_id t.id WHERE t.name 重点客户 OR t.name 待成交;标签算是一种轻量级分组比文件夹式的分组灵活得多。同一个客户可以属于多个标签这符合销售场景的真实需求——重点客户和华东区客户并不冲突两者可以同时存在。4. 核心功能实现与界面逻辑4.1 客户列表从数据到可操作的界面客户列表是CRM的门面也是用户每天停留最久的界面。DeskcommCRM的客户列表在体验上做了一些取舍。第一列表默认只展示核心字段客户名称、类型、状态、标签、下次跟进时间、归属销售。次要信息放到展开行里点击向下的箭头才会显示。这样做的目的是让列表保持清爽一屏能看20个客户而不是被30个字段挤得呼吸困难。第二列表支持排序。最常见的操作是按更新时间排序这样哪些客户最近有动态、哪些客户已经好久没碰过一眼就能看出来。按下次跟进时间排序也很有用可以把今天该联系的客户顶到最上面。第三列表自带快速筛选栏。不是那种复杂的条件筛选器就是几个下拉框加一个输入框。下拉框分别管状态、标签、归属销售输入框做客户名称或手机号的模糊匹配。// 列表查询的核心组装逻辑 function buildQuery({ keyword, status, tagId, ownerId }) { const conditions []; const params {}; if (keyword) { conditions.push((c.name LIKE kw OR c.phone LIKE kw OR c.email LIKE kw)); params.kw %${keyword}%; } if (status) { conditions.push(c.status status); params.status status; } if (ownerId) { conditions.push(c.owner_id ownerId); params.ownerId ownerId; } if (tagId) { conditions.push(EXISTS (SELECT 1 FROM customer_tags ct WHERE ct.customer_id c.id AND ct.tag_id tagId)); params.tagId tagId; } const whereSql conditions.length ? WHERE ${conditions.join( AND )} : ; return SELECT c.* FROM customers c ${whereSql} ORDER BY c.updated_at DESC LIMIT 200; }这里加了一个LIMIT 200算是有意的设计。CRM列表翻到第三页之后用户的注意力基本已经跟不上了强制限制返回行数反而能让查询更快避免一次加载几千条客户数据导致界面卡顿。真正的全量数据检索交给搜索功能去做。4.2 客户详情抽屉把跟客户有关的一切收拢到一起详情抽屉是使用频率最高的界面我在这块花了最多心思。抽屉布局从上到下分成五块头部区客户名称、客户类型、标签、状态这一行是用户判断这个客户现在怎么样的第一眼信息。联系方式区电话、邮箱、微信、地址每个联系方式后面都带一个快捷按钮。点电话会弹出记录通话的操作框点邮箱会生成一封邮件草稿。基本信息区内置字段加自定义字段按顺序展示支持就地编辑。跟进时间线区这个客户所有的跟进记录按时间倒序排列每条记录显示跟进方式、内容摘要、操作人和时间旁边有一个添加跟进的按钮。下一步计划区显示该客户对应的next_follow_up_time可以直接修改。通讯相关的地方在桌面上天然有优势。比如点电话按钮可以用window.open(tel: phone)唤起桌面端的拨号应用点邮件按钮可以用系统自带的方式唤起默认邮件客户端。这比Web端的体验要顺畅很多因为不需要浏览器权限、不需要OAuth授权直接就是系统能力。// 在渲染进程中实现电话和邮件唤起 function callCustomer(phone) { // 通过主进程调用系统协议 window.desktop.shell.openExternal(tel:${phone}); } function emailCustomer(email) { window.desktop.shell.openExternal(mailto:${email}?subject跟进沟通); }这里我特意把shell调用包了一层IPC没有直接用Electron的shell.openExternal原因是以后如果想把桌面端能力迁移到Web端或者换成Tauri做新版本只需要改这一层封装就行。4.3 跟进记录与时间线操作路径最短化新增跟进记录的操作我在设计上要求三步以内完成。第一步在客户详情抽屉的底部找到 添加跟进按钮。 第二步选跟进方式默认是上一次使用的方式填跟进内容设置下次跟进时间。 第三步点保存。为什么这么在意步骤数量因为销售人员记跟进记录最大的心理障碍不是打字而是流程太长了。如果点一个跟进记录需要跳转页面、填十几个字段用户就会选择不填。实际上绝大多数跟进记录只需要三样信息方式、内容、下次时间。所以这个表单就只放这三个字段加一个是否完成本次跟进的开关其他信息比如客户状态调整放在表单下方的折叠区里非必要不展开。时间线的展示也做了一点小优化同一天的记录会折叠成下午 14:30 两通电话、1次面谈这样的小组点击展开才看到每条明细。这样既避免了时间线冗长又能保留完整的记录。4.4 今日跟进视图让系统主动找人干活这是DeskcommCRM里最受欢迎的一个功能。打开应用默认落到今日跟进页页面顶部是三张统计卡片今日应跟进的客户数量、已完成跟进的数量、逾期未跟进的数量。下面就是具体的客户列表每行显示客户名称、上次跟进时间、计划跟进时间以及一个开始跟进按钮。点击按钮直接打开该客户的详情抽屉并自动弹出新增跟进的表单光标停在内容输入框里。这样做是把待办这个概念做成了默认动作。用户不需要自己判断应该跟进谁系统已经算好了。对于销售管理来说这比所有客户都在列表里你自己看要高效得多。上面说到的逾期未跟进是这样计算的SELECT COUNT(*) FROM interactions WHERE next_follow_up_time IS NOT NULL AND done_flag 0 AND next_follow_up_time datetime(now, localtime)逾期列表默认不展开因为一旦逾期记录太多会让用户产生反正也完不成了的破罐子破摔心态。点开折叠面板才看得到算是产品层面的一点小心机。4.5 销售漏斗看板展示最少但最有用的统计看板我做得很简单只有四列潜在客户、跟进中、报价中、成交客户。每个阶段下面列出客户数量旁边标注占总数的百分比。为什么不做那种复杂的多维度漏斗因为到了字段和权限复杂到一定程度大多数销售管理者真正盯的指标只有两个线索转化率和新成交客户数。前者看客户的整体质量后者看近期的产出。漏斗数据的SQL不复杂SELECT SUM(CASE WHEN status 潜在 THEN 1 ELSE 0 END) AS potential_count, SUM(CASE WHEN status 跟进中 THEN 1 ELSE 0 END) AS following_count, SUM(CASE WHEN status 报价中 THEN 1 ELSE 0 END) AS quoting_count, SUM(CASE WHEN status 成交 THEN 1 ELSE 0 END) AS won_count FROM customers WHERE owner_id ownerId AND deleted_flag 0;虽然统计简单但这个看板对团队的日常工作节奏有比较实际的作用。每天早上看一次漏斗就知道当前的精力应该重点放在哪个阶段——是跟进中的客户太多需要推进还是潜在客户太少需要开拓新线索。5. 实操过程中的问题排查与经验记录5.1 Electron IPC 传参中的Date类型丢失这是开发中遇到最隐晦的一个问题。跟进记录里的next_follow_up_time在前端用的是JavaScript的 Date 对象在界面上显示、修改都很正常。传到主进程写进数据库时发现存进去的时间变成了字符串格式比如2024-11-28T14:30:00.000Z而且有时候还会因为时区问题差几个小时。排查后发现这是Electron IPC的结构化克隆造成的问题。Electron的IPC在传输数据时会做序列化Date对象经过序列化再传回渲染进程时并不会还原成Date对象而是保留成ISO字符串。如果代码里没有做类型校验直接用这个字符串去做比较就会出现日期看起来一样但实际对不上的bug。我的解决方案是在主进程写入数据库前统一做一次数据规范化function normalizeDate(value) { if (value instanceof Date) { return formatDateTime(value); } if (typeof value string !isNaN(Date.parse(value))) { return formatDateTime(new Date(value)); } return value; }这个函数对所有进入数据库的时间字段做一次格式化统一成YYYY-MM-DD HH:mm:ss的本地时间字符串。从此数据库里存的时间格式统一了查询和展示都不用再担心时区或类型问题。5.2 SQLite 并发写入时的锁冲突做数据导入功能时遇到过SQLite的经典问题——写入冲突。一次性导入几百条客户记录时主进程用Promise.all同时发了多个插入操作结果SQLite报错SQLITE_BUSY: database is locked。SQLite在默认配置下同一时刻只允许一个写事务。多条插入并发执行时后面的写操作必须等待前面的完成。解决这个问题有两个方向方向一打开WAL模式允许读写并发写入之间仍需排队。方向二在代码层把批量写入改成串行。我两个方向都做了。首先在初始化数据库时执行db.exec(PRAGMA journal_mode WAL;); db.exec(PRAGMA busy_timeout 5000;);WAL模式能显著提升并发读写性能busy_timeout让SQLite在遇到锁时等待而不是立刻报错。同时在导入模块里不再用Promise.all并发插入而是改成逐条写入并开启一个事务包裹所有插入操作db.exec(BEGIN TRANSACTION;); for (const row of rows) { db.prepare(INSERT INTO customers ...).run(row); } db.exec(COMMIT;);这个改进的效果很明显。原来导入500条客户记录大约需要3到4秒开启事务后基本在1秒以内完成。5.3 大型客户列表渲染卡顿处理客户列表超过1000条之后界面开始出现明显的卡顿尤其是快速滚动时。排查发现瓶颈不在SQL查询速度而在React渲染层——列表项组件太重了。每个列表项都包含客户名称、标签、状态等多个元素而且标签的颜色需要根据标签ID去查配置。1000多个组件实例同时渲染还要频繁更新性能自然扛不住。我的优化方案是三步第一步引入react-window做列表虚拟化只渲染可视区域内的行滚动时动态替换。这个库用起来并不复杂import { FixedSizeList as List } from react-window; List height{600} itemCount{customerList.length} itemSize{56} width100% {({ index, style }) ( CustomerRow customer{customerList[index]} style{style} / )} /List第二步把标签配置在进入列表页时预加载到前端全局状态里渲染标签时直接查本地Map不再做异步请求。第三步列表的排序和筛选操作改为防抖处理输入关键字后等200毫秒没有继续输入才触发查询。经过这三步3000条客户的数据量下滚动和筛选都保持了流畅的体验。5.4 数据安全性防手滑、防误删、防丢数据数据安全这部分在我的优先级里排得很高。本地数据库文件虽然简单但正因为简单反而容易遇到没人意识到数据会丢的问题。我在DeskcommCRM里加了四层防护第一层客户记录默认不做物理删除而是软删除。删掉一个客户只是在customers表里把deleted_flag置为1数据还在库里。即使误操作也可以从数据库里捞回来。第二层退出应用前如果发现有待写入的变更弹窗提示用户保存或放弃不给我以为保存了其实没保存留机会。第三层每次应用启动时自动做一个数据库文件的复制备份。备份文件保留最近7天的放在用户数据目录下的backups文件夹里文件命名带日期。第四层设置面板里提供手动备份按钮一键把当前数据库文件复制到用户指定的位置。这套防护并没有用什么高深技术就是几条SQL加几个文件操作但在实际使用中真的救过一次命。某次数据文件损坏打开应用报错打不开直接从备份目录里恢复了前一天的数据损失只有一天内的少数几条记录。5.5 打包发布时的跨平台注意事项DeskcommCRM的打包用的是electron-builder。这里有几个细节值得记录。首先是打包后数据库文件路径的问题。开发环境下数据文件直接放在项目目录里的data.db但打包后应用目录是只读的不能往那写文件。正确做法是把数据库文件放到系统用户数据目录用app.getPath(userData)获取路径const dbPath path.join(app.getPath(userData), deskcommcrm.db);其次是Windows和macOS的图标、签名问题。Windows上如果没有代码签名证书首次运行可能会被SmartScreen拦截。macOS上如果没做签名需要通过右键打开菜单来绕过Gatekeeper。这些不是功能问题但如果不提前处理用户拿到安装包的第一反应就是是不是病毒很影响信任感。第三是自动更新。桌面应用的一个显著问题是更新分发比Web麻烦。DeskcommCRM目前采用的方式是在设置面板里手动点击检查更新然后从内网或GitHub Releases拉取新版本安装包。这种半自动的方式虽然没有做到全自动静默更新但对内部工具来说已经够用。6. 常见问题速查表与避坑指南这一节把实际使用中遇到频率最高的几个问题整理成速查表方便直接查阅。问题现象根本原因解决方法导入Excel客户数据时提示database is lockedSQLite默认不支持并发写开启WAL模式 busy_timeout所有写入放到一个事务里串行执行时间字段保存后比实际少8小时JS Date经过IPC序列化成ISO字符串时区未处理统一用normalizeDate()转成本地时间字符串再入库客户列表滚动卡顿大量DOM节点同时渲染用 react-window 做虚拟滚动标签数据预加载到前端点击保存后数据没写进数据库渲染进程里执行的异步操作没有await检查IPC调用是否有await并捕获ipcRenderer.invoke的返回值打包后应用打开提示找不到数据库打包目录只读开发路径与生产路径不同数据库路径统一使用app.getPath(userData)双击打开安装包后Windows提示未知发布者没有代码签名证书公司内部使用可忽略对外发布建议购买签名证书删除客户后发现需要恢复没有任何恢复手段从backups目录里拷贝最近备份的数据库文件恢复除了这张表再分享几个我认为比较重要、但不太会有人跟你讲的经验第一自定义字段不要用中文做字段名。虽然界面显示可以用中文数据库存储层尽量用英文命名。否则以后写SQL统计报表时被各种编码问题折腾到崩溃。第二桌面应用里所有按钮的点击状态要有即时反馈。本地SQL的查询速度很快如果按钮按下去没有loading状态用户会以为没点到连续点好几下结果插入了好几条重复记录。第三在设计阶段就要考虑数据的导出格式。CRM的数据是人一点点录进去的如果工具本身不能方便地把数据导出成Excel或CSV用户在某个时间点一定会被困住。我在DeskcommCRM里导出的客户表格字段顺序和Excel打开后的显示效果都是调整过的不是简单的原样倾倒。7. 后续可以继续扩展的方向这个项目到目前为止解决的是把客户信息管起来这个基础问题。如果继续往下做我自己的优先级是这样排的第一个方向是搜索能力的升级。目前的模糊搜索在几千条数据量下体验还可以但客户数据积累到几万条加上跟进记录里的正文内容就需要引入更完整的全文检索方案。SQLite自带的FTS5可以做一个词条全文索引让搜索覆盖到跟进内容里出现过XX关键词的客户这对老销售的回忆式检索是真的能节约时间的。第二个方向是数据报表的增强。目前只有基础的漏斗图真正日常管理还需要按周/月统计新增客户数、跟进次数、成交转化周期、销售个人业绩排名。这些报表的底层数据都已经在数据库里缺的只是前端展示层。第三个方向是多人协同。目前DeskcommCRM还是一个单机版本如果要多人在同一个客户池里协作需要引入数据同步服务。比较务实的做法不是直接做实时同步而是做一个导入/导出合并的功能——团队成员各自维护一份数据定期导出并合并。另外更彻底的方案是把SQLite升级为内置的嵌入式网络数据库但这涉及的操作复杂度会明显增加需要拿实际需求来衡量投入产出。第四个方向是电话/短信集成。既然叫DeskcommCRM通讯能力本身还有很大的扩展空间。比如接入硬件话机或软电话系统自动记录通话时长和通话时间这会让跟进记录的录入成本再低一档。这块也是桌面应用相比Web应用最有优势的地方后续如果做大概率是先从这入手。另外还有一个细节层面的想法增加时间线回顾页面按周维度把所有销售人员的跟进动态汇总成列表。这算是给管理者的一个轻量观察窗口而不是直接把权限和审批流都做进去。CRM的价值核心是业务记录不是权限管理这个边界我一直提醒自己不要扩大。回到最开始为什么要做DeskcommCRM这个问题。市面上的成熟CRM确实很多但够用且好用永远是两回事。桌面端的定位让这个工具在数据自主性、系统联动和操作效率上有独特的优势而SQLite加Electron的组合也让整个项目保持在一个人可以持续维护的规模。做这种内部工具最大的收获不是代码量而是你能亲眼看到它每天帮自己省下了一点时间然后那些省下来的时间又会变成更多值得记录的客户数据循环起来事情就越做越顺手了。