Spring Boot自习室预约系统:并发预约冲突控制与状态机设计实战
简介这套基于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_timeWAITING超时未签到EXPIRED写 violationCHECKED_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.x3.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autourl 里必须带 serverTimezoneAsia/Shanghai 和 characterEncodingutf8这两个参数不写后面会遇到“数据库时间比本地早 8 小时”和“中文乱码”两个经典问题。allowPublicKeyRetrievaltrue 只在 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. 用悲观锁锁定该座位这一天的预约记录防止同一时刻重复预约 ListReservation 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 READFOR UPDATE 锁定的记录在事务提交前对其他事务不可见因此判断逻辑不会读到旧快照这一点可以在答疑时作为补充。4.3 定时清理过期预约Scheduled 的坑和替代方案预约系统里一定会有一个定时任务把过了签到时间还没有签到的 WAITING 记录变成 EXPIRED并写违约记录。Spring Boot 里最简单的实现是在启动类或配置类加 EnableScheduling然后在某个 Service 方法上写 Scheduled(cron 0 0/5 * * * ?)每 5 分钟跑一次。写这个任务最需要注意的一点不要对全表扫描而是只查“当前时间减去 30 分钟小于等于开始时间且状态为 WAITING”的记录。SQL 大致形式是SELECT id FROM reservation WHERE statusWAITING 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 里没有写 serverTimezoneAsia/Shanghai驱动会用服务器机器的默认时区处理 DATETIMEJava 侧再用本地时区读取两边一加一减就出现了 8 小时差。解决数据源 URL 中显式加上 serverTimezoneAsia/Shanghai。如果已经出现历史数据偏移不要直接在页面上改先确认数据库连接串改对然后把 create_time 用 UPDATE 语句倒推 8 小时修正一次。另外在配置文件里把 jackson.time-zone 写成 GMT8否则返回给前端的 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:00newStart 是 10:00existingEnd 是 10:0010:00.isBefore(10:00) 为 false因此不冲突正确。已有 09:00-10:00新预约 09:30-10:30newStart 是 09:30existingEnd 是 10:0009:30.isBefore(10:00) 为 true同时 existingStart 是 09:00newEnd 是 10:3010: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_id5、reserve_date2025-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 自习室预约系统源码后先别急着改页面把预约逻辑和状态机跑通它带来的收益远大于换一套好看的皮肤。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →