ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+微信小程序高校教室预约管理系统与并发控制

SpringBoot+微信小程序高校教室预约管理系统与并发控制 简介面向高校计算机相关专业毕业设计与课程设计场景的教室预约管理系统论文文档依托 Spring Boot 框架、Java 语言、MySQL 数据库与微信小程序技术栈围绕教室资源紧张、人工排课与预约冲突等实际问题展开从选题背景、研究现状、需求与可行性分析到系统设计、后端实现与测试的完整论述。文档结构包含绪论、开发工具及关键技术介绍、系统分析等章节并附中英文摘要与关键词可对照查阅微信开发者工具、小程序目录结构、数据库表结构与索引设计等具体环节的实现思路同时讨论系统平台后期的维护、升级与安全等可操作性内容。压缩包内共 1 个 doc 文件约 4.65MB为论文全文可直接用于格式参考、章节框架借鉴与答辩材料整理。目前已有 132 人学习下载适合需要完成同类选题、梳理技术实现脉络或参考论文写作结构的学生与开发者。1. 从纸质登记本到小程序这套系统真正要解决的那一个矛盾开学第一周教务处的桌子前总会排起队有人在群里问 A 楼 301 下午三四节还空不空有人拿着纸质登记本挨个教室贴条还有人明明看到系统显示「空闲」走过去发现门锁着——登记本和实际占用是两套账。高校教室预约管理系统要处理的矛盾就这一个把分散在各学院、各社团、各教务口径的占用信息收敛成一份实时可查的账。用 SpringBoot 做后端、微信小程序做前端是这几年毕设和校内在用系统里最常见的组合学生不用装 App扫码或搜小程序就能查教室、选节次、提交申请教务端在后台审核、导出统计、锁定考试周。它适合两类人想把毕业设计做成真能跑起来的系统的同学以及校内信息化岗想替换纸质登记的人。下面按数据建模、SpringBoot 接口、小程序端、并发与超时释放四段推进每个环节都给能直接抄的建表语句和代码。2. 高校教室预约管理系统的表结构从时段粒度到并发占位先把库设计对后面写接口和写论文都会轻松很多。教室预约最怕的不是功能少而是「同一间教室同一节课出现两条有效记录」。这个问题在数据层解决比在业务代码里写一堆 if 判断可靠得多。2.1 四张核心表怎么切分常见的做法是拆成四张classroom存教室档案reservation存预约单主体room_slot存「某教室某天第几节」的占位记录sys_user存用户与角色。拆出room_slot是这套设计的关键很多人图省事只在reservation里存起止节次结果冲突判定只能写成区间重叠查询索引用不上并发下还挡不住重复插入。reservation记录的是「谁、为什么、什么状态」属于业务单据room_slot记录的是「资源被占了」属于资源账本。两者一对多一张预约单占 3 节就落 3 行 slot。账本一旦独立唯一索引才能建得干净利落。表名作用关键字段量级预估classroom教室档案room_no、building、capacity、status百级reservation预约单主体user_id、room_id、reserve_date、status万级/学期room_slot节次占位账本room_id、reserve_date、slot十万级/学期sys_user用户与角色openid、role、violate_count万级2.2 时段粒度按「节」还是按小时直接决定冲突判定粒度选不对后面全是补丁。高校作息表本身就是按节走的上午 1-2 节、3-4 节下午 5-6 节、7-8 节晚上 9-11 节。按小时建模会出现「预约 8:00-9:40 到底是 1-2 节还是 1-3 节」的扯皮教务排课也对不上。所以slot直接取 1 到 12 的整数一节课一个值。好处有三个唯一索引可以建在(room_id, reserve_date, slot)上小程序端用单选框让用户点节次交互和作息表一致统计「哪间教室利用率高」时直接GROUP BY room_id, slot就行。教室一天最多 12 条 slot一学期按 120 天算500 间教室就是 72 万行MySQL 单表完全扛得住不需要分表。2.3 用唯一索引把并发抢座变成一次插入失败这是整套系统里最值得写进论文的一段。两个学生同一秒点下「提交」如果业务代码里是「先查有没有冲突没有就插入」两步之间必然有间隙两人都能查到「无冲突」。正确做法是让数据库来判room_slot上建唯一索引插入冲突时数据库直接抛DuplicateKeyException事务回滚。这样并发问题从「应用层判断」变成了「存储层保证」无论多少个请求同时进来同一间教室同一节只会成功一次。代价是异常控制的写法要规范捕获后必须重新抛出一个运行时异常不能吞掉否则事务不会回滚会出现主表有单子、账本没占位的脏数据。这一点在Transactional方法里特别容易踩。2.4 建表语句与索引-- 教室档案 CREATE TABLE classroom ( id BIGINT NOT NULL AUTO_INCREMENT, building VARCHAR(32) NOT NULL COMMENT 教学楼如 逸夫楼, room_no VARCHAR(16) NOT NULL COMMENT 教室编号如 A301, capacity INT NOT NULL DEFAULT 0 COMMENT 座位数, has_projector TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否有投影, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可用 0停用, PRIMARY KEY (id), UNIQUE KEY uk_room (building, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约单主体 CREATE TABLE reservation ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, room_id BIGINT NOT NULL, reserve_date DATE NOT NULL, slot_start TINYINT NOT NULL COMMENT 起始节次 1-12, slot_end TINYINT NOT NULL COMMENT 结束节次 1-12, purpose VARCHAR(128) NOT NULL COMMENT 用途如 社团例会, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审 1通过 2驳回 3取消 4已签到 5爽约, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_date (user_id, reserve_date), KEY idx_status_date (status, reserve_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 节次占位账本并发控制的核心 CREATE TABLE room_slot ( id BIGINT NOT NULL AUTO_INCREMENT, room_id BIGINT NOT NULL, reserve_date DATE NOT NULL, slot TINYINT NOT NULL COMMENT 第几节 1-12, reservation_id BIGINT NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_room_date_slot (room_id, reserve_date, slot), KEY idx_reservation (reservation_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_room_date_slot是整套系统的安全阀不要省。idx_reservation用于取消预约时按单据回删占位。idx_status_date用于教务端按状态筛选和定时任务扫描。reservation.status用 TINYINT 而不是字符串是为了后面写状态机方便允许的流转是 0→1、0→2、1→3、1→4、1→5其余一律拒绝。取消时要记得把对应的room_slot行删掉否则时段被永久锁死——这是线上最容易出的「幽灵占用」问题。3. SpringBoot 侧预约接口的事务、校验与状态流转后端只干三件事鉴权、事务里的占位、状态流转。想做毕设加分项可以把教室空闲查询挂 Redis但下单路径一定要走数据库。3.1 依赖选型版本、ORM 和 starter 怎么配用idea创建 SpringBoot 项目时最容易出问题的是版本。SpringBoot 3.x 要求 JDK 17包名从javax.*换成jakarta.*很多老教程里的 JPA 注解会直接编译不过学校机房里装的还是 JDK 8 的话老老实实用 2.7.x 更省事spring-boot-starter-web、mybatis-plus-boot-starter、spring-boot-starter-data-redis、spring-boot-starter-validation四个就够。ORM 推荐 MyBatis-Plusinsert抛出的DuplicateKeyException语义清晰比 JPA 的DataIntegrityViolationException好识别。!-- pom.xml 关键依赖JDK8 场景 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesSpringBoot 的自动装配会扫描application.yml里的spring.datasource配置并注入DataSourceMyBatis-Plus 的 starter 再把它包装成SqlSessionFactory所以只要有配置类和 mapper 接口不用手写 XML。想确认装配是否生效启动时加--debug看自动配置报告里MybatisPlusAutoConfiguration是否为 positive matches。3.2 接口清单与统一返回结构方法路径说明需要的角色POST/api/auth/login小程序 code 换 token匿名GET/api/classroom/free按日期节次查空闲教室学生POST/api/reservation提交预约学生GET/api/reservation/mine我的预约列表学生PUT/api/reservation/{id}/audit审核通过/驳回教务PUT/api/reservation/{id}/cancel取消并释放节次学生本人统一返回{code, msg, data}code0 表示成功。小程序端只判断 code不要把 HTTP 状态码当业务码用否则 401 和业务失败混在一起非常难排查。3.3 预约接口的完整实现Service public class ReservationService { Resource private ReservationMapper reservationMapper; Resource private RoomSlotMapper roomSlotMapper; Resource private UserMapper userMapper; /** * 提交预约主表落单 账本占位靠唯一索引兜住并发 */ Transactional(rollbackFor Exception.class) public Long reserve(ReserveCmd cmd, Long userId) { if (cmd.getSlotEnd() cmd.getSlotStart()) { throw new BizException(节次区间不合法); } // 爽约次数超限的账号禁止再约教务可手动解除 if (userMapper.selectById(userId).getViolateCount() 3) { throw new BizException(爽约次数过多请到教务处处理); } Reservation r new Reservation(); r.setUserId(userId); r.setRoomId(cmd.getRoomId()); r.setReserveDate(cmd.getReserveDate()); r.setSlotStart(cmd.getSlotStart()); r.setSlotEnd(cmd.getSlotEnd()); r.setPurpose(cmd.getPurpose()); r.setStatus(0); // 待审核 reservationMapper.insert(r); // 回填自增 id try { for (int slot cmd.getSlotStart(); slot cmd.getSlotEnd(); slot) { RoomSlot s new RoomSlot(); s.setRoomId(cmd.getRoomId()); s.setReserveDate(cmd.getReserveDate()); s.setSlot(slot); s.setReservationId(r.getId()); roomSlotMapper.insert(s); // 撞唯一索引即失败 } } catch (DuplicateKeyException e) { // 必须重抛运行时异常否则事务不回滚会留下脏单 throw new BizException(该时段已被预约请换一个时段); } return r.getId(); } }逻辑上有两处值得说明。第一先插主表再插账本顺序不能反账本需要reservation_id主表自增 id 是回填的。第二catch里抛出的是自定义BizException它继承RuntimeException配合rollbackFor Exception.class能保证整笔操作原子回滚。如果这里写成打印日志然后return null主表就留下一条永远不会被占位的幽灵单子。参数侧用Validated加注解约束比在方法里手写if干净NotNull管空值Min(1) Max(12)管节次范围FutureOrPresent管日期不能选过去。3.4 参数校验、状态机与失败排查状态流转单独抽一个方法别散落在各个接口里。private static final MapInteger, SetInteger FLOW Map.of( 0, Set.of(1, 2), // 待审 - 通过 / 驳回 1, Set.of(3, 4, 5), // 通过 - 取消 / 签到 / 爽约 2, Set.of(), 3, Set.of(), 4, Set.of(), 5, Set.of() ); public void transfer(Reservation r, int target) { if (!FLOW.getOrDefault(r.getStatus(), Set.of()).contains(target)) { throw new BizException(状态不允许从 r.getStatus() 变为 target); } r.setStatus(target); reservationMapper.updateById(r); if (target 2 || target 3) { roomSlotMapper.delete(new LambdaQueryWrapperRoomSlot() .eq(RoomSlot::getReservationId, r.getId())); // 驳回或取消必须释放节次 } }失败排查按这个顺序看报「时段已被预约」但数据库里查不到冲突行检查是否用了readOnly true的事务导致回滚失效报主键冲突但代码里没写 id检查room_slot是不是建了id自增列表查出来有重复时段检查取消逻辑有没有删room_slot。这三条覆盖了九成以上的现场问题。4. 微信小程序端登录换 token、节次单选与提交联调前端这层最能体现「体验差一点、投诉多一倍」。学生只用三个页面教室列表、预约表单、我的预约。做薄做稳比做花哨重要。4.1 登录wx.login 拿 code后端换 openid 和 token小程序的登录不能在前端拿 openid必须把wx.login返回的临时 code 发给后端由后端携带 AppID 和 AppSecret 去换取 openid 和 session_key再签发自己的 token 返回给小程序。token 存wx.setStorageSync后续请求放Authorization头。// utils/auth.js export async function login() { // 1. 前端只拿 code不接触 AppSecret const { code } await wx.login(); // 2. code 换 tokenbaseUrl 走 https且已配置到 request 合法域名 const res await wx.request({ url: ${getApp().globalData.baseUrl}/api/auth/login, method: POST, data: { code } }); if (res.data.code ! 0) throw new Error(res.data.msg); wx.setStorageSync(token, res.data.data.token); return res.data.data; }wx.login的 code 只能用一次五分钟过期接口返回 401 时不要直接跳登录页先静默重登一次再重试原请求否则弱网下会出现「点一下闪回首页」的体验问题。基础库 2.10.2 之后wx.request、wx.login支持 Promise 化写法低于这个版本要用success/fail回调这是联调期最常见的兼容坑之一。4.2 页面结构、自定义导航栏高度与节次单选框app.json里注册三个页面pages数组第一项即首页。用自定义导航栏时高度必须动态算写死 44px 在刘海屏和安卓上会错位状态栏高度用wx.getWindowInfo().statusBarHeight取胶囊位置用wx.getMenuButtonBoundingClientRect()取导航栏高度等于胶囊底部到状态栏顶部的距离乘以 2 再减去状态栏高度。这套算法在所有机型上都稳。节次选择直接用radio-group一次只选一个起始节次和结束节次比滑块控件容易点准也方便后端校验。!-- pages/reserve/reserve.wxml 片段 -- view classnav styleheight: {{navHeight}}px; padding-top: {{statusBarHeight}}px; {{room.building}}{{room.roomNo}} /view radio-group bindchangeonSlotChange>// pages/reserve/reserve.js 提交逻辑 async submit() { if (this.data.submitting) return; // 前端防连点 if (!this.data.slotStart || !this.data.slotEnd) { return wx.showToast({ title: 请选择节次, icon: none }); } this.setData({ submitting: true }); try { const res await wx.request({ url: ${getApp().globalData.baseUrl}/api/reservation, method: POST, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, data: { roomId: this.roomId, reserveDate: this.data.date, // 必须是 yyyy-MM-dd 字符串 slotStart: this.data.slotStart, slotEnd: this.data.slotEnd, purpose: this.data.purpose } }); if (res.data.code 0) { wx.showToast({ title: 已提交等待审核 }); setTimeout(() wx.navigateBack(), 800); } else { wx.showToast({ title: res.data.msg, icon: none }); } } finally { this.setData({ submitting: false }); } }日期不要用new Date(2025-03-01 08:00)这种写法在 iOS 上解析不同机型结果不一致日期选择器直接返回yyyy-MM-dd字符串透传给后端最省事。submitting标志位只是防手抖真正的一致性保证仍在后端的唯一索引上前端拦截永远不能当作并发方案。4.4 联调阶段最常见的 5 个报错现象原因处理request 域名不合法未在后台配置合法域名或用了 IP开发阶段勾「不校验合法域名」上线前配 https 域名基础库版本过低Promise 化 API 不可用后台把最低基础库调到 2.10.2 以上页面白屏、无报错分包或lazyCodeLoading配置不当先关掉lazyCodeLoading排查再按需开启上传后首次进页面空白未在app.json注册该页面路径补齐pages数组并重新上传提交后一直转圈后端事务未提交或连接池耗尽看后端慢 SQL 日志与连接池活跃数上线前用hbuilderx或开发者工具的「上传」走一遍体验版流程重点验证真机上的自定义导航栏和日期选择模拟器正常不代表真机正常。5. 进阶把空闲查询压到 Redis再让超时占位自己释放系统跑起来之后压力往往不在提交而在「查空闲教室」这个动作上——一学期开学前两天学生反复刷新列表每次都是全表扫room_slot。把这块的结果缓存住收益立竿见影。5.1 Redis 缓存教室空闲位key 按「日期节次」切分value 存空闲教室 id 列表redis在springboot中的使用最常见的就是这种读多写少的场景。public ListLong listFreeRooms(LocalDate date, int slot) { String key free: date : slot; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { return JSON.parseArray(cached, Long.class); } ListLong ids roomSlotMapper.selectFreeRoomIds(date, slot); // 缓存 60 秒足够挡住刷新又不会让新预约长时间不可见 redisTemplate.opsForValue().set(key, JSON.toJSONString(ids), Duration.ofSeconds(60)); return ids; }关键是失效策略而不是缓存本身。任何一次预约成功、取消、驳回都要删掉受影响的 key或者干脆只依赖 60 秒的短过期——短过期实现简单代价是列表最多滞后一分钟对教室预约这个场景完全可以接受。用「先更新数据库再删缓存」的顺序别用「更新缓存」省得并发下写出旧值。5.2 定时任务释放超时未签到的占位预约通过后学生没来时段不能一直锁着。加一个每 10 分钟跑一次的定时任务把「状态为已通过、且开始时间已过 15 分钟仍未签到」的单子置为爽约并释放room_slot。Scheduled(cron 0 */10 * * * ?) public void releaseTimeout() { ListReservation list reservationMapper.selectTimeout( LocalDateTime.now().minusMinutes(15)); for (Reservation r : list) { reservationService.transfer(r, 5); // 5 爽约 userMapper.incrViolateCount(r.getUserId()); // 累计爽约三次进黑名单 } }transfer复用第 3 章的共用方法驳回、取消、爽约三条路径都走同一处释放逻辑避免出现「取消释放了、爽约没释放」的不一致。任务要加分布式锁或单实例部署约束多副本环境下不加锁会导致同一单子被处理两次爽约次数翻倍。5.3 用并发脚本验证「超卖」是否真的被挡住论文里写「解决了高并发问题」是空的给一组可复现的数据才有说服力。开两个线程池对同一间教室同一天同一节发 200 个请求统计成功数。# 前提token 已写入环境变量接口地址为本机 seq 1 200 | xargs -P 50 -I{} curl -s -X POST http://localhost:8080/api/reservation \ -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {roomId:1,reserveDate:2025-06-01,slotStart:3,slotEnd:4,purpose:压测} \ | grep -o code:[0-9]* | sort | uniq -c预期结果是code:0只有一条其余全部是时段冲突。如果出现两条成功去查room_slot的唯一索引是不是没建成功——用SHOW INDEX FROM room_slot确认uk_room_date_slot的Non_unique为 0。再补一个反向验证并发提交 200 个不同教室的请求成功率应该接近 100%说明失败来自资源冲突而不是锁竞争。把这两组数字连同AB或JMeter的聚合报告截图放进论文的测试章节比任何形容词都管用。最后提一个上线前必做的动作在room_slot上按reserve_date建分区或定期归档上学期数据一个学期十万行不痛不痒积累三年后空闲查询的扫描行数会翻十倍那时再改表就要停机了。本文还有配套的精品资源点击获取
返回列表