
1. 培训机构的教学管理不该再靠截图和Excel去年帮一个做少儿编程培训的朋友搭线上课程系统他给我看了一叠“管理工具”课程表在Excel里作业靠家长群发图考试成绩随手拍张照发给学员家长学习记录压根没有。学员请假、补课、续报全靠老师脑子记。他跟我抱怨说这不是小机构的问题稍微有点规模的机构也一样只是大家习惯了。我问他到底想要什么他说得很直接我要一个网站家长能进去看课表、孩子能看课程视频、老师能在线发作业、学员能在线答题考试最后最好还能自动统计一下每个孩子的学习进度。技术栈他也提了希望用Python因为后续他自己也想学方便维护。我选了Flask。这篇文章就是这次实战的完整记录。系统最终跑通并上线使用覆盖课程管理、作业发布与提交、在线考试、自动判分、学习记录采集与进度可视化全部基于Flask框架完成主干代码大概三千行。如果你也在用Flask做类似的在线教学、培训类系统或者你准备从零搭一个带学生管理的Web平台这篇文章能帮你少走很多弯路。先说结论Flask做这种中小型培训系统完全够用重要的是业务模型的划分和数据表的设计框架本身反而是最简单的一层。下面我会按“技术选型—数据模型—项目骨架—核心模块—统计报表—生产部署”的顺序把整套系统的设计思路和落地踩坑全部讲清楚。2. 为什么选Flask而不是Django或FastAPI这不是一个信仰问题是一个预算问题、维护问题、时间问题。朋友当时的约束条件有三个第一个没有专职运维服务器是台2核4G的云主机第二个预计同时在线人数不超过五十人峰值可能出现在晚上七点以后第三个后续二开需求不确定希望保持灵活性。这种场景下Flask是最合适的。2.1 Flask到底适合什么规模的系统有人说Flask太轻生产环境不适合。我的看法恰恰相反轻是优点。对于并发不过百、单机部署、业务边界清晰的系统Flask的简单直观反而降低了出Bug的概率。框架越重隐性约定越多团队不熟的情况下容易踩坑。Flask的路由、请求上下文、模板渲染、ORM集成每一层都可以独立理解和替换出了问题能快速定位。我在设计这套系统时做了个内部约定Flask只负责HTTP层面的路由和视图业务逻辑放在service层数据访问全部走SQLAlchemy。这样后续哪怕把Flask换成FastAPI业务部分也能复用大半。这个约定在小项目中看起来有点“过度设计”但等你要改接口、加定时任务、做数据统计时就会发现它值回票价。2.2 和Django、FastAPI的一次横向对比三者我都用过直接说差异。Django自带Admin后台、ORM、迁移工具、用户体系确实全但它的全是一套偏“重量级”的约定比如固定项目结构、Model定义方式、中间件链。如果团队本身熟悉Django那没问题不熟悉的话光理解settings配置和app拆分就要花不少时间。FastAPI的异步性能和自动文档很亮眼但生态里很多组件没有Flask时期沉淀下来的成熟方案比如文件上传处理、会话管理、后台管理插件都需要自己组装。Flask的定位是“微框架”好处是自由坏处是要自己决定用哪些扩展但只要做几个核心决策整个项目会非常清爽。我的选择依据是项目上线周期只有三周Flask的调试和迭代速度最快有Flask-Login、Flask-SQLAlchemy、Flask-Migrate这一套成熟组合用户认证、数据库迁移、分页这些问题都有成熟解法FastAPI的异步优势在这种量级下完全体现不出来反而多了一层理解成本Django杀鸡用牛刀学员、课程、作业、考试这些模型给DRF反而要写更多代码。2.3 什么时候不要用Flask如果项目规模是几千门课程、几万并发、多租户SaaS平台那Flask前期开发快后期会痛苦。这类场景用FastAPIDjango不如直接用Django或换成Go生态。另外如果你连路由和视图都要别人教Flask的“自由”会让你迷失这时候Django的强约定反而是好事。我的原则是项目越小越灵活越需要明确约束两个人以下的小团队Flask是最安稳的选择。3. 业务拆解和数据模型设计课程的“骨架”怎么立在线教学课程网站的本质不是“课程列表”而是“学员学习过程的管理”。如果不能记录一个孩子从选课、看视频、交作业到考试出分的完整链路那这网站就是个带播放器的宣传页。所以我把系统拆成五个核心域用户、课程、作业、考试、学习记录。这也是整个系统的地基。3.1 五张核心表的关系怎么理用一句话概括用户参与课程课程包含章节章节挂作业和考试用户在学习过程中产生记录。我当时没有用复杂的多对多设计而是尽量遵循直觉减少冗余user表统一存储管理员、教师、学员三类账号用role字段区分。家长和学员共用一个账号家长绑定学员这样家长登录后能直接看孩子的进度。course表课程基本信息名称、封面、介绍、适用年龄。chapter表课程下的章节/课时顺序字段sort标题和内容可挂视频地址。course_enroll表用户和课程的关联存选课时间、当前进度。assignment表作业基本信息所属章节、标题、要求、截止时间。submission表每个学员对每个作业的提交记录含提交时间、文件路径、内容、得分、评价。exam表考试基本信息时长、总分、是否开放。question表题库表关联exam存题目类型、题干、选项、答案、分值。exam_record表学员考试记录开始时间、提交时间、得分、答题明细JSON。learning_log表学习记录表最小粒度是“某个用户在某个时间点学习了哪个章节的哪一部分”。外键关系我用了一点技巧course_enroll是课程和学员的纽带submission是作业和学员的纽带exam_record是考试和学员的纽带。查询一个人的学习全貌本质上就是在这三张表里找他的关联数据。3.2 为什么作业提交表要做成两张而不是在assignment里塞一个提交字段这是我自己优化过的设计。一开始好多菜鸟设计会把提交内容直接存在作业表里一个用户一条记录看起来简单。但一旦要支持“提交后再次修改、多次提交保留历史版本”单表设计就崩了。submission表设计成“一条作业对一个用户的多次提交记录”每次提交插入一行打分时打最后一行的分。这样用户可以重复交作业老师可以看修改过程也方便以后万一出现纠纷查证。考试题目的JSON存储是另一个值得说的设计。question表里我预留了一个options字段存JSON数组“答案”也是单独字段。但exam_record表里我直接把一次考试的答题数据存成一个answer_data字段内容包含题目ID、用户选择、每题得分。这样简化了交卷时的写入逻辑查询单次考试详情时可以一把梭不用去联表拼装题目和答案的关系。代价是不能直接用SQL做微观分析但业务上完全够用且写起来极快。3.3 学习记录表单独拆出来查询效率提升明显learning_log表的拆分是深思熟虑的。因为学习频次最高的接口是“上报学习行为”而老师最常看的页面是“这个孩子最近学了多久、学到哪章”。我把“学到哪章”作为一个冗余字段挂在user_course表里把“每次行为的时间、时长、学习内容”放learning_log这样老师看总进度不用再去扫日志表系统只需要在读进度时查一个简单字段。当然把学习记录拆出来也带来了一个幻觉看似每类数据都有独立表其实还是要花时间统一口径比如“看了10分钟的视频”和“完成了1个章节”的权重怎么算。我最后统一成“学习进度百分比”由前端上报进度和完成状态服务端只做记录。这样起码统计数据时有一个稳定的分母。3.4 数据库迁移别手写用Flask-Migrate开发过程中表结构肯定要变。我是从一开始就接入了Flask-Migrate每次改完models跑一下升级脚本再在服务器上同步。建议不要手写建表SQL不然开发到第二天字段不一致时排查成本比写表还高。4. 项目骨架与基于Flask的核心代码实现代码结构决定项目的可维护性。这一节我给出一套可直接复制的Flask项目骨架以及几个关键模块的实现思路。4.1 目录结构先定下来后面不慌我用的是Flask应用工厂模式加蓝图的经典结构course_platform/ ├── app/ │ ├── __init__.py # 应用工厂注册扩展和蓝图 │ ├── extensions.py # db, login_manager, migrate等实例 │ ├── models/ # SQLAlchemy模型 │ │ ├── user.py │ │ ├── course.py │ │ ├── assignment.py │ │ ├── exam.py │ │ └── learning_log.py │ ├── views/ # 蓝图路由 │ │ ├── auth.py │ │ ├── course.py │ │ ├── assignment.py │ │ ├── exam.py │ │ └── stats.py │ ├── services/ # 业务逻辑层 │ │ ├── user_service.py │ │ ├── course_service.py │ │ ├── submission_service.py │ │ └── exam_service.py │ ├── templates/ │ └── static/ ├── migrations/ # Flask-Migrate ├── run.py # 启动入口 ├── config.py # 配置 └── requirements.txt这个结构的核心思想是“路由薄、模型纯、业务厚”。views里只做参数校验和返回渲染services处理所有业务规则models只定义数据结构和关系这样后期加一个后台管理模块时不会把代码搅成一团。4.2 应用工厂和蓝图的注册# app/__init__.py from flask import Flask from config import Config from app.extensions import db, login_manager, migrate from app.views.auth import auth_bp from app.views.course import course_bp from app.views.assignment import assignment_bp from app.views.exam import exam_bp from app.views.stats import stats_bp def create_app(): app Flask(__name__) app.config.from_object(Config) db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) app.register_blueprint(auth_bp) app.register_blueprint(course_bp, url_prefix/course) app.register_blueprint(assignment_bp, url_prefix/assignment) app.register_blueprint(exam_bp, url_prefix/exam) app.register_blueprint(stats_bp, url_prefix/stats) return app为什么用工厂模式因为测试环境和生产环境可以分别创建不同配置的应用后面部署时也很方便。注册蓝图时带上url_prefix可以让模块之间路径不冲突比如考试模块就都挂在/exam下。4.3 用户认证与角色权限培训系统里用户角色多我用的Flask-Login加自定义装饰器。User模型长这样# app/models/user.py from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from app.extensions import db class User(UserMixin, db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(256), nullableFalse) role db.Column(db.String(20), nullableFalse, defaultstudent) # admin/teacher/student real_name db.Column(db.String(50)) parent_phone db.Column(db.String(20)) 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) def is_admin(self): return self.role admin def is_teacher(self): return self.role teacher登录接口用Flask-Login的login_user注销用logout_user。权限控制我写了个装饰器# app/views/decorators.py from functools import wraps from flask_login import current_user from flask import abort def role_required(*roles): def wrapper(f): wraps(f) def decorated(*args, **kwargs): if not current_user.is_authenticated: return abort(401) if current_user.role not in roles: return abort(403) return f(*args, **kwargs) return decorated return wrapper这个装饰器有个细节它不仅判断角色还处理未登录的情况abort(401)让前端知道要重新登录abort(403)告诉前端权限不足。在实际项目中老师接口催作业、家长只看孩子成绩这种粒度控制一个装饰器就解决了。4.4 核心models的写法课程、章节、选课关系我当时是这样写的# app/models/course.py from app.extensions import db class Course(db.Model): __tablename__ course id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(100), nullableFalse) cover_url db.Column(db.String(200)) description db.Column(db.Text) teacher_id db.Column(db.Integer, db.ForeignKey(user.id)) created_at db.Column(db.DateTime, defaultdb.func.now()) chapters db.relationship(Chapter, backrefcourse, order_byChapter.sort) class Chapter(db.Model): __tablename__ chapter id db.Column(db.Integer, primary_keyTrue) course_id db.Column(db.Integer, db.ForeignKey(course.id)) title db.Column(db.String(100), nullableFalse) sort db.Column(db.Integer, default0) video_url db.Column(db.String(200)) content db.Column(db.Text) class CourseEnroll(db.Model): __tablename__ course_enroll id db.Column(db.Integer, primary_keyTrue) course_id db.Column(db.Integer, db.ForeignKey(course.id)) user_id db.Column(db.Integer, db.ForeignKey(user.id)) enroll_time db.Column(db.DateTime, defaultdb.func.now()) progress db.Column(db.Float, default0.0) # 0.0 - 1.0课程和章节的关系用relationship简化访问但注意别在查询里无脑加载大列表。培训系统的课程章节通常几百个性能没问题但像作业提交记录这种重数据一定要用分页。5. 作业、考试和学习记录系统里最难啃的三个硬骨头课程模块只是展示型功能真正复杂的是作业、考试和日志记录。这三个模块每个都有自己的坑。5.1 作业提交文件上传的安全与存储学员交作业最常传的是图片和文档。我的存储方案是本地磁盘保存路径按“uploads/作业ID/用户ID/时间戳_文件名”组织数据库中只存这个相对路径。要注意的点有三个。第一文件名必须清洗。原始文件名可能包含路径分隔符或中文特殊字符我会用uuid重命名这样防止路径穿越和重名覆盖。第二扩展名白名单校验不能只信前端。第三提交历史保留前文已经说明了submission表的设计逻辑。核心提交逻辑我贴在下面方便大家参考# app/services/submission_service.py import os import uuid from datetime import datetime from flask import current_app from werkzeug.utils import secure_filename from app.extensions import db from app.models import Submission, CourseEnroll ALLOWED_EXTENSIONS {png, jpg, jpeg, gif, pdf, doc, docx, zip, rar} def save_submission(assignment_id, user_id, file, content_textNone): if file and file.filename ! : ext file.filename.rsplit(., 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: raise ValueError(不支持的文件类型) new_filename f{datetime.now().strftime(%Y%m%d%H%M%S)}_{uuid.uuid4().hex[:8]}.{ext} base_dir current_app.config[UPLOAD_FOLDER] course_dir os.path.join(base_dir, str(assignment_id)) os.makedirs(course_dir, exist_okTrue) file_path os.path.join(course_dir, new_filename) file.save(file_path) submission Submission( assignment_idassignment_id, user_iduser_id, file_pathfile_path, contentcontent_text, submit_timedatetime.now() ) db.session.add(submission) db.session.commit() return submission还有个小细节我按照“assignment_id/user_id/time”作为目录是为了方便老师直接访问服务器文件按目录按人整理不要按用户ID分太深否则目录嵌套也麻烦。5.2 在线考试倒计时、防作弊、自动判分考试模块比作业复杂在两个地方一个是要给每个学员独立的考试实例防止出现“两个人同时提交结果写串”的并发问题另一个是判分逻辑要提前设计好不同题型处理方式不同。我的考试流程是学员点击“开始考试”后台创建exam_record生成一份只有该学员可见的题目列表每题作答后存到answer_data里的JSON结构前端用setInterval每30秒调一次保存接口防止刷新丢进度到时间后前端强制提交后端校验数据库记录的“开始时间时长”是否小于当前时间防止通过改前端时间绕过后端判分单选题逐题对比多选题按“全对才得分”处理填空题用去掉两端空格后完全匹配的方式。考试倒计时我做了个很实用的优化倒计时是以服务端时间为准前端初始同步一次然后本地倒计时。到点强制交卷后端还会再校验一次避免“交了卷但数据库还没保存”的尴尬。自动判分代码示例# app/services/exam_service.py def grade_exam(record_id): record ExamRecord.query.get(record_id) if record is None or record.score is not None: return None exam Exam.query.get(record.exam_id) questions Question.query.filter_by(exam_idexam.id).all() q_map {q.id: q for q in questions} answers json.loads(record.answer_data or {}) total_score 0 for q in questions: user_answer answers.get(str(q.id)) if user_answer is None: continue if q.q_type single: if user_answer q.correct_answer: total_score q.score elif q.q_type multiple: if set(user_answer) set(q.correct_answer): total_score q.score elif q.q_type blank: if user_answer.strip() q.correct_answer.strip(): total_score q.score record.score total_score record.status finished record.submit_time datetime.now() db.session.commit() return record这里的“判断已出分就不重复判”很关键可以防止接口被重复调用后分数翻倍。5.3 学习记录的采集策略不靠后端轮询靠前端上报学习记录我分为两类一类是“章节完成事件”由学员点击“完成学习”触发另一类是“时长统计”由前端每隔一分钟上报一次当前学习章节ID和播放进度。后端只保存不实时聚合统计报表在单独的服务里异步生成。这里最大的坑是“学习时长到底怎么算”。我最后定义有效学习时间按“上报心跳”存在的分钟数累加超过5分钟没有上报的中断。也就是说如果学员开着页面去干别的系统不会记录那段时间。这个设计可能在业务上有争议但我认为它至少是公平的因为无法用技术手段完全判断“人是否在屏幕前”。6. 学习记录不吃灰从统计SQL到可视化图表系统跑了半个月朋友说“你说的学习记录我现在只能查日志还是没法一眼看出孩子学得怎么样。”于是我开始做数据可视化。不用太复杂核心是三条曲线课程学习进度曲线、作业完成率曲线、考试得分曲线。6.1 按天分组的学习时长SQLSELECT DATE(created_at) AS study_date, COUNT(DISTINCT user_id) AS active_users, SUM(study_duration) AS total_minutes FROM learning_log WHERE created_at DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND course_id :course_id GROUP BY DATE(created_at) ORDER BY study_date;这里的study_duration是前端上报的分钟数不是后端的请求时间差因为后端请求时间差包含了加载页面的时间不算真正的学习时间。6.2 作业完成率统计作业完成率的定义是“该课程下已提交作业人数/选课总人数”。因为一个学员至少提交一次算完成我做了个派生表统计SELECT a.id AS assignment_id, a.title, COUNT(DISTINCT s.user_id) AS submitted_count, (SELECT COUNT(*) FROM course_enroll e WHERE e.course_id a.course_id) AS total_enrolled, ROUND(COUNT(DISTINCT s.user_id) / (SELECT COUNT(*) FROM course_enroll e WHERE e.course_id a.course_id) * 100, 1) AS completion_rate FROM assignment a LEFT JOIN submission s ON s.assignment_id a.id WHERE a.course_id :course_id GROUP BY a.id;这条SQL很简单但要小心如果课程还没有学员分母为零会出现除零错误。我在实际接口里加了IFNULL判断返回0%。生产环境我已经遇到过一次报错后接口直接500第二天才被家长发现。6.3 前端图表库的选型我用了Chart.js没有用ECharts。原因是培训系统主要就是折线图、柱状图、饼图Chart.js体积小、API简洁中文文档也够全。注意初始化图表时x轴时间标签不要直接塞Date对象格式化一下不然横坐标会非常拥挤。这个问题在“python画图横坐标太密集”这类搜索里非常常见其实只要设置ticks.maxTicksLimit或使用时间适配器问题就解决了。拿到统计接口之后前端的图表组件大概长这样fetch(/stats/course_progress?course_id1) .then(res res.json()) .then(data { const ctx document.getElementById(progressChart).getContext(2d); new Chart(ctx, { type: line, data: { labels: data.dates, datasets: [{ label: 学习时长(分钟), data: data.minutes, borderColor: #4a90d9, fill: false }] }, options: { responsive: true, scales: { x: { ticks: { maxTicksLimit: 8 } } } } }); });这里在options里加了ticks.maxTicksLimit让横轴最多显示8个刻度不然一周七天的数据拉出来小屏幕上字会堆成黑团。6.4 报表对运营的真正价值统计系统上线后朋友发现几个有趣的现象多数孩子的学习时间集中在晚上八点到九点半周末反而低作业完成率和考试成绩呈明显正相关完成率低的班级平均分低了8分。这些发现直接促使他们调整了上课时间续班率有所提升。这就是学习记录的价值——不只是展示给家长看还能反哺运营决策。7. 从开发机到服务器Flask部署的完整流程系统开发完成只是第一步真正考验人的是部署。朋友的服务器是腾讯云轻量应用服务器系统Ubuntu内存4G。我用了比较经典的组合Gunicorn Nginx MySQL SQLite迁移。7.1 为什么从SQLite换到MySQL开发阶段我用SQLite零配置快。但上线前必须换原因是SQLite并发写锁太重。培训机构的场景虽然并发不高但晚上家长集中提交作业那阵子如果发生写入冲突孩子就已经开始哭了。MySQL在并发写入和主从扩展上都更成熟所以我提前做了准备模型层用SQLAlchemy切换数据库只是改一下连接字符串迁移成本很小。7.2 Gunicorn的启动配置Gunicorn配置我放在gunicorn.conf.py里方便维护# gunicorn.conf.py bind 127.0.0.1:8000 workers 3 worker_class gevent timeout 60worker数量为什么是3因为服务器只有2核官方建议是2~4个worker。用gevent是因为文件上传和长请求会阻塞workergevent的协程模型适合这类IO密集场景但不建议开太多毕竟内存有限。每开一个worker就多一份Flask应用镜像2G内存开了5个worker就明显吃力。启动命令gunicorn -c gunicorn.conf.py run:app注意run.py要导出app实例如果有人直接把create_app写在__init__.py里Gunicorn导入时容易乱。我的run.py就两行from app import create_app app create_app()7.3 Nginx反向代理与静态文件Flask本身不擅长处理静态文件和并发连接所以我用Nginx做反向代理把静态文件直接交给Nginx。Nginx配置片段server { listen 80; server_name your-domain.com; client_max_body_size 50m; location /static { alias /path/to/static/; } location /uploads { alias /path/to/uploads/; } 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; } }client_max_body_size一定要设置不然学员交一个几十兆的作业压缩包Nginx直接拒绝接口返回413老师还以为是系统Bug。我踩过这个坑默认1M根本不够用。7.4 HTTPS、备份与安全问题部署上线后我做了三件事申请Lets Encrypt证书配置HTTPS加一个crontab每天晚上3点备份MySQL数据库到OSS清洗了上传文件的类型过滤因为之前只做了扩展名校验没做MIME校验后来发现可以上传文本文件伪装成jpg虽然暂时没出事但风险太高。密码方面全部走了werkzeug的generate_password_hash没有再以明文形式存过任何密码。现在很多培训系统把家长手机号和密码放在一个表里明文存看着都替他们害怕。7.5 部署后最容易被忽略的日志与监控上生产一周后我发现有一些请求特别慢问了下朋友才知道是家长白天也在频繁刷新数据每次统计报表接口都要算全表聚合。我在SQLAlchemy配置里加了echoFalse线上就不开SQL日志了改成用Python标准logging记录慢查询和异常并输出到文件。同时给统计接口加了Redis缓存5分钟刷新一次报表页面的压力立刻降下去了。日志是一切排错的起点千万别省。8. 最后补一刀实操中遇到的四个坑与解决思路这套系统从开发到稳定运行有些问题即使在有经验的情况下也会踩单独列出来作为提醒。第一个坑是Flask-SQLAlchemy的Model.id默认自增但如果从SQLite迁移到MySQL时旧表主键已经存在迁移脚本会报主键冲突。解决方法是迁移前先清空数据或使用Flask-Migrate自动生成增量脚本但迁移后必须手动检查自增起始值。第二个坑是考试交卷的并发问题。前端最终交卷时后端如果检测到提交时间超过截止时间要果断拒绝。但用户可能重复点击提交记录已经被标记为finished第二次提交会把第一次的记录覆盖。我的解决方法是交卷前加一个乐观锁通过version字段判断记录是否已被处理。第三个坑是local_settings和config混在一起造成的环境混乱。我最后严格区分了开发配置和生产配置通过环境变量FLASK_ENV加载不同的config对象所有敏感信息放.env文件不提交到git。这是不少新手团队会忽略的线上环境用了一个下午排查数据库连接串错误最后发现是config写死了本地开发地址。第四个坑是前端的视频进度上报过频。视频学习页面如果每5秒上报一次进度MySQL的写入压力会立刻上去。我改成了每30秒上报且用节流函数在前端限制频率。实际上对业务来说5秒和30秒粒度差别不大但对数据库压力差别很大。9. 这套系统跑了一年我的真实体会系统从交付到现在已经稳定运行了一年多几乎没有出过严重故障。朋友后来自己学会了Flask基本开发还让运维同事给系统加了一个“调课申请”的小功能他说第一次改别人写的代码焦虑了三天。如果让我重新做一遍我会在数据模型设计阶段考虑得更细一些。比如考试题目的版本管理没有做导致老师想调整某道题的选项只能把整套考试题目更新一遍历史考试记录里保存的answer_data关联的是题目ID不是题目快照。下次再做类似系统我会直接把题目和选项放入考试快照不要动态联表。还有些经验值得记录下来慢慢沉淀成团队的知识库。文件上传的磁盘规划要提前考虑加上OSS备份后服务器本地磁盘只要保留最近一个月的作业即可。统计报表不要做成实时聚合每天凌晨跑一次定时任务把数据汇总到summary表报表接口只查汇总表哪怕数据多一百倍也不卡。在线教学系统的核心不是炫技。用Flask做技术栈只是第一步真正解决机构运营中“靠人肉记忆”的混乱局面才是价值。如果这篇文章能帮你少踩一两个我踩过的坑那就够了。