毕业设计常见痛点:为什么“能跑”≠“能毕业”
每年 4 月,校园服务器总会准时出现一批“神秘端口”——它们来自同一门课的毕业设计。点开一看,功能堆得比食堂窗口还多:人脸识别、积分商城、好友圈……结果演示时老师一句“并发 10 人预约”就把系统打挂。总结下来,三大坑位几乎年年有人踩:
- 需求拍脑袋:把“自习室预约”写成“智慧校园操作系统”,功能列表越长,代码越难收尾。
- 工程零规范:没有目录分层、没有虚拟环境、没有测试,README 里只有一句
pip install xxx。 - 技术选型跟风:别人用 Django 我也用,结果只想做几个 REST 接口,却背上了全套 MTV 包袱。
借助 AI 辅助开发,我尝试把“能跑”升级为“能毕业”,核心思路只有一句话:让 AI 做体力活,让自己做决策。下面记录完整实战过程,代码可直接复现。
技术选型:别让框架替你写论文
先给结论:本系统采用FastAPI + SQLModel + SQLite + Redis,理由如下。
| 维度 | Flask | Django | FastAPI |
|---|---|---|---|
| 学习曲线 | 平缓,但生态碎片 | 一站式,重 | 现代异步,自动生成文档 |
| 毕业场景 | 老师看不懂蓝图 | admin 后台很唬人 | Swagger 自动生成,答辩加分 |
| 并发需求 | 需手动补全异步 | ORM 同步为主 | 原生 async,压测友好 |
数据库同理:SQLite 本地零配置,答辩电脑没网也能跑;PostgreSQL 留着生产再换,SQLModel 屏蔽了方言差异。Redis 则用来解决“同一座位被两人同时锁定”的并发竞争,后文详述。
需求建模:用 AI 十分钟出活
把“我要一个自习室系统”喂给 Cursor,先让它生成用户故事,再人工删减,最终锁定三条核心用例:
- 学生注册/登录(含 JWT)
- 查看座位、时段列表
- 预约并支付押金(模拟支付,仅落库)
AI 产出 80% 能用的 PlantUML:
@startuml actor Student Student -> (Register) : email/password Student -> (Login) : JWT Student -> (List Seats) : date Student -> (Book Seat) : seat_id, slot_id (Book Seat) ..> (Check Conflict) : <<include>> @enduml复制到 PlantUML 在线渲染 直接贴论文,节省至少两页截图位。
核心模块实现:AI 写骨架,人填关键逻辑
1. 项目骨架(AI 30 秒生成)
copilot: "fastapi project structure with routers, services, models"得到如下目录:
studyroom/ ├── main.py ├── routers/ │ ├── auth.py │ ├── seats.py │ └── bookings.py ├── models/ ├── services/ ├── core/ └── tests/2. 用户认证:JWT + 哈希
AI 会本能地给出python-jose与passlib的模板,但毕业设计常忽略刷新令牌。我手动加了一个/auth/refresh接口,保证演示时 token 过期不会翻车。
关键代码(已脱敏):
# core/security.py from datetime import datetime, timedelta from jose import jwt from passlib.context import CryptContext SECRET = "CHANGE_ME_IN_PRODUCTION" ALGO = "HS256" pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def create_access_token(sub: str, minutes=30) -> str: expire = datetime.utcnow() + timedelta(minutes=minutes) return jwt.encode({"exp": expire, "sub": sub}, SECRET, algorithm=ALGO)3. 自习室时段锁定:Redis 分布式锁
并发场景下,两个请求同时读到“座位 3 还剩 1 个空位”,就会超卖。这里用 Redis 的SET NX EX做互斥锁,AI 能写出雏形,但要自己补全“锁归属”与“可重入”细节。
# services/lock.py import redis, uuid, time rdb = redis.Redis(host="localhost", port=6379, decode_responses=True) def acquire_lock(key: str, timeout=5) -> str | None: token = str(uuid.uuid4()) ok = rdb.set(key, token, nx=True, ex=timeout) return token if ok else None def release_lock(key: str, token: str): pipe = rdb.pipeline() pipe.watch(key) if pipe.get(key) == token: pipe.multi() pipe.delete(key) pipe.execute()在预约接口里调用:
# routers/bookings.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from services.lock import acquire_lock, release_lock from models.booking import Booking from models.slot import Slot from db import get_db router = APIRouter() @router.post("/book") async def book_slot( seat_id: int, slot_id: int, user=Depends(get_current_user), db: AsyncSession = Depends(get_db) ): lock_key = f"lock:s{seat_id}:t{slot_id}" token = acquire_lock(lock_key) if not token: raise HTTPException(status_code=409, detail="seat locked, retry later") try: # 双检查 slot = await Slot.get(db, slot_id) if slot.remaining <= 0: raise HTTPException(status_code=410, detail="sold out") # 创建订单 booking = Booking(user_id=user.id, seat_id=seat_id, slot_id=slot_id) await booking.save(db) await slot.decrement_remaining(db) await db.commit() return {"booking_id": booking.id} finally: release_lock(lock_key, token)4. 防重复预约:幂等键
前端连点两次提交,会生成两笔订单。解决方式是在表单里带client_id,后端用user_id + client_id做唯一索引。AI 生成的 SQLModel 模型默认不带联合索引,需要手动加:
class Booking(SQLModel, table=True): __tablename__ = "bookings" id: int | None = Field(default=None, primary_key=True) user_id: int client_id: str seat_id: int slot_id: int status: str = "PAID" __table_args__ = (UniqueConstraint("user_id", "client_id"),)性能与安全:别让老师一碰就 500
SQL 注入
SQLModel 默认使用绑定变量,但原生 SQL 拼接时仍需警惕。AI 偶尔会写出f"select * from xxx where id={id}",务必改回:id占位。接口幂等
上文已用联合索引兜底;对更新类接口,用If-Match+ 版本号更保险。冷启动
FastAPI 在首次请求会编译路由,压测前跑一轮locust热身,避免答辩现场第一次请求 502。部署差异
本地 SQLite 不区分大小写,PostgreSQL 默认区分;字段名务必双引号或全小写,防止迁移后找不到列。
生产环境避坑指南
环境变量 > 硬编码
用pydantic.BaseSettings一次性托管配置,AI 能自动生成.env.example,防止把 SECRET 推到 GitHub。日志结构化
不要用print!uvicorn+loguru两行配置即可 JSON 化,方便丢进日志平台加 5 分印象分。容器化
写 Dockerfile 时 AI 会给出python:3.11-slim,记得把uvloop依赖注释掉,否则在 M1 芯片模拟 x86 会踩坑。免费 SSL
如果学校只给 80 端口,用caddy一键反向代理,Let’s Encrypt 自动证书,老师扫码体验不再报“不安全”。
效果展示
压测 50 并发、持续 30 秒,CPU 占用 38%,无超卖,平均响应 120 ms,足以应对答辩围观。
还能怎么玩?留给读者的两个作业
- 把“预约”拆成独立微服务,座位、用户、通知三库分离,用
gRPC + protobuf通信,尝试 Docker Compose 一键起全套。 - 增加邮件通知:预约成功 + 30 分钟前提醒,用
Celery + Redis做异步队列,AI 能写 90% 模板,剩下 10% 的异常重试逻辑留给你。
做完这两个扩展,简历就能从“写过 CRUD”升级为“具备分布式事务意识”。毕业设计不是终点,把系统继续演进,才是真正的工程化开始。