背景痛点:那些年我们一起踩过的坑
做动态网页毕设,最怕“跑通”那一刻:页面能刷出来,老师一追问“如果别人输个';DROP TABLE student;--会怎样?”——瞬间社死。我帮导师评审三年,总结高频翻车现场如下:
- SQL 注入:直接字符串拼接
"SELECT * FROM user WHERE id=" + req.query.id,把数据库当公共厕所。 - 无状态管理:登录后把
uid存前端 hidden 字段,刷新一次页面就“被登出”。 - 硬编码配置:把数据库密码写死在
config/db.js,GitHub 一开源,服务器当天被挖矿。 - 前后端一锅粥:PHP 里嵌 HTML,HTML 里再嵌 PHP,后期加字段要改三个文件,调试靠“玄学打印”。
- 部署靠 U 盘:本地 Windows 不区分大小写,线上 Linux 一跑就 404,回宿舍连夜改代码,心态炸裂。
痛定思痛,这次毕设我决定用“小步快跑、模块解耦”的思路,做一个真正能在生产环境跑起来的“学生信息管理系统”(简称 SIMS)。
技术选型 30 秒对比
| 技术栈 | 学习曲线 | 生态 | 性能 | 结论 |
|---|---|---|---|---|
| PHP + Laravel | 低,教材多 | 成熟 | 同步阻塞,并发一般 | 适合 1 人快速出活,但代码风格容易“意大利面条” |
| Django | 中,ORM 强大 | 电池齐全 | 同步,高并发需上 Celery | 太重,小项目配置比代码多 |
| Node.js + Express | 低,JS 一套到底 | NPM 应有尽有 | 异步 IO,QPS 同硬件下比 PHP 高 30%+ | 毕设尺度刚刚好,后期可平滑升级集群 |
结论:为了“前后端通吃”+“服务器省内存”,我最终锁定 Node.js + Express + MySQL。
核心实现:搭骨架、写接口、渲染页面
1. 项目骨架
sims/ ├─ app.js // 入口 ├─ config/ // 多环境配置 ├─ controllers/ // 业务逻辑 ├─ models/ // 数据访问层 ├─ routes/ // 路由定义 ├─ views/ // EJS 模板 ├─ public/ // 静态资源 ├─ tests/ // 单元测试 └─ docker-compose.ymlClean Code 原则:一个文件只做一件事,函数不超过 30 行,回调不超过两层,拒绝“回调地狱”。
2. 数据库设计
CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, stu_no VARCHAR(20) UNIQUE, name VARCHAR(50) NOT NULL, gender ENUM('M','F'), birthday DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 后端关键代码
3.1 连接池配置(config/db.js)
const mysql = require('mysql2/promise'); const cfg = require('./index'); // 池化 = 复用连接,减少握手延迟 const pool = mysql.createPool({ host : cfg.dbHost, user : cfg.dbUser, password : cfg.dbPwd, database : cfg.dbName, waitForConnections : true, connectionLimit : 10, // 根据 1C2G 服务器实测,10 条足够 queueTimeout : 60000 }); module.exports = pool;3.2 模型层(models/student.js)
const pool = require('../config/db'); exports.insert = async (stu) => { const sql = 'INSERT INTO student(stu_no,name,gender,birthday) VALUES (?,?,?,?)'; const [res] = await pool.execute(sql, [stu.stuNo, stu.name, stu.gender, stu.birthday]); return res.insertId; }; exports.remove = async (id) => { // 软删除,给后期做数据恢复留余地 const sql = 'UPDATE student SET deleted=1 WHERE id=?'; await pool.execute(sql, [id]); };3.3 路由与权限(routes/student.js)
const router = require('express').Router(); const { body, validationResult } = require('express-validator'); const studentCtrl = require('../controllers/student'); const { isLogin, isAdmin } = require('../middleware/auth'); // 新增学生 router.post('/', isLogin, // 必须登录 isAdmin, // 必须管理员 body('stuNo').isLength({ min: 6 }).withMessage('学号太短'), body('name').notEmpty().trim().escape(), // XSS 第一道关 async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); await studentCtrl.create(req, res); });3.4 控制器(controllers/student.js)
const Student = require('../models/student'); exports.create = async (req, res) drainage try { const id = await Student.insert(req.body); res.json({ ok: 1, id }); } catch (e) { // 统一错误处理,避免泄漏堆栈 res.status(500).json({ ok: 0, msg: '服务小哥正在狂奔' }); } };3.5 视图渲染(views/student/list.ejs)
<%- include('partials/header') %> <table class="table"> <thead><tr><th>学号</th><th>姓名</th><th>操作</th></tr></thead> <tbody> <% students.forEach(s=>{ %> <tr> <td><%= s.stu_no %></td> <td><%= s.name %></td> <td> <a href="/student/<%= s.id %>/edit" class="btn btn-sm btn-primary">编辑</a> </td> </tr> <% }) %> </tbody> </table> <%- include('partials/footer') %>EJS 里用<%= %>会自动做 HTML 转义,防止 XSS 输出。
安全性与性能:把“坑”填成“护城河”
- SQL 注入:一律使用占位符
?,MySQL2 驱动底层走预编译,就算用户输入' OR 1=1 --'也当字符串处理。 - XSS:后端
validator.escape()+ 前端 EJS 默认转义,双层保险;上传头像做类型白名单image/*,拒绝.html。 - CSRF:生产环境打开
csurf中间件,前端表单隐藏_csrf字段,接口校验不通过直接 403。 - 幂等性:PUT 更新带版本号
version字段,使用UPDATE ... WHERE id=? AND version=?,返回影响行数 0 即重复提交。 - 连接池:上文已给 10 条,压测 200 并发 qps≈1100,CPU 70%,内存 220 MB,毕业答辩足够。
- 慢查询:给
stu_no建唯一索引,EXPLAIN 检查type=const,0.3 ms 内返回。
生产避坑指南:把“能跑”升级成“稳跑”
- 环境变量:使用
dotenv,把.env加入.gitignore,服务器通过docker-compose environment:注入,拒绝裸密码。 - 日志:winston 按天滚动,保留 30 天,错误级别写独立文件,排查问题不再靠“翻控制台”。
- 路径:用
path.join(__dirname,'../upload')代替./upload,Windows / Linux 双端兼容。 - 进程守护:PM22 或 Docker +
--restart=unless-stopped,服务器重启自拉起,老师半夜访问也不掉链子。 - 备份:MySQL 容器化后,用
mysqldump定时导到宿主机,再同步到云盘,误删表可 5 分钟级回滚。
一键 Docker 部署
# docker-compose.yml version: "3.9" services: sims: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DB_HOST=db - DB_USER=root - DB_PWD=123456 - DB_NAME=sims depends_on: - db db: image: mysql:8.0 volumes: - ./backup:/docker-entrypoint-initdb.d environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: sims本地写完docker-compose up -d,3 分钟完成线上可访问版本,答辩现场直接投大屏,老师点赞。
可扩展方向:让作品不止“能毕业”
- 分页优化:用
LIMIT ?,?+ 自增主键游标,百万级数据翻页不扫全表。 - 导出 Excel:前端点“导出”→后端流式生成
xlsx,Content-Type: application/vnd.openxmlformats,2 万行 3 秒完成。 - 头像裁剪:接入
cropper.js+ 阿里云 OSS 直传,减少服务器带宽。 - 微信小程序:把查询成绩做成扫码即查,附赠“校友情怀”。
- 单元测试:用
mocha + supertest把控制器全盖一遍,CI 跑通再合并,面试加分项。
写在最后
整套做下来,我最深的体会是:毕业设计不是“跑通就行”,而是把“企业级最小闭环”跑通。等你把 SQL 注入、CSRF、连接池、Docker、CI 这些“坑”都踩平,简历上的“熟悉高可用 Web 开发”就不再是空话。别急着封板,试着把 Excel 导出或微信扫码加上去,让系统在真实场景里再跑一圈——你会惊喜地发现,面试官开始反问你“这个并发量级怎么再翻倍”。祝你编码顺利,答辩高分,毕业快乐!