
简介Hostel-Allocation 是一套面向高校学生与宿舍管理员的 Web 宿舍预订系统源码旨在用线上选房替代传统排队登记减少人员聚集与接触。项目采用 Node.js 与 Express.js 搭建后端MySQL 配合 Sequelize 完成数据建模与增删改查前端由 HTML5、CSS3、Bootstrap4 与 jQuery 构建响应式界面并通过 JWT 实现登录鉴权学生可远程查看并预订房间管理员可掌握各房间入住情况。压缩包共 22 个文件约 34KB包含 11 个 js 脚本路由、鉴权、数据库连接与页面逻辑、4 个 html 页面、2 个 json 配置、1 个 css 样式及证书密钥等结构紧凑、模块划分清晰。目前已有 114 人学习下载适合具备一定 JavaScript 与数据库基础、希望实践全栈开发或课程设计的读者参考可从中了解前后端分离思路、ORM 建模方式与令牌鉴权的落地写法。1. 宿舍分配系统从排队抢房到远程选房一个全栈项目能跑出什么每到开学季宿舍分配这件事总能让后勤老师和学生一起头疼。学生排长队等房、管理员拿着纸质表格翻来翻去确认谁住哪间中间还容易出错。Hostel-Allocation 这个项目就是冲着这个场景来的学生端可以远程查看房间状态并提交预订管理员端能实时看到每个房间住了谁、还剩多少床位。整套系统基于 Node.js Express MySQL 构建前端用 jQuery 配合 Bootstrap4 做交互认证走 JWT数据层用 Sequelize 管理。它适合想练手全栈 CRUD 权限控制的开发者也适合学校或培训机构做课程设计参考。下面从技术选型、目录结构、核心代码到二次开发一步步拆开讲。2. 技术选型与目录结构为什么是 Express Sequelize JWT 这套组合2.1 后端选型Express 轻量但够用Sequelize 把 SQL 关进笼子这个项目没有上 NestJS 或 Koa而是选了 Express。原因很直接宿舍分配的业务逻辑不复杂核心就是用户认证、房间查询、预订写入、管理员查看这几件事。Express 的中间件模型足够覆盖而且上手快新手看一遍app.js就能明白请求怎么流转。数据库层用 Sequelize 而不是裸写 SQL主要是为了两件事一是模型定义清晰Student、Room、Allocation三张表的关系用hasMany/belongsTo就能表达二是迁移和种子数据方便换数据库时不用重写查询。常见做法是把数据库配置抽到config/config.js通过环境变量区分开发和生产。认证用 JWT 而不是 Session是因为前后端分离后服务端不需要维护会话状态。登录成功后签发 token前端存 localStorage后续请求带Authorization: Bearer token服务端中间件校验后把用户信息挂到req.user上。这套流程在 Express 里用jsonwebtoken加一个自定义中间件就能实现。注意JWT 的 secret 不要硬编码在代码里放到.env文件并用dotenv加载否则提交到仓库等于把钥匙挂在门上。2.2 前端选型jQuery Bootstrap4 不是过时是够用前端没有用 React 或 Vue而是 jQuery Bootstrap4。这不是技术倒退而是项目定位决定的页面数量少登录、注册、房间列表、预订页、管理员面板交互以表单提交和表格渲染为主jQuery 的$.ajax配合 Bootstrap 的栅格和组件开发速度反而更快。HTML5 和 CSS3 负责页面结构和样式Bootstrap4 提供响应式布局手机端也能正常操作。如果你要二次开发建议保留这套前端结构但把 AJAX 请求封装成一个统一的api.js在里面处理 token 注入和 401 跳转避免每个页面都写一遍headers。2.3 目录结构按职责分层不按文件类型堆项目目录大致如下hostel-allocation/ ├── config/ │ └── config.js # 数据库连接配置 ├── models/ │ ├── index.js # Sequelize 初始化与模型关联 │ ├── student.js # 学生模型 │ ├── room.js # 房间模型 │ └── allocation.js # 分配记录模型 ├── routes/ │ ├── auth.js # 登录注册路由 │ ├── rooms.js # 房间查询与预订路由 │ └── admin.js # 管理员路由 ├── middleware/ │ └── auth.js # JWT 校验中间件 ├── public/ │ ├── css/ │ ├── js/ │ └── pages/ # 静态 HTML 页面 ├── seeders/ │ └── seed.js # 初始房间数据 ├── .env ├── app.js # 入口文件 └── package.json这种分层的好处是路由只负责接收请求和返回响应业务逻辑尽量下沉到模型或独立的 service 层。新手容易犯的错是把所有逻辑塞进app.js改一个功能要翻几百行。这个项目虽然不大但目录结构已经给出了正确的组织方式。2.4 环境准备与启动步骤跑起来之前确认本机有 Node.js建议 14 以上和 MySQL5.7 或 8.0。步骤如下# 1. 克隆项目后进入目录 cd hostel-allocation # 2. 安装依赖 npm install # 3. 创建数据库在 MySQL 中执行 # CREATE DATABASE hostel_db CHARACTER SET utf8mb4; # 4. 配置 .env 文件 # DB_HOSTlocalhost # DB_USERroot # DB_PASS你的密码 # DB_NAMEhostel_db # JWT_SECRET自定义密钥 # 5. 运行种子数据插入初始房间 node seeders/seed.js # 6. 启动服务 npm start启动后访问http://localhost:3000默认管理员账号通常在 seed 脚本里写死比如admin / admin123第一次登录后记得改密码。如果端口被占用在app.js里改PORT变量即可。3. 核心模块拆解认证、房间查询与预订的完整链路3.1 JWT 认证中间件一个请求怎么被放行认证是整个系统的入口。没有有效 token后续所有房间和预订接口都不应该返回数据。中间件代码如下// middleware/auth.js const jwt require(jsonwebtoken); module.exports function (req, res, next) { // 从请求头中提取 token const authHeader req.headers[authorization]; const token authHeader authHeader.split( )[1]; // Bearer token if (!token) { return res.status(401).json({ message: 未提供认证令牌 }); } try { // 验证 token 并解码把用户信息挂到 req.user const decoded jwt.verify(token, process.env.JWT_SECRET); req.user decoded; // { id, role, name } next(); } catch (err) { return res.status(403).json({ message: 令牌无效或已过期 }); } };逻辑说明authHeader.split( )[1]取的是Bearer后面的部分。jwt.verify会同时校验签名和过期时间失败时抛异常被 catch 捕获后返回 403。req.user里存了id、role和name后续路由可以直接用req.user.role判断是不是管理员。参数说明JWT_SECRET必须和签发时用的一致否则所有 token 都会验证失败。签发时通常设置expiresIn: 2h过期后前端需要重新登录。3.2 房间查询与预订事务保证不会超卖学生预订房间时最怕的是两个人同时点“预订”结果房间被分配了两次。这个项目用 Sequelize 的事务来解决// routes/rooms.js const express require(express); const router express.Router(); const { Room, Allocation, sequelize } require(../models); const auth require(../middleware/auth); // 查询所有房间及剩余床位 router.get(/, auth, async (req, res) { const rooms await Room.findAll({ attributes: [id, roomNumber, capacity, occupied] }); res.json(rooms); }); // 预订房间 router.post(/book, auth, async (req, res) { const { roomId } req.body; const studentId req.user.id; const t await sequelize.transaction(); try { // 锁定该行防止并发超卖 const room await Room.findByPk(roomId, { lock: t.LOCK.UPDATE, transaction: t }); if (!room || room.occupied room.capacity) { await t.rollback(); return res.status(400).json({ message: 房间已满 }); } // 检查该学生是否已有分配记录 const existing await Allocation.findOne({ where: { studentId }, transaction: t }); if (existing) { await t.rollback(); return res.status(400).json({ message: 你已预订过房间 }); } // 写入分配记录并更新已住人数 await Allocation.create({ studentId, roomId }, { transaction: t }); await room.increment(occupied, { by: 1, transaction: t }); await t.commit(); res.json({ message: 预订成功 }); } catch (err) { await t.rollback(); res.status(500).json({ message: 预订失败请重试 }); } }); module.exports router;逻辑说明lock: t.LOCK.UPDATE会对该房间行加排他锁另一个并发请求会等待锁释放后再读取此时occupied已经更新就能正确判断是否已满。Allocation.findOne防止同一学生重复预订。room.increment是原子操作比先读再写更安全。参数说明roomId从请求体传入studentId从 JWT 解码后的req.user.id获取不信任前端传的学生 ID。事务的rollback在任何一个失败分支都要调用否则连接不会释放。3.3 管理员查看分配情况联表查询与角色校验管理员需要看到“谁住在哪个房间”这需要联表查询。路由如下// routes/admin.js const express require(express); const router express.Router(); const { Allocation, Student, Room } require(../models); const auth require(../middleware/auth); // 管理员权限校验 function isAdmin(req, res, next) { if (req.user.role ! admin) { return res.status(403).json({ message: 无权限访问 }); } next(); } // 获取所有分配记录包含学生姓名和房间号 router.get(/allocations, auth, isAdmin, async (req, res) { const list await Allocation.findAll({ include: [ { model: Student, attributes: [name, studentId] }, { model: Room, attributes: [roomNumber] } ], order: [[createdAt, DESC]] }); res.json(list); }); module.exports router;逻辑说明isAdmin中间件在auth之后执行确保req.user已经存在。include让 Sequelize 自动生成 JOIN 查询返回结果里嵌套了Student和Room对象。前端拿到后直接渲染表格即可。参数说明attributes限制返回字段避免把密码等敏感信息带出去。order按创建时间倒序最新的分配记录排在最前面。3.4 前端页面与 AJAX 调用token 怎么带、401 怎么处理前端页面用 jQuery 发起请求关键是把 token 放进请求头// public/js/api.js function request(url, method, data) { return $.ajax({ url: url, method: method || GET, data: JSON.stringify(data), contentType: application/json, headers: { Authorization: Bearer localStorage.getItem(token) } }).fail(function (xhr) { if (xhr.status 401 || xhr.status 403) { // token 失效跳回登录页 localStorage.removeItem(token); window.location.href /pages/login.html; } }); }逻辑说明所有请求走request函数统一注入 token。401 和 403 都视为认证失败清除本地 token 并跳转登录页。这样避免每个页面单独处理。参数说明localStorage.getItem(token)在登录成功后由登录页写入。如果后端返回的 token 字段名不同这里要对应修改。4. 避坑与排查跑这个项目时最容易翻车的几个地方4.1 数据库连接报错ER_ACCESS_DENIED_ERROR现象启动时控制台报Access denied for user rootlocalhost。原因通常是.env里的DB_PASS为空或错误或者 MySQL 8.0 的认证插件与mysql2不兼容。解决确认密码正确如果用的是 MySQL 8.0在连接配置里加dialectOptions: { authPlugins: { mysql_clear_password: () () Buffer.from(process.env.DB_PASS \0) } }或者把用户认证方式改为mysql_native_password。4.2 JWT 签发后请求仍然 401现象登录成功拿到 token但请求房间列表时返回 401。原因通常是前端发送的 header 格式不对比如写成了Authorization: token而不是Bearer token。解决检查api.js里的拼接确保Bearer token中间有空格。另一个可能是.env里的JWT_SECRET在签发和验证时不一致重启服务后重新登录即可。4.3 预订时提示“房间已满”但实际还有床位现象房间容量是 4已住 2 人但预订时提示已满。原因通常是occupied字段没有正确初始化或者 seed 数据里occupied默认值不是 0。解决检查Room模型定义确保occupied的defaultValue: 0运行 seed 脚本前先清空表避免旧数据干扰。4.4 管理员登录后看不到分配记录现象管理员账号登录成功但/admin/allocations返回空数组或 403。原因可能是 JWT 里没有包含role字段或者role值不是admin。解决检查登录路由签发 token 时的 payload确保包含role: user.role同时确认数据库里管理员账号的role字段确实是admin。4.5 前端页面样式错乱或按钮无响应现象页面加载后 Bootstrap 样式没生效或者点击按钮没反应。原因通常是静态资源路径不对或者 jQuery 在 Bootstrap 之前加载。解决检查public/pages/下的 HTML 引用路径确保../css/bootstrap.min.css和../js/jquery.min.js能正确加载jQuery 必须在 Bootstrap 的 JS 之前引入。5. 二次开发与改造从能跑到好用几个具体方向这个项目跑通之后最值得改的地方是把“预订”变成“选房”。当前逻辑是学生提交房间 ID但前端没有直观的楼层和房间布局。我一般会加一个GET /rooms/available接口返回按楼层分组的可用房间列表前端用 Bootstrap 的卡片或表格渲染每个房间显示剩余床位点击直接预订。这样学生不用记房间号体验会好很多。第二个方向是加候补队列。当房间满时不是直接返回“已满”而是把学生加入Waitlist表有人退房时按时间顺序自动分配。这需要加一张表和一段定时任务或触发器逻辑。Sequelize 的afterUpdate钩子可以监听Room的occupied变化但更稳妥的做法是在退房接口里显式检查候补队列。第三个方向是导出报表。管理员面板加一个“导出 CSV”按钮后端用json2csv把分配记录转成 CSV 返回前端用Blob下载。这个改动很小但后勤老师会非常需要。验证改造是否成功我习惯用 Postman 先跑一遍接口登录拿 token → 查房间 → 预订 → 管理员查分配 → 退房 → 再查候补。每一步都确认返回码和数据结构符合预期再动前端。这样出问题时能快速定位是后端逻辑还是前端渲染。提示改数据库结构时不要直接改模型然后重启先用sequelize.sync({ alter: true })在开发环境试一次确认 SQL 没问题再上生产。生产环境建议用 migration 文件别偷懒。从那以后我每次改这类项目都强制先写接口测试再动 UI不然前端调半天发现是后端字段名拼错了那种血泪经验一次就够了。希望帮到你。本文还有配套的精品资源点击获取