1. 为什么小团队需要一个"永久在线"的CRM
做过小团队管理的人都有一个共同体会:客户信息散落在微信聊天记录、Excel表格、个人手机通讯录里,销售一走,客户跟着走。市面上成熟的SaaS CRM按人头收费,五个人一年下来少说几千块,而且数据存在别人服务器上,导出还各种限制。免费版功能阉割严重,字段不能自定义,流程不能改,用起来处处掣肘。
我所在的团队一共六个人,做的是企业咨询类业务,客户生命周期长、跟进节点多,对客户管理的需求其实不复杂:记录客户基本信息、跟进记录、成交状态,支持多人协作,数据自己能掌控。试过几款免费CRM之后发现两个核心痛点——一是数据不在自己手里,二是功能改不动。于是决定自建一套,用Flask + PostgreSQL + Nginx这套组合,部署在一台低配云服务器上,实现"永久在线"的客户管理系统,也就是标题里说的 DeskcommCRM。
这套方案的核心思路很朴素:Flask 负责业务逻辑和页面渲染,PostgreSQL 负责数据持久化,Nginx 负责反向代理和静态资源分发。三者各司其职,都是久经考验的成熟技术,社区资料丰富,出问题好排查。整套系统跑在一台 2核4G 的服务器上绰绰有余,成本一年不到两百块,数据完全自主可控。
这篇文章适合两类人看:一类是小团队负责人或运营,想自己搭一套客户管理系统但不知道从哪下手;另一类是有 Python 基础、想找一个完整项目练手的开发者。我会把选型逻辑、部署步骤、踩过的坑、优化技巧全部摊开讲,你照着做基本能复现一套可用的系统。
2. 技术选型背后的取舍逻辑
2.1 为什么是 Flask 而不是 Django
很多人第一反应是"做CRM这种带后台管理的系统,Django 不是更合适吗?自带 Admin、自带 ORM、自带用户体系"。这话没错,Django 确实能让你在半天内搭出一个能用的后台。但我最终选了 Flask,原因有三个。
第一,这套系统的页面交互是高度定制化的。CRM 的客户列表不是简单的表格,需要按跟进状态筛选、按负责人分组、按最后联系时间排序,还要内嵌跟进记录的时间线。Django Admin 虽然能改,但改起来要跟它的抽象层搏斗,不如 Flask 直接写路由和模板来得痛快。
第二,Flask 的学习曲线对小团队更友好。团队里如果有非专职开发的人要维护这套系统,Flask 的代码结构一目了然——一个路由对应一个函数,一个函数操作一个模型,没有魔法。Django 的 settings、middleware、signal 这些概念对新手来说是负担。
第三,依赖更轻。Flask 核心包很小,配合 SQLAlchemy 做 ORM、Flask-Login 做认证、Jinja2 做模板,整套依赖装下来不到 30MB。Django 全家桶装完轻松上百MB,在低配服务器上启动和内存占用都有差距。
当然,Flask 的代价是很多东西要自己搭。用户认证、权限控制、表单验证、数据库迁移,这些 Django 开箱即用的功能,Flask 需要自己选型组装。但正因为如此,每一块都是可控的,出了问题知道去哪找。
2.2 PostgreSQL 相比 MySQL 的实际优势
热词里有人搜"mysql和postgresql语句差异",说明不少人在纠结选哪个。我的建议是:新项目如果没有历史包袱,优先选 PostgreSQL。原因不是性能,而是几个实际用起来很爽的特性。
JSONB 字段类型是最大的杀手锏。CRM 里客户信息经常要存一些非结构化的东西,比如"客户偏好"、"沟通注意事项"、"自定义标签"。用 MySQL 你得建一堆关联表或者存 TEXT 再自己解析,PostgreSQL 直接用一个 JSONB 字段搞定,还能对 JSON 内部字段建索引、做查询。比如查所有标签里包含"重点客户"的记录,一条 SQL 就够。
全文检索也是原生支持。客户跟进记录多了之后,想搜"上次提到预算"这种关键词,PostgreSQL 的to_tsvector和to_tsquery直接能用,不用额外装 Elasticsearch。
窗口函数和 CTE在处理"每个客户最近一次跟进记录"这类需求时特别顺手。MySQL 8.0 之后也支持了,但 PostgreSQL 的实现更成熟,语法也更自然。
至于安装,Linux 上用包管理器一条命令的事,Windows 和 Mac 都有官方安装包。热词里"postgresql下载哪个版本"这个问题,我的建议是选当前稳定大版本的最新小版本,比如 16.x,不要追 beta 版,也不要守着已经停止维护的老版本。
2.3 Nginx 在这套架构里到底干什么
有人会问:Flask 自己就能对外提供服务,为什么还要套一层 Nginx?这个问题值得说清楚,因为很多人部署时跳过 Nginx,结果上线后各种问题。
Nginx 在这里承担四个角色。第一是反向代理,把外网的 80/443 端口请求转发到 Flask 监听的本地端口(比如 5000),Flask 本身不直接暴露在公网,安全性更好。第二是静态资源服务,CSS、JS、图片这些文件由 Nginx 直接返回,不走 Flask,速度提升明显,也减轻了 Python 进程的压力。第三是 SSL 终止,HTTPS 证书配在 Nginx 上,Flask 只管处理明文请求,配置简单。第四是缓冲和限流,Nginx 能扛住慢客户端,避免 Flask 的工作进程被拖死。
热词里"nginx反向代理"、"nginx配置"、"nginx安装"这些搜索量很高,说明这是部署环节的核心难点。后面我会给出完整的配置模板,直接抄就行。
3. 从零搭建:环境准备与依赖安装
3.1 服务器基础环境
我用的是一台 2核4G 的云服务器,系统选的 AlmaLinux 9(热词里有人搜"almalinux 9 安装nginx",说明这个系统用的人不少)。如果你用 Ubuntu 22.04 或 Debian 12,命令略有差异,但逻辑一样。
先更新系统并装基础工具:
sudo dnf update -y sudo dnf install -y python3 python3-pip python3-devel gcc gitPython 版本建议 3.10 以上,Flask 3.x 和 SQLAlchemy 2.x 都要求较新的 Python。AlmaLinux 9 自带的 Python 3.9 也能跑,但建议用python3.11或更高版本,避免一些类型注解的兼容问题。
3.2 PostgreSQL 安装与初始化
Linux 上安装 PostgreSQL 最省事的方式是用官方仓库。以 AlmaLinux 9 为例:
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo dnf -qy module disable postgresql sudo dnf install -y postgresql16-server postgresql16-contrib sudo /usr/pgsql-16/bin/postgresql-16-setup initdb sudo systemctl enable --now postgresql-16装完之后要做几件事。第一,设置 postgres 用户密码:
sudo -u postgres psql ALTER USER postgres WITH PASSWORD '你的强密码'; \q第二,创建业务数据库和专用用户。不要用 postgres 超级用户跑应用,权限太大不安全:
sudo -u postgres psql CREATE DATABASE deskcomm_crm; CREATE USER crm_user WITH ENCRYPTED PASSWORD '业务密码'; GRANT ALL PRIVILEGES ON DATABASE deskcomm_crm TO crm_user; \c deskcomm_crm GRANT ALL ON SCHEMA public TO crm_user; \q第三,调整认证方式。默认的pg_hba.conf可能只允许本地 socket 连接,如果 Flask 和数据库在同一台机器上,用 localhost 连接即可,不用改。如果分机器部署,需要把pg_hba.conf里对应行的peer或ident改成md5,然后sudo systemctl reload postgresql-16。
注意:PostgreSQL 16 默认的
pg_hba.conf对 localhost 的 IPv4 连接用的是scram-sha-256,这是好事,别改成trust,否则任何本地进程都能无密码连库。
3.3 Python 虚拟环境与依赖
强烈建议用虚拟环境,别把包装到系统 Python 里:
cd /opt sudo mkdir deskcomm && sudo chown $USER deskcomm cd deskcomm python3.11 -m venv venv source venv/bin/activate pip install --upgrade pip pip install flask flask-sqlalchemy flask-login flask-wtf psycopg2-binary gunicorn python-dotenv这里解释一下每个包的作用。Flask是核心框架;Flask-SQLAlchemy是 ORM,让你用 Python 类操作数据库;Flask-Login管用户会话;Flask-WTF处理表单和 CSRF 防护;psycopg2-binary是 PostgreSQL 驱动;gunicorn是生产环境的 WSGI 服务器,Flask 自带的开发服务器不能用于生产;python-dotenv用来读.env配置文件,避免密码硬编码。
4. 核心功能模块的设计与实现
4.1 数据模型设计
CRM 的核心是数据模型。我设计了四张表:用户表、客户表、跟进记录表、标签表。用 SQLAlchemy 定义如下:
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) password_hash = db.Column(db.String(256), nullable=False) display_name = db.Column(db.String(64)) class Customer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), nullable=False, index=True) company = db.Column(db.String(128)) phone = db.Column(db.String(32)) email = db.Column(db.String(128)) status = db.Column(db.String(32), default='潜在') owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) extra = db.Column(db.JSON, default=dict) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class FollowUp(db.Model): id = db.Column(db.Integer, primary_key=True) customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) content = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)几个设计要点值得说明。extra字段用 JSON 类型,存自定义属性,比如{"预算": "50万", "决策人": "张总"},这样不用为每个团队改表结构。status用字符串而不是枚举,方便后续加状态不用改数据库。created_at和updated_at都建索引,因为列表页经常按时间排序。FollowUp表的customer_id建索引,因为查某个客户的跟进记录是高频操作。
4.2 客户列表的筛选与分页
客户列表是使用频率最高的页面,性能优化重点在这里。核心查询逻辑:
@app.route('/customers') @login_required def customer_list(): page = request.args.get('page', 1, type=int) status = request.args.get('status', '') keyword = request.args.get('q', '') query = Customer.query if status: query = query.filter(Customer.status == status) if keyword: query = query.filter( db.or_( Customer.name.ilike(f'%{keyword}%'), Customer.company.ilike(f'%{keyword}%') ) ) pagination = query.order_by(Customer.updated_at.desc()).paginate( page=page, per_page=20, error_out=False ) return render_template('customers.html', pagination=pagination)这里有个细节:用ilike而不是like,因为 PostgreSQL 的ilike是大小写不敏感匹配,中文场景下虽然没区别,但英文客户名搜索时体验更好。分页用paginate,每页 20 条,避免一次查太多数据。
实操心得:如果客户量超过一万条,
ilike '%关键词%'会走全表扫描,速度明显下降。这时候可以给name和company字段建 GIN 索引配合pg_trgm扩展,或者直接用 PostgreSQL 的全文检索。小团队几千条客户的话,普通索引完全够用。
4.3 跟进记录的时间线展示
客户详情页要展示所有跟进记录,按时间倒序。这里用 SQLAlchemy 的关系查询:
customer = Customer.query.get_or_404(customer_id) followups = FollowUp.query.filter_by(customer_id=customer_id)\ .order_by(FollowUp.created_at.desc()).all()模板里用 Jinja2 循环渲染成时间线。每条记录显示跟进人、时间、内容。这里有个小技巧:把跟进内容里的换行符转成<br>,用content|replace('\n', '<br>')|safe,但要注意 XSS 风险,所以内容入库前要用bleach或markupsafe做转义。
4.4 用户认证与权限隔离
小团队 CRM 的权限模型不用太复杂,两种角色够了:管理员能看所有客户,普通成员只能看自己负责的客户。用 Flask-Login 实现:
from flask_login import LoginManager, login_required, current_user login_manager = LoginManager() login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) @app.route('/customers') @login_required def customer_list(): if current_user.is_admin: query = Customer.query else: query = Customer.query.filter_by(owner_id=current_user.id) # ... 后续筛选逻辑密码存储用werkzeug.security的generate_password_hash和check_password_hash,默认是 PBKDF2 算法,够安全。千万别用 MD5 或 SHA1 存密码,这是底线。
5. 部署上线:Gunicorn + Nginx 完整配置
5.1 Gunicorn 启动 Flask 应用
开发时用flask run没问题,生产环境必须用 Gunicorn。启动命令:
gunicorn -w 4 -b 127.0.0.1:5000 --access-logfile - --error-logfile - app:app参数解释:-w 4是 4 个工作进程,一般设为 CPU 核数的 2 倍加 1,2核机器设 4 或 5 都行;-b 127.0.0.1:5000只监听本地,不暴露公网;--access-logfile -把访问日志输出到标准输出,方便 systemd 收集。
用 systemd 管理 Gunicorn,创建/etc/systemd/system/deskcomm.service:
[Unit] Description=Deskcomm CRM Gunicorn After=network.target postgresql-16.service [Service] User=youruser WorkingDirectory=/opt/deskcomm Environment="PATH=/opt/deskcomm/venv/bin" ExecStart=/opt/deskcomm/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app Restart=always [Install] WantedBy=multi-user.target然后sudo systemctl daemon-reload && sudo systemctl enable --now deskcomm。Restart=always是关键,进程崩了自动拉起,这就是"永久在线"的基础保障。
5.2 Nginx 反向代理配置
安装 Nginx(AlmaLinux 9):
sudo dnf install -y nginx sudo systemctl enable --now nginx配置文件/etc/nginx/conf.d/deskcomm.conf:
server { listen 80; server_name crm.yourdomain.com; client_max_body_size 10M; location /static/ { alias /opt/deskcomm/app/static/; expires 30d; add_header Cache-Control "public, immutable"; } location / { proxy_pass http://127.0.0.1:5000; 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; } }几个关键点。location /static/单独处理,静态文件由 Nginx 直接返回,不走 Flask,速度提升非常明显。proxy_set_header那几行是必须的,否则 Flask 里拿到的客户端 IP 全是 127.0.0.1,日志和限流都没法做。client_max_body_size控制上传文件大小,默认 1M 太小,客户资料附件经常超。
配完sudo nginx -t测试语法,然后sudo systemctl reload nginx。
5.3 HTTPS 与域名
有域名的话,用 Let's Encrypt 免费证书。装 certbot:
sudo dnf install -y certbot python3-certbot-nginx sudo certbot --nginx -d crm.yourdomain.comcertbot 会自动改 Nginx 配置,加上 443 监听和证书路径,还会配置自动续期。这一步做完,你的 CRM 就是 HTTPS 的了,浏览器地址栏有小锁,客户数据在传输过程中加密,这是基本要求。
注意:certbot 自动续期依赖 systemd timer,装完用
systemctl list-timers | grep certbot确认一下。我遇到过服务器时间不对导致续期失败的情况,建议开个监控,证书到期前 30 天提醒。
6. 常见问题排查与避坑实录
6.1 数据库连接问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
could not connect to server | PostgreSQL 没启动 | systemctl status postgresql-16 |
password authentication failed | 密码错或认证方式不对 | 检查.env和pg_hba.conf |
database does not exist | 库名写错或没建库 | sudo -u postgres psql -l |
| 连接数超限 | 连接池配置不当 | 检查 SQLAlchemy 的pool_size |
SQLAlchemy 默认连接池是 5 个连接,Gunicorn 4 个 worker 每个都要连库,加上管理连接,可能超过 PostgreSQL 默认的 100 上限。小团队一般不会遇到,但如果报too many clients,在create_engine时加pool_size=5, max_overflow=10。
6.2 Nginx 502 错误的排查思路
502 是部署后最常见的问题,意思是 Nginx 连不上后端的 Gunicorn。排查顺序:
systemctl status deskcomm看 Gunicorn 是否在跑curl http://127.0.0.1:5000直接测后端是否响应sudo tail -f /var/log/nginx/error.log看 Nginx 报什么错- 检查 SELinux 是否拦截,AlmaLinux 默认开启,用
sudo setsebool -P httpd_can_network_connect 1放行
我踩过最坑的一次是 SELinux 拦截了 Nginx 到 5000 端口的连接,日志里只写Permission denied,查了半天才想到是 SELinux。如果你用 AlmaLinux 或 CentOS,SELinux 是绕不开的,要么学会配策略,要么关掉(不推荐)。
6.3 静态文件 404 的典型原因
Nginx 配了location /static/但访问 404,八成是路径不对。alias后面的路径必须是目录的绝对路径且以斜杠结尾,/opt/deskcomm/app/static/不能写成/opt/deskcomm/app/static。另外确认 Flask 应用里的静态文件确实在那个目录,Flask 默认的静态目录是app/static/,如果你改了static_folder参数,Nginx 配置也要跟着改。
6.4 数据备份:别等丢了才后悔
自建系统最大的风险是数据丢失。PostgreSQL 备份很简单,写个脚本每天跑:
#!/bin/bash DATE=$(date +%Y%m%d) pg_dump -U crm_user -h localhost deskcomm_crm | gzip > /backup/crm_$DATE.sql.gz find /backup -name "crm_*.sql.gz" -mtime +30 -delete加到 crontab 里每天凌晨 3 点执行。备份文件要传到另一台机器或对象存储,别只放本机,硬盘挂了就全没了。恢复的时候用gunzip -c crm_20240101.sql.gz | psql -U crm_user deskcomm_crm。
实操心得:光有备份不够,要定期演练恢复。我有次备份文件其实是空的,因为
pg_dump报错但脚本没检查退出码。后来在脚本里加了set -e和错误邮件通知,才踏实。
7. 性能优化与后续扩展方向
7.1 数据库层面的优化
系统跑起来之后,如果客户量涨到几万条,列表页开始变慢,可以从这几个方向优化。第一,给高频查询字段建索引,比如status、owner_id、updated_at。第二,用EXPLAIN ANALYZE分析慢查询,PostgreSQL 会告诉你哪一步耗时最长。第三,考虑加 Redis 缓存,把客户列表、统计数据这些变化不频繁的结果缓存几分钟,能显著降低数据库压力。
热词里有人搜"postgresql增量同步软件",如果以后要做多机部署或者数据分析,可以用逻辑复制把数据同步到另一台机器,主库只管写,从库负责读和报表,互不影响。
7.2 应用层面的优化
Flask 应用本身可以做几件事。开启模板缓存,生产环境app.config['TEMPLATES_AUTO_RELOAD'] = False。用flask-caching缓存页面片段,比如侧边栏的统计数字。静态资源加版本号,避免浏览器缓存旧文件,Jinja2 里可以用url_for('static', filename='style.css', v=app_version)。
7.3 功能扩展的想象空间
这套系统跑稳之后,可以按需加功能。邮件集成,跟进记录自动发提醒邮件;数据导出,支持导出 Excel 给老板看;移动端适配,用响应式布局让手机也能用;操作日志,记录谁改了哪个客户,方便追溯。热词里"crm系统改造"说明很多人有定制需求,Flask 的好处就是改起来自由,想加什么加什么。
我个人在实际操作中的体会是,自建 CRM 最大的价值不是省钱,而是数据主权和灵活性。客户字段想加就加,流程想改就改,不用等厂商排期。这套 Flask + PostgreSQL + Nginx 的组合,部署一次大概两小时,之后基本不用管,systemd 自动拉起,备份脚本自动跑,真正做到了"永久在线"。如果你也在纠结要不要自建,我的建议是:团队超过三人、客户超过一百个,就值得动手了。