基于Python加Vue的毕业设计实战:从零构建全栈项目并规避常见陷阱
毕业设计季,实验室里最常见的对话是:
A:你前端调通了吗?
B:通了,但 422 报错,后端说我字段不对。
A:你字段名是不是又和下划线混着用了?
如果你也卡在类似循环,这篇笔记把“从 0 到可上线”踩过的坑一次性摊开。技术栈锁定 Python + Vue3,项目载体选了一个“任务管理系统”——功能简单,却足以覆盖认证、分页、搜索、文件上传、Docker 部署等毕设必备要素。读完你可以直接套壳换成“图书管理”“疫情登记”“二手交易”等任何业务场景。
1. 全栈毕设六大痛点
- 跨域:浏览器拦截 OPTIONS 预检,Vue 开发环境 8080 端口调不到后端 5000。
- 接口文档缺失:今天改字段,明天忘同步,前端永远不知道后端到底返回啥。
- 状态管理混乱:Vue 组件里直接写 axios,token 存 localStorage,刷新 404。
- 无统一错误处理:后端抛 500,前端控制台一片红,用户只看到空白页。
- 部署流程空白:本地 npm run dev 一切正常,上到服务器 404 刷新就崩。
- 安全“裸奔”:密码明文、JWT 永不过期、CORS 全开,答辩现场被评委当场锤。
2. 技术选型:FastAPI vs Flask & Vue3 组合 API
2.1 后端抉择
| 维度 | Flask | FastAPI |
|---|---|---|
| 自动生成文档 | 需三方插件(flask-restx) | 原生 Swagger&ReDoc |
| 异步支持 | 通过 asgiref 勉强 | 原生 async/await |
| 数据校验 | 手动或 marshmallow | Pydantic 强制 |
| 学习曲线 | 平缓 | 稍陡,但类型提示友好 |
结论:
- 想最快跑通 MVP,可选 Flask;
- 想“写完接口即文档”,并顺手把性能拉高,直接 FastAPI。下文代码以 FastAPI 为主,Flask 差异点用注释补充。
2.2 前端为什么 Vue3
- 组合式 API 让逻辑复用更直观,适合毕设这种“页面不多但逻辑杂”的场景;
- Vite 冷启动 300 ms,告别 webpack 三分钟;
- 官方生态(Router、Pinia)一并升级,坑比 Vue2 少。
3. 核心实现拆解
3.1 项目骨架
back/ # Python 后端 ├─ main.py ├─ models.py ├─ schemas.py ├─ deps.py └─ requirements.txt front/ # Vue3 前端 ├─ src/ │ ├─ api/ │ ├─ stores/ │ ├─ views/ │ └─ components/ └─ vite.config.ts docker-compose.yml3.2 统一依赖与配置
back/main.py
from fastapi import FastAPI from contextlib import asynccontextmanager from tortoise.contrib.fastapi import register_tortoise from core.config import settings # 集中读取 .env from api.v1 import auth, task # 路由分层 @asynccontextmanager async def lifespan(app: FastAPI): # 启动事件 yield # 关闭事件 app = FastAPI( title="TaskMan", version="1.0.0", lifespan=lifespan, docs_url="/docs" if settings.DEBUG else None, ) # 注册路由 app.include_router(auth.router, prefix="/api/v1") app.include_router(task.router, prefix="/api/v1") # ORM 绑定 register_tortoise( app, db_url=settings.DATABASE_URL, modules={"models": ["models"]}, generate_schemas=True, add_exception_handlers=True, )核心思想:
- 配置与代码分离,十二因子应用;
- 生产环境关闭 Swagger,避免接口裸奔。
3.3 用户认证:JWT + 哈希
back/schemas.py
from pydantic import BaseModel, EmailStr, Field class UserCreate(BaseModel): email: EmailStr password: str = Field(min_length=6) class Token(BaseModel): access_token: str token_type: str = "bearer"back/auth.py
from passlib.context import CryptContext from jose import jwt from datetime import datetime, timedelta from core.config import settings pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(pw: str) -> str: return pwd_ctx.hash(pw) def verify_password(pw: str, hashed: str) -> bool: return pwd_ctx.verify(pw, hashed) def create_token(sub: str) -> str: expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE) return jwt.encode({"sub": sub, "exp": expire}, settings.SECRET_KEY, algorithm="HS256")- 使用 passlib,算法 bcrypt,成本因子 12;
- ACCESS_TOKEN_EXPIRE 可在 .env 按分钟配置,答辩演示时调成 5 分钟,评委直呼专业。
3.4 依赖注入:自动验签
back/deps.py
from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import jwt, JWTError from core.config import settings oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") async def get_current_user(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) user_id: str = payload.get("sub") if user_id is None: raise HTTPException(status_code=401, detail="Token无效") except JWTError: raise HTTPException(status_code=401, detail="Token无效") return user_id之后在任意路由函数里加user_id: str = Depends(get_current_user)即可拿到当前用户,逻辑层与鉴权层彻底解耦。
3.5 业务示例:任务分页
back/task.py
from fastapi import APIRouter, Depends, Query from typing import Optional from deps import get_current_user from models import Task from tortoise.contrib.pydantic import pydantic_queryset_creator TaskOut = pydantic_queryset_creator(Task) @router.get("/tasks", response_model=list[TaskOut]) async def list_tasks( page: int = Query(1, ge=1), size: int = Query(10, ge=1, le=100), user_id: str = Depends(get_current_user), ): offset = (page - 1) * size qs = Task.filter(owner_id=user_id).offset(offset).limit(size) return await TaskOut.from_queryset(qs)- 利用 Tortoise-ORM 的 pydantic_queryset_creator,省去手写序列化;
- page & size 参数自动加入 Swagger 调试页,前端同学感动落泪。
3.6 Vue3 端调用封装
front/src/api/request.ts
import axios, { AxiosError } from "axios"; import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, // 不同环境读取不同 .env timeout: 10000, }); request.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); request.interceptors.response.use( (resp) => resp.data, (err: AxiosError) => { if (err.response?.status === 401) { localStorage.removeItem("token"); useRouter().push("/login"); } ElMessage.error((err.response?.data as any)?.detail || "请求异常"); return Promise.reject(err); } ); export default request;- 统一弹出错误提示,避免每页写 catch;
- 401 自动跳转,刷新掉线问题秒解。
组件内使用
import request from "@/api/request"; export function listTasks(params: { page: number; size: number }) { return request.get("/tasks", { params }); }配合<script setup>:
const tasks = ref([]); async function load() { const { data } = await listTasks({ page: 1, size: 20 }); tasks.value = data; } onMounted(load);4. 性能与安全:把“能跑”变成“能上线”
密码哈希已用 bcrypt;
JWT 签发加 exp,刷新机制可选“滑动刷新”,毕设阶段先硬过期,降低复杂度;
开启 HTTPS 后,CSRF 威胁下降,但仍建议:
- 后端 POST/PUT/DELETE 统一走 JSON,不解析
application/x-www-form-urlencoded,天然免疫 CSRF; - 若需表单提交,在 FastAPI 中加
CSRFProtect中间件,前端在 Cookie 获取csrf_token后手动回传 HeaderX-CSRF-Token;
- 后端 POST/PUT/DELETE 统一走 JSON,不解析
接口幂等性:
- 创建资源用 POST,返回 201 + 资源 URI;
- 关键操作提供幂等键(如客户端生成 UUID),后端以唯一索引兜底,防止重复提交;
限流:
- 生产环境用
slowapi(Flask)或fastapi-limiter(依赖 Redis),毕设演示可在 Nginx 层limit_req一把梭。
- 生产环境用
5. 生产环境避坑指南
.env 管理
- 后端用
python-dotenv,前端 Vite 以VITE_*前缀暴露; - 绝不提交
.env.production到 Git,CI 通过仓库 Secret 注入。
- 后端用
静态资源 404
- Vue 路由
history模式下,刷新页面会请求后端/task/123,Nginx 需配:
location / { try_files $uri $uri/ /index.html; }- 同时把
/assets指到alias /app/front/dist/assets;开启gzip_static on,首屏提速 30%。
- Vue 路由
跨域消失术
- 生产环境让 Nginx 同一端口反代前后端,CORS 仅保留
allow-origin: *的 OPTIONS 预检消失,评分老师再挑不出刺。
- 生产环境让 Nginx 同一端口反代前后端,CORS 仅保留
Docker 多阶段构建
# front/Dockerfile FROM node:18-alpine as builder WORKDIR /app COPY package*.json . RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /etc/nginx/conf.d/default.conf /etc/nginx/conf.d COPY --from=builder /app/dist /usr/share/nginx/html- 最终镜像仅 23 MB,push 到阿里云镜像仓库,服务器 1 分钟拉完。
日志与监控
- 后端统一 JSON 日志,容器 stdout 收集;
- 用
prometheus + grafana做进程监控,毕设答辩放一张 QPS 面板,瞬间高大上。
6. 代码可维护性再进一步
- 单元测试:
- 后端:pytest + pytest-cov,把 JWT 依赖用
override_dependencies注入假用户,CI 要求覆盖率 ≥ 80%; - 前端:vitest 测纯函数,Cypress 跑通登录→新建任务→断言列表;
- 后端:pytest + pytest-cov,把 JWT 依赖用
- CI/CD:
- GitHub Actions 监听
main分支 push,跑测试→构建镜像→SSH 到云主机docker compose up -d; - 提交 PR 自动评论预览地址,老师体验新功能不用远程桌面。
- GitHub Actions 监听
7. 结语:把“能交”升级成“能秀”
毕业设计不是终点,而是第一次把“写代码”变成“交付产品”。把上面的目录结构、认证流程、部署脚本原封不动拷过去,换成你的业务模型,就能在两周内拿出一套可上线、可演示、可扩展的全栈作品。下一步,挑一个最不顺眼的模块写单测,再把 GitHub Actions 跑通,让评委在答辩现场看到自动部署的 green pass——那一刻,你不再只是“学生”,而是可以签字的“工程师”。