我第三次做 Web 大作业的时候,终于想明白了一件事:前两次的代码根本算不上“项目”,顶多叫“页面拼图”。第一次用 Table 布局,第二次用 Bootstrap 套模板,到第三次如果还停留在“把页面做出来”的水平,那这课等于白上了。所以这次我给自己定的目标很明确——做一个真正前后端分离、有数据交互、能部署上线、还能禁得住简单攻击自测的完整 Web 项目。
这篇文章会把我的完整思路、技术选型原因、核心代码实现、部署踩坑记录、安全自检方法全部摊开来讲。如果你也正在为 Web 课设、期末大作业、结课项目头疼,想从“页面仔”进阶到能写完整系统的人,这篇文章应该能帮你少走不少弯路。
1. 先回答“这次到底要做什么”:选题与功能边界的确定
第三次作业最容易犯的错,不是做不出来,而是根本不知道做什么。我见过太多人最后交上去的是一个“图书管理系统”——这当然没问题,问题是所有人都写图书管理系统,所有系统长得还都一样,老师扫两眼就关了。
1.1 一个观点:关键词是“第三次”,不是“大作业”
这句话怎么理解?第一次大作业是入门,能跑就行;第二次大作业是熟练,结构规范、交互合理;第三次大作业必须体现“系统思维”。也就是说,这次作业的核心考核点大概率不是“你会不会写标签”,而是“你有没有全栈视角、有没有工程化意识、有没有安全意识、有没有项目部署能力”。
所以我最终把选题定成了一个小型内容管理平台,核心功能包括:
- 用户注册、登录、会话保持(Token 认证)
- 文章列表、详情、分类筛选
- 后台发布 / 编辑 / 删除文章(仅管理员)
- 评论功能(用户登录后可用)
- 访问量统计(基于 Redis 缓存 + 数据库持久化)
- 数据可视化面板(ECharts 展示文章热度趋势)
听起来功能不少,但拆开看,每一个模块都不会超过课程大纲范围:用户系统对应 Cookie/Session/Token,文章模块对应增删改查和分页,评论对应表关联,统计对应 SQL 聚合。真正拉开差距的是“把这些功能粘合起来的方式”。
1.2 素材管理:交互设计比视觉堆砌重要
很多同学的页面素材丰富到爆炸:轮播图、动画、粒子特效、渐变背景。但交互一塌糊涂——点按钮没反馈,表单不校验,页面跳转重新加载所有资源。
我这次刻意把视觉做“轻”,把交互做“重”。CSS 只用了自定义变量 + Flex/Grid 布局,没有引任何 UI 框架(为了演示对布局原理的掌握,不用 Bootstrap),但每个按钮都有 loading 状态、每次提交都有防抖、每个删除操作都有二次确认。老师在现场演示时,操作体验上的顺滑程度,比视觉上的炫酷更说明问题。
1.3 交付物清单:做完这三样才算完整
- 可运行的源码(Git 仓库,含 README 和环境要求)
- 数据库初始化脚本(含测试数据)
- 部署文档 + 线上可访问地址(我用了一个月的轻量服务器)
如果你能主动把这三样交齐,平时分基本就已经到手了。
2. 技术选型:为什么是 Vue3 + FastAPI,而不是别的组合
第三次大作业的技术栈,大概率学校不会强制指定,或者只给一个模糊范围。这时候选型就是在“稳妥”和“展现能力”之间找平衡。我最终选了 Vue 3 + FastAPI + MySQL + Redis 的组合,理由我会逐个说清楚。
2.1 前端方案:Vue 3 在前端框架里的生态之位
备选方案不是没有:React 更流行,但在课程作业这个体量下,Vue 的渐进式上手路线更友好;原生 JavaScript 当然也可以,但等你要维护一个十几个组件的单页应用时,你就会明白为什么不推荐纯手写 DOM。
我用 Vue 3 + Vite + Pinia + Vue Router。组件化让代码结构清晰到每一块都“所见即所得”。比如文章列表拆成一个ArticleCard组件,列表页只是一个数据源 + 排列组合。以后想改成网格视图,只需要加一个grid/list状态的切换类,不需要动结构。
2.2 后端方案:FastAPI 到底好在哪
这里我专门把“python web框架有哪些”这个问题重新捋了一遍。大三做 Web 作业,主流方案无非那么几条:
| 框架 | 上手难度 | 适合场景 | 我的判断 |
|---|---|---|---|
| Flask | 低 | 小型应用、学习用 | 太自由,速度稍慢,适合入门 |
| Django | 低~中 | 大型应用、自带后台管理 | 太重,作业周期内发挥空间小 |
| FastAPI | 中 | API 服务、前后端分离、高性能 | 现代、清晰、文档自动生成,适合展示 |
FastAPI 最吸引我的一点是它天然支持异步和现代 Python 类型注解,接口文档靠 swagger 自动生成。这个特性在答辩时特别加分——你什么都不用做,只要打开/docs路径,老师就能看到你的所有 API 结构和参数说明,这比对着 PPT 里手截图强的多。
2.3 数据库怎么做:MySQL 负责存储,Redis 负责加速
有人贪图省事用 SQLite,一个文件搞定,方便是方便,但并发下会有锁问题,而且“部署到服务器”这个环节的演示效果会大打折扣。我用的 MySQL 8.0,结构上建了用户表、文章表、分类表、评论表、访问统计表。Redis 则用来做 Token 黑名单、文章阅读量缓存、防重复提交的窗口计数。
2.4 为什么还要上 Nginx
传统大作业做完就是python manage.py runserver,然后浏览器访问127.0.0.1:8000截图交差。我这次特意多走一步,用 Nginx 做了静态资源服务 + 反向代理:前端打包产物由 Nginx 直接托管,所有/api请求反向代理到 FastAPI 进程(Uvicorn 启动)。这个架构一句话就能说清楚:Nginx 在外面守门,FastAPI 在后面干活,各司其职。
3. 前端页面实现清单:从路由到组件、从状态到请求
前面说了那么多设计层面的东西,这里开始上真格的。前端看起来页面不少,但核心套路只有“组件化 + 状态管理 + API 封装”这三个词。
3.1 页面路由规划:6 个页面是怎么组织进来的
我按“游客 / 普通用户 / 管理员”三种身份规划了页面:
/首页:文章流 + 分类导航 + 轮播推荐位/article/:id文章详情:内容展示 + 评论列表 + 发评论入口/login与/register:认证页面/admin后台:文章管理表格 + 新增/编辑表单/dashboard数据面板:ECharts 可视化(按月统计发文量、阅读量 Top5 文章)
在 Vue Router 里注册这些路由时,我把 meta 字段作为权限判断的依据。{ meta: { requiresAuth: true } }表示需要登录,{ meta: { requiresAdmin: true } }表示必须是管理员。前端每次路由跳转前查一下当前用户状态,不符合条件就跳登录页。这个功能每次作业都有人做,但做明白的人不多——多半是只写了判断,没有考虑“登录之后应该回到原来想去的页面”。我处理方式是:跳登录页时在 query 里带上redirect参数,登录成功后再router.replace(redirect),体验直接拉满。
3.2 状态管理:Pinia 里到底存了什么
我把用户信息、Token、文章列表缓存、全局加载状态都放进了 Pinia。比如用户模块的示例代码:
// stores/user.js import { defineStore } from 'pinia' import { loginApi, getUserInfoApi } from '@/api/auth' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: null }), getters: { isLoggedIn: (state) => !!state.token, isAdmin: (state) => state.userInfo?.role === 'admin' }, actions: { async login(form) { const res = await loginApi(form) this.token = res.token localStorage.setItem('token', res.token) this.userInfo = await getUserInfoApi() }, logout() { this.token = '' this.userInfo = null localStorage.removeItem('token') } } })为什么要用 Pinia 而不是每次在组件里直接发请求?因为文章的阅读量统计、评论后数量刷新、管理员编辑文章后列表更新,这些操作会直接改变多个页面的数据状态。如果你还停留在“父子组件传参 +$emit”的思路,这些联动会写到你怀疑人生。集中管理之后,store里一个 action 一改,所有依赖该状态的组件自动刷新。
3.3 请求封装与错误处理:这是后台反馈的“隐形加分项”
我封装了一个request.js,核心做了四件事:带上 Token 请求头、响应拦截统一处理401过期、错误信息用ElMessage弹出、加载中状态统一管理。
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' import { useUserStore } from '@/stores/user' const service = axios.create({ baseURL: '/api', timeout: 8000 }) service.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) service.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.logout() router.push({ path: '/login', query: { redirect: router.currentRoute.value.fullPath } }) } ElMessage.error(error.response?.data?.detail || '请求异常,请稍后重试') return Promise.reject(error) } ) export default service这个封装的逻辑,是从“CTF 比赛里 XSS 要把 Cookie 偷出来才能绕认证”这类攻防思路里反向学的——认证信息必须放在请求头而不是 URL 参数里,同时要把失效后的处理流程做干净。平时看安全题的时候觉得是花架子,写工程代码时才发现全是基本功。
3.4 文章列表页的组件化结构
页面最终结构是这样分的:
ArticleList.vue负责拉数据、传参ArticleCard.vue负责单篇文章的展示PaginationBar.vue负责分页CategoryNav.vue负责分类筛选
每个组件只干一件事,调试时不需要翻几百行代码找 bug。尤其注意:列表页的分页参数page和category要放进 URL query 里。这样做的原因是,刷新页面后用户的浏览位置不会丢,把链接分享给朋友,对方打开也是同一个列表状态。这是我以前做纯data状态管理时完全没想到的细节,直到被老师问了一句“你这个刷新之后怎么回到第一页了”才发现问题。
4. 后端设计与数据库建模:如何让表关系既简洁又禁得住问
后端是整个项目的核心骨架。FastAPI 速度确实快,但真正体现系统设计水平的还是那几个 JWT 认证的接口和数据库表的设计。如果只是堆接口,答辩时老师随便问一个“为什么评论要关联两次用户表”就能把你问住。
4.1 用户表设计的细节:密码加密与角色字段
用户表结构:
CREATE TABLE `user` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,登录用', `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希值', `role` ENUM('user','admin') NOT NULL DEFAULT 'user', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像路径', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码不用明文,也不用简单的 MD5,而是用passlib配合bcrypt算法。工作因子设为 12,意思是每次哈希计算要故意消耗一定的计算时间。这听起来像是性能浪费,但这就是安全成本:攻击者拿数据库之后想暴力跑出密码,每试一个都要付出同样的代价,成本直接翻十几倍。
4.2 JWT 认证流程:从注册到请求带 Token 的完整链路
认证逻辑我写成了一套:
- 注册时,后端校验用户名唯一性,密码通过
bcrypt哈希 - 登录成功后,生成
JWT Token,有效期 2 小时 - 前端把 Token 存到
localStorage,每次请求通过Authorization: Bearer <token>发送 - 后端依赖
OAuth2PasswordBearer解析 Token 并获取当前用户
后端核心代码:
from datetime import datetime, timedelta from jose import JWTError, jwt from passlib.context import CryptContext SECRET_KEY = "your-secret-key-change-in-production" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 120 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def verify_token(token: str): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload.get("sub") except JWTError: return None需要注意,SECRET_KEY我放在.env里,并通过pydantic-settings读取,没有写在代码里。虽然只是作业,但养成“配置不进代码”的习惯,老师问起来也是一个亮点。
4.3 文章的增删改查:为什么说 RESTful 风格套路一样
文章模块接口严格按 RESTful 风格写:
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/articles | 文章列表(分页、分类筛选) |
| GET | /api/articles/{id} | 文章详情 |
| POST | /api/articles | 新增文章(仅管理员) |
| PUT | /api/articles/{id} | 编辑文章(仅管理员) |
| DELETE | /api/articles/{id} | 删除文章(仅管理员) |
列表接口的 SQL 用SQLAlchemy的select().where().order_by()链式写法,配合limit/offset做分页。这类代码看起来简单,但有一个细节值得注意:详情接口返回文章正文时不能把正文全量塞进列表接口里。
否则列表页一次性拉 100 篇文章的全文内容,性能和流量都很浪费。正确的做法是列表接口只返回summary(摘要字段),详情接口才返回content全文。
4.4 评论统计的 SQL 优化:JOIN 查询背后的分组思维
评论表需要与文章表关联,核心 SQL:
SELECT a.id, a.title, COUNT(c.id) AS comment_count FROM article a LEFT JOIN comment c ON a.id = c.article_id GROUP BY a.id, a.title ORDER BY comment_count DESC LIMIT 5;这条语句为什么用LEFT JOIN而不是INNER JOIN?因为INNER JOIN会自动过滤掉没有评论的文章,而我的列表需要显示“0 条评论”的文章,LEFT JOIN能保留这些行,再用COUNT(c.id)统计结果为 0。这就是很多前端同学学 SQL 时最容易忽略的区别。
4.5 种子数据:老师不会告诉你但查作业会用到的技巧
作业交上去之后,老师是不会有耐心先注册一个账号、再登录、再发 30 篇文章测试的。我准备了一个seed.py脚本,跑一遍就会自动创建管理员账号、生成 8 个分类、往文章表里插入 50 篇带有 lorem ipsum 中文摘要的文章,评论和阅读量也都有随机分布的数据。
这 50 篇数据让分页、分类、图表统计这些功能看起来“活”了起来。如果把系统交上去空空如也,就算功能都实现了,观感也会差一个档次。
5. 联调、调试与报错排查:从登录态丢失到端口占用的完整链路
作业做得再规整,运行环境不配合也白搭。我在整个开发过程中踩了无数个坑,把几个印象最深的报错和排查思路记录下来,这些大概率也是你们要遇到的。
5.1 CORS 跨域问题的三个解决方法
我的前端跑在5173端口,后端跑在8000端口,前端直接请求后端的/api路径,第一反应就是浏览器报跨域错误。解决方式有三层,建议按顺序排查:
- 后端
CORSMiddleware配置allow_origins=["http://localhost:5173"] - 前端 Vite 配置
server.proxy代理,把/api代理到http://localhost:8000 - 生产环境让 Nginx 统一反向代理,同源访问,从根上消灭跨域
# FastAPI 后端 CORS 配置 from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )5.2 Token 登录状态丢失问题:刷新页面后回到登录页
调试时遇到一个很诡异的现象:登录成功跳转到首页,一切正常,但只要按 F5 刷新,前端立刻跳回登录页。排查链路很重要:
先看localStorage里 Token 在不在——如果在,再看 Pinia 初始化时有没有从localStorage恢复 Token;如果没恢复,看用户信息是不是在页面刷新后被重置了。最后发现问题:Pinia 初始化时只恢复了 Token,却请求用户信息的动作放在了登录函数里,刷新后 Token 在但userInfo是null,路由守卫一看isLoggedIn为 false,直接拦截。
解决办法是写了一个init()方法,在App.vue的onMounted里调用:如果token存在,就拉一次用户信息放进 store。然后再走路由守卫,逻辑就通了。
// App.vue onMounted(async () => { const userStore = useUserStore() if (userStore.token && !userStore.userInfo) { try { await userStore.fetchUserInfo() } catch (e) { userStore.logout() } } })5.3 后端 403 而不是 401:权限审计的大坑
调试时管理员接口返回 403,我一度以为 Token 解析出问题。后来才发现我的路由里get_current_user依赖声明在/admin路由后面,而权限判断依赖current_user.role。问题的根因是管理员账号在数据库里的role字段默认值是空字符串,并不是没有权限,而是角色字段写错了。
排查这类问题有个好习惯:先把当前 Token 解析出来的用户信息print出来,看看数据库里的字段到底是什么。很多时候不是代码逻辑错,而是数据脏了。
5.4 端口占用问题的快速定位
有一次后端怎么都起不来,报错[Errno 98] Address already in use。很多人第一反应是重启电脑,其实一行命令就能找到罪魁祸首:
lsof -i :8000 kill -9 <pid>如果是在 Windows 上,则用netstat -ano | findstr :8000再taskkill /PID <pid> /F。
5.5 WebGL 报错引起的反思
开发过程中,我还遇到了一个和项目没什么直接关系的报错:three.webglrenderer: a webgl context could not be created。当时只是想在首页加一个 Three.js 的 3D 背景效果,结果发现浏览器加载不出来。排查后确认是浏览器扩展和显卡加速设置的问题,而不是代码问题。
这件事给我的启发是:作业里的视觉效果一定要在答辩用的机器上提前测试,不要赌老师教室电脑的硬件配置。3D 特效在微信小游戏和互动网站上很酷,但如果环境不同意,临时换方案比现场死磕容易得多。
6. 部署与上线:怎么把作业变成别人可以访问的网站
本地跑通的作业,严格意义上还不算“网站”。部署这一步能淘汰掉一半人,因为涉及 Linux 命令、进程管理、Nginx 配置,对很多平时只写代码的同学来说,相当于另一个世界。
6.1 服务器环境准备:从零到 Nginx
我用的是 2 核 2G 的轻量云服务器,系统选了 Ubuntu 22.04。核心步骤:
# 更新源 sudo apt update && sudo apt upgrade -y # 安装 Nginx、MySQL、Redis sudo apt install nginx mysql-server redis-server -y # 安装 Python 虚拟环境和项目依赖 python3 -m venv venv source venv/bin/activate pip install -r requirements.txt前端构建产物放在/var/www/frontend,后端项目放在/home/app/server。为了让服务在断网、挂机后还能自动重启,我用了 systemd 来管理 Uvicorn 进程:
# /etc/systemd/system/webapp.service [Unit] Description=FastAPI Web Application After=network.target [Service] User=www-data WorkingDirectory=/home/app/server Environment="PATH=/home/app/server/venv/bin" ExecStart=/home/app/server/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 Restart=always [Install] WantedBy=multi-user.target这个配置最容易被忽略的是Restart=always。没有它,服务器重启一次,服务就永久掉线,等老师访问时四五个 502 直接挂掉。
6.2 Nginx 配置:静态资源与 API 转发
server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/frontend; index index.html; # 前端单页应用路由重写 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html是 SPA(单页应用)路由的核心:打开/admin时服务器找不到这个物理路径,就回退到根目录的index.html,然后由 Vue Router 接管渲染。没有这一行,刷新后台页面就会 404。
6.3 HTTPS 配置:原来一张证书这么简单
现在主流浏览器对 HTTP 站点的标记越来越不友好,好在部署 HTTPS 已经不是麻烦事。我用certbot申请了免费的证书:
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d your-domain.com它自动修改 Nginx 配置并开启 HTTPS 跳转。前后五分钟搞定。评分时“地址栏那个安全锁”也是一种隐蔽的加分项。
6.4 数据备份:被老师检查之前必须做的事
我写了一个backup.sh脚本,每天凌晨 3 点用 crontab 执行一次,把 MySQL 的数据 dump 出来然后上传到对象存储。脚本核心就两行:
mysqldump -u root -p'password' webapp > /backup/webapp_$(date +%Y%m%d).sql find /backup -name "*.sql" -mtime +7 -delete第二行是保留最近 7 天的备份,旧自动删除。免得到时候磁盘被备份文件塞满,又成为一个新的故障点。
7. 从 CTF 思路看 Web 安全自检:作业里不能只有功能没有防御
相关热词里出现了大量 CTF、Web 安全的关键词的迭代。我本身也玩过一些 CTF,在“查找 flag 夺旗赛”里积累了不少攻防视角。做这次作业时,我特意用 CTF 的思维给自己的项目做了一轮完整的安全自检。建议每个做 Web 作业的认真跑一遍下面的检查单,比你答辩时吹“系统安全”有说服力得多。
7.1 SQL 注入:第一必修课
常规参数接口是最容易触发问题的地方。我的自检方式是直接用curl模拟常见攻击 payload:
curl "http://localhost:8000/api/articles?category=1' OR 1=1 -- "如果接口返回了所有分类的文章,说明输入的category拼接进了 SQL。正确做法是全程用 SQLAlchemy 的参数化查询,像下面这样:
stmt = select(Article).where(Article.category_id == category_id)参数化意味着数据库驱动会把“值”和“命令”分开处理,任何输入都只是数据,不会成为 SQL 指令的一部分。
7.2 XSS 防御:接口转义 + 前端转义,两边都要做
很多同学以为 XSS 是前端的事,其实后端接口也要做输出编码。我在接口返回文章正文时,对 HTML 做了特殊字符转义;前端展示时再通过框架的插值语法{{ }}而不是v-html渲染。两头都设防,即使某条数据违规了,也弹不出脚本。
import html def safe_render(content: str) -> str: return html.escape(content)7.3 越权测试:把非管理员用户的 Token 用到管理接口
用普通用户登录后,把 Token 复制出来,直接PUT /api/articles/1。如果返回 200,说明权限校验是假的。我实现的require_admin依赖会在请求进来时校验current_user.role。这一步是真的能拦住攻击,而不是只在前端把后台菜单藏起来。
7.4 文件上传与路径穿越
我的项目没有开放文件上传功能——头像用的是外链 URL。这个决定有赛博层面的考量:文件上传是 Web 安全里高危区,涉及后缀白名单、MIME 嗅探、存储路径随机化、图片内容二次校验。作业周期内把这些做对非常难,做了错反而可能成为安全隐患。所以在功能边界上,我选择“宁缺毋滥”。
7.5 一个真实的失败案例:我代码里的 Token 曾经被泄露
开发时我图方便,在前端代码的后台接口里硬编码了一个管理员的 Token,想着反正数据库是自己电脑上的,不会被发现。结果代码提交到了 Git 仓库发布在网上后,Bot 扫描工具在半小时内就拿到了那个 Token。它试了一次管理接口,失败的日志在服务器上清清楚楚。
这件事让我把“硬编码密钥”列为自己写代码的高压线。如果老师能扫一眼你的 Git 仓库,发现里面有明文密码,这个印象分基本就没了。
7.6 快速安全自检清单:提交前跑一遍
- 用 Navicat 或命令行直接执行常见 SQL 注入 payload,确认返回结果不受影响
- 用普通用户+管理员的 Token 分别请求管理接口,确认能拦截
- 登录后点击“退出”,确认 localStorage 里 Token 被清除
- 无登录状态访问后台路由,确认被重定向到登录页
- 查看 Nginx 日志,确认没有异常扫描请求把 500 刷爆
8. FastAPI 性能与接口设计进阶:如果你想让成绩更进一步
到这里,作业的完整链路已经能跑通了。但如果你的目标是拿高分,或者想展示“我真的懂了 Web 开发”,下面这几个进阶点是绝对不能错过的加分项。
8.1 FastAPI 异步接口:一次查询与两次查询的差别
FastAPI 默认支持async def路由,配合httpx.AsyncClient或直接SELECT查询,高并发下吞吐量明显优于同步写法。我在统计数据面板时,就用了并发请求三个独立统计接口:
from asyncio import gather @app.get("/api/dashboard") async def dashboard(): article_count, user_count, comment_avg = await gather( get_article_count(), get_user_count(), get_comment_stats() ) return {"articles": article_count, "users": user_count, "comments": comment_avg}这个接口在页面打开时的响应速度,比串行请求三个接口快了一倍左右。答辩时随口说一句“这里用了 asyncio.gather 做并发查询”,懂行的老师就会点头。
8.2 Redis 缓存:点击量统计的并发策略
文章阅读量的高频更新,如果每次都直接写 MySQL,会被锁拖垮。我的思路是:
- 第一次访问文章时,把阅读量从数据库加载到 Redis,初始值
1 - 后续每次访问,在 Redis 对
article:{id}:views执行INCR - 后台每 10 分钟把 Redis 里的增量批量刷回 MySQL
import redis.asyncio as redis r = redis.from_url("redis://localhost:6379") async def increment_views(article_id: int): key = f"article:{article_id}:views" await r.incr(key)这样即使用户疯狂刷新,数据库的写压力也很小。把这个逻辑写进 README,是比堆砌功能更高级的架构思维。
8.3 分页、搜索、排序的综合接口设计
列表接口支持了多个查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| page | int | 页码 |
| page_size | int | 每页数量 |
| keyword | str | 标题模糊搜索 |
| category | int | 分类筛选 |
| sort | str | latest/hottest/most_commented |
通过Query()声明默认值,并封装成一个PaginationParams依赖:
from fastapi import Query, Depends class PaginationParams: def __init__( self, page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=50), keyword: str = Query(None), category: int = Query(None), sort: str = Query("latest") ): self.page = page self.page_size = page_size self.keyword = keyword self.category = category self.sort = sort这种设计让接口既灵活又安全:ge=1保证页码不会为负数,le=50避免一次拉走全表。我在写前端列表页时,直接把 URL query 参数原样转发给后端,刷新、分享、回退全部自动保留状态,体验非常顺畅。
8.4 邮件通知:不该做的功能坚决不做
有人建议我加一个“用户注册后发欢迎邮件”的功能。我的判断是:不加。因为邮件服务需要额外的 SMTP 配置、验证码、有效期内倒计时,这些功能加不加,都不会影响这个项目的核心评分点。相反,多一个功能就多一个攻击面、多一个部署故障点。做 Web 作业,功能边界意识和代码质量同等重要。
9. 写文档、录演示视频:这些形式主义其实很加分
项目代码写完之后,真正花精力的地方不是再写功能,而是把一个可演示、可复现的完整交付物做好。文档和演示视频,恰恰是很多技术能力不错但不懂展示的同学最容易丢分的地方。
9.1 README 应该怎么写
很多 README 一行“这是一个 Web 作业”就完了。我的 README 结构是:
- 项目简介:一句话说清楚功能
- 功能清单:分用户端、管理端、统计端,每个功能对着一个截图
- 技术栈:前端、后端、数据库、部署工具
- 本地运行步骤:clone、建库、装依赖、启动,三步以内能跑起来
- 线上地址:只要服务器不关,老师点开链接就能看
写“本地运行步骤”时,我特意找了一个完全没配过前端环境的室友来按文档走一遍。他卡在哪,文档就在哪补。这份文档的价值,在自己电脑上体现不出来,等换一台电脑配置环境时就懂了。
9.2 演示视频的节奏安排:每个功能 10 秒
作业如果需要演示视频,我建议按以下节奏:
- 开场 5 秒:项目名 + 技术栈 + 页面整体展示
- 注册登录:30 秒
- 浏览文章:20 秒
- 发表评论:20 秒
- 管理员发布文章:40 秒
- 数据统计面板:30 秒
- 安全测试演示:30 秒
- 结束:服务器地址 + 域名
总时长控制在三分钟内。视频里不要展示“实现思路总结”的 PPT,而是展示“能看的结果”。老师不太可能在视频上看你讲五分钟原理,他能记住“这个系统能登录、能发文章、有统计图”就够了。
9.3 答辩环节最容易被问的问题
预判老师会问什么,提前准备好答案。以下是我整理的五个高频问题:
- “为什么用 Token 而不是 Session?”——答:前后端分离后 Token 天然跨域、服务端无状态、便于移动端复用。但作业里为了增加可靠性,我把 Session 和 JWT 做了对比说明。
- “数据库表之间是什么关系?”——答:用户与文章一对多,文章与评论一对多,分类与文章一对多。
- “高并发量大的时候哪里会先瓶颈?”——答:数据库写操作。目前用 Redis 做了缓存,后期可以加消息队列削峰。
- “部署用的什么?”——答:Nginx 反向代理 + Uvicorn 进程 + MySQL/Redis,systemd 守护进程。
- “上线之后如果发现内存溢出怎么排查?”——答:先看
htop定位进程,再查 Uvicorn 日志,用tracemalloc/memory_profiler做栈分析。
这些问题我在答辩前对着镜子练了三遍,确保每句话能在 30 秒内说清楚。等老师真的问出来的时候,你会发现平时所有调试的辛苦都在这一刻兑现了。
10. 写在最后的心得体会
第三次 Web 大作业,我一个月前开始做,真正交付给老师那天,回想整段过程,最大的收获其实不是某一道题解出来了,而是我终于把 Web 开发“从浏览器到数据库”这条链路摸了一遍:写过前端组件、调过后端接口、查过数据库表、配了 Nginx、申请过 HTTPS 证书、在服务器上看到自己的系统被访问时日志一格一格地滚动——这种感觉和平时在本地runserver完全不一样。
如果你现在也正在赶大作业,我唯一的建议是:先别急着敲代码,花一天时间把技术栈选清楚、把表结构画出来、把接口清单列出来。磨刀不误砍柴工,这句话在 Web 开发里前所未有的准确。
另外一个很实用的技巧:项目代码一定从第一天就放进 Git 仓库。我每天做完一个小功能就 commit 一次,出问题随时能回退。最后交作业时,Git 的提交记录本身就是一份完整的“开发日志”,比任何口头描述都有说服力。我给自己定了一条规矩:每一次 commit message 写清楚“我改了什么、为什么改、出了什么问题”。答辩的时候老师翻一遍提交记录,就知道你不是三天熬夜赶出来的,而是认认真真做了整一个月。
最后一个也许对你有用的经验:如果你觉得可视化面板、用户系统、后台管理这些功能都能独立实现,就是合在一起时不知所措,那就用一个最简单的办法——拆分。把一个“大系统”拆成“登录功能”和“文章功能”两个部分独立开发,每个部分做完先跑通,再通过路由和状态管理把两个部分衔接起来。拆到不能再拆,复杂系统就变成了多个简单系统的组装。这个思路在课程作业上管用,在真实企业级项目开发里同样管用。