FastAPI在线课程学习系统实战:从环境搭建到JWT认证与接口联调
2026/9/16 0:20:02 网站建设 项目流程

简介:这是一套基于FastAPI框架构建的在线课程学习系统Python源码,附带完整项目说明,适合具备一定Python基础、正在筹备毕业设计或课程设计的开发者。系统围绕用户、课程、学习、互动、管理五大模块展开,涵盖注册登录、课程增删改查、进度跟踪、笔记记录、讨论答疑等功能,能够帮助读者理解FastAPI在实际项目中的模块化组织方式。压缩包共56个文件,主要包含17个py源码文件、32个pyd编译模块、3个pyc缓存文件,以及requirements依赖清单、README说明和项目配置文件,整体仅4.37MB,结构紧凑。目前已有35人学习下载。通过研读源码与说明,可以掌握FastAPI路由设计、JWT鉴权、数据库CRUD等常用开发技巧,同时借鉴其分层目录结构与最佳实践,对提升项目实战能力有明显帮助。

1. 从「在线课程学习系统」这一行标题,先读出FastAPI后端要做的事

FastAPI 教程和各类源码包里,真正难的不是 FastAPI 语法,而是从压缩包解压到打开 Swagger 文档之间那段没人替你走的路。基于 FastAPI 框架的在线课程学习系统,这个标题把技术栈、业务场景和交付形态一次写明:后端用 FastAPI 提供接口,业务围绕课程和学习两个动作展开,压缩包里同时放 Python 源码和项目说明。这类系统很少追求复杂权限,常见做法是学生和管理员两类角色,用课程、章节、学习进度三张核心表,再加一个存放视频与课件的静态目录,就能撑起一次完整学习闭环。它适合想快速跑通 FastAPI 项目、再把课程表改成自己业务的人,也适合第一次用 FastAPI 写真实系统的开发者,沿源码看明白注册登录、表关系和文件服务分别放在哪层。下面按解压一份 FastAPI 课程系统源码后的正常顺序,从依赖环境、数据模型、核心 API 到前后端联调拆分步骤。

2. 解压FastAPI课程系统源码后,用venv和uvicorn先跑起最小骨架

2.1 先看懂在线课程学习系统的FastAPI目录划分

拿到在线课程学习系统 python 源码后的第一件事,不是马上pip install,而是用目录树把“哪一块管路由、哪一块管数据库、哪一块存文件”找出来。网上下载的 FastAPI 项目源码有两种常见布局:一种把所有路由写在main.py单文件里,适合教学演示;另一种拆出routers/models/schemas,适合继续加业务。判断标准很简单,看根目录下有没有app包,或者看项目说明里是否提到前后端分离。

我一般按下面这个层次去对照,它基本覆盖了在线课程系统会涉及的模块:

. ├── main.py # FastAPI 应用入口,注册路由、静态文件、CORS ├── requirements.txt # 依赖清单,先看FastAPI版本 ├── .env.example # 数据库连接、JWT密钥示例 ├── app/ │ ├── database.py # SQLAlchemy engine 与 SessionLocal │ ├── models/ # User、Course、CourseSection、LearningProgress │ ├── schemas/ # Pydantic 请求体与返回结构 │ ├── routers/ # auth.py、courses.py、progress.py │ ├── core/ # 配置读取、JWT 工具、密码哈希 │ └── static/ # 视频、课件、图片,通过 StaticFiles 挂载 └── 项目说明文档.pdf # 描述接口清单与启动方式

代码把入口、配置、模型、路由、静态资源五层分开,是 FastAPI 项目实战里最不容易跑偏的结构。models 和 schemas 分开是重点:models 对应数据库表,schemas 对应接口收发的字段,两者混用时,课程表里加一个字段往往连带接口文档跟着错。static 单独建目录,方便后面挂载成http://127.0.0.1:8000/static/xxx.mp4

如果解压出来发现只有单个main.py,也不用重写,后续加路由时再拆。最该优先确认的是requirements.txt里是否锁了fastapiuvicornsqlalchemy这三个基础依赖,缺一不可,其次再确认有没有python-josepyjwtpasslib,这类带着认证、密码字段的项目基本都要用。

2.2 用python -m venv或uv包管理器创建FastAPI专用虚拟环境

Python 项目最忌讳直接装在全局环境。尤其是下载来的源码可能锁了 pydantic v2 或特定 SQLAlchemy 版本,装进全局后,其他项目一升级就会相互覆盖。常见做法是用 venv 隔离这个 FastAPI 项目:

cd <解压后的项目目录> python -m venv .venv

创建完成后激活:

# Windows PowerShell: .venv\Scripts\Activate.ps1 # Linux / macOS: source .venv/bin/activate

如果机器上装了较新的 uv 包管理器,也可以用 uv 创建虚拟环境并安装依赖,速度会比 pip 快不少,这是现在 FastAPI 项目里越来越常见的做法:

uv venv .venv uv pip install -r requirements.txt

两种方式效果等价,区别只在解析依赖和下载并发上,uv 更快,pip 兼容性更稳。激活之后执行python -m pip install -r requirements.txt,注意用python -m pip而不是直接敲pip,后者可能指向另一个解释器。pycharm 安装 FastAPI 失败报错的场景里,十次有八次是解释器选错,把项目解释器切到.venv下即可。

2.3 用uvicorn --reload启动FastAPI,并打开Swagger验证接口

依赖装完后,启动命令是:

uvicorn main:app --host 127.0.0.1 --port 8000 --reload

main:appmain.py里的app对象,入口文件名不是main.py时要把前半段换掉;--host 127.0.0.1只允许本机访问,需要局域网调试时改成0.0.0.0--port是端口,8000 被占用就换 8001;--reload会在改代码后自动重启,生产环境不要加。

启动后不要急着测业务,先看两个地址:

curl http://127.0.0.1:8000/docs curl http://127.0.0.1:8000/openapi.json

/docs是 FastAPI 自带的 Swagger 界面,能直接看到这个在线课程系统已注册的所有接口;/openapi.json是接口定义文件,Vue3 或 layui 前端可以用它自动生成请求代码。两个地址都正常返回,说明环境配通,再进入数据表设计。

如果启动时报错,对照下面这张表排查:

报错特征造成原因处理方式
ModuleNotFoundError: No module named fastapi依赖没装上重新执行 pip install 并确认虚拟环境已激活
pydantic_core 相关报错pydantic v1/v2 被混装删除 .venv 后重建,用 requirements.txt 一次装齐
Address already in use8000 端口被占用换 --port 8001,或查占用进程后释放
启动成功但 /docs 空白浏览器代理干扰了本地回环请求换无痕窗口,或访问 127.0.0.1 而不是 localhost

3. 给课程学习系统建核心表:SQLAlchemy模型设计与SQLite换MySQL的落地点

3.1 为什么在线课程系统用SQLAlchemy 2.x而不是裸SQL

对课程学习这类以 CRUD 为主的业务,裸 SQL 能写,但后续加字段、联表查课程和章节时,维护成本会快速涨起来。SQLAlchemy 2.x 的Mappedmapped_column写法把类型和约束收拢到模型类里,IDE 能提示,迁移时有据可依。与 FastAPI 搭配时,Pydantic 负责请求校验,SQLAlchemy 负责数据库映射,两者边界越清楚,后面改接口就越省事。

一个在线课程系统的最小数据模型通常不超过四张表。用户表存学生和管理员,课程表存课程基本资料,章节表存每个课程下的视频或文档,学习进度表记录用户看到哪个章节、看了多少秒。表多了反而难维护,真正要扩展时再补“收藏表”“订单表”,属于后续需求,不建议起步阶段设计得太满。

3.2 User、Course、CourseSection、LearningProgress四张核心表模型

model 的写法按 SQLAlchemy 2.x 推荐风格,用类型注解声明字段,下面这段是四个模型的最小实现:

# app/models.py from __future__ import annotations from datetime import datetime from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(32), unique=True, index=True, nullable=False) hashed_password: Mapped[str] = mapped_column(String(128), nullable=False) role: Mapped[str] = mapped_column(String(16), default="student") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) class Course(Base): __tablename__ = "courses" id: Mapped[int] = mapped_column(Integer, primary_key=True) title: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(Text, default="") cover_url: Mapped[str] = mapped_column(String(256), default="") sections: Mapped[list[CourseSection]] = relationship( back_populates="course", cascade="all, delete-orphan" ) class CourseSection(Base): __tablename__ = "course_sections" id: Mapped[int] = mapped_column(Integer, primary_key=True) course_id: Mapped[int] = mapped_column(ForeignKey("courses.id"), index=True) title: Mapped[str] = mapped_column(String(128), nullable=False) video_url: Mapped[str] = mapped_column(String(256), default="") sort_order: Mapped[int] = mapped_column(Integer, default=0) course: Mapped[Course] = relationship(back_populates="sections") class LearningProgress(Base): __tablename__ = "learning_progress" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) section_id: Mapped[int] = mapped_column(ForeignKey("course_sections.id"), index=True) progress_seconds: Mapped[int] = mapped_column(Integer, default=0) is_completed: Mapped[bool] = mapped_column(Boolean, default=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow )

User 表里单独留了role字段,值为studentadmin,后续做管理端接口时可以直接用这个字段控制权限,不用改表结构。Course 和 CourseSection 是一对多关系,cascade="all, delete-orphan"表示删除课程时会连带删除章节记录,避免数据库里留孤儿数据。LearningProgress 通过user_idsection_id定位学习记录,progress_seconds存视频播放到的秒数,前端每隔一段时间提交一次即可。

实际项目中,user_id + section_id应该加一个联合唯一约束,防止同一用户对同一章节出现多条进度记录,下载的源码里如果没有,建议自己补上。

3.3 create_all建表能跑通,SQLite切换MySQL时改哪几处

建表和会话管理放在database.py,写一个get_db依赖,FastAPI 每个请求都会调用它拿一个数据库会话:

# app/database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session DATABASE_URL = "sqlite:///./course.db" engine = create_engine( DATABASE_URL, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) def get_db(): db = SessionLocal() try: yield db finally: db.close()

check_same_thread=False只对 SQLite 需要,这是为了允许 FastAPI 的多线程请求共用连接;换成 MySQL 后这个参数要去掉。数据库连接串决定底层用哪种库,切换时下面这张表对照着改:

配置项SQLite 本地开发MySQL 生产
DATABASE_URLsqlite:///./course.dbmysql+pymysql://user:pass@127.0.0.1:3306/course_db?charset=utf8mb4
额外依赖pip install pymysql
connect_argscheck_same_thread=False不需要,可删除
并发能力写操作慢,适合单机演示支持多连接,适合正式部署

切换后执行建表语句,用Base.metadata.create_all(bind=engine)或直接在main.py启动事件里调一次。注意create_all只会建新表,不会修改已存在的表结构,字段变更要交给 Alembic 生成迁移,而不是改了模型后反复建表。

4. JWT认证与课程核心API:FastAPI里从注册到上报进度的最小闭环

4.1 注册和登录:FastAPI路由里用bcrypt哈希密码并签发JWT

在线课程系统的所有接口都要区分“谁在访问”,认证是最先要落地的部分。常见做法是注册时用 bcrypt 哈希密码,登录成功签发 JWT,后续请求带Authorization: Bearer <token>。下面是 auth 路由的最小实现:

# app/routers/auth.py from datetime import datetime, timedelta import jwt from fastapi import APIRouter, Depends, HTTPException from passlib.context import CryptContext from sqlalchemy.orm import Session from app.database import get_db from app.models import User router = APIRouter(prefix="/api/auth", tags=["认证"]) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") SECRET_KEY = "dev-secret-change-it" # 生产环境从 .env 读取 ALGORITHM = "HS256" def create_token(user_id: int) -> str: payload = {"sub": str(user_id), "exp": datetime.utcnow() + timedelta(days=7)} return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) @router.post("/register") def register(username: str, password: str, db: Session = Depends(get_db)): if db.query(User).filter(User.username == username).first(): raise HTTPException(status_code=400, detail="用户名已存在") user = User(username=username, hashed_password=pwd_context.hash(password)) db.add(user) db.commit() db.refresh(user) return {"id": user.id, "username": user.username} @router.post("/login") def login(username: str, password: str, db: Session = Depends(get_db)): user = db.query(User).filter(User.username == username).first() if not user or not pwd_context.verify(password, user.hashed_password): raise HTTPException(status_code=400, detail="用户名或密码错误") return {"token": create_token(user.id), "token_type": "bearer"}

密码用pwd_context.hash后落库,数据库里绝不能存明文;登录校验用verify对比哈希值。JWT 的sub放用户 id,exp设置为 7 天后过期,想让登录态保持更久就调大timedeltaSECRET_KEY在演示项目里可以写死,上线前必须挪到.env并通过 Pydantic Settings 读取。

代码里直接写了username: strpassword: str,FastAPI 会把它们当作 JSON 请求体字段解析,请求长这样:

curl -X POST http://127.0.0.1:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"alice","password":"123456"}'

这种方式逻辑最直观,适合源码阅读;规范项目里可以改成 Pydantic 模型承接请求体,校验规则更细。

4.2 Depends依赖注入:课程接口只对已登录用户开放

有了 token,再定义一个get_current_user依赖,凡是需要登录的接口,只要在参数里声明user=Depends(get_current_user),FastAPI 就会先执行解析和校验:

# app/routers/deps.py import jwt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session from app.database import get_db from app.models import User SECRET_KEY = "dev-secret-change-it" ALGORITHM = "HS256" oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db), ) -> User: try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = int(payload.get("sub")) except (jwt.PyJWTError, TypeError, ValueError): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="登录状态失效", ) user = db.get(User, user_id) if not user: raise HTTPException(status_code=401, detail="用户不存在") return user

这段依赖的价值在于把“当前用户是谁”统一收口,后续课程、进度接口不用重复写 token 解析逻辑。OAuth2PasswordBearer会自动从请求头取Authorization: Bearer,并把缺失 token 的情况转换成 401 响应。

4.3 课程列表、章节详情、学习进度上报三个核心API

课程系统的业务接口,建议按下面这张表规划,表里覆盖了最常用的五个接口:

路径方法请求体说明
/api/auth/registerPOSTusername、password创建学生账号
/api/auth/loginPOSTusername、password返回 JWT token
/api/coursesGET需要 Bearer Token,返回课程列表
/api/courses/{id}/sectionsGET按课程 id 返回章节
/api/progressPOSTsection_id、progress_seconds、is_completed保存学习进度

对应的 courses 路由实现如下:

# app/schemas/progress.py from pydantic import BaseModel class ProgressIn(BaseModel): section_id: int progress_seconds: int = 0 is_completed: bool = False
# app/routers/courses.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.database import get_db from app.models import Course, LearningProgress, User from app.routers.deps import get_current_user from app.schemas.progress import ProgressIn router = APIRouter(prefix="/api", tags=["课程"]) @router.get("/courses") def list_courses( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): return db.query(Course).all() @router.get("/courses/{course_id}/sections") def list_sections( course_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): course = db.get(Course, course_id) if not course: raise HTTPException(status_code=404, detail="课程不存在") return course.sections @router.post("/progress") def save_progress( payload: ProgressIn, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): record = ( db.query(LearningProgress) .filter_by(user_id=user.id, section_id=payload.section_id) .first() ) if not record: record = LearningProgress(user_id=user.id, section_id=payload.section_id) db.add(record) record.progress_seconds = payload.progress_seconds record.is_completed = payload.is_completed db.commit() return {"ok": True}

章节接口直接返回course.sections,靠的是 3.2 里的relationship,不用手动写第二条查询。进度接口先查同一条记录,存在就更新,不存在就新建,能应付大多数前端定时上报场景;重复调用时不会重复插入数据。ProgressInprogress_secondsis_completed都有默认值,前端只提交section_id也能通过校验。

5. 静态资源与联调验证:FastAPI视频目录、CORS和TestClient冒烟测试

5.1 用StaticFiles挂载视频目录,让FastAPI直接提供课程课件

课程视频和文档一般放在服务器磁盘,而不是数据库里。数据库只存相对路径,FastAPI 用StaticFiles把目录挂出来:

# main.py from fastapi import FastAPI from fastapi.staticfiles import StaticFiles app = FastAPI(title="在线课程学习系统") app.mount("/static", StaticFiles(directory="app/static"), name="static")

挂载后,app/static/course_01.mp4就能通过http://127.0.0.1:8000/static/course_01.mp4访问,章节表的video_url存相对路径/static/course_01.mp4即可。前端拿到这个地址直接丢给 video 标签播放,后端不需要处理流媒体协议,这是小型课程系统最稳妥的交付方式。

5.2 配置CORS并让Vue3开发服务器代理到FastAPI接口

前后端分离时,前端开发服务器和 FastAPI 不在同一个端口,浏览器会拦截跨域请求。常见做法是 FastAPI 侧加 CORSMiddleware:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], allow_methods=["*"], allow_headers=["*"], )

allow_origins按实际前端地址填,Vite 默认跑在 5173 端口,layui 项目可能是 8000 或其他端口,允许多个来源时写成列表。如果前端用 Vue3 且只想在开发期调试,还可以在 vite.config.ts 里配代理,让/api开头的请求统一交到 FastAPI:

export default { server: { proxy: { '/api': 'http://127.0.0.1:8000' } } }

配了代理后,前端代码里请求/api/courses即可,浏览器看到的是同源地址,跨域问题自然消失。两种方式二选一即可,别同时配出重复的允许来源。

5.3 用TestClient验证FastAPI接口,并处理两个安装坑

部署前最好做一次接口冒烟测试。FastAPI 官方依赖TestClient提供内存级测试能力:

# tests/test_course_api.py from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_register_login_and_list_courses(): client.post("/api/auth/register", json={"username": "stu01", "password": "123456"}) login_resp = client.post("/api/auth/login", json={"username": "stu01", "password": "123456"}) token = login_resp.json()["token"] headers = {"Authorization": f"Bearer {token}"} resp = client.get("/api/courses", headers=headers) assert resp.status_code == 200

执行时用pytest tests/ -v,不要起 uvicorn,TestClient 直接驱动 FastAPI 应用跑完整路由逻辑。这个测试覆盖了“注册到登录再到课程列表”的核心链路,能证明解压后的源码在本地是通的,后续改任何表结构或接口,回归一下这十几秒就能暴露问题。

最后提醒两个高频安装坑:如果用 pycharm 安装 FastAPI 依赖失败,优先检查终端里的 Python 是不是项目虚拟环境;另一个是把 bcrypt 锁到 4.1 以上的较新版本时,passlib 会报__about__属性相关错误,把requirements.txt里改为bcrypt<4.1再重装,就能稳定跑通认证和课程接口。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询