资讯动态

自研轻量级CRM:基于SQLite的DeskcommCRM设计与实践

发布时间:2026/10/19 4:40:21 来源:尧图企业网站定制
先讲一个真实背景。我所在的团队负责一块很具体的业务——面向中小客户的销售与售后支持。过去很长一段时间客户信息散落在Excel、个人微信聊天记录、手机通讯录里谁跟过哪个客户、上次说到什么程度、下一步该做什么基本靠记忆。后来尝试过几款市面上的SaaS CRM要么功能太重配置了一周还点不到核心页面要么按坐席收费人一多成本就上去了。最不能忍的是数据放在别人的服务器上我们想导出自定义报表还得先研究他们那套导出规则。反复折腾之后我决定动手写一套足够简单、足够贴合自己业务流程的CRM系统也就是DeskcommCRM。DeskcommCRM这个名字拆开看Desk是桌面comm是沟通记录communication合起来就是“围绕桌面端使用、以沟通记录为核心的客户管理系统”。它解决的问题很具体客户档案统一管理、跟进过程自动沉淀、待办任务准时提醒、常用报表一键导出。它适合的人群也很明确——没有专职运维人员的小型业务团队、独立顾问、代理商团队以及那些被通用CRM配置流程折磨过的人。下文就从设计思路、数据库建模、关键流程实现到部署维护完整复盘这个项目。1. 为什么做DeskcommCRM三种主流CRM方案的取舍在动手写代码之前我把市面上可选的方案认真扫了一遍最终才决定自研。这个决策过程比写代码本身更重要因为很多项目死就死在选型阶段。1.1 现成SaaS CRM为什么不适合我们市面上的SaaS CRM普遍在走“大而全”的路线。打开后台左侧菜单从线索管理、客户管理、商机管理、合同管理一直到工单、回款、报销恨不得把整个公司的业务流程全部装进去。功能多本身不是问题问题在于每一次业务调整都意味着字段变化、状态流转改动、审批流配置。我见过同行为了在CRM里加一个“客户来源”下拉框折腾了一下午按钮权限这种成本在小团队里完全不可接受。SaaS CRM另一个麻烦是数据所有权。客户资料是公司最核心的资产之一放在第三方平台上一旦续费出现问题或者平台调整产品方向数据导出格式和完整度都要打个问号。我们曾经想迁移一个老平台的数据发现自定义字段导出的文件乱码不说几条长时间没更新的跟进记录直接被系统归档掉了找客服恢复要等三个工作日。这种不可控直接触发了自研的想法。1.2 自研的边界不做什么比做什么更重要自研最大的风险是没完没了地加需求。为了避免这个坑我给自己定了三条硬性边界。第一不做复杂权限体系。团队就几十个人分成销售和主管两层角色就够了不需要矩阵权限、数据权限分组、字段级权限。权限设计过重先拖垮的是自研项目的开发排期。第二不做复杂审批流。客户折扣、合同审核这些流程照旧走线下的企业微信和纸质签批CRM只负责记录结果。把CRM当成业务的“结果登记处”而不是“流程发动机”复杂度会低一个量级。第三不做移动端。我们这个团队每天绝大多数时间在电脑前工作桌面端网页版足够用。移动端适配的工作量非常大后文会讲为什么这个决定后来被证明是正确的。1.3 为什么叫“通信记录优先”的CRM名字里的comm是我特意放进去的。在小客户销售场景里客户关系不是靠合同维系的而是靠一次次电话、微信、邮件和见面沟通堆起来的。很多CRM把跟进记录设计成一张简单表格时间、内容、下次跟进时间但这丢失了真实的沟通过程。DeskcommCRM的整体设计围绕一条原则客户档案是骨架沟通记录是血肉。系统里每个客户主页都有一根完整的时间轴从第一次打电话、添加微信、发送报价单、客户犹豫、再次跟进到最终成交或流失全部按时间排列。任何人打开这个客户的详情页不需要翻阅历史聊天记录就能还原完整的跟进脉络。这条时间轴的设计贯穿了整个项目包括数据库表结构、页面交互和报表口径。2. 核心功能拆解与数据库建模整个系统最核心的模块可以拆成五个客户档案、联系人、商机、跟进活动、任务提醒。数据模型上我用的是经典的“客户—联系人—商机—活动”四层结构再加一个独立的用户表。2.1 五张核心表的定义与关系客户表customers承担最基本的公司维度信息包括公司名称、行业、规模、来源渠道和添加人。联系人表contacts挂在客户下面因为一个客户公司里经常会同时对接采购、使用部门和财务三个联系人三个人的说话口径完全不一样。商机表deals记录潜在成交金额、预计成交时间、当前阶段。活动表activities则是整个系统的核心它记录了每一次沟通。CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_name TEXT NOT NULL, industry TEXT, scale TEXT, source TEXT, owner_id INTEGER NOT NULL, 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, title TEXT, phone TEXT, wechat TEXT, email TEXT, is_primary INTEGER DEFAULT 0 ); CREATE TABLE deals ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), title TEXT NOT NULL, amount REAL, stage TEXT DEFAULT 意向, expect_close_date TEXT, owner_id INTEGER NOT NULL ); CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), deal_id INTEGER, type TEXT CHECK(type IN (电话, 微信, 邮件, 见面, 报价, 其他)), content TEXT NOT NULL, next_follow_date TEXT, owner_id INTEGER NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL REFERENCES customers(id), title TEXT NOT NULL, due_date TEXT NOT NULL, done INTEGER DEFAULT 0, owner_id INTEGER NOT NULL );这套表结构看起来简单但有几个细节是后来实践才慢慢打磨出来的。第一为什么把商机单独立表而不是在客户表上加字段。因为一个客户可能同时有几个不同的产品线在谈每个商机的金额、阶段和预计成交时间都不一样。如果只用一个“商机金额”字段就丢失了多线销售的颗粒度。第二activities表里的type字段我用了CHECK约束。很多系统允许用户任意填写活动类型结果就是同一个意思有五六种写法“电话”“打电话”“电联”“Phone”。报表统计时非常痛苦。用固定枚举值虽然看起来不灵活但保证了后续统计时永远不需要清洗数据。第三tasks表是单独拆出来的没有直接放在activities里。原因是有些任务并不一定是针对某一次沟通产生的它可能是客户生日关怀、每季度回访这些计划性动作。任务和执行记录分开语义上更干净。2.2 时间轴的设计如何还原完整跟进过程客户详情页的时间轴是整个系统使用频率最高的部分。它的实现原理不复杂就是在查询activities表时按created_at升序排列然后在前端渲染成一条纵向时间线。但在设计阶段我曾经纠结过一个问题时间轴上应该展示哪些内容最初想法是只展示activities表里的沟通记录。后来实际用起来发现不行。一个客户被改价、被转移负责人、被修改了行业分类这些关键节点如果不在时间轴上后面复盘时根本说不清楚当时为什么做这个决定。所以我加了第二种事件类型操作日志。activities表只记录业务沟通内容操作日志单独存在operation_logs表里。每当客户档案、商机阶段、负责人发生变更系统自动写入一条日志。前端时间轴把两种数据合并后按时间排序操作日志用灰底说明沟通记录用白底说明。这样既保留了人的主观判断过程又记录了系统的客观变化复盘时非常完整。2.3 权限模型的简化实践前面说了不做复杂权限体系但最基本的角色区分还是需要的。DeskcommCRM最终只做两个角色普通成员和管理员。普通成员能看自己名下或自己参与过的客户能创建、编辑、跟进自己的客户但看不到其他人的客户列表。管理员可以看到全部客户可以把某个客户从一个成员名下转给另一个成员可以调整系统参数。这个权限模型在数据库里落地非常直接。customers表、deals表、activities表都带有owner_id字段所有查询默认带上owner_id 当前用户id的条件。管理员在页面上多一个“全部客户”的入口查询时不加owner过滤条件。这个设计的唯一问题在于客户转交给别人后历史跟进记录算谁的我们的处理是跟进记录仍然挂在客户下新负责人可以看到完整的活动时间轴但操作日志里会明确记录“用户A于某时将客户转移给用户B”。这个细节保证了交接时新人不会被蒙在鼓里。2.4 自定义字段一个折中方案每个团队对客户资料的关注点不一样。我们做销售时关注行业、规模、来源代理商团队可能更关心区域和渠道层级。为了不改表结构我预留了一个custom_fields表。CREATE TABLE custom_fields ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT NOT NULL, entity_id INTEGER NOT NULL, field_key TEXT NOT NULL, field_value TEXT );这个方案比较粗暴查询时需要做一次动态行转列。但因为每个客户的自定义字段数量有限实际查询性能完全可以接受。比起在customers表里提前预留20个无意义的字段这种键值对方案反而更干净而且前端可以动态渲染不同的表单控件。3. 关键流程的实现细节数据模型设计得再漂亮最终还是要在业务流程里跑起来才算数。这一部分挑三个最关键的流程展开分别是客户导入与查重、跟进任务提醒、报表统计口径。3.1 客户Excel导入与查重的正确姿势任何团队从Excel切换到CRM第一步一定是批量导入客户数据。这一步要做不好整个项目可能从第一天就失去信任。导入流程我设计成三步上传模板、字段映射、预览执行。上传模板前系统会要求下载标准模板模板里第一行是字段名下面每一行是一个客户。字段映射这一步是给老数据准备的因为每个团队维护的Excel列名都不一样比如“公司名称”有叫“客户名称”的也有叫“单位名称”的自动识别经常出错让用户手动选一下最稳妥。查重是最容易被忽视的环节。我的实现是导入时按公司名称的MD5指纹建立索引如果发现已有相同名称的客户直接提示并跳过不强制合并。原因是Excel里的历史数据和系统里的新数据很可能根本不是同一个主体盲目合并会造成客户历史信息混乱。如果用户确实要合并可以在导入完成后在客户详情页手动操作“合并重复客户”。这里分享一个坑千万不能在导入时直接把Excel里的所有字段都塞进系统。Excel里经常有备注列、临时统计列、无关紧要的公式计算列。我在设计字段映射时只开放系统真实存在的字段其余一概忽略。这样数据一进来就是干净的后续维护成本大大降低。3.2 跟进任务自动提醒定时任务与出现频率的权衡跟进任务提醒是整个系统里感知最强的一个功能。客户跟进如果靠人自觉黄花菜都凉了一定要系统主动提醒。实现上我在tasks表里存了due_date然后写了一个每分钟执行的定时任务扫描当天需要完成的任务给对应负责人发送提醒。提醒渠道有三个系统内待办角标、企业微信机器人推送、邮件摘要。当时没有做短信提醒因为客户跟进不属于那种需要手机短信强提醒的场景企业微信已经够了。这里要特别注意提醒频率的控制。曾经有个版本提醒逻辑写得比较简单任务到期当天早上9点发一次如果没有完成每小时再提醒一次。结果到了下午三点销售同事说被提醒消息刷屏了反而把真正重要的任务淹没了。后来改成每天三次早上9点、下午2点、晚上5点每一次提醒都会带上“已逾期X天”的文案。逾期第一天和逾期第七天的感受完全不一样销售经理看到逾期数字会自然去关心原因。3.3 报表与统计口径选对维度比做图更重要报表模块是销售管理层最关心的部分。一开始我做了很多图表月度业绩趋势、商机转化漏斗、行业分布饼图、部门排行榜。后来发现真正高频使用其实就三个客户新增数、跟进次数、商机金额。转化漏斗的实现关键在stage字段的设计。DeskcommCRM把商机阶段定义为意向、方案确认、报价、谈判、成交、流失六个阶段。每个阶段用户在界面上手动推进系统记录推进时间和操作人。计算漏斗时只需要统计商机经过某个阶段的数量即可。有一个统计口径的问题值得单独拎出来讲。商机金额什么时候计入业绩我们采用的是“进入谈判阶段才计入预测收入”而不是一创建商机就算进去。原因太现实了销售为了好看会在月初给所有潜在客户建商机把金额写大月底发现根本签不下来。有了这个口径销售主管看预测报表时数据明显更理性。来源分析相对简单按customers表的source字段分组计数即可。但这里也踩过一个坑来源渠道数据的质量取决于最开始的录入习惯。强制下拉选择禁止自由填写之后才有可靠的来源报表。我会在字段设计时用CHECK约束或前端下拉框把可选项固定住避免后台出现十几个五花八门的来源名称。4. 部署架构、备份策略与日常维护系统开发完只是第一步真正让它稳定运行、数据永不丢失需要认真对待部署和维护。这一步如果省事后面的麻烦会成倍增加。4.1 SQLite加内网服务器的轻量部署方案数据库选型上我选了SQLite而不是MySQL或者PostgreSQL。三个理由第一团队规模决定了并发连接数非常有限SQLite完全扛得住第二备份工作变得极其简单直接拷贝一个文件就可以第三SQLite不需要单独安装服务、也不需要网络权限配置部署门槛极低。整个系统跑在一台内网的Linux服务器上前端是网页通过Nginx反向代理访问。数据文件放在/data/deskcomm/data.db。客户端用浏览器访问不需要安装任何东西。有人可能会担心SQLite的并发写入问题。以DeskcommCRM的实际负载每天新增的客户、跟进记录、任务数量在几百条级别写入频率远低于SQLite的WAL模式能支撑的极限。真正需要关心的反而是另一个问题如果以后业务量突然增长到几百人同时在线这套架构会吃紧。但目前阶段我宁可做一个小而稳定、数据好备份的系统也不愿意为了想象中的高并发引入一整套数据库运维负担。4.2 自动备份与灾难恢复数据是无价之宝我在项目第一天就做了备份方案。每天凌晨2点服务器定时任务执行一次SQLite数据库的在线备份生成一个带日期后缀的文件保留最近30天。每个周末再执行一次全量备份把数据库文件加密后同步到私有对象存储上保留3个月。这里要强调一个SQLite备份的细节千万不要直接复制data.db文件进行备份。因为数据库可能正在写入直接复制出来的文件可能损坏或缺失最近几条记录。正确做法是使用SQLite的在线备份API或者使用sqlite3命令的.backup指令。sqlite3 /data/deskcomm/data.db .backup /data/backup/deskcomm_$(date %Y%m%d).db恢复流程也做过两次演练。流程很简单停掉应用服务把备份文件复制回数据目录启动服务。全程只需要几分钟。这次演练证明了方案的可行性。一个能跑得起来的恢复流程比备份动作本身更重要。4.3 版本升级与数据迁移自研系统的好处是升级节奏自己掌握。但升级时最怕的是数据库结构变更。这里我用了自己的一个简化迁移方案每次发布新版本先用SQL脚本备份表结构再执行迁移文件。迁移文件按时间戳命名系统启动时自动检测并执行未运行的迁移。比如有一次要往customers表加一个“年营收”字段迁移文件长这样ALTER TABLE customers ADD COLUMN annual_revenue TEXT DEFAULT ;这段SQL会被记录到schema_migrations表里下次启动时不会再执行。凡是删除或重命名字段这类危险操作我都会额外写一个数据备份逻辑先把整表复制到带_bak后缀的临时表迁移完没问题再删除临时表。宁可多留一手也不要在客户数据上冒险。5. 高频问题排查与避坑实录系统上线半年多积累了一些具体问题的排查经验。这里总结成几个高频场景供参考。5.1 客户数据被误改或误删怎么办桌面CRM最大的风险不是被黑客攻击而是操作者手滑。有一次同事在编辑一个客户时不小心把客户名称清空了。查问题时发现系统界面上没有任何修改痕迹。解决这个问题的办法是给操作日志增加“旧值/新值”快照。每次更新记录时自动对比修改前后的关键字段如果有变化就写入operation_logs表并记录具体字段名和前后值。这样管理员在后台打开操作日志就能看到“某用户于某时将客户名称从A修改为空”。对于误删情况我的处理方式是软删除。customers表增加deleted字段删除时只做标记列表页默认过滤掉但30天内管理员可以到回收站还原。个别确实需要彻底清除的数据再单独做物理删除操作。这套机制实现了“给错误留一条后路”。5.2 多人同时编辑同一条客户记录导致覆盖团队多人共用系统后很快会遇到一个问题销售A和销售B同时打开同一个客户页面A保存了联系方式B保存了行业分类最后谁保存得晚谁就覆盖了另一个人的修改。排查后发现问题根源在于页面保存时提交了整个表单对象。解决方案是加一个并发控制客户表增加updated_at字段页面在加载时会保存这个时间戳保存时把当前时间戳一并提交。后台比较数据表里的updated_at和请求里的时间戳如果不一致就拒绝保存提示“该客户已被其他人修改请刷新后再操作”。这个方案虽然让偶尔的点击保存失败但比默默覆盖数据可靠得多。5.3 搜索变慢与索引优化客户列表页的搜索框支持按客户名称模糊匹配。团队几百个客户时很流畅但数据量积累到几千条后搜索开始有明显的延迟。排查过程很简单直接用EXPLAIN QUERY PLAN查看查询计划发现customers表上没有公司名称的索引每次搜索都是全表扫描。解决方法是给company_name字段加上索引CREATE INDEX idx_customers_company_name ON customers(company_name);除了客户名称activities表的next_follow_date和customers表的owner_id也都建了索引。这样“今天的待跟进任务”和“当前用户负责的客户列表”这两个高频查询都跑在索引上速度提升非常明显。这一点提醒我不要一上来就提前建一堆索引要在实际数据量增长以后根据慢查询日志和EXPLAIN结果针对性地补索引。5.4 导入Excel中文乱码一开始导入功能支持常见的.xlsx格式后来有同事用.xls老格式的文件里面包含中文就乱码了。排查发现是因为老格式的编码问题。处理方式很简单统一要求模板使用.xlsx格式服务端解析前端上传的Excel在解析时明确使用UTF-8编码。另外在字段映射页面上增加“预览前10行”功能导入前让用户核对一下数据乱码和数据错位问题在预览阶段就能被拦住。这个预览功能虽然简单但帮我们挡掉了相当多的脏数据。5.5 定时任务不触发部署初期出现过一次待办提醒没有按时发出的故障。排查过程比较曲折因为定时任务的日志一切正常但企业微信消息就是没收到。后来发现是脚本里的一个时间判断问题。服务器设置的时区是UTC而业务数据库里存的时间是本地时间。定时任务在扫描当天任务时拿北京时间去和UTC时间比较自然会出现偏差。修复方法是统一所有时间字段的存储格式在应用层统一转换为指定时区再输出。从那以后我再也没有用服务器默认时区存业务时间而是统一在代码里转成特定时区。6. 一些掏心窝的经验DeckcommCRM做到现在我最大的体会是小团队自研CRM最怕的不是技术难度而是把需求的边界无限扩大。曾经有同事提了一个需求“能不能在客户详情页嵌入一个在线表单功能让客户自己填写满意度调查”这个需求听起来很合理但仔细一想它涉及在线表单引擎、公网访问、身份校验、结果汇总几乎等于在CRM里再做一个第三方工具。我当时做了一个决定这类需求一律不做系统内实现而是通过外链方式引导——邮件模板里附上问卷链接客户点开的是腾讯问卷结果手动录入或通过接口回传。最后只花了半小时就满足了业务方而且没有给系统增加一丝复杂度。另一个深刻的体会是数据录入习惯的重要性。一个CRM系统是否好用有八成取决于录入的数据是否标准、是否完整。系统再漂亮前端再流畅只要数据乱后面所有报表和分析都是空中楼阁。所以我在项目里强制做了很多校验来源只能从下拉框选跟进类型只能是固定枚举商机阶段必须按顺序推进。这些限制会让使用者在最初几天有点不适应但坚持两周后系统里的数据质量会让日常使用体验发生质变。最后说说这套架构的扩展空间。目前DeskcommCRM依然运行在一台简陋的双核小服务器上数据库文件大小不到500MB完全没有性能压力。如果哪天团队规模扩大第一件要做的事是引入真正的数据库把SQLite替换成PostgreSQL然后增加按团队分组的租户隔离再考虑更细的权限。但这条路我暂时不会走上去因为小团队CRM的根本价值是“让大家愿意用一个工具沉淀客户数据”而不是“做一个功能无限的管理后台”。这一点想清楚了后面的路就稳了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑