简介:这套基于Spring Boot框架的自习室管理与预约系统源码,适合计算机专业学生、初级Java开发者用于课程设计、毕业设计或熟悉典型Web项目开发流程。系统采用MVC分层架构,前台支持用户注册登录、自习室预约、座位与开放时段查看,后台提供自习室信息管理、用户管理和预约记录处理,业务覆盖较完整。资源包一共828个文件,压缩后约30.6MB,其中121个Java文件对应控制器与业务逻辑,63个Vue文件和47个HTML文件构建前后台界面,159个JS与52个CSS负责交互和样式,162个SVG和79个GIF用于图标与演示动效,另有SQL脚本与可执行bat脚本,方便初始化数据库并一键启动项目。目前已有52人学习下载,代码结构清晰、注释可读,项目目录按模块划分,不仅便于直接运行和二次开发,也能让读者从源码层面理解预约类系统的完整实现路径,适合作为实战参考。
1. 自习室预约系统,一个 Spring Boot 实战工程的三个价值点
很多同学拿到“自习室管理与预约系统”这类源码包,第一反应是“赶紧跑起来截几张图交作业”。我替人改过几十份课设、毕设代码,可以明确说一句:这类预约系统的真正含金量不在增删改查页面,而在“同一分钟两个人预约同一个座位时,谁成功、谁失败”这一条逻辑上。基于 Spring Boot 框架来自习室管理与预约系统源码,价值主要体现在三块:一是覆盖了 Java Web 课程设计里最常见的业务场景,房间、座位、时段、预约记录、违约记录一套数据模型完整;二是能直接用来当毕业设计底座,把预约对象换成会议室、实验室、琴房就是另一个题;三是并发预约的核心代码讲得清,面试被问“怎么防止重复预约”也有东西可说。这篇文章就按我实际交付课设的习惯,把需求、表设计、核心代码、参数调整和踩坑一次讲完。
2. 先拆需求和表:预约系统的数据边界与状态机
2.1 角色与功能:学生端、管理端各自管什么
自习室管理与预约系统的业务边界,比大多数课设题要清晰。常见的角色划分是学生和管理员,顶多加一个系统管理员用来初始化数据。学生端能做的事一般收敛成四件:浏览自习室和座位、提交预约、取消预约、查看自己的预约记录与违约情况。管理员端则负责三件:维护自习室与座位信息、查看或强制结束预约、统计座位使用率。
很多源码包默认没有“注册”功能,而是管理员统一导入账号。我不建议自己改成开放注册,课设答辩时老师问“怎么防止恶意注册占座”,你答不上来;直接说“账号由管理员在后台创建,确保一个人一个账号”,反而显得考虑过安全问题。同样,不需要在系统里实现支付、门禁联动、座位传感器监测——不做的功能越清晰,核心预约逻辑就能做得越扎实,这是课设作品和真实商用系统之间最关键的区别。
权限控制方面,课设源码里最常见的做法是拦截器按 session 里的 role 字段放行,管理员页面单独建目录。虽然不如 Spring Security 正规,但能满足需求,而且代码量少,适合快速读懂。如果你打算在答辩时把这条路讲好,可以先说明“这是基于角色的简单访问控制”,再点一句“生产环境会换 Spring Security 或 Sa-Token”,就已经超出多数同学的理解深度。
2.2 四张核心表:座位、预约、违约、账号的字段怎么定
预约系统的数据模型核心是“座位-预约”这对关系。我一般建议新建四个业务表加一个关联表,能少则少,字段名用下划线风格,保持和 MyBatis-Plus 驼峰映射一致。第一张是账号表 account,字段包括 id、username、password、role、student_no、phone、create_time;密码存 MD5 或 BCrypt 加密串,不要存明文,这是答辩时几乎必被问到的一点。
第二张是自习室表 room,字段为 id、name、floor、capacity、open_start、open_end、status。open_start 和 open_end 是自习室开放时间段,用来在预约校验时判断用户选的时间是否在开放范围内。第三张是座位表 seat,字段为 id、room_id、seat_no、is_valid,座位与自习室是多对一关系,is_valid 用来软删除损坏座位,不直接物理删除,避免历史预约记录悬空。
第四张是预约记录表 reservation,这是整张设计里最需要花心思的表。字段至少包括 id、user_id、seat_id、reserve_date、start_time、end_time、status、checkin_time、create_time。reserve_date 存预约的日期,start_time 和 end_time 存当天内的时间段,比如 2025-06-10 的 09:00 到 11:00。status 用字符串枚举更直观:WAITING、CHECKED_IN、CANCELLED、COMPLETED、EXPIRED。第五张违约记录表 violation 用来记录迟到、超时未离开等行为,字段为 id、user_id、reservation_id、reason、create_time。
索引设计上,reservation 表要加两个索引:一个是 (seat_id, reserve_date, status),用来加速“这个座位这天是否已被预约”的判断;另一个是 (user_id, reserve_date),用来在用户提交预约时快速统计当天已预约次数。不加索引的后果是数据量超过几千条后,列表页分页变慢,答辩现场容易被老师点出来。
2.3 预约状态机:从 WAITING 到 COMPLETED 的流转规则
预约系统的状态机,是整个工程里最值得反复讲的一块。一份预约记录从创建开始有五个状态,流转必须严格受控。学生提交预约成功,记录初始状态 WAITING,表示“已预约,尚未签到”。如果学生在预约开始前主动取消,状态变 CANCELLED;如果到了开始时间还迟迟不签到,定时任务把记录置为 EXPIRED 并写入一条违约记录。
学生到场后在系统点击“签到”,状态从 WAITING 变 CHECKED_IN;预约结束时间到达后,系统在用户主动确认或定时任务扫描时把状态改为 COMPLETED。如果签到后没有按时离开,超过结束时间一定时长,也会被记一次违约。这个状态机比很多课设里只用 0/1 表示“已预约/未预约”要规范得多,因为它把时间维度容纳了进来,后面所有统计查询都会变得容易。
用表来描述这套规则会更直观:
| 当前状态 | 触发动作 | 下一状态 | 附加动作 |
|---|---|---|---|
| WAITING | 用户取消 | CANCELLED | 无 |
| WAITING | 开始时间前签到 | CHECKED_IN | 记录 checkin_time |
| WAITING | 超时未签到 | EXPIRED | 写 violation |
| CHECKED_IN | 正常结束 | COMPLETED | 无 |
| CHECKED_IN | 超时未离开 | COMPLETED | 写 violation |
状态机在代码里的落地方式很简单:在 Service 层写一个 statesCanChange 方法或直接用 switch 判断,禁止在前端传状态值直接更新数据库。我见过不少源码包在 Controller 里接收 status 参数直接 update,这是最容易被攻击的地方,也最容易在答辩时被问到“如何防止用户把违约记录改成正常”。正确做法是后端只接收动作类型,例如 cancel、checkin、complete,状态流转由 Service 层内部决定。
3. 把源码跑起来:环境准备、建库脚本与三个核心模块
3.1 解压与项目结构:先看 pom.xml 再动代码
拿到源码包,先把 zip 解压到一个没有中文和空格的路径下。常见结构是 src/main/java 存放代码,包名一般类似 com.example.studyroom 或 com.xxx.reservation,里面分 controller、service、mapper、entity、config 几层;src/main/resources 下是 application.yml、mapper XML 文件、静态页面模板。不要急着启动,先打开 pom.xml 看三样东西:Spring Boot 父版本、持久层框架、数据库驱动。
大部分课程设计源码用的是 Spring Boot 2.x 加 MyBatis-Plus,因为这套组合写代码量最少,也最容易过查重整改。Spring Boot 2.x 对应的 Java 版本是 8 或 11,如果你本机装的是 Java 17 或 21,可能出现启动失败或依赖版本冲突。解决办法是不要盲目升级 Spring Boot 为 3.x,3.x 要求 Java 17 起步,且 javax 包名改成了 jakarta,老代码会大量报错。最稳妥的做法是保持源码自带的版本不变,为本项目单独配置 JDK 8 或 11。
在 IDE 里用 Maven 面板执行 clean 和 compile,先把编译错误清掉。这一步能提前暴露一半问题,比如 lombok 版本过低、缺少 MySQL 驱动、maven 仓库源无法访问。国内网络环境下建议在 pom.xml 或 maven 的 settings.xml 里配置阿里云镜像,否则下载依赖可能要等很久。看到 BUILD SUCCESS 之后,再进入数据库配置阶段。
3.2 建库建表与初始化数据:utf8mb4 和时区一起配好
建库脚本一般会放在 sql 目录下,没有的话自己建一份。我先给出一个最小可用的建库脚本注释版本,命名和字段与 2.2 节对应:
CREATE DATABASE IF NOT EXISTS study_room DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE study_room; CREATE TABLE room ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, floor VARCHAR(20), capacity INT NOT NULL DEFAULT 0, open_start TIME NOT NULL DEFAULT '08:00:00', open_end TIME NOT NULL DEFAULT '22:00:00', status TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE seat ( id BIGINT AUTO_INCREMENT PRIMARY KEY, room_id BIGINT NOT NULL, seat_no VARCHAR(10) NOT NULL, is_valid TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE reservation ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, seat_id BIGINT NOT NULL, reserve_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'WAITING', checkin_time DATETIME, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_seat_date (seat_id, reserve_date, status), KEY idx_user_date (user_id, reserve_date) );这段脚本里我特别强调两个细节:数据库字符集必须指定 utf8mb4,否则预约人姓名里出现生僻字或表情符号时会报 Incorrect string value 错误;reservation 表的 reserve_date 用 DATE 类型,start_time、end_time 用 TIME 类型,不要拼成 DATETIME,因为跨天对比时会变得更复杂。in 语句里对 seat_id 和 user_id 暂时不建外键,一是减少删除时的约束麻烦,二是用应用层保证数据一致性就够了,课设阶段完全没必要引入物理外键。
建完表后再看 application.yml,数据源配置是启动失败的第二大来源。参考配置如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/study_room?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: "你的密码" jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autourl 里必须带 serverTimezone=Asia/Shanghai 和 characterEncoding=utf8,这两个参数不写,后面会遇到“数据库时间比本地早 8 小时”和“中文乱码”两个经典问题。allowPublicKeyRetrieval=true 只在 MySQL 8 的某些驱动版本下需要,顺手写上可以少踩一个坑。端口建议保持 8080,如果本机已经被其他程序占用,改成 8081 或 8082,记得浏览器访问地址也要跟着变。
3.3 预约 Service:事务、冲突检测与状态更新的最小实现
预约核心方法在 service 包里,方法名一般是 createReservation 或 addReservation。很多源码包是 Controller 里写业务逻辑,这种写法能跑但没法讲清楚并发控制。我一般会建议把它改造成 Service 层方法,核心代码如下:
@Service @AllArgsConstructor public class ReservationServiceImpl implements ReservationService { private final ReservationMapper reservationMapper; private final SeatMapper seatMapper; @Transactional(rollbackFor = Exception.class) @Override public Long createReservation(Long userId, Long seatId, LocalDate reserveDate, LocalTime startTime, LocalTime endTime) { // 1. 参数合法性:时段不能为空,结束必须晚于开始 if (startTime == null || endTime == null || !startTime.isBefore(endTime)) { throw new BusinessException("预约时段不合法"); } // 2. 用悲观锁锁定该座位这一天的预约记录,防止同一时刻重复预约 List<Reservation> lockedList = reservationMapper.selectForUpdate( seatId, reserveDate); // 3. 判断是否存在时间重叠的预约,status 只考虑 WAITING 与 CHECKED_IN boolean hasConflict = lockedList.stream().anyMatch(r -> r.getStartTime().isBefore(endTime) && r.getEndTime().isAfter(startTime) && !"CANCELLED".equals(r.getStatus()) && !"EXPIRED".equals(r.getStatus())); if (hasConflict) { throw new BusinessException("该座位在此时间段已被预约"); } // 4. 校验当天预约次数,超过 3 次拒绝 Long todayCount = reservationMapper.countByUserIdAndDate( userId, reserveDate); if (todayCount >= 3) { throw new BusinessException("当天预约次数已达上限"); } // 5. 创建预约记录并返回 id Reservation r = new Reservation(); r.setUserId(userId); r.setSeatId(seatId); r.setReserveDate(reserveDate); r.setStartTime(startTime); r.setEndTime(endTime); r.setStatus("WAITING"); r.setCreateTime(LocalDateTime.now()); reservationMapper.insert(r); return r.getId(); } }selectForUpdate 是这段代码的关键,它对应一条SELECT * FROM reservation WHERE seat_id = ? AND reserve_date = ? FOR UPDATE。加了 FOR UPDATE 之后,同一时间只有第一个事务能读到这天的预约记录,其他事务在这个事务提交前会阻塞等待。这比“先查询再判断再插入”的方式安全得多,因为后者在并发时会产生检查间隙,两个请求同时通过判断,都执行插入,座位就重复预约了。
参数说明如下:startTime.isBefore(endTime) 用来保证结束时间必须晚于开始时间,而不是简单判断两者不相等,否则会出现开始 10:00、结束 09:00 这种当天内负时长数据;状态过滤只查 WAITING 和 CHECKED_IN,因为 CANCELLED 和 EXPIRED 的时段已经释放,不用参与冲突判断;countByUserIdAndDate 在 2.2 节设计的 user_id + reserve_date 索引上执行,数据量大时不会拖慢预约接口。BusinessException 建议使用 Spring 的 @RestControllerAdvice 统一捕获,返回到前端时提示信息比默认的 500 错误友好得多。
需要注意一点:@Transactional 事务里不要写耗时的网络请求或导出文件操作,锁定的行会在事务结束才释放。课程设计里没人给你做压测,但面试官可能会追问“FOR UPDATE 锁多久”,你能答出“事务提交才释放,所以事务代码要精简”就已经加分。
3.4 管理端统计:用一道 SQL 算出上座率和使用时长
管理端最常用的功能是看今日上座率和单个座位的累计使用时长。源码里如果只是做个列表,会显得项目单薄,建议补上这两个统计。上座率的统计口径是“当前有效预约的座位数除以开放座位总数”,看了标题里的自习室管理两个字,管理者最关心的就是这个数字。
-- 今日上座率:今日有生效预约的座位数 / 总开放座位数 SELECT COUNT(DISTINCT r.seat_id) / COUNT(DISTINCT s.id) AS occupy_rate FROM seat s LEFT JOIN reservation r ON r.seat_id = s.id AND r.reserve_date = CURDATE() AND r.status IN ('WAITING', 'CHECKED_IN') WHERE s.is_valid = 1;这段 SQL 用 LEFT JOIN 从 seat 表出发,保证没有被预约的座位也参与分母。COUNT(DISTINCT r.seat_id) 统计的是“有多少个座位在今天有生效预约”,不会因为有学生反复预约同一个座位导致计数虚高。如果自习室有多个房间,需要按房间看数据,就在分组里加 s.room_id,再用 GROUP BY s.room_id 输出。页面展示时用 ECharts 柱状图会好看很多,也有不少源码包直接配了图表插件,没有的话可以自己引入一个 CDN 版本。
4. 预约参数与时序控制:单次时长、并发上限和定时任务怎么设
4.1 可预约参数一览:哪些放配置文件,哪些进数据库
预约规则的参数如果不集中管理,后续改需求时你会发现自己在一个个 Java 文件里找魔法数字。我习惯把所有规则类参数分成两部分:需要频繁调整的放进数据库配置表,比如单次最大预约时长、每日最大预约次数、可提前预约的天数、违约封禁阈值;几乎不动的放进 application.yml,比如定时任务执行周期。放数据库的好处是管理员可以后台改,不用重启服务。
常见参数值如下:单次预约最短 1 小时,最长 4 小时;每天最多预约 3 条;只能预约未来 7 天内的时段;预约开始前 30 分钟允许签到,超过开始时间 30 分钟未签到自动取消;累计违约 3 次禁用预约权限 7 天。这套数值不是源码包里固定的,我见过很多系统的参数都比这个宽松,答辩时老师问“为什么设置 4 小时上限”,答“防止一个人长时段占座,提高座位周转率”会比“接口文档里写的”有说服力得多。
前端下拉框的时段粒度一般设 30 分钟或 1 小时。如果粒度是 30 分钟,后端在接收 startTime 和 endTime 时也要校验分钟数必须是 0 或 30,这套校验放在前端容易绕过,后端必须再写一次。有些源码包没做后端校验,提交 09:15 这种非约定时段也能成功,运营上会出现碎片时间,统计上座率时也不好解释。
4.2 高并发下的冲突控制:悲观锁还是乐观锁
核心冲突控制我上一章用的是悲观锁 FOR UPDATE,这是课程设计里最稳的方案。它的优点是写起来直接,一个 SQL 就锁住了并发入口;缺点是锁的粒度比较粗,同一座位同一天的预约记录都会串行化。如果自习室的座位是 200 个,数据库压力其实不大,完全够用。还有一种方案是乐观锁,在 reservation 表加 version 字段,插入前先尝试更新,用受影响行数判断是否冲突,适合“冲突概率低”的业务。对于自习室热门座位冲突率不低的情况,乐观锁反而会让代码复杂,因为你要在捕获 DuplicateKeyException 或影响行数为 0 时重新组织错误提示。
我自己在课设阶段更推荐悲观锁,还有一个原因是容易向答辩老师解释。你可以画一条时间线:请求 A 和请求 B 同时到达,A 先拿到行锁,B 阻塞;A 提交事务并插入预约记录,B 醒来读到的结果里已经包含 A 插入的数据,于是 B 的冲突判断生效,返回“该座位此时间段已被预约”。这个过程能讲清楚,说明你真的理解了数据库锁,而不是只会调用接口。
如果需要压测,可以先把 validation 关闭,再用两个终端同时 curl 提交同座位同时间段,观察最终只有一条 WAITING 记录。实际上 MySQL 的 InnoDB 默认隔离级别是 REPEATABLE READ,FOR UPDATE 锁定的记录在事务提交前对其他事务不可见,因此判断逻辑不会读到旧快照,这一点可以在答疑时作为补充。
4.3 定时清理过期预约:@Scheduled 的坑和替代方案
预约系统里一定会有一个定时任务:把过了签到时间还没有签到的 WAITING 记录变成 EXPIRED,并写违约记录。Spring Boot 里最简单的实现是在启动类或配置类加 @EnableScheduling,然后在某个 Service 方法上写 @Scheduled(cron = "0 0/5 * * * ?"),每 5 分钟跑一次。
写这个任务最需要注意的一点:不要对全表扫描,而是只查“当前时间减去 30 分钟小于等于开始时间,且状态为 WAITING”的记录。SQL 大致形式是SELECT id FROM reservation WHERE status='WAITING' AND reserve_date <= CURDATE() AND CONCAT(reserve_date, ' ', start_time) < DATE_SUB(NOW(), INTERVAL 30 MINUTE)。这里的 CONCAT 比较在数据量大时不会走索引,但课设阶段几百条记录无所谓,写出清晰的业务条件比优化索引更重要。
第二个注意点是定时任务方法必须加 try-catch,否则一次执行抛异常会导致整个任务中断,后续不再触发。日志输出也要看一下,用 Lombok 的 @Slf4j 打印每次清理了多少条,方便运维排查。如果你觉得 Spring 自带的 @Scheduled 在分布式环境下会重复执行,可以在配置里加一个 boolean 开关,默认 true,部署单机时没有影响;若将来扩展成多实例部署,再引入 ShedLock 或 XXL-JOB 替代。这个点可以作为加分项写在课设报告里,体现你考虑过扩展性。
5. 自习室预约系统避坑:5 个换了三个版本才确认的问题
5.1 预约时间永远差 8 小时:原因在 JDBC 时区不在代码
现象:本地开发环境,预约一个 09:00 的座位,数据库里reserve_date 正确但 create_time 比实际时间少了 8 小时;或者页面上显示的可选时段全部偏移,明明添加的开放时间是 08:00,页面显示却是 00:00。
原因:MySQL 驱动版本在 8.0 之后默认将服务器时区识别为 UTC,而你的操作系统是东八区。如果 application.yml 里没有写 serverTimezone=Asia/Shanghai,驱动会用服务器机器的默认时区处理 DATETIME,Java 侧再用本地时区读取,两边一加一减就出现了 8 小时差。
解决:数据源 URL 中显式加上 serverTimezone=Asia/Shanghai。如果已经出现历史数据偏移,不要直接在页面上改,先确认数据库连接串改对,然后把 create_time 用 UPDATE 语句倒推 8 小时修正一次。另外,在配置文件里把 jackson.time-zone 写成 GMT+8,否则返回给前端的 JSON 时间格式也可能带上 T 或 UTC 标记。
5.2 同一座位同一分钟被两次预约成功:边界比较漏了等号
现象:学员同时提交两个预约请求,一个是从 09:00 到 10:00,另一个是从 10:00 到 11:00,系统错误地提示时间冲突,或者反过来,两个完全重叠的时段都显示预约成功。更隐蔽的是请求从 09:00 到 10:00,另一个从 09:30 到 10:00,边界完全贴合时被放行。
原因:冲突判断条件写错。判断两段时间重叠的正确逻辑是newStart < oldEnd AND newEnd > oldStart,但很多源码会写newStart < oldEnd AND newEnd >= oldStart,或者把等号加到了 end 那边,导致刚好接棒的时段被当成冲突,而真正重叠的部分因为 start 边界恰好卡住被漏掉。
解决:把条件统一成 strict 比较。startTime.isBefore(existingEndTime) 与 endTime.isAfter(existingStartTime) 配对,两个都不含等号。用具体例子验证:已有 09:00-10:00,新预约 10:00-11:00,newStart 是 10:00,existingEnd 是 10:00,10:00.isBefore(10:00) 为 false,因此不冲突,正确。已有 09:00-10:00,新预约 09:30-10:30,newStart 是 09:30,existingEnd 是 10:00,09:30.isBefore(10:00) 为 true,同时 existingStart 是 09:00,newEnd 是 10:30,10:30.isAfter(09:00) 为 true,判定冲突,正确。
5.3 事务没回滚:同 Service 内方法自调用导致的失效
现象:预约方法里先插入预约记录,接着调用同类里的另一个方法写违约记录,违约记录写入抛异常,但预约记录还是保留下来了,没有随事务回滚。
原因:Spring 的 @Transactional 是基于 AOP 动态代理实现的,只有通过代理对象调用方法时事务注解才生效。同类里 this.saveViolation() 这种调用走的是原始对象,不是代理对象,事务注解被完全忽略。这个问题在写在同一个 Service 里时极其隐蔽,因为代码看起来完全正常。
解决:把写违约记录的方法拆到另一个 Service 类里,比如 ViolationService,然后在 ReservationService 里注入它来调用;或者在类内部注入自身代理 ApplicationContext.getBean(CurrentService.class)。我习惯用第一种,职责也更清晰。顺带提一句,在测试时验证事务回滚可以故意抛业务异常,再看数据库里有没有多出脏数据,这条验证路径建议在答辩前自己走一遍。
5.4 跨天时段放行:结束时间小于开始时间被当成当天
现象:预约时间校验的正确性在白天时段没问题,但当用户在 23:00 提交凌晨 01:00 到 02:00 的预约时,系统提示“结束时间必须晚于开始时间”,或者更糟的是直接校验通过,把凌晨时段当成当天时段存了,导致座位时间冲突计算错误。
原因:把日期和时间语义混在一起。reserve_date 是 DATE 类型,start_time 和 end_time 是 TIME 类型,TIME 没有日期信息。如果预约的开始时间 23:00,结束时间 02:00,看似结束时间在开始时间之前,其实是结束时间在第二天凌晨。
解决:业务上如果允许跨天预约,就必须把 start_time 和 end_time 在判断时拼成 LocalDateTime,判断 endDateTime 是否晚于 startDateTime,而不是只看 TIME。比如LocalDateTime startDateTime = LocalDateTime.of(reserveDate, startTime),LocalDateTime endDateTime = LocalDateTime.of(reserveDate, endTime)。如果 endTime.isBefore(startTime),还要在 endDateTime 上 plusDays(1) 再比较。多数自习室系统会把凌晨时段单独开放,比如 22:00 到次日 08:00 的夜间场,这种场景尤其需要处理。
5.5 源码包解压后启动失败:先检查这三处配置
现象:源码包导入 IDE 后,服务启动时报错五花八门,最常见的有三类:找不到主类、数据库连接拒绝、mapper 绑定异常。很多同学第一反应是代码有问题,实际上三处配置没对齐。
原因:第一处是 Java 版本不匹配。Spring Boot 2.3 左右的源码用 JDK 8 编译,你用 JDK 17 打开后 lombok 插件版本过低,会提示找不到 getter/setter 方法。第二处是数据库密码和 URL 里的库名与建库脚本不一致。第三处是 MyBatis-Plus 的 mapper 接口扫描路径写错,或者 XML 文件没有放在 resource 目录对应的位置。
解决:按顺序排查:IDEA 中 File > Project Structure 确认 Project SDK 版本;检查 application.yml 的 database 名称、用户名、密码;在启动类上确认 @MapperScan 扫描的包名与 mapper 接口所在包一致。如果 mapper XML 文件放在 java 目录下而不是 resource 目录,需要修改 pom 的 resources 配置,或者直接移动到 resource/mapper/ 下。日志里出现 “Invalid bound statement (not found)” 时,优先怀疑 XML namespace 和接口全限定名不匹配,对照一遍即可。
6. 把预约系统当验证场:并发脚本、日志回滚与压测数据
6.1 用并发脚本检验冲突控制是否真的生效
把系统跑通只是第一步,真正值得花时间的是验证预约逻辑经不经得起并发。我在交付课设前一般会用一个简单的 Shell 脚本模拟两个并发请求,目标是确认同座位同时段只会产生一条有效预约。脚本大致思路是同时对预约接口发送两个 curl,各携带相同的 seatId、date、startTime、endTime,然后查询数据库里的记录条数。
curl -s -X POST "http://localhost:8080/api/reservation/create" \ -H "Content-Type: application/json" \ -d '{"userId":1,"seatId":5,"reserveDate":"2025-06-15","startTime":"09:00","endTime":"10:00"}' & curl -s -X POST "http://localhost:8080/api/reservation/create" \ -H "Content-Type: application/json" \ -d '{"userId":2,"seatId":5,"reserveDate":"2025-06-15","startTime":"09:00","endTime":"10:00"}' & wait两个 curl 同时进入预约接口,分别来自 userId 1 和 userId 2。最终数据库里 seat_id=5、reserve_date=2025-06-15、时间段 09:00-10:00 的记录应该只有一条,另一条返回“已被预约”的业务错误。如果反而出现两条,说明你的 Service 查询和插入之间没有锁保护,或事务没生效,回到 3.3 节和 5.3 节排查。这个测试还可以加一个循环压力版,用并发 20 次请求验证不会出现第二条成功记录,数据能说明问题,比任何口头解释都有说服力。
6.2 时间一长,从预约数据里挖出真正的运营数据
预约记录会积累下来,这时候系统就从一个“选座工具”变成了“运营数据源”。我会在源码的统计页里保留三个查询:一天中各时段的预约量分布、每个座位的累计使用时长、违约记录最多的用户排行。这三个数据分别是“什么时候座位最紧张”“哪些座位利用率最高”“哪些人有占座不来的习惯”。答辩时聊到这层,老师通常会对系统的完成度和思考深度给出不错的评价。
一个示例查询是统计各时段的预约量分布:
SELECT start_time, COUNT(*) AS cnt FROM reservation WHERE reserve_date BETWEEN '2025-06-01' AND '2025-06-15' AND status IN ('CHECKED_IN', 'COMPLETED') GROUP BY start_time ORDER BY cnt DESC;用条形图展示后能直观看到上午 09:00 和下午 14:00 是高峰,晚上 19:00 也有一个小波峰。这个结论可以反哺参数设置:高峰期把单次预约上限从 4 小时缩到 3 小时,能提高座位周转率;低峰期则放开时段限制,鼓励学生来使用。我自己的习惯是每完成一个课设,就顺手把这类统计数据生成为图片放进报告附录,既不显得堆砌,又能让评委看到你做了真实的数据验证。希望这一套从建表到避坑再到压测的路径能帮到你——课设和毕设拿到这套 Spring Boot 自习室预约系统源码后,先别急着改页面,把预约逻辑和状态机跑通,它带来的收益远大于换一套好看的皮肤。
本文还有配套的精品资源,点击获取