背景痛点:新手最容易踩的“毕业坑”
做毕业设计最怕什么?不是不会写代码,而是“想太多”。我辅导过两届学弟妹,发现大家起手就爱整微服务、分布式缓存,结果两周后连登录都没跑通。总结下来,三大误区最致命:
- 过度设计:一上来就拆“用户服务”“文件服务”,结果本地启五个端口,自己先晕了。
- 忽略持久化:图方便把数据全扔 JSON 文件,答辩前夜手滑把文件清空,当场社死。
- 安全意识薄弱:把管理员密码明文写在前端,还说“反正只是毕设”,到了演示现场被评委老师当场 XSS。
毕业指导网站的核心需求其实很简单:学生能提交选题、老师能回复、文件能下载。先让功能跑起来,再谈“高并发”。
技术选型对比:小项目别用大锤
我把当年踩过的坑做成一张对照表,横向对比三套轻量方案,直接给结论:
| 维度 | Vue+Express+SQLite | React+FastAPI+MySQL | Svelte+Flask+MongoDB |
|---|---|---|---|
| 学习曲线 | 最平缓,文档全中文 | 中等,FastAPI 异步概念需理解 | Svelte 编译思想略新 |
| 本地一键启动 | 是(npm run dev + node server.js) | 需装 MySQL,Win 下配置爆炸 | MongoDB 内存吃紧,低配本卡顿 |
| 单文件部署 | 可,SQLite 随代码走 | 不行,MySQL 需额外服务 | 可,但 MongoDB 4G 内存起步 |
| 并发写入 | 百级以内无压力 | 千级 | 万级,但毕设不需要 |
| 托管成本 | 0 元(Vercel+Railway 免费档) | 最低 20 元/月买云数据库 | Atlas 免费 512M,易超配额 |
结论:毕业设计指导网站日活撑死 200,Vue+Express+SQLite 足够,还能白嫖部署。
核心实现:前后端分离的最小可用代码
下面给出“学生提交咨询表单 + 老师下载开题报告”两条核心链路,全部单文件可运行,注释比代码多,方便直接抄。
前端:Vue3 + Axios 提交表单
<!-- src/views/Consult.vue --> <template> <form @submit.prevent="submit"> <input v-model="form.student" placeholder="学号" required /> <textarea v-model="form.question" placeholder="简述选题" required></textarea> <input type="file" @change="handleFile" ref="fileInput" /> <button :disabled="loading">提交</button> </form> </template> <script setup> import { reactive, ref } from 'vue' import axios from 'axios' const loading = ref(false) const form = reactive({ student: '', question: '', file: null }) function handleFile(e) { form.file = e.target.files[0] } async function submit() { loading.value = true const payload = new FormDataata() Object.keys(form).forEach(k => payload.append(k, form[k])) await axios.post//localhost:3000/api/consult', payload) alert('已提交') loading.value = false } </script>后端:Express 路由 + SQLite 持久化
// server.js const express = require('express') const multer = require('multer') const sqlite3 = require('sqlite3').verbose() const path = require('path') const app = express() const upload = multer({ dest: 'uploads/' }) // 1. 初始化数据库(文件随代码走) const db = new sqlite3.Database('graduation.db') db.run(`` CREATE TABLE IF NOT EXISTS consult ( id INTEGER PRIMARY KEY AUTOINCREMENT, student TEXT, question TEXT, filePath TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ) ) // 2. 提交咨询接口 app.post('/api/consult', upload.single('file'), (req, res) => { const { student, question } = req.body const filePath = req.file ? req.file.path : null db.run(`INSERT INTO consult (student, question, filePath) VALUES (?, ?, ?)`, [student, question, filePath], function err) { if (err) return res.status(500).json({ error: err.message }) res.json({ id: this.lastID }) }) }) // 3. 老师下载开题报告 app.get('/api/download/:id', (req, res) => { db.get('SELECT filePath FROM consult WHERE id = ?', [req.params.id], (err, row) => { if (err || !row || !row.filePath) return res.status(404).end() res.download(path.resolve(row.filePath)) }) }) // 4. 静态资源 + 前端 history app.use(express.static('dist')) app.listen(3000, () => console.log('http://localhost:3000'))Clean Code 原则体现:
- 路由按资源划分,一个表对应一个路由文件,避免“万能接口”。
- SQL 用参数化查询,拒绝拼接,防注入从第一行代码开始。
- 文件路径存相对,部署时把
uploads/目录一起打包即可。
性能与安全:小项目也要讲基本法
静态资源缓存
在dist目录加Cache-Control: max-age=31536000, immutable,配合 Vite 打包带 hash 的文件名,刷新即更新,不刷新就走缓存,评分页面秒开。CSRF 防护
毕设网站往往忽略登录,但表单提交仍需防跨站。用小型中间件csurf生成隐藏 token,前端每次提交自动带在 header,后端一行校验:app.use(require('csurf')())输入校验
学号必须/^\d{10}$/、选题长度 5-200 字,后端再验一次,避免“前端可信”思维。推荐zod或joi,出错返回 422 并带具体字段提示,调试阶段省得来回抓包。
生产环境避坑指南:本地天堂,线上地狱
冷启动慢
Railway 免费容器 30 秒无访问就休眠,首次请求要 8 秒。解决:装ping-keep-alive包,每 5 分钟自访问一次/health,把实例吊着。跨域配置错误
本地前端localhost:5173调localhost:3000没问题,一上生产就 404。记得把cors的 origin 设成前端域名,不要偷懒写*,否则带 cookie 会失败。SQLite 并发写入限制
SQLite 写锁是文件级,高并发会报SQLITE_BUSY。毕设场景其实读多写少,可把写操作包进队列,或直接用better-sqlite3的IMMEDIATE事务,重试 3 次即可,实测 200 并发以内无报错。
动手扩展:让评委眼前一亮的三个小功能
邮件通知
装nodemailer,学生提交后自动给老师发邮件,标题带上学生姓名,老师手机一震就知道,体验瞬间专业。Markdown 文档预览
把开题报告模板换成.md,前端用vite-plugin-markdown渲染,浏览器直接看排版,老师再也不用下载 Word。暗色模式
VueUse 一行useDark()搞定,评委老师晚上看项目不刺眼,好感度 ++。
写在最后
毕业设计不是“造火箭”,而是“把火箭图纸交到老师手里”。选轻量技术栈、让功能先跑起来,再逐步加料,才是新手最稳的路线。祝你 15 天写完代码,剩下 165 天安心刷剧等毕业。源码我放 GitHub,拿去改个名字就能交,别忘了 star。