做外包这些年,接到的管理系统需求五花八门,但像“北京旅游信息管理平台”这种项目,几乎每年都会碰到几次。表面看就是一个信息管理系统:把北京主要景点的资料、游客评论、推荐路线管起来。实际上手之后才会发现,真正费神的不是写接口,而是选型、数据模型设计、部署适配这一连串看起来不起眼的决定。这篇文章我打算把整个项目从零到上线的过程完整复盘一遍,包括为什么用 Flask 而不是 Django 或 FastAPI,数据表怎么设计才不容易返工,部署时会踩到哪些跟环境和服务器相关的坑,以及上线后怎么扩展成带预订功能的产品。无论你是刚学 Flask 的初学者,还是手头正好接到旅游类 CMS 订单的开发者,按这个思路做都会省很多时间。
1. 技术选型:为什么这类管理系统我用 Flask 而不是 Django
1.1 先看清这个项目的真实需求
接到需求时,客户给的原话往往很简单:“做一个北京旅游信息管理平台,能看景点、能看路线、能评论就行。”但把这句话翻译成技术语言,你需要搞清楚背后几件事。
第一,平台的用户是谁。前台访客通常只是查景区介绍、看推荐路线、浏览评价,偶尔注册账号后发表评论。后台管理员需要维护景区资料、上下架路线、审核评论。也就是说,这是一个典型的 C 端展示加 B 端管理组合平台,不是纯粹的数据报表系统。
第二,数据量有多大。北京旅游信息管理平台即使做得很全,核心景点的数量也就在几百到几千这个量级,路线几十条,评论可能几万条。这个体量下,任何主流的关系型数据库都能轻松扛住,完全不需要一开始就引入复杂的搜索中间件、缓存集群。
第三,开发周期和团队维护成本。做这类项目的人,通常是一个小团队甚至一个人。这意味着框架越重,你写起来反而越束手束脚;框架太轻,所有东西都要自己搭又累得慌。Flask 的“微框架”定位在这里非常合适:路由、模板、请求处理开箱即用,ORM 接入 SQLAlchemy 后也够规范,剩下的钱和精力留给业务本身。
提示:管理系统类项目最忌讳一上来就堆技术。先把页面清单、角色清单、数据字段清单列出来,再谈框架,选型自然就清晰了。
1.2 Flask、Django 和 FastAPI 的取舍逻辑
这个话题如果拿到论坛上讨论,往往吵得不可开交,但放到“北京旅游信息管理平台”这个具体场景里,答案其实非常明确。我自己的习惯是先做一张对比表再下结论。
| 对比维度 | Flask | Django | FastAPI |
|---|---|---|---|
| 学习成本 | 低,核心简洁 | 偏高,概念多 | 中等,依赖类型标注理解 |
| 自带功能 | 少,按需扩展 | 全,Admin、ORM、表单都有 | 少,主打 API 能力 |
| 模板渲染 | Jinja2 很顺手 | 自带模板引擎 | 通常配合前端分离使用 |
| 异步支持 | 需要额外配置 | 需要版本适配 | 原生异步,性能强 |
| 适用场景 | 中小型全栈网站 | 后台复杂的大型系统 | 纯 API 服务、高并发实时接口 |
| 部署难度 | 低,灵活 | 中,结构固定 | 低,但配合反向代理更合理 |
放到这个项目里来看,Django 的自带 Admin 确实能省掉一部分后台页面,但定制起来反而不如自己写 Flask 后台顺手。而且 Django 对模型层有一套相对重的约定,后面想调整表结构时的束缚感很强。FastAPI 性能是好,但如果项目需要服务端渲染页面,而不是纯前后端分离,FastAPI 在这方面就得靠额外挂模板,绕了一圈还不如 Flask 直接。
网上那些“Flask 与 FastAPI 比较”的文章,大多是在比接口性能、比异步高并发。可旅游信息管理平台的瓶颈根本不在并发,而在页面组织、表单处理、后台操作逻辑。Flask 在这一点上的优势是:它不绑架你,蓝图、SQLAlchemy、Jinja2 组合起来就是一套完整又轻巧的网站骨架。
1.3 最终选型结论
我选了 Flask 2.x + Flask-SQLAlchemy + Jinja2 + Bootstrap,数据库开发阶段用 SQLite,正式部署换成 MySQL 8.0。Flask 的启动方式很简单,pip install flask之后十几行代码就能跑起页面,配合工厂模式后扩展性也足够。
还有一点很重要:项目标题里写明了“基于 flask”,这本身也是一种约束。客户既然指定了技术栈,说明他们之后可能有二次开发团队接手,选一个生态稳定、大家都会的框架,比追求炫技更重要。Flask 的源码少而清晰,遇到问题看源码基本能解决,这种确定性在交付时非常值钱。
2. 从需求到表结构:北京旅游信息管理平台的数据模型设计
2.1 功能模块先拆分,再谈建表
很多新手拿到项目就开始写建表语句,结果写到一半发现字段对不上需求,反复改表,非常伤。我习惯先用一张功能清单把页面和操作锁死。
前台模块包含:景点列表页、景点详情页、旅游路线列表页、旅游路线详情页、用户注册登录、发表评论、个人收藏。后台模块包含:管理员登录、景点管理(增删改查、上下架)、路线管理、评论审核、基础数据统计。把这些模块列出来后,数据表之间的关系就自然浮现了。
核心表无非是这几张:用户表、景点表、路线表、评论表,以及一张多对多的景点路线关联表。如果还要做收藏,就再加一张收藏表。不要一上来就设计几十张表,旅游信息管理平台不是 ERP,先满足核心流程,等真有扩展需求时再迁移。
注意:建表之前花半天时间画清楚“页面 → 操作 → 数据字段”的对应关系,后面写代码的速度会快很多。返工最多的一定不是写页面,而是反复加字段。
2.2 核心数据表是怎么设计的
我以一个极简但完整的模型为例,这段代码基本可以直接抄走。
from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() # 景点和路线是多对多关系,需要一张中间表 route_attractions = db.Table( 'route_attractions', db.Column('route_id', db.Integer, db.ForeignKey('route.id'), primary_key=True), db.Column('attraction_id', db.Integer, db.ForeignKey('attraction.id'), primary_key=True) ) class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False, comment='是否管理员') created_at = db.Column(db.DateTime, default=datetime.now) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Attraction(db.Model): __tablename__ = 'attraction' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False, index=True, comment='景点名称') category = db.Column(db.String(50), index=True, comment='分类:古迹/公园/博物馆等') address = db.Column(db.String(200), comment='地址') description = db.Column(db.Text, comment='景点介绍') cover_image = db.Column(db.String(200), comment='封面图路径') is_active = db.Column(db.Boolean, default=True, comment='是否上架') created_at = db.Column(db.DateTime, default=datetime.now) class Route(db.Model): __tablename__ = 'route' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False, comment='路线标题') days = db.Column(db.Integer, default=1, comment='游玩天数') price = db.Column(db.Numeric(10, 2), default=0, comment='参考价格') description = db.Column(db.Text) is_active = db.Column(db.Boolean, default=True) attractions = db.relationship('Attraction', secondary=route_attractions, backref='routes') class Comment(db.Model): __tablename__ = 'comment' id = db.Column(db.Integer, primary_key=True) attraction_id = db.Column(db.Integer, db.ForeignKey('attraction.id'), index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) content = db.Column(db.Text, nullable=False) rating = db.Column(db.Integer, default=5, comment='评分1-5') status = db.Column(db.Integer, default=0, comment='0待审核 1通过 2拒绝') created_at = db.Column(db.DateTime, default=datetime.now)每个字段都有它的理由。Attraction.name加了索引,是因为景点名称会出现在搜索和列表筛选中,不加索引的话数据量上来后 LIKE 查询会慢。price用Numeric(10, 2)而不是浮点型,是因为钱这种字段用浮点数做运算会产生精度问题。User.password_hash只存哈希值,绝不存明文密码,这是安全底线。
评论表的status字段很关键,它决定了审核机制怎么做。用户发表的评论默认状态是 0,前台页面只查status == 1的数据,后台管理员可以在审核列表中看到待审核内容。用数字而不是布尔值,是因为审核结果不只有“通过/不通过”两种,后续还可能加入“自动审核”状态,留出的余地正好。
2.3 关联表和多对多关系的处理
路线和景点是典型的多对多关系:一条路线可以包含多个景点,一个景点也可以出现在多条路线中。既然是多对多,就不能直接在任意一张表里加外键字段,必须拆出一张中间表。我上面的route_attractions就是干这个用的。
db.relationship里设置了secondary=route_attractions之后,SQLAlchemy 会帮我们自动处理关联查询。比如想获取第一条路线的所有景点:
route = Route.query.get(1) attractions = route.attractions反过来,想查某个景点出现在哪些路线里:
attraction = Attraction.query.get(1) routes = attraction.routes这种双向查询在页面展示时非常方便。景点详情页可以展示“包含该景点的推荐路线”,路线详情页可以按顺序展示景点列表。在设计阶段把这种关联关系想清楚,后面写视图函数就是一行查询的事。
2.4 开发用 SQLite,上线换 MySQL
Flask-SQLAlchemy 的数据库连接串只需要在配置里改一行,就能完成开发环境到生产环境的切换。
# config.py import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key') SQLALCHEMY_DATABASE_URI = os.environ.get( 'DATABASE_URL', 'sqlite:///tour.db' ) SQLALCHEMY_TRACK_MODIFICATIONS = False本地跑的时候用 SQLite 零配置,直接一个文件存所有数据。生产环境部署前把DATABASE_URL设为mysql+pymysql://username:password@host/dbname?charset=utf8mb4,然后用 Flask-Migrate 执行迁移就可以了。要注意的是 MySQL 的字符集一定要指定utf8mb4,否则中文和 emoji 都可能出现乱码。
3. 核心页面和接口如何一步步跑通
3.1 用 Blueprint 拆分模块,别把所有路由堆一个文件里
Flask 最容易被忽视的优点就是蓝图机制。很多教程项目把路由全部写在app.py里,页面少还好,页面一多你就会发现,找一条路由要翻几百行,改一处功能还可能误伤别处。
我习惯这样组织目录:
app/ __init__.py # 工厂函数,初始化 Flask 扩展 models.py # 数据模型 views/ main.py # 前台页面路由 auth.py # 注册登录路由 admin.py # 后台管理路由 templates/ main/ # 景点列表、详情等模板 auth/ # 登录注册模板 admin/ # 后台管理模板 static/ css/ js/ uploads/ run.py # 启动入口 config.py # 配置在app/__init__.py里像这样注册蓝图:
from flask import Flask from .models import db from .views.main import main_bp from .views.auth import auth_bp from .views.admin import admin_bp def create_app(): app = Flask(__name__) app.config.from_object('config.Config') db.init_app(app) app.register_blueprint(main_bp) app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(admin_bp, url_prefix='/admin') return app这样做的直接好处是:前台、后台、用户体系各管各的,互相不干扰。后面如果想给后台加一个统计报表模块,直接新开一个report.py蓝图,注册进去就行,完全不用改动原有代码。
3.2 前台景点列表与详情页的实现
前台是整个平台的门面,也是游客最关心的部分。列表页要展示封面图、景点名、分类、简介和评分,详情页要展示完整介绍、地图位置、评论列表和评分。关键是分页查询和时间消耗要控制好。
# views/main.py from flask import Blueprint, render_template, request, abort from ..models import Attraction, Route, Comment from sqlalchemy import func main_bp = Blueprint('main', __name__) @main_bp.route('/') def index(): page = request.args.get('page', 1, type=int) keyword = request.args.get('keyword', '').strip() category = request.args.get('category', '').strip() query = Attraction.query.filter_by(is_active=True) if keyword: query = query.filter(Attraction.name.contains(keyword)) if category: query = query.filter(Attraction.category == category) pagination = query.paginate(page=page, per_page=12, error_out=False) attractions = pagination.items return render_template( 'main/index.html', attractions=attractions, pagination=pagination, keyword=keyword, category=category ) @main_bp.route('/attraction/<int:attraction_id>') def attraction_detail(attraction_id): attraction = Attraction.query.filter_by( id=attraction_id, is_active=True ).first_or_404() comments = Comment.query.filter_by( attraction_id=attraction_id, status=1 ).order_by(Comment.created_at.desc()).all() avg_rating = db.session.query( func.avg(Comment.rating) ).filter( Comment.attraction_id == attraction_id, Comment.status == 1 ).scalar() or 0 return render_template( 'main/detail.html', attraction=attraction, comments=comments, avg_rating=round(float(avg_rating), 1) )这段代码里有个容易忽略的点:filter_by(is_active=True)一定要加上。管理员在后台把某个景点下架后,前台就应该立刻看不见,但 URL 如果直接输景点 ID,仍可能访问到详情。加上这个条件后,下架景点访问会直接返回 404,逻辑上更严谨。
评分聚合用了func.avg,直接在数据库里算平均值,而不是把评论全部查出来后在 Python 里循环求和。数据量上万条后,这种细节是性能的分水岭。
3.3 评论功能与用户登录的关系
游客要发表评论,必须先登录。实现上并不复杂,核心就是记住“评论跟人走”这件事。用户在表单里填完内容后,后台要从登录状态中取用户 ID,而不是让用户自己传用户 ID。这一点非常关键,否则任何人都可以伪造别人的评论。
# views/main.py 中发表评论的接口 from flask import request, redirect, url_for, flash from flask_login import login_required, current_user from ..models import Comment @main_bp.route('/attraction/<int:attraction_id>/comment', methods=['POST']) @login_required def post_comment(attraction_id): content = request.form.get('content', '').strip() rating = request.form.get('rating', 5, type=int) if not content: flash('评论内容不能为空') return redirect(url_for('main.attraction_detail', attraction_id=attraction_id)) rating = max(1, min(5, rating)) comment = Comment( attraction_id=attraction_id, user_id=current_user.id, content=content, rating=rating, status=0 ) db.session.add(comment) db.session.commit() flash('评论已提交,等待管理员审核') return redirect(url_for('main.attraction_detail', attraction_id=attraction_id))@login_required来自 Flask-Login,它会自动拦截未登录的用户并跳转到登录页。用户提交的评分在入库前又做了一次max/min限制,防止通过篡改表单数据提交超出 1-5 范围的数值。外部输入默认不可信,这个习惯一定得养成。
3.4 后台管理和权限控制
后台页面不需要漂亮的布局,但要非常明确:谁能进、谁能改、谁只能看。普通用户即使猜到后台 URL,也不能让他进。我用的方法是在用户模型里加上is_admin字段,再封装一个装饰器。
from functools import wraps from flask import abort from flask_login import current_user def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated or not current_user.is_admin: abort(403) return f(*args, **kwargs) return decorated_function然后后台所有视图函数都加一行@admin_required。Flask-Login 的current_user.is_authenticated只表示“登录过”,不能代表“是管理员”,所以两个条件必须同时判断。
后台景点管理页需要支持新增、编辑、删除、上下架。删除操作我建议做软删除,也就是把is_active设为 False,而不是物理 delete。原因很现实:景点一旦被关联到历史路线或历史订单里,物理删除会让前台页面出现悬空引用,排查起来非常痛苦。
4. 部署上线:Flask 开发服务器别直接扛生产流量
4.1 开发服务器和生产服务器不是一回事
Flask 自带的开发服务器会在终端打印一行“WARNING: This is a development server”,那句话不是吓唬人。开发服务器在处理并发请求时能力弱,也没有专门针对生产环境的超时和优雅关闭处理。用它跑生产环境,遇到流量稍微上来一点,页面就会卡住。
生产环境需要的是一个 WSGI 服务器。Linux 上首选gunicorn,Windows 上则用waitress。原因是 gunicorn 在 Windows 下兼容性很差,而 waitress 是跨平台的。我部署过不少项目,这个坑踩得特别实在。
Linux 部署命令:
gunicorn -w 4 -b 127.0.0.1:8000 "run:app"Windows 部署命令:
waitress-serve --host=127.0.0.1 --port=8000 run:app-w 4表示启动 4 个 worker 进程,可以更好地利用 CPU 多核。但要注意 worker 数目不是越大越好,进程太多会导致内存占用高、数据库连接数过多。一个 2 核 4G 的云服务器,配 4 个 worker 就很合适。
注意:
run.py里通常要提供一个app对象供 WSGI 服务器加载,不要把app.run()直接放在模块顶层执行,否则生产环境也会启动开发服务器。
4.2 Nginx 反向代理和静态文件分离
生产环境一般不会让用户直接访问 8000 端口,前面要加一层 Nginx 做反向代理。它的作用有两个:一是把外部请求转发给 gunicorn,二是不让 Flask 处理 CSS、JS、图片这些静态文件。
Nginx 配置片段:
server { listen 80; server_name example.com; client_max_body_size 20m; location /static { alias /var/www/tour/app/static; expires 7d; } location /uploads { alias /var/www/tour/app/static/uploads; expires 30d; } 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-Proto $scheme; } }为什么要单独配置/uploads?因为上传的景点封面图体积通常比前端静态资源大,而且内容会动态变化,不应该和静态资源混淆。expires 30d是给浏览器一个缓存时间,减少重复请求。但要注意,如果后面做了图片替换功能,缓存时间设置太长会让用户看到旧图,这一点要跟业务确认。
client_max_body_size 20m是限制上传文件大小,防止有人传一个巨大的文件把服务器磁盘塞满。我见到的很多 Flask 项目都忘了配这一项,结果上线后被人传了几十 G 的垃圾文件。
4.3 环境变量和配置拆分
配置文件里写数据库密码是很多项目的通病。代码一旦传到 git 仓库,密码就等于泄露了。我在这个项目里把敏感信息全部放到环境变量里,本地开发用.env文件,生产环境在 systemd 服务里加载。
# .env 示例 SECRET_KEY=your-secret-key DATABASE_URL=mysql+pymysql://tour_user:password@127.0.0.1/tour_db?charset=utf8mb4Python 启动时加载:
import os from dotenv import load_dotenv load_dotenv()如果服务器上用 systemd 管理 gunicorn 进程,可以在 service 文件里指定环境变量,这样进程重启后配置也会自动加载。整个流程下来,代码仓库里永远不会出现真实密码。
4.4 上线前必须检查的清单
app.debug = False,这个不解释太多,开着 debug 模式等于把服务器内部信息暴露给用户。SECRET_KEY换成强随机值,不要用默认值。- 数据库执行过迁移,表结构没问题。
- 静态文件目录权限正确,上传目录可写。
- 备份策略:每天凌晨用
mysqldump备份数据库,保留最近 7 份。 - 日志:gunicorn 的错误日志单独写一个文件,方便排查线上问题。
gunicorn -w 4 -b 127.0.0.1:8000 \ --error-logfile /var/log/tour/error.log \ --access-logfile /var/log/tour/access.log \ "run:app"这些检查看着琐碎,但任何一个遗漏都可能在上线后变成半夜被叫醒的理由。
5. 上线之后的维护心得和扩展方向
5.1 中文乱码和时区问题要提前处理
北京旅游信息管理平台是中文系统,字符集问题会非常突出。MySQL 建库时一定要指定:
CREATE DATABASE tour_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;如果已经建好了库,也要检查表字符集。我接手过的一个项目,页面显示正常但写入数据库后变成问号,最后查出来是某张表的字符集还停留在 latin1,改完建表语句才解决。
Flask 侧连接串里加charset=utf8mb4只是第一步,后台写数据时最好统一用datetime.now()上层的服务器时间。如果之后要接入国际化用户,最好把所有时间字段统一存 UTC,展示层再转本地时间。现阶段只做北京本地旅游平台,直接存本地时间问题不大,但框架上提前留好timezone参数会省掉未来不少事。
5.2 查询优化从这几个方向先动手
平台上线初期数据少,性能问题看不出来。等评论和浏览记录积累到几万条后,最容易出问题的位置其实是景区详情的评论列表和搜索页面。
第一个优化点是给高频查询字段加索引。Attraction.name、Attraction.category、Comment.attraction_id、Comment.status这四个字段都是查询条件,也该进索引。SQLAlchemy 的模型定义里已经写了部分index=True,剩下的可以在数据库层面手动补。
第二个优化点是警惕 N+1 查询。在模板里循环景点时,如果每个景点都额外查一次评论数,那就是 N 次额外查询。正确做法是用一次聚合查询把景点 ID 对应的评论数全部查出来,再在模板里做映射。数据量到十万级别时,这种优化效果非常明显。
第三个优化点是给列表页加个简单的缓存。Flask 里可以用 Flask-Caching 配合 Redis,设置 5 分钟缓存:
from flask_caching import Cache cache = Cache(app, config={'CACHE_TYPE': 'redis', 'CACHE_DEFAULT_TIMEOUT': 300}) @main_bp.route('/') @cache.cached(timeout=60) def index(): # 原有逻辑不变 ...但缓存有代价:后台修改景点资料后,前台可能要等缓存过期才能看到。这种场景下更稳妥的方式是后台每次修改数据后主动cache.clear(),或者对不同的 URL 用不同的缓存 key。我个人的经验是,数据量没到百万级别前,先别急着上缓存,把 SQL 写好更重要。
5.3 从“信息管理”升级到“预订交易”的扩展路径
“北京旅游信息管理平台”这个名字里的关键词是“管理”,但如果客户接着提需求,十有八九会往预订、门票、线路下单方向走。这个时候的扩展不是简单加一张订单表,而是要动整个数据模型。
建议的扩展顺序是:先加一张booking_order表,记录用户预订了哪条路线、出发日期、人数、支付状态。需要注意订单表关联的是“路线快照”而不是简单的路线 ID。因为路线价格和内容会变化,如果用户在 1 月下单,路线在 2 月改了价,订单里存的应该是用户当时看到的价格,实时去查路线表只会得到错误结果。所以订单表里需要冗余字段:路线名称、单价、总价、下单时快照等。
接着再加支付对账表。虽然这个阶段可能只是对接微信支付或支付宝的简单回调,但如果一开始就把支付回调数据单独存一张表,未来对账会省很多事。很多小项目做到后面乱,就是因为支付、订单、退款全堆在一张表里。
如果未来要做手机端,可以再考虑把 Flask 的部分页面改造成 API 接口,但这个阶段我不建议直接推倒重来。用 Flask 的蓝图继续维护现有的服务端渲染页面,另外开一个api蓝图提供 JSON 接口,两边共存过渡,风险最小。
5.4 维护阶段的经验沉淀
这类信息管理平台,真正开发时间可能只有一两周,但维护时间可能长达一两年。所以在写代码阶段就当作“未来会有别人接手”来写,能省非常多沟通成本。
模型字段里写清楚comment注释,路由函数命名清晰,页面模板的分块命名直观,这些看起来不重要的东西,在半年后自己回头修 bug 时会变成救命稻草。另外我强烈建议每次上线前记录一份变更日志,哪怕只是两三行文字,也能帮客户搞清楚这次更新到底改了什么。
在 git 提交信息里,我也习惯遵循“类型 + 摘要”的格式,比如feat: 增加评论审核状态筛选、fix: 修复景点分页丢失搜索关键词。这种习惯看似笨拙,但当你同时维护四五个项目时,检索历史记录的速度会完全不一样。
提示:客户往往不关心你用了什么设计模式,但他们会关心平台能不能稳定跑、改功能快不快。代码结构的价值最终都会体现在“改需求”的速度上。
最后说几句实在的
做完这个北京旅游信息管理平台,我最大的体会是:Flask 这类轻框架真正的优势不是“性能强”,而是“问题易定位”。源码少、依赖链路短、出 bug 时能快速判断是路由问题、数据库问题还是模板问题,这种掌控感在做业务系统时特别重要。如果以后再接到类似项目,我会在第一天就把数据库迁移工具接入,而不是等项目上到生产环境后才补;也会把评论审核功能提前做成可配置的,因为客户十有八九中途会改变审核规则。希望这篇复盘能让你少走几趟冤枉路,也欢迎在评论区聊聊你部署 Flask 项目时遇到过的坑。