背景痛点:为什么“能跑”≠“能毕业”
每年 3 月,实验室的走廊里总会响起熟悉的哀嚎:“本地跑得好好的,老师电脑一开就报错!”——这几乎是所有 Python 毕设的宿命。把 Jupyter 里的 cell 按顺序粘成.py文件,再配个main.py就算“系统”,结果答辩现场 U 盘一插,依赖冲突、硬编码路径、神秘乱码轮番上演。总结下来,踩坑集中在三点:
- 代码组织混乱:所有逻辑挤在一个文件,函数名从
f1到f99,老师想改个字段,得先玩“大家来找茬”。 - 依赖管理缺失:
pip install时顺手加--user,到了机房只剩ModuleNotFoundError,版本号全看天。 - 零测试、零日志:异常直接
print,服务器 500 错误返回的是 HTML 源码,评委一看血压拉满。
毕设不是刷算法题,交付物得“别人能跑、自己能改、半年后可复现”。把“能跑”升级成“能毕业”,需要一次彻底的工程化改造。
技术选型对比:Flask vs FastAPI vs Django
选框架就像选鞋,不是越贵越好,而是合脚最重要。下面从“开发速度、API 友好度、ORM 需求”三个维度,给三款主流框架打个标签,方便你对号入座。
| 维度 | Flask | FastAPI | Django | |---|---|---|---|---| | 开发速度 | 轻量,脚手架少,需自己拼积木 | 自带异步脚手架,代码生成器爽点高 | 全家桶,admin 后台一键生成 | | API 友好度 | 需拼插件,REST 风格靠自觉 | 基于 OpenAPI,/docs 页面自动生成 | 用 DRF 才爽,否则啰嗦 | | ORM 需求 | SQLAlchemy 任选,灵活但配置多 | 自由搭配,推荐 SQLModel | 自带 Django ORM,迁移一条龙 |
一句话结论:
- 只想写接口 + 异步加持 → FastAPI
- 想快速出个带后台的管理系 → Django
- 老师指定“轻量”且你爱折腾 → Flask
下文示例以 FastAPI 演示,理由很简单:类型提示+异步+自动生成文档,答辩现场把/docs页面一投影,老师瞬间觉得高大上。
核心实现:可维护的分层骨架
先给出目录结构,再拆关键代码。整个项目叫graduation_project,平铺直叙,方便老师一眼定位。
graduation_project ├── app │ ├── api │ │ └── v1 │ │ └── student.py │ ├── core │ │ ├── config.py │ │ └── errors.py │ ├── service │ │ └── student_service.py │ ├── utils │ │ └── logger.py │ └── main.py ├── tests ├── requirements.txt ├── Dockerfile └── .env.example- 配置与环境隔离
app/core/config.py统一用pydantic读取环境变量,避免硬编码。
from pydantic import BaseSettings class Settings(BaseSettings): database_url: str = "sqlite:///./dev.db" jwt_secret: str log_level: str = "INFO" class Config: env_file = ".env" settings = Settings()- 统一异常处理
app/core/errors.py把业务异常翻译成前端能懂的 JSON。
from fastapi import FastAPI, Request from fastapi.responses import JSONResponse class BizException(Exception): def __init__(self, code: int, msg: str): self.code, self.msg = code, msg def register_exceptions(app: FastAPI): @app.exception_handler(BizException) def biz(req: Request, exc: BizException): return JSONResponse( status_code=400, content={"code": exc.code, "msg": exc.msg} )- 路由只当“交通警察”
app/api/v1/student.py只做参数校验与转发,不写 SQL。
from fastapi import APIRouter from app.service.student_service import add_student router = APIRouter() @router.post("") async def create(name: str, age: int): sid = await add_student(name, age) return {"student_id": sid}- 业务层打包事务
app/service/student_service.py里写逻辑,方便单元测试 mock。
from app.core.config import settings from sqlalchemy.ext.asyncio import AsyncSession from app.models.student import Student async def add_student(name: str, age: int, db: AsyncSession): new_one = Student(name=name, age=age) db.add(new_one) await db.commit() return new_one.id- 日志再也不是
printapp/utils/logger.py用标准库logging写文件+控制台双通道,支持按天轮转。
import logging, os from logging.handlers import TimedRotatingFileHandler def get_logger(name: str): log = logging.getLogger(name) if log.handlers: # 避免重复挂载 return log log.setLevel(os.getenv("LOG_LEVEL", "INFO")) fmt = logging.Formatter( "%(asctime)s | %(levelname)s | %(name)s | %(message)s" ) sh = logging.StreamHandler() sh.setFormatter(fmt) fh = TimedRotatingFileHandler( "logs/app.log", when="midnight", backupCount=7 ) fh.setFormatter(fmt) log.addHandler(sh); log.addHandler(fh) return log分层之后,老师想改“年龄必须大于 18”?只在 service 层动一行;想换 MySQL?改.env里的database_url即可,无需满世界找sqlite3.connect()。
部署方案:Docker 一条命令跑通
- 写 Dockerfile——多阶段镜像,编译依赖与运行环境分离,镜像体积减半。
# 阶段1:编译 FROM python:3.11-slim as builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt # 阶段2:运行 FROM python:3.11-slim WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . ENV PATH=/root/.local/bin:$PATH CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "-b", "0.0.0.0:8000"]- 写
docker-compose.yml把 Nginx 也带起来,静态资源、反向代理一条龙。
version: "3.9" services: web: build: . env_file: .env volumes: - ./logs:/app/logs nginx: image: nginx:alpine ports: - "80:80" volumes: - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - web- Nginx 关键配置——开 gzip、转发真实 IP、屏蔽直接访问 8000 端口。
upstream app { server web:8000; } server { listen 80; location / { proxy_pass http://app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }本地测试docker compose up,浏览器打开http://localhost/docs,看到自动生成的 Swagger,你就拥有了“一键复现”能力,再也不怕老师电脑没依赖。
安全性与性能:别让“小项目”成为靶子
- SQL 注入:ORM 已参数化,就别手拼 SQL;万不得已用
text()时务必绑定变量。 - 秘钥隔离:JWT 密钥、数据库密码全进环境变量,
.env写进.gitignore,仓库公开即“社死”。 - 阻塞 I/O:文件上传、发邮件等耗时操作交给
BackgroundTasks或 Celery,避免前端转菊花。 - 限流与超时:用
slowapi给接口加令牌桶,Nginx 层client_max_body_size限制上传大小,双保险。
生产环境避坑指南:把“能跑”做成“能躺”
虚拟环境固化:
python -m venv venv && source venv/bin/activate后,立刻pip install pip-tools,用pip-compile生成可锁版本的requirements.txt,杜绝“当年新版”突然不兼容。requirements 生成:
pip freeze > requirements.txt会把系统包也带进去,正确姿势是:
pip-compile --output-file=requirements.txt pyproject.toml日志轮转:
上文TimedRotatingFileHandler已演示,记得把logs/目录挂到宿主机,容器重建也不丢。健康检查:
FastAPI 自带/health路由返回 200,Docker 的HEALTHCHECK指令配合curl -f http://localhost:8000/health,CI/CD 可自动重启异常容器。备份策略:
SQLite 直接cp会锁库,用.backup命令;MySQL 设cron每日mysqldump,文件同步到云盘,七天滚动,防止“最后一晚把库删了”的惨剧。
把课程项目变成工程作品:下一步怎么做?
工程化不是“多写几行配置”,而是把“可复现、可扩展、可交接”养成肌肉记忆。把今天这套模板直接套到你的旧代码上:
- 先拆目录,把 SQL、业务、路由拆三层;
- 补
.env与requirements.txt,让同学电脑能跑; - 写三个单元测试,答辩演示一键
pytest,老师刮目相看; - 用 Docker 打包,放云服务器,二维码扫码即访问。
当你能淡定地回复老师“您稍等,我两分钟重新部署一版”,就已经把“课程作业”升级成“工程作品”。毕业设计不是句号,而是把代码写得“像个人样”的起点。祝你重构顺利,答辩高分!