1. 为什么我要自己动手写一套轻量CRM市面上的客户管理系统我前前后后试过不下十款从开源的套件到按年付费的SaaS功能列表一个比一个长但真正落到日常使用场景里八成以上的功能我压根没点开过。销售漏斗、自动化营销、AI预测这些听起来很唬人可对于一个三五人的小团队或者独立接单的自由职业者来说核心诉求其实就三件事记住每个客户是谁、上次聊到哪了、下一步该干什么。为了这三件事去养一套重型系统纯属给自己找罪受。所以我决定自己写一套简单版本的CRM。所谓“简单版本”不是功能残缺而是只保留最核心的客户管理闭环去掉一切为了显得专业而堆砌的模块。这套系统最终实现的效果是打开浏览器就能用客户信息一目了然跟进记录随手可查待办提醒不会漏。整个项目从设计到跑通我花了大概一周的业余时间代码量控制在两千行以内部署在一台最低配的云主机上绰绰有余。这篇文章我会把整套设计思路、技术选型、数据库结构、核心代码实现、以及我在实际使用中踩过的坑全部摊开来讲。适合谁看如果你是个体从业者、小团队负责人、或者刚入门的开发者想拿一个真实项目练手这套方案可以直接抄作业。我不打算讲什么高深架构就讲怎么用最朴素的技术解决最实际的问题。2. 整体设计思路与技术选型2.1 核心需求拆解到底要管什么在动手写代码之前我拿纸笔列了整整两页的需求然后一条条划掉最后剩下的才是真正要做的。这个过程很痛苦但非常值得。最终确定的核心需求只有四条客户档案管理记录客户的基本信息包括姓名、联系方式、来源渠道、当前状态。状态我简化为“潜在、跟进中、已成交、已流失”四种再多就是给自己找麻烦。跟进记录每次和客户沟通后能快速记一笔包括沟通时间、方式、内容和下一步计划。这是CRM的灵魂没有跟进记录的CRM就是个通讯录。待办提醒根据跟进记录里的“下一步计划”自动生成待办事项到期在首页提醒。这个功能让我再也没漏掉过回访。简单统计首页能看到客户总数、本月新增、本月成交、待跟进数量这几个数字。够用了饼图柱状图都是虚的。至于权限管理、多租户、自定义字段、工作流引擎这些我全部砍掉。一个人用或者小团队用大家共享一套数据就行搞那么复杂纯属内耗。2.2 技术选型为什么选这套“老土”的组合技术选型上我刻意避开了时髦的方案。后端用Python Flask数据库用SQLite前端用服务端渲染的Jinja2模板 少量原生JavaScriptCSS用Water.css这个极简框架。整套东西没有Node.js构建步骤没有Docker没有Redis没有消息队列。为什么这么选理由很直接Flask足够轻一个文件就能跑起来路由和视图函数一目了然。我不需要Django那种大而全的框架它的Admin和ORM对我来说太重了。SQLite是单文件数据库备份就是复制一个文件迁移就是拷贝走。对于数据量在十万条以内的场景性能完全够用。我实测过五千个客户加上五万条跟进记录查询响应都在毫秒级。服务端渲染意味着我不需要写API、不需要处理跨域、不需要管理前端状态。表单提交后刷新页面简单粗暴但极其可靠。Water.css是一个无需class的CSS框架引入一个链接就能让页面变得干净整洁。我不需要设计师也不需要调样式调到半夜。提示这套选型的前提是你的用户量在几十人以内数据量在十万条以内。超过这个量级SQLite的写入并发会成为瓶颈到时候再换PostgreSQL也不迟Flask的ORM层改起来很快。2.3 数据库表结构设计三张表搞定整个系统只需要三张表我反复推敲过字段确保每个字段都有明确的用途没有一个是“以后可能用得上”的。客户表customers字段名类型说明idINTEGER主键自增nameTEXT客户姓名必填phoneTEXT电话可选emailTEXT邮箱可选sourceTEXT来源渠道如“转介绍”“线上咨询”statusTEXT状态potential/following/won/lostnoteTEXT备注记录客户偏好等created_atDATETIME创建时间updated_atDATETIME最后更新时间跟进记录表follow_ups字段名类型说明idINTEGER主键自增customer_idINTEGER外键关联客户methodTEXT沟通方式电话/微信/邮件/见面contentTEXT沟通内容摘要next_actionTEXT下一步计划next_dateDATE下一步计划日期可为空created_atDATETIME记录时间待办表todos这张表其实可以省略直接从跟进记录里查next_date小于等于今天的记录即可。但我还是单独建了一张表原因是有些待办不来自跟进记录比如“整理合同模板”这种内部事务。表结构很简单id、title、due_date、done、created_at。三张表之间的关系非常清晰一个客户有多条跟进记录一条跟进记录可能生成一条待办。没有多对多没有自关联新手也能看懂。3. 核心功能模块的实操实现3.1 客户列表页搜索、筛选与分页客户列表是使用频率最高的页面我的设计目标是三秒内找到目标客户。页面上方是搜索框支持按姓名和电话模糊搜索下方是状态筛选标签点击“跟进中”就只显示跟进中的客户再下面是分页列表每页二十条。搜索的实现我用的是SQLite的LIKE语句虽然效率不如全文索引但在几千条数据下完全无感。代码大概长这样app.route(/customers) def customer_list(): keyword request.args.get(q, ).strip() status request.args.get(status, ) page request.args.get(page, 1, typeint) query Customer.query if keyword: query query.filter( db.or_( Customer.name.like(f%{keyword}%), Customer.phone.like(f%{keyword}%) ) ) if status: query query.filter(Customer.status status) pagination query.order_by(Customer.updated_at.desc()).paginate( pagepage, per_page20, error_outFalse ) return render_template(customers.html, paginationpagination, keywordkeyword, statusstatus)这里有个细节值得说排序用的是updated_at降序而不是created_at。为什么因为最近有互动的客户应该排在最前面这样我打开列表就能看到活跃客户而不是被一堆陈年旧客淹没。注意LIKE查询在数据量超过五万条时会明显变慢。如果你预计客户量会很大可以在SQLite上启用FTS5全文索引或者干脆换PostgreSQL用pg_trgm扩展。但说实话小团队一年能积累五千个客户就算不错了。3.2 客户详情页信息聚合与快速操作点进一个客户详情页要在一屏内展示所有关键信息。我的布局是左侧是客户基本信息和状态切换按钮右侧是跟进记录时间线时间线顶部有一个“添加跟进”的快捷表单。状态切换我用了四个按钮点击后通过一个简单的POST请求更新状态页面刷新后按钮高亮当前状态。这个交互很土但比下拉框选完再点保存要快得多。代码实现就是一个路由app.route(/customer/int:id/status, methods[POST]) def update_status(id): customer Customer.query.get_or_404(id) new_status request.form.get(status) if new_status in [potential, following, won, lost]: customer.status new_status customer.updated_at datetime.now() db.session.commit() return redirect(url_for(customer_detail, idid))跟进记录的时间线用了一个简单的循环渲染每条记录显示沟通方式图标、内容摘要、下一步计划和记录时间。如果next_date已经过期这条记录会标红提醒。这个视觉提示非常有效我一眼就能看出哪些客户的跟进已经拖延了。添加跟进的表单我做了防重复提交处理提交按钮点击后变成禁用状态并显示“提交中”。这个细节看似不起眼但如果没有网络稍慢时用户连点两下就会产生两条重复记录。我踩过这个坑后来在表单提交的JavaScript里加了一行this.disabled true就解决了。3.3 待办提醒让跟进不再遗漏待办提醒是我认为这套系统最有价值的功能。逻辑很简单每天第一次打开首页时系统查询所有next_date 今天且未完成的待办以及所有next_date 今天的跟进记录合并后显示在首页顶部。实现上我写了一个get_today_todos()函数def get_today_todos(): today date.today() # 来自跟进记录的待办 follow_todos db.session.query(FollowUp).filter( FollowUp.next_date today, FollowUp.next_date.isnot(None) ).order_by(FollowUp.next_date.asc()).all() # 来自手动添加的待办 manual_todos Todo.query.filter( Todo.due_date today, Todo.done False ).order_by(Todo.due_date.asc()).all() return follow_todos, manual_todos首页模板里这两类待办分开展示每条待办旁边有一个“完成”按钮。点击后跟进记录生成的待办会把next_date置空手动待办则把done设为True。这样待办列表就会自动清理不会越积越多。提示我建议把next_date设置为可空。有些跟进确实没有明确的下一步时间强行填一个日期只会制造假待办。空着就空着等有明确计划时再填。3.4 数据导入导出迁移与备份的保命手段虽然系统简单但数据是无价的。我做了两个保命功能CSV导出和CSV导入。导出功能把所有客户和跟进记录导出成一个CSV文件字段用逗号分隔编码用UTF-8 with BOM这样用Excel打开不会乱码。导入功能支持从CSV批量创建客户我预设了一个模板格式用户填好后上传即可。导入时做了去重判断如果姓名和电话都相同就跳过不重复创建。这两个功能代码量不大但给了我极大的安全感。万一哪天服务器挂了我手里还有一份CSV换个地方十分钟就能重新跑起来。4. 实际使用中踩过的坑与排查技巧4.1 常见问题速查表问题现象可能原因排查方法解决方案页面打开空白模板变量未传或拼写错误查看Flask控制台报错检查render_template的参数中文显示乱码数据库编码或响应头问题检查HTML的meta charset确保UTF-8贯穿始终提交表单后数据没保存数据库会话未提交查看是否有db.session.commit()补上提交语句搜索无结果LIKE语句大小写敏感用SQLite命令行测试统一转小写或用COLLATE NOCASE待办不显示日期比较逻辑错误打印today和next_date的值确保都是date类型部署后静态文件404路径配置错误查看浏览器网络面板用url_for生成静态路径4.2 三个让我印象深刻的坑第一个坑SQLite的日期类型。我一开始把next_date存成了字符串结果比较的时候“2024-1-5”和“2024-01-05”被当成不同的值导致待办提醒时有时无。后来统一用Python的date对象存入SQLAlchemy会自动处理格式问题才解决。这个坑让我明白日期字段一定要用数据库的日期类型不要图省事存字符串。第二个坑并发写入。有一次我同时开了两个浏览器标签页一个在添加跟进一个在修改客户状态结果后提交的那个把前一个的修改覆盖了。SQLite的默认隔离级别是SERIALIZABLE但Flask-SQLAlchemy的会话管理在并发下还是会有问题。我的解决方案很简单所有写操作都加一个全局锁虽然粗暴但对于单用户或小团队场景完全够用。具体就是在写操作的路由上加一个threading.Lock()。第三个坑备份策略。我一开始觉得SQLite就是个文件直接复制就行。后来发现如果复制的时候正好有写入操作复制出来的文件可能损坏。正确的做法是用SQLite的.backup命令或者先停掉应用再复制。我现在设置了一个每天凌晨三点的定时任务用sqlite3命令的.backup功能生成备份文件保留最近三十天。4.3 几个提升效率的小技巧快捷键提交在跟进记录表单里我加了CtrlEnter提交的快捷键。写跟进记录时手不用离开键盘效率提升明显。默认值填充添加跟进时沟通方式默认选中“微信”因为这是我最常用的方式。减少一次点击就是节省一秒。颜色标记客户状态用不同颜色标记潜在是灰色跟进中是蓝色已成交是绿色已流失是红色。列表页一眼扫过去就能判断客户分布。快速搜索搜索框支持输入后自动聚焦页面加载完光标就在搜索框里直接打字就行。5. 部署与日常维护的实操建议5.1 最低成本部署方案部署我选的是最朴素的方案一台最低配的云服务器装Python和Nginx用Gunicorn跑Flask应用Nginx做反向代理。没有Docker没有Kubernetes没有CI/CD。部署步骤就四步把代码传到服务器创建虚拟环境并安装依赖用Gunicorn启动应用绑定到本地端口配置Nginx转发请求到GunicornNginx配置里我加了Basic Auth因为这套系统没有登录功能但直接暴露在公网又太危险。Basic Auth虽然简陋但配合HTTPS足够安全。HTTPS证书用Lets Encrypt免费申请自动续期。注意如果你打算多人使用Basic Auth就不够了需要加一个简单的用户表。但我的建议是小团队直接用一套共享账号就行没必要搞复杂的权限体系。5.2 日常维护清单这套系统我维护了半年多总结了一个每周花十分钟就能完成的维护清单检查备份文件确认每天的备份任务正常执行随机打开一个备份文件验证完整性。清理无用数据把“已流失”超过一年的客户归档减少列表页的干扰。更新依赖每月检查一次Python包的安全更新用pip list --outdated查看。查看日志检查Gunicorn的错误日志看看有没有异常请求或报错。这套系统最大的优势就是维护成本极低。没有复杂的中间件没有需要调优的参数没有半夜告警。它就像一个安静的记事本需要的时候打开不需要的时候完全不打扰我。5.3 后续可以扩展的方向虽然我说“简单版本”但并不意味着永远不扩展。如果将来需求变了我会按这个优先级来加功能第一优先级是邮件集成把客户邮件自动关联到对应客户的时间线。第二优先级是简单的数据看板用Chart.js画一个月度成交趋势图。第三优先级是移动端适配现在的页面在手机上能用但不够顺手。但这些都是后话。至少目前这套两千行的系统完美满足了我的需求。它让我把精力花在客户身上而不是花在折腾工具上。这大概就是“简单版本”的真正意义工具应该消失在背景里让你专注于真正重要的事。我在实际使用中最大的体会是CRM的核心从来不是功能多强大而是你愿不愿意每天打开它。一个你愿意每天用的简单工具远胜过一个功能齐全但你懒得打开的复杂系统。如果你也在纠结要不要自己写一套我的建议是先拿纸笔列出你真正需要的三个功能然后用最熟悉的技术把它们实现出来。不要追求完美先跑起来再慢慢改。