1. 为什么一个几十人的销售团队需要自己造CRM这辆“车”
1.1 从Excel和微信工作群里的“客户管理”说起
每个销售团队在规模超过某个临界点后,都会经历一段极其痛苦的“混沌期”。客户资料散落在销售的个人Excel表格里,有些沉淀在微信聊天记录里,还有一些躺在各类名片扫描App中。管理员想统计本月新增客户数,问一圈下来,每人交上来的表格格式都不一样,光整理数据就要花半天。这个问题不是某家公司特有的,几乎所有中小型销售团队成长到20人左右时都会遇到。
DeskcommCRM这个项目的起点,正是我们团队在某个月度复盘会上暴露出来的数据问题。当时我们做B2B企业服务,销售周期长,客单价高,一个客户往往需要跟进几个月,中间要经历多次沟通、方案调整、报价确认。人少的时候,销售自己心里有本账,客户状态、下一步计划都装在脑子里。但人一多,再加上人员流动,客户资料交接不干净,丢客户、撞客户、漏跟进的情况开始频繁出现。领导问起来,销售说“在推进”,但具体推进到哪一步,谁也说不清。
1.2 被通用CRM折磨的三个月:功能很多,能落地的很少
决定上CRM之后,我们第一时间试用了几款市面上主流的SaaS产品。说实话,产品本身不小,客户管理、销售漏斗、工作流自动化、呼叫中心集成……功能清单拉出来很长,试用期一开始大家也很兴奋。但用了不到两个月,问题就暴露出来了。
最大的问题不是功能不够,而是功能太多、配置太灵活。灵活意味着用户需要花大量时间去设计流程、配置字段、画审批流,才能真正用起来。负责牵头的人如果本身没有足够的业务理解,很容易把系统配成一个看起来很专业、实际用起来非常别扭的“四不像”。其次是权限问题。SaaS产品的权限模型往往是通用型的“角色-权限”模式,但我们的业务场景里有非常特殊的“客户认领”“销售保护期”“公海回收”逻辑,通用CRM很难通过简单配置实现,只能做得很曲折。最后是数据安全。客户资料是我们公司最核心的资产之一,全部放在别人的云服务器上,虽然合同里有保密条款,但心里始终不踏实。
试用三个月的结论是:功能强大不代表适合自己。我们需要的是门槛低、贴合业务、能随时改的CRM,而不是一个功能百科全书。
1.3 DeskcommCRM的立项目标:做一套能真正随业务走的工具
正是这段经历,让我们萌生了自研一套桌面CRM的想法。DeskcommCRM这个名字,当时取的是“Desk Communication CRM”的意思,定位是一个跑在桌面端、强调沟通与协作的客户管理系统。选择桌面端而不是Web端,是因为销售日常工作大量依赖本机数据,客户资料、沟通记录如果能在本地存储,性能和离线使用体验会好很多。
立项时我们定了三个核心目标:第一,数据必须自主可控,所有客户资料存在本地数据库,备份和迁移足够简单;第二,业务逻辑必须贴合自己的流程,比如客户认领规则、跟进计划提醒、销售保护期等,这些都要按我们的规则来实现;第三,使用门槛要低,销售团队没有太多时间学习复杂系统,打开就能用,最好不用培训。
后来的实践证明,这三个目标定得还算明确,但实现起来每一步都有不少坑。在正式讲技术细节之前,先说明一点:DeskcommCRM并不是一个商业产品,它只是我们内部团队用了很长时间的一套业务工具。这篇文章想把其中比较核心的设计思路、实现细节和踩坑经验写出来,给那些同样在考虑自研内部系统的团队一些参考。
2. DeskcommCRM的技术选型与数据模型:先把地基夯实
2.1 为什么选Electron + React + SQLite
技术选型是项目启动后第一件要拍板的事。我们团队之前主要写JavaScript和前端,所以桌面端技术选型上,优先考虑的是我们拿得动的方案。
桌面端框架,我们在Electron和Tauri之间犹豫过。Tauri的包体积小、内存占用低,但当时Rust的生态还不算成熟,团队里没人写过Rust,遇到问题很难搞。Electron虽然被很多人吐槽“跑个桌面应用跟开个浏览器一样”,但它最大的优势是生态成熟、文档多、出问题能搜到答案。对一个内部工具来说,稳定性和可维护性远比虚荣的包体积重要,所以最终选了Electron。
前端界面用的是React + Ant Design。Ant Design对于这种后台管理类界面太合适了,表格、表单、日期选择器都是现成的,能省下大量开发时间。打包工具用的electron-builder,数据库用了SQLite,通过better-sqlite3这个库来访问。为什么选SQLite而不是直接连服务器上的MySQL?因为产品定位是桌面端优先,数据先落在本地,再通过同步机制和服务器交互。本地用SQLite最合适,零配置、单文件、性能足够。
这里也说一个反直觉的经验:Electron的内存占用确实不低,但在现代办公电脑上完全可接受。实际使用中,DeskcommCRM常驻内存大概在180MB左右,对于一个需要长期开着、随时录入数据的工具来说,这个量级没有造成任何困扰。
2.2 核心数据表设计:客户、联系人、跟进记录
数据模型是整个系统的灵魂。如果表结构设计得不好,后面加功能的时候会非常痛苦。我们第一版大致设计了这些表:customers、contacts、follow_ups、tasks、users、customer_owner_history、operation_logs。下面把最核心的几张表拿出来讲。
customers表是核心,存客户的基本信息和管理字段。基本信息包括公司名称、行业、规模、来源渠道、客户状态等;管理字段包括归属人、保护期截止时间、最后跟进时间、下次跟进时间、创建时间等。
contacts表存客户下的联系人。一个客户可能有多个联系人,比如决策人、技术对接人、财务对接人。联系人信息包括姓名、职位、手机、微信、邮箱等。follow_ups表存每次跟进记录,这是销售团队的“工作日志”,包括跟进方式(电话、微信、上门、邮件)、跟进内容、下一步计划等。
-- 客户主表(部分字段) CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_name TEXT NOT NULL, industry TEXT, company_scale TEXT, source_channel TEXT, status TEXT DEFAULT 'new', -- new: 新客户, following: 跟进中, won: 已成交, lost: 已流失 owner_id INTEGER, -- 归属销售 protected_until TEXT, -- 保护期截止时间 last_follow_up_at TEXT, -- 最后跟进时间 next_follow_up_at TEXT, -- 下次跟进时间 created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE INDEX idx_customers_owner ON customers(owner_id); CREATE INDEX idx_customers_status ON customers(status); CREATE INDEX idx_customers_next_follow ON customers(next_follow_up_at);设计时有一个反复权衡的点:跟进记录和任务到底该分成两张表,还是合成一张表?最后我们分开了。跟进记录是历史,不能变;任务是待办,会改状态。混在一起的话,查询历史时会很别扭。这个分法后来被证明是对的,因为当团队开始做数据统计时,跟进记录直接被拿来算工作量,而任务表主要服务于提醒和待办列表,两者完全独立。
2.3 客户归属与权限模型的设计
客户归属是CRM里最敏感的业务逻辑。在我们的销售流程里,一个客户一旦被销售认领,就进入该销售的名下,其他销售默认不能查看。但如果销售超过一定时间没有跟进,客户会被释放回“公海”,其他销售可以重新认领。这套逻辑在很多CRM产品里叫“公海池”和“回收规则”。
权限模型我们做得很简单,没有用复杂的RBAC,就是三种角色:管理员、销售组长、销售。管理员能看到所有数据,销售组长能看到本组所有销售的数据,销售只能看到自己的客户。为什么不用更细的权限?因为业务阶段决定了,授权模型越复杂,出问题的地方越多。员工离职交接时,我们只需要把客户从离职销售名下批量转移给指定销售,或者释放回公海就够了。
还有一个细节是数据可见性。销售之间默认不可见,但同组销售在“撞单”时又需要核对。我们的处理是:同组销售之间可以看到客户名称,但看不到联系方式和跟进记录;如果要“申请协作”,得由组长审批后打开共享。这个设计在权限和协作之间取得了平衡。
2.4 数据字典与字段扩展性
刚用自研工具的人,大概率都会遇到一个问题:过了三个月,业务部门说“我们要在客户上加一个字段”。如果代码里硬编码字段,就会很麻烦。我们一开始就把字段定义做成了数据字典模式,客户表的扩展字段存成JSON,需要新增字段时在界面配置即可,不需要重新建表。
这么做有个好处,也有个坏处。好处是灵活,加字段快;坏处是JSON字段无法参与索引,查询性能会受到一定影响。我们目前的处理方式是:高频查询的字段仍然使用独立的业务列,低频的、只是展示用的字段才放JSON。比如客户列表页的筛选条件中,“行业”和“来源渠道”是高频筛选字段,所以它们在customers表里有独立列和索引;像“客户预算范围”“采购决策链”这类只用来详情展示的字段,就统一丢进extra_info的JSON列里。这个折中方案实践下来效果不错。
3. 核心功能模块的落地:从原型到可用的距离
3.1 客户池与公海回收机制:不止是定时任务这么简单
客户池(公海)是销售CRM的核心机制之一。销售如果长期不跟进客户,系统应该自动把客户释放到公海,让其他有意愿、有精力的销售去接手。这个逻辑看起来简单,实现起来要解决几个实际问题。
第一个问题:什么叫“长期不跟进”?我们在系统里定义的是“超过7天没有新增跟进记录,客户自动释放”。判断依据是customers表的last_follow_up_at字段。这个逻辑通过一个定时任务扫描实现,每天凌晨自动跑一次。但实际问题比这复杂,比如周末算不算7天里?法定节假日要不要顺延?后来我们做了个配置项,允许管理员自定义“N个自然日未跟进”和“跳过节假日”两个参数,默认值还是7天。
第二个问题:释放之后客户保留多少历史数据?释放时不能把跟进记录清了,新销售认领后必须能看到历史跟进记录,才能快速了解客户情况。所以释放操作只是改了客户归属,业务数据统统保留。我们专门建了customer_owner_history表,记录每一次归属变更,包含从谁转移到谁、什么时间、什么原因(手动分配、公海释放、离职交接),这张表后来帮我们解决了很多次“客户到底是谁的”这类纠纷。
第三个问题:保护期。我们给新认领的客户设置了7天保护期。在保护期内,组长可以把客户收回重新分配给其他人(比如认领错了),但其他销售不能抢。过了保护期,就按照“超过7天未跟进”的规则正常流转。这块如果用SQL实现,就是每次查询时先过滤一下当前时间和protected_until的关系,代码不复杂,但对业务理解的要求很高。
3.2 跟进计划、日程提醒与任务中心:销售的大脑外包
销售跟进客户不能靠脑子记,系统必须主动提醒。我们做了一套“任务中心”,每个客户都可以生成跟进任务。比如在跟进记录里勾选“两周后再联系”,系统会自动在任务中心生成一条待办,时间到了自动提醒。
提醒的实现方式包括两种:一种是Electron主进程里的Notification通知,直接弹Windows系统通知;另一种是应用内红点提醒。Electron的Notification接口用起来很简单,但要注意Windows上的Toast通知需要配合app.setAppUserModelId才能正常显示,这个坑我们踩过。如果不设置,在某些Windows版本上通知可能弹不出来,或者弹出来不显示应用名称,直接显示“Electron”四个字,非常尴尬。
任务还可以分级和设置优先级。我们的任务是按“到期时间+客户状态”动态排序的:今天到期的任务排最前面,高价值客户(状态为“跟进中”且客单价高)的任务排前面。这个排序逻辑上线后,销售每天打开系统的第一件事,就是看任务中心,从这里知道今天该干什么。
3.3 可视化看板:从统计报表反推需求
销售管理者最需要的是数据看板。我们第一版做了一个简单的“今日概览”,包括今日新增客户数、今日跟进次数、今日到期任务数、本周成交金额等。后来又加了“销售排行”和“客户状态分布”两个图表,让组长能看到每个人的工作量和业务转化情况。
图表库用了Ant Design Charts,最初是为了快速集成,后续如果不够用再换ECharts。这里有个真实经验:Charts对常见需求覆盖得不错,柱状图、饼图、折线图都很顺手,但一旦要做复杂的自定义图表——比如带联动筛选的下钻漏斗,或者某个特定场景的甘特图,就会觉得还是ECharts更灵活。我们目前是两者共存:90%的场景用Charts,遇到特殊需求时再单独引入ECharts。
看板数据的实时性也值得聊一下。最初我们做的是每次打开页面时重新查询数据库,后来发现销售们习惯把看板页面一直开着挂在大屏上,频繁查询没有必要,就改成了每5分钟自动刷新一次。对于内部管理工具这种“准实时”的刷新频率已经足够,没必要追求秒级。
3.4 搜索与全局检索:从LIKE到FTS5
客户多了之后,快速找到目标客户是非常核心的操作。我们在顶部做了一体化搜索框,支持按客户名称、联系人姓名、手机号、微信号搜索。
由于数据在本地SQLite里,最简单的方案就是SQL的LIKE查询。但到了几万条数据之后,LIKE '%keyword%'的前缀匹配会变慢,特别是多表关联时的检索更明显。我们的排查过程是这样的:先用EXPLAIN QUERY PLAN查看SQLite的执行计划,发现每次搜索都会做全表扫描;然后尝试给高频字段加索引,但必须承认,LIKE '%keyword%'这种前后模糊的写法根本走不了普通索引。
最后的方案是:主要的客户列表和联系人搜索用带索引的列,搜索时支持手机号精确匹配、客户名称的FTS5全文搜索。FTS5是SQLite内置的全文搜索引擎,建虚拟表和触发器同步后,搜索速度提升非常明显。比如原来搜“科技”两个字,要遍历全部5万条客户记录,耗时大概在600到800毫秒;改用FTS5后,基本在100毫秒以内返回结果。
值得一提的是,FTS5的虚拟表同步也是需要小心处理的。如果你直接在customers表上建了FTS5触发器,那么每次INSERT、UPDATE、DELETE都要触发同步。批量导入数据时,如果逐条插入,性能会惨不忍睹。我们的做法是:批量导入时先关掉触发器,导入完再重建FTS索引。SQLite提供了INSERT INTO fts_table(fts_table) VALUES('rebuild')这种内建命令,一行代码就能全量重建索引,速度比逐条同步快好几个数量级。
4. 开发过程中的关键实现:选几个有代表性的功能深入讲讲
4.1 批量导入与客户查重:Excel迁移的第一道坎
团队从Excel迁移到DeskcommCRM的第一个障碍,就是历史客户数据怎么导进去。Excel导出成CSV,再写一个导入页面,用户上传文件,后端解析,写入数据库。看起来不难,但有几个坑。
第一个坑是编码。中文Windows下,Excel导出的CSV默认是GBK编码,程序读取时如果不判断编码,中文会变成乱码。我们的处理方式是上传文件后先用jschardet检测编码,再读取内容。现在看这一点好像很简单,但当时是上线后才遇到的问题,用户导出一批数据全是乱码,非常头疼。
第二个坑是数据清洗。Excel里经常有合并单元格、换行符、多余空格、数字被自动变成科学计数法等问题。我们写了一个清洗函数,把常见问题统一处理掉,包括把手机号强制转成文本、去首尾空格、把空值统一为NULL。这里分享一个细节:手机号如果以文本格式存,从Excel复制出来特别容易带隐藏的零宽字符,肉眼看不出来,但程序比对时就是不一致。我们后来在清洗函数里显式去掉了\u200b、\u200c、\u200d这类特殊字符。
第三个坑是查重。客户导入时必须判断是否已存在。我们规定:手机号完全一致,就认为是同一个联系人;联系人完全一致,就把客户信息合并到已有客户上。查重逻辑不能做得太严格,否则导入一批有微小差异的客户会全部失败;也不能太宽松,否则会漏掉重复数据。我们做了两档匹配:精确匹配手机号、模糊匹配姓名+公司名称的相似度。模糊匹配用的字符串编辑距离算法(Levenshtein Distance),阈值为0.85。
批量导入的代码不算复杂,但数据容错非常重要。一条数据格式不对不应该导致整个导入失败,应该记录错误原因,让用户下载错误报告后修改再导入。我们的导入页面会实时显示进度,结束后生成一份“导入错误报告.csv”,每行标注是哪一条数据、哪个字段有问题、该怎么改。这个体验细节让用户对系统的信任度提升了不少。
4.2 提醒系统的实现:从轮询到主动推送
前面提到的任务提醒,我们第一版用的是每30秒轮询一次数据库,查有没有到期的任务,有就弹通知。这个方案实现简单,但问题在于:如果任务到期时间正好落在两个轮询周期的中间,提醒会晚几十秒。对内部工具来说,晚几十秒倒无所谓,但轮询的额外开销和桌面应用应有的体验不符。
后来改成了Electron主进程和渲染进程的IPC联动:每次打开应用时主动拉取一次所有未完成任务;操作任务时,通过IPC通知主进程更新提醒状态;主进程仍然保留定时器做兜底,但缩短到每5分钟一次。改完之后,提醒基本是实时的,而且定时器的资源占用小了很多。
Electron实现系统通知的代码很简单,核心就这几行:
// 主进程 const { Notification } = require('electron'); app.setAppUserModelId('com.deskcomm.crm'); function showTaskNotification(task) { if (!Notification.isSupported()) return; const notification = new Notification({ title: `待跟进:${task.customer_name}`, body: `${task.content}(截止时间:${task.due_at})`, silent: false, }); notification.on('click', () => { mainWindow.show(); mainWindow.focus(); mainWindow.webContents.send('navigate-to-task', task.id); }); notification.show(); }点击通知时唤起主窗口并跳转到对应任务,这个交互对销售来说很贴心。他们看到弹窗点一下,系统直接定位到那条任务,不需要自己在列表里翻。
4.3 本地数据的同步与冲突处理:追加式日志是救命稻草
数据全部存在本地SQLite,那多人协作怎么办?销售A添加了一个客户,销售B怎么能看到?我们的做法是:每个客户端在本地维护一个同步表(sync_queue),所有写操作都先写入本地库,同时往同步表里塞一条待同步记录。后台线程持续把待同步记录推送到服务器,服务器再把变更广播给其他在线客户端。
同步方向是双向的,冲突处理简单粗暴:以最后修改时间为准(Last Write Wins)。对于客户资料这种低频更新场景,LWW策略足够用。但如果涉及到金额、状态这类敏感字段的并发修改,LWW可能产生覆盖。我们这边的经验是:重要的状态变更尽量做成追加式日志,而不是更新字段。比如“客户状态从A变成B”的操作,在数据库里插入一条状态变更记录,而不是直接改customers表的状态字段。这样即使同步冲突,也能从历史记录里找出事实来源。
这里有一个很现实的问题:同一时间两个销售都在自己的客户列表里把同一个客户的状态改成了“已成交”,以最后修改时间为准,可能其中一个人的修改被覆盖了。但因为状态变更是追加日志的模式,管理员在操作日志里能看到两条记录,必要时可以人工纠正。如果直接改字段,被覆盖的那个销售可能要过很久才会发现自己的修改丢了。
4.4 桌面端的用户体验细节:快捷键和离线能力
桌面应用和Web应用的使用体验差异,在几个小细节上体现得很明显。
快捷键。销售录跟进记录是高频操作,我们绑定了全局快捷键Ctrl+Shift+K来快速呼出“新建跟进”窗口,这样即使销售在别的窗口写邮件、查资料,也能一键呼出系统录入信息。这个功能上线后,大家反馈“终于不用来回切窗口了”,使用频率比我们想象的还高。
离线可用。由于数据在本地,即使办公网络断了,销售依然可以正常查看客户资料、录入跟进记录。等网络恢复后,同步线程自动把离线期间的变更推送到服务器。对经常跑外勤的销售来说,这个体验是Web版CRM完全给不了的。
还有一个细节是启动速度。第一版应用启动要3到4秒,主要是初始化数据库连接和重建索引。后来我们把数据库预热加到了启动加载页里,启动时显示一个非常轻量的品牌页面,后台并行做数据初始化。整体启动时间控制在1.5秒左右,用户的感知好了很多。
5. 上线后的复盘:性能优化的坑和体验改进的迭代过程
5.1 当客户数据到5万条时,列表和搜索都开始卡了
上线前我们测过几千条数据,一切流畅。上线三个月后,客户总量接近5万条,列表滚动、全局搜索开始出现明显卡顿。这属于“能用,但很不爽”的状态。
定位问题花了点时间。第一个瓶颈是列表渲染:一次查询加载了全部符合条件的记录,比如2000条客户,每行又有多个列,渲染开销一下就上去了。解决方案是后端分页加前端虚拟滚动。React的antd表格自带分页能力,但我们做了大量自定义操作,一开始没接好。后来把列表改成了服务端分页,每页50条,加上一个轻量级的虚拟滚动库,卡顿问题解决了一大半。
第二个瓶颈是索引缺失。我们有last_follow_up_at字段参与排序,最初建表时没加索引,导致ORDER BY越来越慢。检查SQLite的EXPLAIN QUERY PLAN之后,给高频排序字段加上了索引,查询时间从几百毫秒降到了几十毫秒。这里也提醒了大家:不要等到卡了才建索引,表在建完后就应该把常用的查询条件列出来,把索引一次性配好。
-- 查看SQLite执行计划 EXPLAIN QUERY PLAN SELECT * FROM customers WHERE owner_id = 1 ORDER BY last_follow_up_at DESC LIMIT 50; -- 加索引后的执行计划 CREATE INDEX idx_customers_owner_follow ON customers(owner_id, last_follow_up_at DESC);第三个瓶颈是表格列太多。客户列表有十几个字段,每行都要渲染十几个列,即使数据量不大,也会造成滚动时的性能压力。我们的做法是:默认只展示8个核心字段,其余字段通过“列设置”按需勾选。这个改动表面上是为了性能,实际上也提升了操作效率——信息密度降低后,销售更容易聚焦到关键字段上。
5.2 数据安全与备份策略:本地数据绝不能只存一份
数据在本地有个风险:电脑坏了怎么办?文件被误删怎么办?所以备份策略必须在一开始就想好。我们的做法是三层备份。
第一层是服务器端主库,所有变更实时同步到服务器,服务器数据库可以做每日快照。第二层是客户端自动备份,每天应用退出时,本地数据库文件压缩成一个带日期的备份文件,保留最近30天。第三层是手动备份,在设置界面提供“导出全部数据”按钮,导出成一个加密的JSON文件,方便做最终归档。
这里有个细节值得强调:SQLite的备份不能直接复制数据库文件。因为在写入过程中直接复制文件,很容易得到一份损坏的数据库。我们用的是SQLite官方推荐的在线备份API,即sqlite3的backup()函数,通过这个接口生成一致性快照。在better-sqlite3中,可以通过db.backup()方法实现。
5.3 从“不愿意用”到“离不开”的三次迭代
第一版上线时,销售们其实是抵触的。很多人觉得:我已经有自己的Excel了,为什么要用你这个系统?最开始两周,录入率和更新率都很低。我们没有强制要求,而是观察大家的使用习惯,找到阻力最大的环节。结果发现,大家不愿意用不是因为懒,而是录入成本高。原来的Excel里,客户信息、跟进记录都是手打的自由文本;新的系统里,我们设计了十几个字段,其中很多是必填的,录一个客户要花三分钟,太费劲。
于是我们做了第一次重要调整:把必填字段从十几个减少到四个,录入页面重新设计,打开后光标自动定位到公司名称输入框。操作路径缩短后,录入率明显上升。
第二次调整是把“录入客户”和“录跟进”这两个孤立动作串联起来:录完一个客户,系统自动生成一条待办——“给这个客户打第一通电话”。这条待办会出现在任务中心里,销售打开系统就能看到今天该做什么。
第三次调整是加入“销售排行榜”看板。虽然有点“内卷”的味道,但销售团队确实有竞争心理,看到自己的名字排在后面,自然会想把客户跟进得更勤一些。三次迭代之后,系统从“公司要求用”变成了“我们自己想用”。有个销售在换电脑后第一句话是“赶紧帮我把DeskcommCRM装回来,我客户资料都在里面”,听到这句话,我就知道这个项目算是立住了。
6. 如果你也想做一套公司内部的业务工具:几点掏心窝的建议
6.1 哪些场景适合自研,哪些不适合
自研一套内部业务系统,既要考虑成本,也要考虑风险。我觉得最核心的判断标准是:你的业务流程是否足够特殊、足够稳定?如果你们跟同行一样用一套标准的销售流程,大概率直接买成熟的CRM更划算,别折腾。但如果你们的流程里有一些不标准但约定俗成的规则,比如特殊的分配机制、特定的保护期逻辑,那么自研能让你把这些规则变成系统的默认行为,而不是在通用系统里靠一堆折中方案硬凑。
另外,自研工具得考虑维护成本。内部系统一般没有专职团队长期维护,如果开发这个系统的人离职了,后面谁来改代码?所以代码质量、文档、部署方式都要有意识地面向“未来的接盘侠”来做。我们当时给DeskcommCRM写了一份简单的架构文档,里面记录了技术栈、目录结构、同步流程和常见问题,后来新同事接手时帮了大忙。
6.2 低成本起步的一些建议
如果你决定做,我的建议是别一上来就放大炮。第一版不要追求功能多,先把“录客户、查客户、录跟进、看数据”这四个最核心的动作做扎实。至于报表、权限、公海、自动化,放到第二期、第三期再做。很多团队在自研工具上失败,不是技术不行,而是初期需求范围失控,做了半年还在做“平台”。
技术栈方面,我强烈建议用团队最熟悉的语言。DeskcommCRM选Electron是因为团队会JavaScript;如果你团队会Python,那可以考虑PyQt或Tauri加其他前端;但凡是能让自己快速上手的方案,就是好方案。不要为了追求所谓的技术先进性,选一个团队没人熟悉的架构,那样项目大概率会烂尾。
6.3 对DeskcommCRM后续的规划
这个系统到目前为止,解决了我们的核心问题:客户数据集中管理了,跟进记录完整了,销售交接有底了。但要说它完美,肯定不是。当前最让我头疼的是报表能力还太弱,销售管理者想要的漏斗分析、转化率、回款周期这些,现在还没有做到位。下一步我会重点补上这块,同时考虑做一个简单的API层,方便以后和其他系统(比如财务软件)对接。
另外还有一点是移动端。销售外出拜访客户时,偶尔需要在手机上快速查一下客户资料。我们目前还停留在桌面端,移动端最多是把数据同步到服务器,用Web版临时应急。长期来看,做一个轻量的移动端应用或者至少是移动适配的H5页面,会是提升使用体验的重要方向。不过这属于下一阶段的事了,内部工具的开发要跟着业务节奏走,不能为了堆功能而堆功能。