1. 为什么我要自己动手写一套轻量CRM
市面上成熟的客户管理系统一抓一大把,功能表拉出来能翻好几屏,从线索打分到营销自动化再到BI看板,几乎你能想到的需求它都有。但问题恰恰出在这里:功能越多,上手成本越高,团队里真正愿意每天打开它的人就越少。我见过太多小团队花了几万块买了一套系统,结果销售还是用表格记客户,系统最后沦为“给老板看的摆设”。
这套简单版本CRM客户管理系统就是在这种背景下动手做的。它的定位非常明确:只解决三个核心问题——客户信息集中存、跟进记录不丢失、销售阶段看得清。没有花哨的自动化,没有复杂的权限矩阵,一个销售新人十分钟就能上手。适合谁参考?适合五到二十人规模的小团队、独立开发者接私活时给客户交付的轻量方案,以及想自己练手全栈开发的朋友。
我前后迭代了三版,第一版功能堆太多自己都嫌重,第二版砍到只剩骨架反而好用,第三版补上了几个真正高频的细节。下面把整套设计思路、数据模型、关键实现和踩过的坑完整拆一遍,你可以直接照着搭一套属于自己的版本。
2. 整体设计思路与方案选型拆解
2.1 先想清楚“不做什么”比“做什么”更重要
做轻量系统最容易犯的错,就是照着大厂CRM的功能清单做减法,减着减着发现每个功能都“好像有点用”,最后又变成四不像。我的做法是反过来:先列出团队每天真正会执行的动作,只保留这些动作对应的功能。
实际调研下来,小团队销售每天的动作就四个:录入新客户、查某个客户的历史沟通、更新客户当前阶段、看这周该跟进谁。对应的功能就是客户档案、跟进记录、阶段流转、待办提醒。像合同管理、回款计划、产品目录这些,要么用现有工具(比如财务软件)解决,要么等团队规模上来再说。砍功能不是偷懒,是让系统活下来。
2.2 技术选型:够用、好维护、一个人能扛
选型我遵循一个原则:如果半夜出问题,我一个人能不能在半小时内定位并修复。基于这个原则,技术栈定得很朴素。
| 层级 | 选型 | 选择理由 |
|---|---|---|
| 前端 | 原生HTML+轻量JS框架 | 不引入重型构建工具,改完直接刷新,部署零负担 |
| 后端 | Python Flask | 代码量少,路由清晰,一个人维护毫无压力 |
| 数据库 | SQLite | 单文件存储,备份就是复制一个文件,小团队数据量完全够 |
| 部署 | 单台轻量服务器 | 一个进程跑起来,不需要容器编排那套东西 |
有人会问SQLite能不能扛住并发。实测下来,二十人以内的团队,日常操作QPS峰值也就个位数,SQLite的写锁完全不是瓶颈。真到了扛不住的那天,迁移到PostgreSQL也就是改个连接字符串的事,前期没必要为不存在的规模焦虑。
2.3 数据模型:三张表撑起整个系统
整个系统的核心就是三张表,关系非常清晰。
- 客户表(customer):存客户的基本信息,包括名称、联系人、电话、来源、当前阶段、负责人、创建时间。
- 跟进记录表(follow_up):每一条记录关联一个客户,存沟通时间、沟通方式、沟通内容、下次跟进时间。
- 用户表(user):存销售账号,用于区分“我的客户”和“全部客户”。
客户和跟进记录是一对多的关系,一个客户可以有多条跟进。这里有个设计细节:客户的“当前阶段”不单独存历史,而是每次更新时覆盖,同时把变更动作写进跟进记录里。这样既省了一张阶段历史表,又能通过跟进记录回溯完整的阶段变化过程。对于小团队来说,这种“以记录代历史”的做法比单独建表更实用,因为销售本来就要写跟进,顺手就把阶段变更记了。
3. 核心功能模块的细节解析与实操要点
3.1 客户档案:字段设计里的取舍学问
客户表看着简单,字段怎么定却直接影响使用体验。我第一版加了二十多个字段,结果销售录入时嫌烦,很多字段空着。第三版砍到只剩八个必填加几个选填,录入时间从两分钟降到二十秒。
必填字段就这些:客户名称、联系人、联系电话、客户来源、当前阶段、负责人。选填的是备注和下次跟进时间。这里有个经验:“客户来源”这个字段一定要做成下拉选项而不是自由输入,否则销售会填出“朋友介绍”“朋友推荐的”“老客户介绍”三种写法,后期想统计来源分布时根本没法聚合。
阶段字段我定义了几个固定值:初步接触、需求确认、方案报价、谈判中、已成交、已流失。阶段值不要太多,超过七个销售就记不住自己该选哪个了。每个阶段对应一个默认的“停留天数阈值”,比如“方案报价”阶段超过七天没动静,系统就在待办里标红提醒。这个阈值我放在配置文件里,不同团队可以根据自己的成交周期调整。
3.2 跟进记录:让销售愿意写才是好设计
跟进记录功能最大的挑战不是技术实现,而是怎么让销售愿意写。我试过强制填写跟进内容才能保存,结果销售就写“聊了聊”三个字应付。后来改成两个措施:一是把输入框做大,默认给一个模板提示“本次沟通要点 / 客户反馈 / 下一步动作”;二是保存后立刻在客户详情页顶部显示最新一条记录,让销售看到自己写的东西被展示出来了,有正向反馈。
跟进记录表的关键字段是next_follow_time,这个字段驱动了整个待办提醒功能。每次保存跟进记录时,如果销售填了下次跟进时间,系统就自动把这个客户加入待办列表。如果没填,默认按当前阶段的阈值自动算一个时间。这个“自动兜底”很关键,因为销售经常忘记填下次跟进时间,没有兜底的话待办列表就会漏掉这些客户。
提示:跟进内容建议限制最少十个字,太短的记录后期回看时毫无价值。但不要限制太多,否则销售会想办法凑字数。
3.3 阶段流转与待办提醒:把“该跟进谁”这件事自动化
待办提醒是这个系统里销售用得最频繁的功能。实现逻辑不复杂:每天定时扫描所有未成交客户,找出next_follow_time小于等于今天的,按负责人分组展示。但有几个细节决定了它好不好用。
第一,待办要区分“今天到期”和“已逾期”,逾期的用不同颜色标出来,视觉上要有紧迫感。第二,待办列表里直接放一个“快速记录”按钮,点开就能写跟进,不用先跳转到客户详情页。这个快捷入口把“查看待办→记录跟进”的操作路径从四步缩短到两步,使用频率明显提升。第三,周末和节假日不推送提醒,否则销售休息时收到一堆待办会很烦躁,反而降低对系统的信任。
阶段流转还有一个隐藏价值:通过阶段分布看销售漏斗。虽然轻量系统不做复杂的BI,但一个简单的阶段数量统计就能让管理者看出问题。比如“方案报价”阶段堆积了二十个客户但“谈判中”只有两个,说明报价后的转化环节出了问题。这个统计我做成一个简单的柱状图放在首页,数据实时算,不存中间表。
4. 完整实操过程与关键环节实现
4.1 数据库建表与初始化
先把三张表建起来。SQLite的建表语句很直观,注意几个细节:客户表的stage字段加默认值,created_at用数据库自动时间戳;跟进记录表的外键关联客户表并设置级联删除,这样删客户时跟进记录自动清理,不会留孤儿数据。
CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, password_hash TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE customer ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, contact TEXT, phone TEXT, source TEXT, stage TEXT DEFAULT '初步接触', owner_id INTEGER NOT NULL, remark TEXT, next_follow_time DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES user(id) ); CREATE TABLE follow_up ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, content TEXT NOT NULL, method TEXT DEFAULT '电话', next_follow_time DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customer(id) ON DELETE CASCADE );建完表后插入一个默认管理员账号,密码用哈希存储,不要明文。初始化脚本里顺便插入几条测试数据,方便开发时调试页面。
4.2 后端接口设计:RESTful但不必教条
接口设计我遵循RESTful的大方向,但没有严格到每个动作都对应一个HTTP方法。小系统里可读性和开发效率比规范更重要。核心接口就这几个:
GET /api/customers获取客户列表,支持按阶段、负责人、关键词筛选POST /api/customers新建客户PUT /api/customers/<id>更新客户信息(包括阶段流转)DELETE /api/customers/<id>删除客户GET /api/customers/<id>/follow_ups获取某客户的跟进记录POST /api/customers/<id>/follow_ups新增跟进记录GET /api/todos获取当前用户的待办列表
这里有个实现要点:更新客户阶段时,后端要自动生成一条跟进记录,内容类似“阶段从‘需求确认’变更为‘方案报价’”。这样阶段变化就有了时间线,不需要单独的历史表。代码里就是在更新接口里判断stage字段是否变化,变了就插一条记录。
@app.route('/api/customers/<int:cid>', methods=['PUT']) def update_customer(cid): data = request.get_json() old = query_one("SELECT stage FROM customer WHERE id=?", (cid,)) # ... 执行更新 ... if 'stage' in data and data['stage'] != old['stage']: insert_follow_up(cid, f"阶段从「{old['stage']}」变更为「{data['stage']}」", method='系统') return jsonify({"ok": True})4.3 待办列表的查询逻辑与性能考量
待办查询看起来简单,但要考虑几个边界情况。基础查询是找出next_follow_time <= 今天且stage不是“已成交”和“已流失”的客户。但有些客户可能从来没填过next_follow_time,这些客户不应该出现在待办里,否则列表会被大量“僵尸客户”淹没。
SELECT c.*, u.display_name AS owner_name FROM customer c JOIN user u ON c.owner_id = u.id WHERE c.next_follow_time IS NOT NULL AND c.next_follow_time <= date('now') AND c.stage NOT IN ('已成交', '已流失') AND c.owner_id = ? ORDER BY c.next_follow_time ASC;数据量小的时候这个查询毫秒级返回。如果客户数到了几千条,给next_follow_time和owner_id建个联合索引就够了。我实测过五千条客户数据,加了索引后查询稳定在十毫秒以内。
4.4 前端页面的三个关键交互
前端我没有用重型框架,就是原生JS加fetch请求。页面不多,主要就四个:登录页、客户列表页、客户详情页、待办页。重点说三个交互细节。
第一个是客户列表的即时搜索。输入关键词后不按回车,直接监听输入事件做防抖,三百毫秒后发请求。这样销售边打字边看结果,体验比按按钮搜索流畅很多。防抖函数很简单,但效果提升明显。
第二个是客户详情页的跟进记录倒序展示。最新的跟进在最上面,销售打开客户第一眼就看到最近一次沟通内容,不用往下翻。每条记录显示时间、方式、内容和记录人,阶段变更的系统记录用灰色区分。
第三个是待办页的快速记录弹窗。点击待办项旁边的“记录”按钮,弹出一个轻量弹窗,只有内容输入框和下次跟进时间两个字段,保存后待办项自动消失。这个交互把记录跟进的操作成本降到最低,是提升系统使用率的关键设计。
5. 常见问题与排查技巧实录
5.1 销售不愿意用系统怎么办
这是所有内部工具面临的头号问题,技术手段只能解决一半。我的经验是三个措施配合:一是降低录入成本,能自动带出的字段绝不让人填,能选绝不让人打字;二是让销售先尝到甜头,比如待办提醒确实帮他记住了一个快要流失的客户,他自然就愿意用了;三是管理者带头用,如果主管自己都不看系统里的数据,只在群里问“那个客户怎么样了”,销售就会觉得系统是多余的。
5.2 数据备份与迁移的实操方案
SQLite的备份极其简单,写个定时任务每天凌晨把数据库文件复制一份到另一个目录,保留最近三十天的备份。迁移的时候把文件拷过去,改一下配置文件里的路径就能跑。但要注意备份时数据库可能正在写入,直接复制文件有概率拿到损坏的副本。正确做法是用SQLite自带的备份命令:
sqlite3 crm.db ".backup /backup/crm_$(date +%Y%m%d).db"这个命令会加锁保证备份一致性,比直接cp安全得多。我踩过一次直接复制的坑,恢复时发现文件损坏,幸好当时数据量小手动补回来了。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 待办列表为空 | 客户未填下次跟进时间 | 检查客户记录,补填时间或调整阶段阈值兜底逻辑 |
| 阶段变更没生成记录 | 更新接口未判断stage变化 | 检查PUT接口中stage字段的比较逻辑 |
| 页面加载慢 | 客户列表一次返回全部数据 | 加分页,默认每页二十条 |
| 删除客户后跟进记录还在 | 外键未设置级联删除 | 重建表时加上ON DELETE CASCADE |
| 多人同时编辑同一客户冲突 | 无并发控制 | 加一个updated_at字段做乐观锁校验 |
5.4 几个我踩过的坑
第一个坑是阶段阈值设得太死。一开始我把“方案报价”阶段的提醒阈值设成三天,结果销售抱怨说报价后客户经常要一周才回复,三天就提醒太频繁了。后来改成可配置,不同阶段不同阈值,默认值放宽到七天,销售自己也能在设置里调。
第二个坑是客户名称允许重复。早期没做唯一性校验,结果同一个客户被两个销售各建了一条,跟进记录分散在两处。后来加了“名称+电话”的联合唯一约束,新建时如果重复就提示“该客户可能已存在,是否查看”。
第三个坑是时间字段的时区问题。服务器用UTC时间,但销售看的是本地时间,导致待办提醒有时候早一天有时候晚一天。解决办法是数据库统一存UTC,展示和查询时都转成本地时区,转换逻辑封装成一个工具函数,所有地方统一调用。
6. 后续可以怎么扩展
这套系统的骨架已经能支撑小团队的日常客户管理了,但如果想继续往上长,有几个方向可以按需加。第一个是简单的数据统计页,比如每月新增客户数、各阶段转化率、销售个人业绩排名,用图表库画几个图就行,数据实时算不存中间表。第二个是客户导入导出,支持从表格批量导入,导出成表格给财务或老板看。第三个是操作日志,记录谁在什么时候改了什么,方便追溯。但每加一个功能都要问自己:团队真的会用吗?如果答案不确定,就先不加。
我在实际使用中最大的体会是,轻量系统的价值不在于功能多,而在于每个功能都被真正用起来。与其做一个功能齐全但没人打开的“完整版”,不如做一个功能很少但每天都在用的“简单版”。这套CRM到现在跑了快一年,客户数据攒了几百条,跟进记录上千条,团队已经离不开它了。这就够了。