小团队自建永久在线CRM:Flask+PostgreSQL+Nginx实战
发布时间:2026/9/26 17:00:50来源:尧图网络
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 监听的本地端口比如 5000Flask 本身不直接暴露在公网安全性更好。第二是静态资源服务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_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) display_name db.Column(db.String(64)) class Customer(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(128), nullableFalse, indexTrue) 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, defaultdict) created_at db.Column(db.DateTime, defaultdatetime.utcnow) updated_at db.Column(db.DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow) class FollowUp(db.Model): id db.Column(db.Integer, primary_keyTrue) customer_id db.Column(db.Integer, db.ForeignKey(customer.id), indexTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) content db.Column(db.Text, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow, indexTrue)几个设计要点值得说明。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, typeint) 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( pagepage, per_page20, error_outFalse ) return render_template(customers.html, paginationpagination)这里有个细节用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_idcustomer_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_idcurrent_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 倍加 12核机器设 4 或 5 都行-b 127.0.0.1:5000只监听本地不暴露公网--access-logfile -把访问日志输出到标准输出方便 systemd 收集。用 systemd 管理 Gunicorn创建/etc/systemd/system/deskcomm.service[Unit] DescriptionDeskcomm CRM Gunicorn Afternetwork.target postgresql-16.service [Service] Useryouruser WorkingDirectory/opt/deskcomm EnvironmentPATH/opt/deskcomm/venv/bin ExecStart/opt/deskcomm/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app Restartalways [Install] WantedBymulti-user.target然后sudo systemctl daemon-reload sudo systemctl enable --now deskcomm。Restartalways是关键进程崩了自动拉起这就是永久在线的基础保障。5.2 Nginx 反向代理配置安装 NginxAlmaLinux 9sudo dnf install -y nginx sudo systemctl enable --now nginx配置文件/etc/nginx/conf.d/deskcomm.confserver { 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 与域名有域名的话用 Lets Encrypt 免费证书。装 certbotsudo 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 serverPostgreSQL 没启动systemctl status postgresql-16password authentication failed密码错或认证方式不对检查.env和pg_hba.confdatabase does not exist库名写错或没建库sudo -u postgres psql -l连接数超限连接池配置不当检查 SQLAlchemy 的pool_sizeSQLAlchemy 默认连接池是 5 个连接Gunicorn 4 个 worker 每个都要连库加上管理连接可能超过 PostgreSQL 默认的 100 上限。小团队一般不会遇到但如果报too many clients在create_engine时加pool_size5, max_overflow10。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 或 CentOSSELinux 是绕不开的要么学会配策略要么关掉不推荐。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, filenamestyle.css, vapp_version)。7.3 功能扩展的想象空间这套系统跑稳之后可以按需加功能。邮件集成跟进记录自动发提醒邮件数据导出支持导出 Excel 给老板看移动端适配用响应式布局让手机也能用操作日志记录谁改了哪个客户方便追溯。热词里crm系统改造说明很多人有定制需求Flask 的好处就是改起来自由想加什么加什么。我个人在实际操作中的体会是自建 CRM 最大的价值不是省钱而是数据主权和灵活性。客户字段想加就加流程想改就改不用等厂商排期。这套 Flask PostgreSQL Nginx 的组合部署一次大概两小时之后基本不用管systemd 自动拉起备份脚本自动跑真正做到了永久在线。如果你也在纠结要不要自建我的建议是团队超过三人、客户超过一百个就值得动手了。
网站建设高端定制企业官网