1. 为什么小团队需要一个永久在线的CRM先说一个我观察到的现象很多三五人的小团队客户信息散落在微信聊天记录、Excel表格、飞书文档、甚至某个人手机备忘录里。等到需要复盘一个客户跟进了多久、上次沟通说了什么、报价发到哪个版本全组人开始翻聊天记录翻半小时翻不出来。这不是能力问题是工具缺位。市面上的SaaS CRM按人头收费五个人一年下来少说几千块功能还只用到两成。更关键的是客户数据放在别人服务器上导出受限、字段不能随便加、流程不能随便改。于是自建CRM这件事就变得很有吸引力——数据在自己手里功能按需定制一次搭好长期用。但自建两个字吓退了不少人。大家脑子里浮现的是要买服务器、要配数据库、要搞运维、要防攻击……听起来像是一个需要专职运维才能撑住的事。实际上用Flask PostgreSQL Nginx这套组合一个懂点Python的人花一个周末就能把核心功能跑起来而且跑得相当稳。这篇文章要聊的就是这件事怎么用轻量技术栈搭一个属于自己团队的客户管理系统让它永久在线——不是指服务器永不宕机而是指这套系统不依赖某个第三方服务的存续你自己完全掌控随时能迁移、能备份、能续命。关键词里的DeskcommCRM就是这类自建方案的典型代表它的思路值得拆开来看。适合谁读三到二十人的小团队负责人、独立开发者、想给自己业务做一套客户跟进工具的技术同学。不需要你是运维专家但需要你能看懂基本的Linux命令和Python代码。2. 技术选型为什么是Flask而不是Django2.1 小团队CRM的真实负载有多大先算一笔账。一个十人团队每人每天新增或更新20条客户记录一天200条写入一年按250个工作日算5万条记录。每条记录假设2KB含备注、标签、跟进历史总共100MB。这个量级说句不好听的SQLite都能扛。所以选型的第一原则是不要为不存在的规模做过度设计。很多团队一上来就上微服务、上消息队列、上分布式数据库结果维护成本比业务本身还高。CRM这种读多写少、并发极低的场景单机单进程完全够用。那为什么不用SQLite因为CRM会有多个人同时写的情况SQLite的写锁在并发写入时容易出问题而且备份和远程访问不如PostgreSQL方便。PostgreSQL在这个量级下性能绰绰有余还自带全文检索、JSON字段、窗口函数这些实用能力后面想加功能不用换库。2.2 Flask和Django的取舍逻辑Django自带Admin、ORM、认证、迁移开箱即用很多人第一反应是选它。但我实际搭过几套之后小团队CRM这个场景我更倾向Flask原因有三第一CRM的界面高度定制。客户列表要按行业分组、按跟进阶段着色、按负责人筛选这些Django Admin改起来反而别扭你得跟它的模板体系较劲。Flask没有预设你想怎么渲染就怎么渲染前端用原生HTML一点JS就能做出很顺手的界面。第二字段经常变。小团队的CRM需求是长出来的今天加个客户来源明天加个下次跟进提醒Django的迁移体系虽然完善但每次改模型都要生成迁移文件、处理冲突。Flask配SQLAlchemy或者直接写SQL改起来更直接。第三学习曲线和调试成本。Flask一个文件就能跑起来出问题一眼能看到底。Django的层次多新人接手时定位问题要翻好几层。当然Django不是不好如果你的团队已经有Django经验或者需要复杂的权限体系选Django完全合理。选型的核心是匹配团队现状不是追新。2.3 三个组件的分工组件角色为什么需要它FlaskWeb框架处理HTTP请求、路由、模板渲染轻量灵活PostgreSQL数据库存客户数据支持并发写、全文检索、JSON字段Nginx反向代理对外提供稳定入口处理静态文件、HTTPS、限流这个架构里Flask应用本身监听本地端口比如127.0.0.1:8000Nginx监听80/443对外。外部请求先到NginxNginx再转发给Flask。这样做的好处是Flask不用直接暴露在公网安全性高静态文件CSS、JS、图片由Nginx直接返回不占用Flask进程将来要加HTTPS、加访问限制都在Nginx层做不动应用代码。提示不要图省事让Flask直接监听0.0.0.0:80对外。Flask自带的开发服务器性能差、安全性弱只适合本地调试。生产环境必须用Nginx在前面挡着。3. 从零搭起PostgreSQL与Flask的落地细节3.1 PostgreSQL安装选对版本和安装方式热词里postgresql下载哪个版本postgresql安装教程windowslinux安装postgresql出现频率很高说明这一步卡住了不少人。我的建议很明确生产环境用Linux版本选当前稳定大版本的最新小版本。以AlmaLinux 9为例热词里出现了almalinux 9 安装nginx说明这个系统有不少人用安装PostgreSQL的步骤# 添加官方仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm # 禁用系统自带的postgresql模块避免版本冲突 sudo dnf -qy module disable postgresql # 安装PostgreSQL 16 sudo dnf install -y postgresql16-server postgresql16-contrib # 初始化数据库 sudo /usr/pgsql-16/bin/postgresql-16-setup initdb # 启动并设置开机自启 sudo systemctl enable --now postgresql-16Windows下安装更简单去官网下载安装包一路下一步即可但要注意两点一是安装时设置的postgres用户密码要记牢二是端口默认5432如果本机已有其他数据库占用要改。安装完验证一下sudo -u postgres psql -c SELECT version();能输出版本号就说明装好了。3.2 建库建用户别用postgres超级用户跑应用这是很多人踩的坑图省事直接用postgres超级用户连接数据库。一旦应用有SQL注入漏洞攻击者就能拿到整个数据库实例的控制权。正确做法是给应用单独建一个受限用户-- 用postgres用户登录后执行 CREATE USER crm_app WITH PASSWORD 你的强密码; CREATE DATABASE deskcomm_crm OWNER crm_app; -- 只给必要的权限 GRANT ALL PRIVILEGES ON DATABASE deskcomm_crm TO crm_app;然后编辑pg_hba.conf把本地连接方式从peer改成md5或scram-sha-256让应用能用密码登录# 找到这行 local all all peer # 改成 local all all scram-sha-256改完重启PostgreSQL生效。这一步不做Flask连接时会报peer authentication failed是新手最常见的报错之一。3.3 Flask连接PostgreSQL连接池是必须的Flask本身不带数据库连接管理需要装驱动。推荐用psycopg2同步或psycopg新版配合SQLAlchemy做ORMpip install flask sqlalchemy psycopg2-binary连接字符串这样写from flask import Flask from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] \ postgresqlpsycopg2://crm_app:密码127.0.0.1:5432/deskcomm_crm app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_size: 5, max_overflow: 10, pool_pre_ping: True, # 关键连接前先ping避免用到失效连接 pool_recycle: 3600, # 一小时回收一次连接 } db SQLAlchemy(app)pool_pre_ping这个参数我要重点说。PostgreSQL默认会断开空闲超过一定时间的连接如果Flask的连接池里存着已经失效的连接下一个请求就会报server closed the connection unexpectedly。加上pool_pre_pingTrue每次取连接前先探活失效的就重建这个报错基本就消失了。这是我在生产环境踩过好几次才总结出来的。3.4 客户表怎么设计先想清楚跟进流程CRM的核心表其实就几张客户表、联系人表、跟进记录表、用户表。设计时最容易犯的错是字段拍脑袋加导致后面查询和统计很痛苦。我的经验是先画出团队的跟进流程再倒推字段。比如流程是线索→初步接触→需求确认→报价→成交/流失那客户表里就要有一个stage字段存当前阶段一个stage_updated_at存阶段变更时间。这样后面统计每个阶段平均停留多久才有数据支撑。CREATE TABLE customer ( id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, industry VARCHAR(100), source VARCHAR(50), -- 客户来源 stage VARCHAR(30) DEFAULT lead, owner_id INTEGER REFERENCES app_user(id), tags JSONB DEFAULT [], -- 标签用JSONB灵活 created_at TIMESTAMPTZ DEFAULT now(), stage_updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_customer_stage ON customer(stage); CREATE INDEX idx_customer_owner ON customer(owner_id);tags用JSONB是个实用技巧。小团队的标签体系经常变用JSONB存数组查询时用tags [重点客户]就能筛出来不用为标签单独建表建关联。PostgreSQL的JSONB支持GIN索引数据量大了也不慢。4. Nginx配置让CRM稳定对外的关键几行4.1 反向代理的基本配置Nginx装好之后AlmaLinux下sudo dnf install nginxUbuntu下sudo apt install nginx核心配置就一个server块server { listen 80; server_name crm.yourdomain.com; # 静态文件直接由Nginx返回 location /static/ { alias /var/www/deskcomm/static/; expires 7d; add_header Cache-Control public; } # 其他请求转发给Flask location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; } }X-Real-IP和X-Forwarded-For这两行很重要。Flask拿到的request.remote_addr默认是Nginx的地址127.0.0.1加上这两个头之后应用里通过request.headers.get(X-Real-IP)才能拿到真实客户端IP。做登录日志、异常检测都靠它。4.2 静态文件交给NginxFlask只处理动态请求很多人把CSS、JS、图片也交给Flask返回这是浪费。Flask处理一个静态文件请求要经过完整的WSGI流程而Nginx直接读磁盘返回性能差一个数量级。配置里那个location /static/块就是干这个的。Flask项目里静态文件默认放在static/目录模板里用url_for(static, filenamestyle.css)生成路径。部署时把这个目录软链或者复制到Nginx能访问的位置即可。4.3 用systemd守护Flask进程Flask应用不能靠python app.py裸跑终端一关就没了。用systemd做成服务# /etc/systemd/system/deskcomm.service [Unit] DescriptionDeskcomm CRM Flask App Afternetwork.target postgresql-16.service [Service] Userwww-data WorkingDirectory/opt/deskcomm ExecStart/opt/deskcomm/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app Restartalways RestartSec5 [Install] WantedBymulti-user.target这里用gunicorn而不是Flask自带的服务器。-w 4表示4个工作进程小团队够用了。Restartalways保证进程崩溃后自动拉起这是永久在线的基础保障。sudo systemctl daemon-reload sudo systemctl enable --now deskcomm sudo systemctl status deskcomm看到active (running)就说明服务起来了。5. 那些文档里不会写的踩坑记录5.1 数据库连接数被打满有一次团队反馈CRM突然打不开报too many connections。登服务器一看PostgreSQL的连接数到了100上限。原因是Flask的gunicorn开了4个worker每个worker的连接池pool_size5加上max_overflow10理论上最多4×1560个连接再加上一些残留连接就顶到上限了。解决办法有两个一是调小连接池pool_size3, max_overflow5二是调大PostgreSQL的max_connections。我建议两个都做连接池调小是治本因为小团队根本用不到那么多并发连接。-- 查看当前连接数 SELECT count(*) FROM pg_stat_activity; -- 查看最大连接数 SHOW max_connections;5.2 时间字段的时区陷阱PostgreSQL的TIMESTAMPTZ存的是UTC时间Flask取出来默认也是UTC。如果前端直接显示用户看到的时间会比北京时间少8小时。这个坑我踩过客户跟进记录的时间全乱了。处理方式在Flask里统一转换或者用pytz/zoneinfo在展示层转。我的做法是在应用启动时设置import os os.environ[TZ] Asia/Shanghai然后在模板里用过滤器统一格式化。不要在每个地方手动加8小时那样迟早出错。5.3 Nginx的client_max_body_sizeCRM里经常要上传合同、报价单附件。Nginx默认允许的请求体大小是1MB超过就报413错误。这个报错很隐蔽因为Flask那边根本收不到请求日志里也没有。server { ... client_max_body_size 20m; # 按需调整 }加上这行重启Nginx即可。20MB对大多数附件够用了如果经常传大文件考虑用对象存储而不是直接存数据库。5.4 备份不是可选项永久在线的前提是数据不丢。PostgreSQL的备份很简单# 每天凌晨3点全量备份 0 3 * * * pg_dump -U crm_app -h 127.0.0.1 deskcomm_crm | gzip /backup/crm_$(date \%Y\%m\%d).sql.gz配合find /backup -name crm_*.sql.gz -mtime 30 -delete保留30天。备份文件最好再同步一份到另一台机器或者对象存储别只放本机——本机磁盘挂了备份也没了。注意备份完一定要定期做恢复演练。我见过备份脚本跑了半年结果真要恢复时发现备份文件是空的因为pg_dump的密码认证一直失败但脚本没检查返回值。备份脚本里加上set -e和错误通知。6. 让CRM真正被团队用起来的几个设计6.1 录入成本决定使用率技术搭好了最大的挑战其实是让团队成员愿意用。我见过太多自建系统搭完只有搭建者自己在用。核心原因是录入太麻烦。降低录入成本的做法客户列表页支持快速新建只填名字和来源两个字段就能存其他信息后面慢慢补。跟进记录支持在客户详情页直接输入回车即存不用跳转页面。这些交互细节比功能多少更重要。6.2 用看板视图代替表格销售团队对表格的接受度远低于看板。把客户按stage字段渲染成看板每个阶段一列客户卡片可以拖拽换阶段这个体验比表格筛选直观得多。实现上不需要复杂的前端框架原生JS的拖拽API加上一个更新接口就够了。// 拖拽结束更新阶段 async function onDrop(customerId, newStage) { await fetch(/api/customer/${customerId}/stage, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({stage: newStage}) }); }后端收到请求更新stage和stage_updated_at字段。就这么简单但使用率会明显提升。6.3 提醒机制别让客户被遗忘CRM最有价值的不是记录是提醒。一个客户三天没跟进、报价发出去一周没回音系统应该主动提醒负责人。实现方式可以很简单每天定时任务扫描stage_updated_at超过阈值的客户生成提醒列表登录时在首页展示。# 每天早上的定时任务 def check_stale_customers(): threshold datetime.now(timezone.utc) - timedelta(days3) stale Customer.query.filter( Customer.stage_updated_at threshold, Customer.stage.notin_([won, lost]) ).all() # 生成提醒记录这个功能不需要推送、不需要邮件就在首页显示一个需要跟进的列表效果已经很好。7. 关于永久在线这件事的真实理解最后聊聊标题里永久在线这四个字。它不是指99.99%的可用性——那是大厂用钱堆出来的。对小团队自建系统来说永久在线的真实含义是这套系统不依赖任何你控制不了的东西。SaaS服务可能涨价、可能改条款、可能哪天就关停了你的数据在别人手里。自建系统跑在自己的服务器上代码在自己手里数据在自己手里哪怕服务器到期了换个机器把备份一恢复半小时就能重新跑起来。这种掌控感才是永久的来源。当然代价是你得自己维护。系统出问题没人帮你兜底安全补丁要自己打备份要自己盯。所以我的建议是如果团队里没有一个人愿意承担这个维护责任那还是老老实实用SaaS。自建不是免费的它只是把付费换成了投入时间。对于愿意投入的团队Flask PostgreSQL Nginx这套组合的性价比极高。它足够简单简单到你花一个周末就能理解全部又足够强大强大到能支撑一个团队几年的客户管理需求。DeskcommCRM这类方案的价值也在这里——它证明了小团队完全有能力拥有自己的客户管理系统而不必把命脉交给别人。我在实际维护中最大的体会是别追求功能全追求数据准。一个只有客户列表和跟进记录、但每条数据都真实及时的系统比一个功能齐全但没人维护的系统有价值一百倍。先把核心流程跑通让团队养成录入习惯后面加什么功能都是水到渠成的事。
企业数字化 ERP 产品动态
相关推荐
多分类与多标签分类:从踩坑到落地的完整指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 1:23:05
从PPT到产线:智慧工厂解决方案落地的数据采集与系统建设实务 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 1:23:05
Eden伊甸模拟器配置全攻略:固件密钥与图形后端调优 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 1:23:05
库存数据迁移实战:从清洗、方案到核对的完整指南 1. 背景:为什么会有“古早库存”,以及它带来的问题看到这个标题,很多做后台开发或者数据治理的同学应该会心一笑。“古早库存”,翻译过来就是老系统里遗留了很久的历史数据;“搬家”,放到工程语境里&#x… · 2026/9/26 2:05:56
ZEBASE光学设计库:高效复用验证结构的工程实践指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:05:56
ArcPy自动填充Excel到GIS字段:零依赖高效集成方案 简介:本资源是一款面向GIS开发者与空间数据处理人员的自动化工具包,聚焦Excel属性表与ArcGIS地理要素属性表的智能关联与字段填充问题,适用于城市规划、土地管理、环境监测等需高频集成空间与属性数据的业务场景。包内共5个文件,包… · 2026/9/26 2:05:56
Corundum 100G NIC 移植到 Bittware VV4:Arria 10 平台工程搭建与调试实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:05:50
Delta模拟器金手指:3步激活、常用码速查与没效果时的排查办法 Delta模拟器金手指:3步激活、常用码速查与没效果时的排查办法 【免费下载链接】Delta Delta is an all-in-one classic video game emulator for non-jailbroken iOS devices. 项目地址: https://gitcode.com/GitHub_Trending/delt/Delta
经典游戏打到一半卡… · 2026/9/26 2:05:31
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46