做了个小项目:Flask + Vue 的精品课程网站,开发环境是 Pycharm,中途顺带对比了 Django 的写法。这个项目前前后后花了我大概两周的业余时间,踩了不少坑,也沉淀了一些能直接用的经验。这篇文章不是那种官方的入门教程,而是把我从零搭建、数据库设计、视频处理、前端联调、再到服务器部署的完整过程捋一遍,哪些方案省事、哪些方案是坑、为什么这么选,都写清楚。不管你是要用 Flask 还是 Django,刚接触前后端分离还是已经在写小项目但想系统性梳理一遍,这篇都适合你。
注意:文中涉及的操作我都基于 Flask 2.x + Vue 3 为主,Django 作为另一套同功能方案做了对比讲解,代码片段都经过简化,但核心逻辑是可以在自己机器上直接跑通的。
1. 技术选型与项目整体设计思路
很多人在选型的时候容易纠结,其实核心就看你的项目规模和团队习惯。我这个课程网站的实际需求是:用户注册登录、课程分类浏览、课程详情、视频播放、评论互动、后台管理课程,数据量属于中小型,没有高并发压力,所以选型时重点考虑的是开发效率和维护成本。
1.1 为什么选 Flask + Vue,而不是只用一个框架
当时我对比了三套方案:纯 Flask + Jinja2 模板、Flask + Vue 前后端分离、Django + Vue 前后端分离。纯模板方案开发最快,但课程网站的交互比较重——视频进度记录、评论实时展示、后台数据可视化,这些用模板渲染会非常别扭。Jinja2 适合做服务端渲染的内容站,但一旦前端逻辑复杂,模板里塞 JavaScript 会变得难以维护。
所以我选了前后端分离:后端只提供 API,前端用 Vue 负责所有交互。这样做的直接好处是,课程列表、视频播放器、后台管理这三个模块可以独立开发互不干扰,出 bug 时排查边界很清晰。Flask 作为后端比 Django 轻量,一个课程网站所需的 API 数量并不多,用 Flask 的蓝图模块化组织起来非常舒服,开发调试周期比 Django 短。
Django 我也在同一阶段尝试了同款功能,它的优势在于自带 Admin 后台和 ORM 更强大,写课程管理界面几乎不用自己动手。但如果你需要高度定制前端交互,Django 内置模板和 Admin 反而会形成限制,需要额外配置才能完全前后端分离。
1.2 开发环境准备和合理配置
先交代我的本地环境:
- Windows 11,Python 3.10,Pycharm 2023.2 Professional
- Node.js 16.20,npm 8.19
- MySQL 8.0(本地开发用 SQLite 过渡,生产切 MySQL)
- Redis 可选(生产环境用在缓存)
Pycharm 用社区版也能做这个项目,但专业版对 Vue 的支持好很多,自带 Vue 插件、模板语法高亮、调试器直接识别前端代码。我开头用社区版做 Flask 部分没任何问题,后来做 Vue 前端时发现缺少 JavaScript 调试支持,补了专业版。
提示:Pycharm 打开前后端分离项目时,建议把前端和后端作为两个独立的项目窗口打开,或者在同一个窗口配置两个 Run/Debug 配置。不要用 Pycharm 直接打开整个项目根目录然后硬编前端目录,否则模块解析经常会出问题。
虚拟环境我用的是 Pycharm 自带的 virtualenv,Python 解释器指向 3.10。Flask 装的是 2.3.x,Django 装的是 4.2.x。为什么不用最新版?新版本往往刚发布时第三方库兼容性还没跟上,比如 Django 5.x 对 mysqlclient 的支持,在写这篇文章时还没有完全稳定。做项目求稳,选经过社区验证的版本是更明智的。
2. 数据库设计和后端 API 实现
一个课程网站核心的数据模型,按我的划分是五大块:用户、课程分类、课程、章节视频、评论。这五块关系清晰,适合示范如何设计 RESTful API。
2.1 用户模块和 Token 认证
用户表我用的是 Flask 的扩展 Flask-SQLAlchemy 定义,相比 Django 的模型,代码上两者其实长得非常像。唯一区别是 Flask 需要自己安装扩展,而 Django 的django.contrib.auth内置了用户认证体系。
在 Flask 中的用户模型:
from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db = SQLAlchemy() class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False, index=True) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) avatar = db.Column(db.String(256), default='') role = db.Column(db.String(16), default='student') # student / teacher / admin created_at = db.Column(db.DateTime, default=datetime.utcnow) 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)这里有个关键点:密码一定不能明文存储。Werkzeug 的generate_password_hash默认使用 pbkdf2:sha256 算法加盐,对课程网站这种场景安全性足够了。之前在项目里见过有人直接存明文密码的,上线一周数据库泄露就被刷了一遍弱口令,必须引以为戒。
认证方案我用的是 JWT,比 Flask-Login 更适合前后端分离。Flask 里用 PyJWT 实现非常简单:
import jwt from functools import wraps from flask import request, jsonify from datetime import datetime, timedelta SECRET_KEY = 'your-secret-key' def generate_token(user_id, role): payload = { 'user_id': user_id, 'role': role, 'exp': datetime.utcnow() + timedelta(days=7) } return jwt.encode(payload, SECRET_KEY, algorithm='HS256') def token_required(f): @wraps(f) def wrapper(*args, **kwargs): token = request.headers.get('Authorization', '') if not token.startswith('Bearer '): return jsonify({'code': 401, 'msg': '未登录或token失效'}), 401 try: payload = jwt.decode(token[7:], SECRET_KEY, algorithms=['HS256']) request.user_id = payload['user_id'] request.user_role = payload['role'] except jwt.ExpiredSignatureError: return jsonify({'code': 401, 'msg': '登录已过期'}), 401 except jwt.InvalidTokenError: return jsonify({'code': 401, 'msg': '无效token'}), 401 return f(*args, **kwargs) return wrapper我把这段逻辑写成了一个独立模块auth.py,这样注册接口、登录接口、课程接口都能引用来做鉴权。前后端联调时,每次请求只要在 Header 里带Authorization: Bearer <token>即可。
Django 里做同样的事有 Django REST Framework + djangorestframework-simplejwt 这套组合。如果项目从一开始就知道会频繁迭代权限体系,选 Django 会更省心;但如果只是要快速响应前端需求,Flask 手动实现并不麻烦。
2.2 课程、分类和评论的模型关系
课程和分类是典型的树形关系:一个分类下面多个课程,一个课程下面多个章节,一个章节下面多个视频资源。评论表单独关联到课程。
我的模型如下:
class CourseCategory(db.Model): __tablename__ = 'course_category' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True, nullable=False) sort_order = db.Column(db.Integer, default=0) courses = db.relationship('Course', backref='category', lazy='dynamic') class Course(db.Model): __tablename__ = 'course' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(128), nullable=False) subtitle = db.Column(db.String(256), default='') cover_url = db.Column(db.String(256), default='') teacher_id = db.Column(db.Integer, db.ForeignKey('user.id')) category_id = db.Column(db.Integer, db.ForeignKey('course_category.id')) price = db.Column(db.Numeric(10, 2), default=0) difficulty = db.Column(db.String(16), default='beginner') # beginner/intermediate/advanced created_at = db.Column(db.DateTime, default=datetime.utcnow) chapters = db.relationship('Chapter', backref='course', lazy='dynamic', order_by='Chapter.sort_order') class Chapter(db.Model): __tablename__ = 'chapter' id = db.Column(db.Integer, primary_key=True) course_id = db.Column(db.Integer, db.ForeignKey('course.id')) title = db.Column(db.String(128), nullable=False) sort_order = db.Column(db.Integer, default=0) video_url = db.Column(db.String(512), default='') duration = db.Column(db.Integer, default=0) # 秒为单位 is_free = db.Column(db.Boolean, default=False) class Comment(db.Model): __tablename__ = 'comment' id = db.Column(db.Integer, primary_key=True) course_id = db.Column(db.Integer, db.ForeignKey('course.id')) 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)在设计时我特别把课程拆成了Course和Chapter两层,而不是直接把视频地址挂在课程下面。为什么?因为一个课程往往包含多个章节视频,如果只做课程表加一个 video_url 字段,后续每次增加章节都要额外建表,而且章节的排序、是否免费试看这些属性没法表达。拆成两张表后,课程列表接口返回课程元信息,课程详情接口再返回所有章节,数据量上也非常可控。
评论我特意没有做回复嵌套。很多课程网站的评论实际只需要一层,用户针对课程本身留言,不需要复杂的多级回复。做了多级回复意味着要做递归查询、缩进渲染、删除策略,全都会拖慢开发进度。这是典型的克制设计。
2.3 用蓝图组织路由
Flask 的蓝图模块化是组织路由的工具,类似于 Django 的 app 概念。我按功能切了三个蓝图:
from flask import Blueprint auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') course_bp = Blueprint('course', __name__, url_prefix='/api/course') admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin')主入口文件app.py:
from flask import Flask from flask_cors import CORS from config import Config from models import db from api.auth import auth_bp from api.course import course_bp from api.admin import admin_bp def create_app(): app = Flask(__name__) app.config.from_object(Config) CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) db.init_app(app) app.register_blueprint(auth_bp) app.register_blueprint(course_bp) app.register_blueprint(admin_bp) with app.app_context(): db.create_all() return app app = create_app() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)CORS 配置在开发阶段必须显式指定允许的前端地址。Vue 开发服务器默认跑在 5173 端口,Flask 跑在 5000 端口,不同端口就是跨域,浏览器会拦截响应。我最初调试时前端拿不到任何数据,打开浏览器控制台才看到 CORS 报错。用 Flask-CORS 大概解决了。
生产环境不建议用 CORS 全开的方式,把origins改成具体的域名,或者前置 nginx 做反向代理,由 nginx 统一配置跨域头,后端代码就不用关注了。
2.4 API 响应格式的统一设计
前后端联调时最怕的就是每个接口返回结构不一致。有的接口返回{code: 0, data: ...},有的返回{success: true, data: ...},前端就要为每个接口单独写处理逻辑。
我把所有接口返回格式统一为:
{ "code": 0, "message": "ok", "data": {} }错误时 code 不为 0:
{ "code": 40001, "message": "用户名或密码错误", "data": null }具体接口中直接返回这个结构。做了这个约定后,前端 axios 拦截器里统一处理响应,业务代码只需要关心data部分,代码量少了一半。
Django 项目中我用 Django REST Framework 也做了同样的统一处理,通过自定义 exception handler 把校验错误、权限错误、404 全部转成统一格式,效果相同。
3. Vue 前端与课程播放核心功能实现
前端这部分,我选择了 Vue 3 + Vite + Pinia + Vue Router。为什么不选 Vue 2?课程网站不是特别复杂的 SPA,Vue 3 的 Composition API 在处理视频播放状态、进度记录这类需要逻辑复用的场景时非常顺手,而且项目是全新的,没必要守着旧生态。
3.1 Vite 项目创建和环境变量
用 npm 创建 Vue 3 项目:
npm create vue@latest这个命令会交互式询问是否安装 Router、Pinia、ESLint 等,按需选择即可。Vite 比 Webpack 快很多,开发时修改代码热更新几乎是秒级,调试体验非常关键。
环境变量我拆成了三份:
# .env.development VITE_API_BASE_URL=http://localhost:5000/api # .env.production VITE_API_BASE_URL=/api # .env.staging VITE_API_BASE_URL=https://staging.example.com/apiVue 代码里通过import.meta.env.VITE_API_BASE_URL获取。这个配置在部署阶段直接决定前端代码是否要重新构建。我在第一次部署时偷懒,直接把开发环境地址打进生产包,结果用户访问的是 localhost,所有请求打到用户自己的电脑上,排查了半天才意识到环境变量没切。
3.2 axios 封装和路由守卫
axios 前端请求拦截器必须统一加 token,响应拦截器统一处理错误码。我封装了一个request.js:
import axios from 'axios' import { useUserStore } from '@/stores/user' import router from '@/router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { // 统一错误提示 return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { // token失效,跳转登录页 useUserStore().clearAuth() router.push({ name: 'login' }) } return Promise.reject(error) } ) export default requestVue Router 的路由守卫在 Turbine 应用中很关键。未登录用户可以浏览课程列表和课程详情页,但观看完整视频和发表评论需要登录。所以我在路由 meta 里标记哪些需要认证:
const routes = [ { path: '/', name: 'home', component: HomeView }, { path: '/course/:id', name: 'course-detail', component: CourseDetail }, { path: '/login', name: 'login', component: LoginView }, { path: '/dashboard', name: 'dashboard', component: DashboardView, meta: { requiresAuth: true } } ] router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { next({ name: 'login', query: { redirect: to.fullPath } }) } else { next() } })3.3 m3u8 视频在 Vue 中的播放
课程网站的核心是视频播放,这部分我踩的坑最多。最初我直接把 MP4 文件放在服务器上,前端用<video>标签播放。很快发现问题:MP4 文件太大,用户没等缓冲完就关掉了,而且拖进度条体验不好。最终决定用 HLS 协议,把视频转成 m3u8 切片。
m3u8 的本质是先把一个视频文件切分成很多小片段(通常是 .ts 文件),然后生成一个索引文件(.m3u8),播放器按索引文件顺序加载片段。好处是支持自适应码率,用户网速不太好时可以自动降清,不用把整个文件下载完才能播放。
Vue 播放 m3u8 的推荐组合是hls.js:
import Hls from 'hls.js' export function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 }) hls.loadSource(url) hls.attachMedia(videoElement) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoElement.play() }) return hls } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { // Safari 原生支持 HLS videoElement.src = url videoElement.play() } }注意:hls.js 在打包时体积不小,建议用动态 import 实现按需加载,只在视频播放页面引入,避免首屏 bundle 过大。
视频切片我用的是 ffmpeg:
ffmpeg -i input.mp4 -codec: h264 -acodec: aac -hls_time 10 -hls_playlist_type vod -hls_segment_filename "output_%03d.ts" output.m3u8参数说明:-hls_time 10表示每个切片 10 秒,-hls_playlist_type vod表示点播模式不变的索引文件。切片时长不要设太短或太长,太短(比如 2 秒)会导致请求数过多,太长(比如 60 秒)会拖动时缓冲明显,实测 10 秒左右体验比较好。
3.4 视频权限控制与防盗链
课程网站必须控制视频访问权限:免费章节允许未登录用户观看,付费章节只有购买后能看。如果直接把 m3u8 地址暴露在前端代码里,用户复制链接就能无限播放,哪怕后端接口做了权限校验,也无法阻止用户绕过前端直接请求视频文件。
我用了两种方式应对:
第一种是简单 referer 防盗链,在 nginx 层配置只允许自己的域名引用视频资源。这种方式防君子不防小人,但成本最低,能挡住绝大多数普通用户。
第二种是给 m3u8 地址加上签名参数,后端在返回章节信息时,对视频地址生成一个有时效性的签名:
import hashlib import time def generate_signed_url(video_path): expires = int(time.time()) + 3600 # 1小时有效 raw = f"{video_path}:{expires}:{SECRET_KEY}" sign = hashlib.md5(raw.encode()).hexdigest() return f"/video/{video_path}?expires={expires}&sign={sign}"服务端在视频路由中校验签名是否有效。即便别人拿到了链接,一小时后自动失效,有效降低资源被无限盗用的风险。
3.5 前端常见问题:vue 打包布局异常与样式问题
开发模式下页面显示正常,打包部署后布局全乱,这类问题在 Vue 项目里非常常见。我遇到的有两种:
第一种是 CSS 的base路径问题。Vite 默认base是/,如果把打包产物部署到子路径如https://example.com/course/,所有静态资源请求都会指向根路径,导致 CSS、JS 加载失败,页面光秃秃一片。解决办法是在vite.config.js中设置base: '/course/',或者用相对路径base: './'确保资源路径正确。
第二种是样式优先级问题,常见于第三方 UI 库和自定义样式冲突。开发时热更新掩盖了加载顺序问题,打包合并后 CSS 顺序变化导致样式覆盖错乱。建议给自定义样式加上更具体的选择器或使用 scoped 样式,尽量不要用全局无前缀的 class 名。
还有一类很容易忽略的:图片资源放在public目录下,打包后路径没问题;放在src/assets下,代码中如果没有通过 import 或 new URL 方式引用,打包后会出现图片丢失。
4. 后台管理:基于 Django Admin 的对比体验
课程网站需要后台管理,我最初在 Flask 里手动实现了一套管理界面。前端页面用 Vue 写表格、表单,后端提供 CRUD API。功能上没有问题,代码量略多。后面我试了用 Django 快速搭建同款系统,不得不承认 Django Admin 确实强大。
4.1 在 Django 中创建课程 app 和模型
Django 创建 app 的命令是:
python manage.py startapp course然后在course/models.py中定义模型,和 Flask 的 SQLAlchemy 写法很像。但 Django 会自动生成数据库迁移文件:
python manage.py makemigrations python manage.py migrate这点对比 Flask:Flask-SQLAlchemy 的db.create_all()只能创建表结构,不能修改已有表的字段,而 Django 的迁移系统在字段变更时会有更完善的处理方案。如果项目迭代频繁改字段,Django 的迁移机制确实比 Flask 舒服。
4.2 让 Django Admin 更好看
Django Admin 默认界面比较朴素,如果想用于课程管理的内容运营,需要稍作美化。推荐django-simpleui或django-jazzmin这类第三方主题包。我用的是django-simpleui,主题看起来现代不少,还自带菜单图标,配置很简单,主要是在settings.py的 INSTALLED_APPS 里加进去。
同时把课程列表页配置为可排序、可搜索、可筛选:
from django.contrib import admin from .models import Course, Chapter @admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ('id', 'title', 'category', 'price', 'difficulty', 'created_at') list_filter = ('category', 'difficulty') search_fields = ('title', 'subtitle') ordering = ('-created_at',) list_per_page = 20这些配置让运营人员可以直接在后台管理课程分类、上下架状态、设置价格,不需要前端管理页面,大大降低开发成本。
提示:Django Admin 适合给管理员和运营人员用,不适合直接暴露给普通用户。普通用户端仍然要走 Vue 前端页面,通过 API 交互。
4.3 Django 和 Flask 实现课程 API 的对比
同一个"获取课程详情"的接口:
Flask 写法:
@course_bp.route('/<int:course_id>', methods=['GET']) def get_course_detail(course_id): course = Course.query.get(course_id) if not course: return jsonify({'code': 40400, 'message': '课程不存在', 'data': None}), 404 chapters = course.chapters.all() data = { 'id': course.id, 'title': course.title, 'subtitle': course.subtitle, 'chapters': [{ 'id': c.id, 'title': c.title, 'is_free': c.is_free, 'duration': c.duration } for c in chapters] } return jsonify({'code': 0, 'message': 'ok', 'data': data})Django REST Framework 写法:
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Course from .serializers import CourseDetailSerializer class CourseDetailView(APIView): def get(self, request, course_id): try: course = Course.objects.get(id=course_id) except Course.DoesNotExist: return Response({'code': 40400, 'message': '课程不存在'}, status=status.HTTP_404_NOT_FOUND) serializer = CourseDetailSerializer(course) return Response({'code': 0, 'message': 'ok', 'data': serializer.data})两者核心逻辑类似。Django 用序列化器自动处理字段嵌套,数据模型复杂时省不少代码;Flask 用列表推导手动构造字典,简单场景更灵活。如果课程嵌套层级越来越多(分类、标签、讲师信息、章节、测评题),Django 的序列化器优势会更明显。
5. 部署上线与常见问题排查
本地开发跑通和真正上线差距不小,这个项目部署到 Linux 服务器上的过程我也完整记录下来。部署方式选择了宝塔面板 + Gunicorn + Nginx,先讲 Flask 版。
5.1 使用 Gunicorn 启动 Flask 服务
在 Linux 下不建议用 Flask 自带的开发服务器跑生产,性能和安全都不过关。我用的 Gunicorn:
pip install gunicorn gunicorn -w 4 -b 127.0.0.1:5000 app:app参数说明:-w 4表示 4 个 worker 进程,worker 数一般设置为 CPU 核心数的 2 到 4 倍。课程网站的接口以 IO 操作为主,4 个 worker 对 2 核 4G 服务器足够。app:app中前者是模块名(app.py),后者是 Flask 实例。
5.2 宝塔面板部署 Django 时注意的坑
Django 部署和 Flask 最大的区别是需要处理静态文件和 ALLOWED_HOSTS。使用宝塔部署 Django 时:
- 先在宝塔把 Python 项目创建好,选择 Python 3.10 和对应的框架类型
- 安装依赖:
pip install -r requirements.txt - 执行
python manage.py collectstatic收集静态文件 - 配置 Nginx 反向代理到 Gunicorn 或 uWSGI
Django 的ALLOWED_HOSTS必须填上你的域名或 IP,否则访问直接报 DisallowedHost 错误:
ALLOWED_HOSTS = ['yourdomain.com', '你的服务器IP']还有个坑是 MySQL 驱动问题。Django 连 MySQL 通常用mysqlclient,在 Linux 上安装前需要先装依赖:
yum install mysql-devel gcc gcc-c++ python3-devel pip install mysqlclient如果在纯净系统上直接pip install mysqlclient,大概率报编译错误。也可以换用pymysql,在settings.py里加上:
import pymysql pymysql.install_as_MySQLdb()两者的区别是mysqlclient是 C 扩展,性能更好;pymysql是纯 Python 实现,安装省事但性能稍弱。中小型课程网站用 pymysql 完全足够。
5.3 Nginx 配置前端静态资源和 API 反向代理
前端打包后生成dist目录,把里面的文件上传到服务器/var/www/course-web,然后 Nginx 配置:
server { listen 80; server_name yourdomain.com; # 前端静态资源 root /var/www/course-web; index index.html; # Vue Router history 模式需要配置 try_files location / { try_files $uri $uri/ /index.html; } # API 反向代理到 Flask/Django location /api/ { 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; } # 视频文件缓存 location /video/ { proxy_pass http://127.0.0.1:5000; proxy_cache_valid 200 1h; } }这里的try_files $uri $uri/ /index.html;非常关键。Vue Router 启用了 history 模式后,前端路由如/course/1是虚拟路径,后端没有真实目录。如果少了这行,用户刷新课程详情页会变成 404。
5.4 后端开发中容易踩的隐藏问题
开发过程中很多坑是隐藏的,只在特定条件下爆发。我把遇到过的列成速查表:
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 数据库乱码 | 中文显示为问号 | MySQL 表字符集不是 utf8mb4 | 建库时指定DEFAULT CHARSET=utf8mb4 |
| 时区差 8 小时 | 课程发布时间差了整整 8 小时 | MySQL 和 Python 时区不一致 | 设置app.config['TIMEZONE'] = 'Asia/Shanghai',数据库中存 UTC,展示层转本地 |
| 上传图片 413 | Nginx 拒绝大文件 | Nginx 默认 body 大小 1M | 在 Nginx 配置加client_max_body_size 20M; |
| 跨域请求被拦截 | 浏览器 console 报 CORS 错误 | 后端没有设置允许的前端域名 | Flask 用 Flask-CORS,Django 用 django-cors-headers |
| JWT 过期后静默失败 | 用户点课程没反应 | 前端没有捕获 401 状态 | 在 axios 响应拦截器里统一跳登录页 |
| 视频播放进度丢失 | 刷新页面从头播放 | 没有记录用户观看进度 | 前端定时上报当前播放时间,后端记录到数据库 |
Django 部署到宝塔后还有个常见坑:耗时接口超时。Gunicorn 默认timeout = 30秒,如果导入视频转码或发送邮件这类耗时超过 30 秒的接口,会被强制杀掉。需要在启动命令加参数:
gunicorn -w 4 -b 127.0.0.1:5000 --timeout 120 app:app5.5 数据库类型转换与查询优化
后端开发中 Python 类型和数据库类型的转换也需要留心。Flask-SQLAlchemy 里常见场景:前端传price字段是字符串,存到Numeric(10,2)类型的数据库列中,MySQL 会自动转换,但如果传了空字符串,MySQL 会报错。所以接口里入参校验必须做,不能依赖数据库容错。
课程列表页的查询也容易写慢,特别是嵌套关系多时。我用 Flask-SQLAlchemy 时的优化思路:
# 错误写法:每个循环都查一次数据库 courses = Course.query.all() for c in courses: print(c.category.name) # N+1查询问题 # 正确写法:用 joinedload 一次性关联查询 from sqlalchemy.orm import joinedload courses = Course.query.options(joinedload(Course.category)).all()Django 里对应的优化是select_related和prefetch_related:
courses = Course.objects.select_related('category').all()这类问题在数据量少的时候看不出差别,一旦课程数超过几百,性能差距会很明显。这也是我在做这个项目时最有体感的一个优化点。
6. 项目扩展和我的体会
做完这个课程网站,我最大的感受是:技术选型的核心不是追求最新最热,而是匹配自己团队和项目的实际需求。
Flask 的优势是轻量、灵活、掌控感强,适合中小规模项目和想要把每个环节都搞清楚的学习者;Django 的优势是全家桶式的一体化解决方案,尤其在后台管理、权限系统、安全防护这些方面开箱即用。两者没有绝对的高下之分,关键在于你愿意花多少时间在基础设施上。
如果你从零开始做类似项目,我的路径建议是:
- 第 1 周:先确定需求,画出页面原型,明确数据模型。这一步省不了,数据模型设计的好坏直接决定后续改接口的工作量
- 第 2 周:后端 API 开发。用 Flask 或 Django 把用户、课程、评论的核心接口跑通,用 Postman 或 Apifox 测好
- 第 3 周:前端页面开发。Vue 这边重点是组件划分和路由设计,视频播放单独一个组件,方便复用和测试
- 第 4 周:前后端联调和部署。这部分预留的时间建议多一点,跨域、权限、打包、服务器环境,各种细节问题都是在联调阶段暴露出来的
最后再分享一个实用小技巧:视频切片文件如果很多,建议放在单独的磁盘目录或者对象存储中,不要塞进项目目录,不然每次部署拉代码会非常痛苦。我就因为视频文件直接放在 media 目录下,导致 git 仓库体积膨胀到几个 G,后面不得不清理历史提交记录才解决。
这个项目的完整代码我已经整理好了,不过这篇文章已经很长了,就不贴完整源码了。如果你在搭建过程中遇到具体报错,欢迎在评论区留言,我会尽量帮你看。