基于Node.js与Express的宿舍分配系统:JWT鉴权与床位锁定实践

发布时间:2026/10/23 22:11:33
基于Node.js与Express的宿舍分配系统:JWT鉴权与床位锁定实践 简介Hostel-Allocation 是一套面向高校宿舍场景的全栈 Web 预订系统源码适合具备 Node.js 与前端基础、希望练习完整项目开发的在校学生或初级开发者。它解决学生线下排队订房、管理员人工分配效率低的问题学生可远程查看并预订房间管理员能实时掌握入住情况减少人员聚集与接触。资源包共 22 个文件约 34KB以 11 个 js 脚本、4 个 html 页面为主辅以 json 配置、css 样式及证书密钥文件覆盖登录注册、房间管理、预订页面与后端路由等模块。技术栈融合 Express.js 与 Sequelize 操作 MySQL前端采用 jQuery、HTML5、CSS3 与 Bootstrap4 构建响应式界面并通过 JWT 实现身份认证前后端逻辑划分清晰。已有 114 人学习读者可借此理解路由设计、ORM 建模、鉴权流程与页面交互的完整实现适合作为课程设计或全栈练手参考。1. 从一张排队长龙照片说起宿舍分配系统到底该解决什么问题每年开学季宿舍楼下总会出现一条蜿蜒的队伍。学生攥着纸质申请表宿管阿姨翻着厚厚的登记本一个名字一个名字地找空床位。这个场景在不少高校依然存在而它带来的问题远不止“排队累”——人群聚集、信息不透明、分配结果靠手写记录容易出错后续查寝、调宿、退宿全是一笔糊涂账。这套 Hostel-Allocation 方案要解决的就是把“人找人、纸对纸”的流程搬到线上。学生远程登录就能看到哪些楼栋、哪些楼层还有空位提交预订后系统自动锁定床位管理员在后台能一眼看清谁住在哪个房间、哪个床位调宿退宿都有记录可查。技术栈选的是 Node.js Express MySQL Sequelize JWT前端用 jQuery 配合 Bootstrap4 和 HTML5/CSS3没有上 React 或 Vue原因后面会讲。适合正在做 Web 开发方向毕设或课程设计的同学也适合需要快速交付一套内部管理系统的开发者参考。2. 需求拆解与技术选型为什么用 Express Sequelize 而不是全家桶2.1 三类角色的核心诉求先把角色分清楚后面所有设计都围绕这三类人展开。学生端最关心的是“我能不能订到”“我订的是哪个床位”“能不能改”。具体功能包括注册登录、查看宿舍楼与房间列表、查看床位占用状态、提交预订、查看自己的预订记录、申请调宿或退宿。管理员端最关心的是“谁住在哪”“还有多少空位”“怎么快速调整”。功能包括登录后台、查看所有楼栋房间的入住情况、按楼栋/楼层/房间号筛选、手动分配床位、审批调宿申请、导出住宿名单。系统本身要保证的是床位不能被重复预订、用户只能操作自己的订单、管理员操作要有日志可追溯。这三条是后面数据库设计和接口鉴权的出发点。2.2 技术选型的取舍逻辑很多同学做毕设喜欢直接上 Vue Element UI Spring Boot前后端分离看起来很“正规”但实际开发周期会拉长至少一倍。这套方案选 jQuery Bootstrap4 的原因很直接页面交互复杂度不高主要是表格渲染、表单提交、状态切换jQuery 的 DOM 操作和 AJAX 足够用Bootstrap4 的栅格和组件能快速搭出可用的管理界面。省下来的时间可以花在业务逻辑和边界条件上而不是折腾构建工具和状态管理。后端选 Express 而不是 Koa 或 NestJS是因为 Express 的中间件生态最成熟JWT 鉴权、请求校验、错误处理都有现成的中间件可以直接接入。Sequelize 作为 ORM 负责 MySQL 操作好处是模型定义清晰、关联查询方便、迁移脚本可版本化。对于宿舍分配这种关系型数据密集的场景用 ORM 比手写 SQL 更不容易在关联查询上翻车。提示如果导师明确要求前后端分离或使用特定框架优先满足导师要求。这套方案的价值在于业务逻辑和数据库设计技术栈可以替换。2.3 数据库表结构设计核心表一共五张关系不复杂但约束要写清楚。-- 用户表区分学生和管理员 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role ENUM(student, admin) DEFAULT student, real_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 楼栋表 CREATE TABLE buildings ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, gender ENUM(male, female) NOT NULL, floors INT NOT NULL ); -- 房间表 CREATE TABLE rooms ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_number VARCHAR(10) NOT NULL, floor INT NOT NULL, capacity INT DEFAULT 4, FOREIGN KEY (building_id) REFERENCES buildings(id) ); -- 床位表这是分配的最小单位 CREATE TABLE beds ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, bed_number VARCHAR(5) NOT NULL, status ENUM(available, occupied, maintenance) DEFAULT available, FOREIGN KEY (room_id) REFERENCES rooms(id) ); -- 预订记录表 CREATE TABLE allocations ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, bed_id INT NOT NULL, status ENUM(active, checked_out, transferred) DEFAULT active, allocated_at DATETIME DEFAULT CURRENT_TIMESTAMP, released_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (bed_id) REFERENCES beds(id) );床位表单独拆出来而不是在房间表里用数字表示剩余床位是为了让“一个床位一条记录”成为硬约束。预订时直接对 beds 表的 status 字段做更新配合数据库事务就能防止并发重复预订。allocations 表保留历史记录退宿或调宿时不删数据只改状态和释放时间这样后续查历史有据可依。3. 后端接口与鉴权实现JWT 中间件和床位锁定逻辑3.1 JWT 鉴权中间件的写法登录成功后签发 token后续请求在 Header 里带上Authorization: Bearer token。中间件负责校验 token 并把用户信息挂到req.user上。// middleware/auth.js const jwt require(jsonwebtoken); const SECRET process.env.JWT_SECRET || hostel-secret-key; // 校验 token解析出用户信息 function authenticate(req, res, next) { const header req.headers.authorization; if (!header || !header.startsWith(Bearer )) { return res.status(401).json({ message: 未登录或 token 缺失 }); } const token header.slice(7); try { const payload jwt.verify(token, SECRET); req.user { id: payload.id, role: payload.role }; next(); } catch (err) { return res.status(401).json({ message: token 无效或已过期 }); } } // 角色校验管理员接口专用 function requireAdmin(req, res, next) { if (req.user.role ! admin) { return res.status(403).json({ message: 无权限执行此操作 }); } next(); } module.exports { authenticate, requireAdmin };authenticate只负责“你是谁”requireAdmin负责“你能不能干这个”。两个中间件分开写的好处是学生接口只挂authenticate管理员接口挂authenticate requireAdmin职责清晰。token 过期时间建议设 2 小时太长不安全太短学生填个表就掉线。3.2 床位预订的事务处理这是整个系统最容易出 bug 的地方。两个学生同时点同一个床位如果不加锁就会超卖。// routes/allocation.js const { sequelize, Bed, Allocation } require(../models); router.post(/allocate, authenticate, async (req, res) { const { bedId } req.body; const t await sequelize.transaction(); try { // 锁定该床位行防止并发读取到相同状态 const bed await Bed.findByPk(bedId, { lock: t.LOCK.UPDATE, transaction: t }); if (!bed || bed.status ! available) { await t.rollback(); return res.status(400).json({ message: 该床位已被占用 }); } // 检查该学生是否已有 active 预订 const existing await Allocation.findOne({ where: { user_id: req.user.id, status: active }, transaction: t }); if (existing) { await t.rollback(); return res.status(400).json({ message: 你已有预订请先退宿或调宿 }); } // 更新床位状态并写入分配记录 await bed.update({ status: occupied }, { transaction: t }); await Allocation.create({ user_id: req.user.id, bed_id: bedId, status: active }, { transaction: t }); await t.commit(); res.json({ message: 预订成功 }); } catch (err) { await t.rollback(); res.status(500).json({ message: 预订失败请重试 }); } });关键在lock: t.LOCK.UPDATE这一行它会对该床位行加排他锁第二个请求必须等第一个事务提交后才能读取。配合事务的原子性要么床位状态和分配记录同时写入要么都不写。existing检查是防止一个学生占多个床位这个约束在业务上必须卡死。3.3 管理员查看入住情况的关联查询管理员需要一张表看到“楼栋-房间-床位-学生”的完整链路用 Sequelize 的 include 做嵌套关联。// routes/admin.js router.get(/occupancy, authenticate, requireAdmin, async (req, res) { const beds await Bed.findAll({ include: [{ model: Room, include: [{ model: Building, attributes: [name, gender] }] }], order: [[room_id, ASC], [bed_number, ASC]] }); // 查出所有 active 分配记录按 bed_id 映射学生信息 const allocations await Allocation.findAll({ where: { status: active }, include: [{ model: User, attributes: [real_name, username] }] }); const allocMap {}; allocations.forEach(a { allocMap[a.bed_id] a.User; }); const result beds.map(bed ({ building: bed.Room.Building.name, room: bed.Room.room_number, bed: bed.bed_number, status: bed.status, student: allocMap[bed.id] ? allocMap[bed.id].real_name : null })); res.json(result); });先查床位再查分配记录在内存里做映射比在 SQL 里写多层 LEFT JOIN 更直观也方便前端直接渲染表格。数据量大的时候可以加分页但一般宿舍楼也就几百个床位一次性返回没问题。4. 前端页面与交互jQuery 表格渲染和表单校验4.1 学生端床位选择页学生登录后看到的是按楼栋分组的床位卡片可用床位显示为绿色可点击已占用显示为灰色。// public/js/student.js function loadBeds(buildingId) { $.ajax({ url: /api/beds?buildingId buildingId, headers: { Authorization: Bearer localStorage.getItem(token) }, success: function (beds) { var html ; beds.forEach(function (bed) { var cls bed.status available ? bed-card available : bed-card occupied; var disabled bed.status available ? : disabled; html div classcol-md-3 div class cls p房间 bed.roomNumber /p p床位 bed.bedNumber /p button classbtn btn-sm btn-primary disabled onclickallocate( bed.id )预订/button /div/div; }); $(#bed-list).html(html); }, error: function (xhr) { if (xhr.status 401) { window.location.href /login.html; } } }); } function allocate(bedId) { if (!confirm(确认预订该床位)) return; $.ajax({ url: /api/allocate, method: POST, contentType: application/json, headers: { Authorization: Bearer localStorage.getItem(token) }, data: JSON.stringify({ bedId: bedId }), success: function () { alert(预订成功); loadBeds(currentBuilding); }, error: function (xhr) { alert(xhr.responseJSON.message); } }); }用 jQuery 的$.ajax统一处理 token 注入和 401 跳转比在每个请求里手写 fetch 更省事。Bootstrap4 的col-md-3让床位卡片自适应排列available和occupied两个 class 控制颜色和禁用状态。confirm弹窗虽然简陋但能防止误触答辩时演示也够用。4.2 管理员端入住总览表格管理员页面用 Bootstrap4 的 striped table 展示所有床位状态支持按楼栋筛选。// public/js/admin.js function loadOccupancy() { $.ajax({ url: /api/admin/occupancy, headers: { Authorization: Bearer localStorage.getItem(token) }, success: function (rows) { var html ; rows.forEach(function (r) { var badge r.status occupied ? span classbadge badge-danger已入住/span : span classbadge badge-success空闲/span; html trtd r.building /tdtd r.room /tdtd r.bed /tdtd badge /tdtd (r.student || —) /td/tr; }); $(#occupancy-table tbody).html(html); } }); }表格只做展示调宿和退宿操作通过行内按钮触发单独的接口。管理员页面不需要花哨的交互信息密度和可读性最重要。Bootstrap4 的 badge 组件用来区分状态比纯文字更直观。5. 避坑与常见问题从重复预订到 token 过期5.1 床位超卖并发请求下的重复分配现象是同一个床位被两个学生同时预订成功数据库里出现两条 active 记录。原因是没有加行锁两个请求同时读到status available。解决方法是像 3.2 节那样在事务里用LOCK.UPDATE锁定床位行第二个请求会阻塞到第一个事务提交再读到的就是occupied。如果用的是 MySQL 默认的 REPEATABLE READ 隔离级别不加锁的普通 SELECT 读的是快照更容易出问题。5.2 token 过期后页面无响应现象是学生填完表单点提交页面卡住没反应控制台报 401。原因是 token 过期后接口返回 401但前端没有统一处理。解决方法是在$.ajaxSetup里加全局的 error 回调遇到 401 就清除本地 token 并跳转登录页。不要在每个请求里单独写容易漏。$.ajaxSetup({ error: function (xhr) { if (xhr.status 401) { localStorage.removeItem(token); window.location.href /login.html; } } });5.3 Sequelize 关联查询字段名对不上现象是管理员页面学生姓名列全是 undefined。原因是include里的模型别名和实际返回的对象键不一致。Sequelize 默认用模型名作为键如果定义了as别名就要用别名取。排查方法是先console.log(JSON.stringify(result, null, 2))看实际结构再决定用a.User还是a.user。这个坑几乎每个用 Sequelize 的人都会踩一次。5.4 密码明文存储现象是数据库里能看到用户密码原文。原因是注册接口直接存了req.body.password。解决方法是用 bcrypt 做哈希注册时bcrypt.hash(password, 10)登录时bcrypt.compare(password, hash)。这个不是可选项是必须做的。答辩时如果被问到安全性密码哈希是基本要求。5.5 前端直接暴露管理员接口现象是学生用浏览器控制台直接调/api/admin/occupancy也能拿到数据。原因是管理员接口只挂了authenticate没挂requireAdmin。解决方法是在所有/api/admin/*路由上统一加requireAdmin中间件或者在路由注册时用router.use(authenticate, requireAdmin)批量挂载。鉴权要放在后端做前端隐藏按钮只是障眼法。6. 验收与答辩怎么证明这套系统真的能用验收不是把页面点一遍就完事。我一般会按下面这张清单走一遍每项都要有明确的通过标准。验收项操作步骤通过标准注册登录新用户注册后登录token 正常签发跳转对应角色首页床位展示学生端查看楼栋床位可用/已占用状态正确颜色区分明显预订锁定两个浏览器同时预订同一床位只有一个成功另一个提示已占用重复预订同一学生再次预订提示已有预订不允许占多个床位管理员总览管理员查看入住表楼栋、房间、床位、学生姓名完整对应调宿退宿学生申请退宿后管理员查看床位状态变回 available历史记录保留权限隔离学生 token 调管理员接口返回 403不泄露数据密码安全查看数据库 users 表password_hash 为 bcrypt 哈希值答辩时老师最常问的三个问题床位并发怎么处理的、token 怎么防伪造、数据库表之间什么关系。前两个在 3.1 和 3.2 里有现成答案第三个把 2.3 的建表语句讲清楚就行。如果老师问“为什么不用 Vue”就按 2.2 的取舍逻辑回答交互复杂度低jQuery 够用省下的时间花在业务约束上。还有一个小技巧答辩前把数据库清空现场从注册开始演示一遍完整流程。比对着已有数据念页面更有说服力也能证明系统不是“只能看不能跑”的花架子。从那以后我每次做完这类管理系统都会在交付前强制走一遍并发测试和权限越权测试这两项过了基本就不会在答辩现场翻车。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询