☰
Java共享自习室系统实战:Spring Boot+MyBatis Plus预约计费与并发控制
2026/10/9 10:32:26 网站建设 项目流程

简介:一套功能完整的基于Java的共享自习室系统毕业设计源码包,面向高校计算机专业学生及Java Web开发者,围绕自习室预约管理核心场景,帮助解决座位利用率低、预约流程繁琐等问题,并系统演练需求分析、数据库设计、前后端接口联调与项目部署等全流程开发技能。压缩包共664个文件,其中280个java源码文件构成后端业务逻辑,130个js与102个vue文件完成前端交互界面,另有xml映射与配置文件、环境属性配置、json数据文件以及图标图片等资源,整体体积仅4.77MB,目录结构清晰,便于按模块定位代码。目前已有151人学习下载。解压后可以获得完整前后端代码、静态资源、工程配置文件与开发辅助文件,涉及用户管理、自习室信息维护、预约并发控制、权限校验等典型功能,既能作为课程设计或毕业设计的参考基线,也适合用来理解MVC分层、RESTful接口设计及数据库表关系等Java Web核心知识点。

1. 共享自习室系统在管什么:从预约到计费的闭环,为什么 Java 这套组合最稳

共享自习室系统,说白了就是把自习室里最头痛的三件事——座位预约、计时计费、会员储值——串成一条不靠人盯的流水线。小自习室的老板通常靠微信群接龙订座,管理员拿 Excel 排座,高峰期一桌两卖是常事;而这个系统要做的,就是让用户在小程序或网页上看到实时座位状态,预约后到店签到,系统自动计时,离座结算扣费,全程不需要前台盯着计时器。

基于 Java 来做这套系统,原因是生态里现成的东西太多:Spring Boot 管接口和事务,MyBatis Plus 管数据库操作,MySQL 管数据落地,这套组合从上手速度到部署成本都适合“小团队 + 单机起步”的形态。标题里的 zip 包,是这个领域最常见的交付方式——源码、配置、SQL 脚本打成压缩包,解压即得工程骨架,而不是给你一套需要申请机器和域名的云服务。适合的人群也很明确:有 Java 基础、想拿课程设计当实战项目的学生,以及想低成本搭一套内部管理系统的自习室经营者。

这篇就按我平时接手这类工程的习惯,从解压开始,把选型逻辑、核心业务代码、部署踩坑和进阶验证一条线讲完。

2. 从 zip 包到能跑的工程:先摸清技术选型和目录结构再动手

接到基于Java的共享自习室系统.zip这类包,我一般不会急着解压后直接启动。先花十分钟把包里的结构和依赖看清楚,后面能省掉大量查日志的时间。

2.1 为什么是 Spring Boot + MyBatis Plus,而不是 SSM 或 JPA

共享自习室系统的业务体量,决定了它不需要微服务,不需要分布式事务,甚至 Redis 都可以后置。最常见的合理选型是一套 Spring Boot 单体应用:Spring Boot 负责把接口、定时任务、事务管理打包成一个可执行 Jar,MyBatis Plus 负责把实体类和 SQL 之间的映射降到最低,MySQL 8 负责存储座位、订单、会员这些核心数据。

不选传统 SSM 的原因很直接:SSM 的 XML 配置量在 2025 年看来完全是负资产,一个@SpringBootApplication能替代的配置,没必要写三份 XML。不选 Spring Data JPA 的原因则是共享自习室这类系统里大量存在“条件更新 + 状态机流转”的 SQL,JPA 的实体生命周期管理在这种场景下反而绑手绑脚,而 MyBatis Plus 的UpdateWrapper可以直接拼出UPDATE ... WHERE status = ?,语义一眼能看懂。这套选型逻辑同时也是 Java 面试题里常被拿出来拆的经典组合,你把“为什么不用 JPA”讲清楚,比背十道八股文更有说服力。

2.2 解压后先核对工程骨架:pom、启动类、SQL 脚本是否齐备

一个规范交付的 zip 包,解压后应该有这几块内容。我习惯先列一个清单,缺哪块心里有数:

目录/文件作用缺失时的表现
pom.xmlMaven 依赖与打包配置IDE 里项目无法识别为 Maven 工程
src/main/java启动类、Controller、Service、Mapper启动类找不到,直接启动失败
src/main/resources/application.yml端口、数据源、MyBatis 配置启动后连不上数据库
sql/或doc/下的建表脚本建库建表与初始数据运行时报表“Table doesn't exist”
README.md启动步骤与环境要求只能靠猜 JDK 和 MySQL 版本

拿到包之后第一步是解压,然后用编辑器打开pom.xml看依赖坐标。常见的pom.xml核心依赖长这样:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里有两个点要注意。第一,mybatis-plus-boot-starter的版本要和 Spring Boot 主版本对齐,3.5.x对应2.7.x没问题,但如果你把 Spring Boot 升到3.x,MyBatis Plus 要换成mybatis-plus-spring-boot3-starter,否则启动时会因为javax到jakarta的包名迁移直接报NoClassDefFoundError。第二,Lombok 要标optional,避免它打进最终的 Jar 包。

2.3 数据源配置与 MySQL 8 的时区坑:application.yml 里最容易被忽略的两行

application.yml是解压后第二个要核对的文件。共享自习室系统里所有业务都依赖数据库,数据源配错,后面全是白忙。一份能跑起来的配置大致如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/study_room?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

url里的serverTimezone=Asia/Shanghai是 MySQL 8 最常见的坑——不配时区,连接会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,新手第一次看到这种乱码报错会以为是编码问题,实际上是时区。allowPublicKeyRetrieval=true是给 MySQL 8 的caching_sha2_password认证用的,不配上,部分驱动版本会在首次连接时要求显式获取公钥,连接直接超时。

logic-delete-field是 MyBatis Plus 的全局逻辑删除配置。共享自习室的座位和会员记录一般不做物理删除,配置了这一行之后,所有delete操作自动变成UPDATE ... SET deleted=1,查询自动带deleted=0。但注意,这个全局配置只对 MyBatis Plus 自动生成的 SQL 生效,你手写的 XML 或@Select注解里的 SQL 它管不到,这是后面排查“删了还能查出来”问题时的重点。

3. 核心业务模块的 Java 落地:座位状态机、预约并发与计时计费

工程能跑起来之后,真正决定这个系统好不好用的是业务逻辑。共享自习室系统和普通 CRUD 的最大区别,在于“座位状态”是一个有生命周期的对象,任何一步状态没转对,都会出现座位明明被占却还能预约的翻车现场。

3.1 数据模型先行:座位、预约、订单、会员四张表的关系与唯一索引

做业务之前先把表结构定清楚。我一般会设计四张核心表,座位表存静态信息,预约表存每一次预约的流转记录,订单表存结算结果,会员表存储值和等级。其中预约表是状态机的载体,也是并发控制的重点:

CREATE TABLE t_seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, seat_no VARCHAR(20) NOT NULL COMMENT '座位编号', room_id BIGINT NOT NULL COMMENT '自习室房间ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已预约 2使用中 3维护', deleted TINYINT NOT NULL DEFAULT 0 ) COMMENT '座位表'; CREATE TABLE t_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, seat_id BIGINT NOT NULL, member_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0已预约 1已签到 2已取消 3已完成', start_time DATETIME, end_time DATETIME, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, version INT NOT NULL DEFAULT 0, deleted TINYINT NOT NULL DEFAULT 0, UNIQUE KEY uk_seat_time (seat_id, status) ) COMMENT '预约表';

uk_seat_time (seat_id, status)这个唯一索引是防“一桌两卖”的关键。同一个座位在同一时刻只允许存在一条“有效预约”,而status为 0(已预约)或 1(已签到)都算有效状态,所以索引要建立在“座位 + 有效状态”上。这里有一个取舍:如果系统允许一个人预约未来多个时段,索引需要调整成(seat_id, start_time),但大多数小自习室只做“现在预约现在用”,上面的设计就够了。

MyBatis Plus 根据实体类生成建表 SQL 的能力在起步阶段很好用,Mapper接口继承BaseMapper<T>之后,mybatis-plus会自动生成单表 CRUD。但注意,它只会生成单表操作,JOIN查询和上面这种带业务约束的索引还是得靠手写 SQL 脚本。所以我的习惯是:建表脚本永远手工维护,实体类只负责映射,不依赖代码生成器。

3.2 预约→签到→开始计时:用条件更新保证状态流转不串

状态流转最怕的就是“查到状态是空闲,然后改成占用”这两步之间被别人插一脚。Java 里给方法加synchronized只能挡单机多线程,挡不住将来多实例部署,而且synchronized在 Spring 事务代理下还容易失效。正确的做法是把状态判断写进UPDATE的WHERE条件里,让数据库来保证原子性。

@Service @RequiredArgsConstructor public class ReservationService { private final SeatMapper seatMapper; private final ReservationMapper reservationMapper; @Transactional(rollbackFor = Exception.class) public Long reserve(Long seatId, Long memberId) { // 条件更新:只有 status=0(空闲)的座位才能被预约 int updated = seatMapper.update(null, new LambdaUpdateWrapper<Seat>() .set(Seat::getStatus, 1) .eq(Seat::getId, seatId) .eq(Seat::getStatus, 0)); if (updated == 0) { throw new BizException("座位已被占用,请重新选座"); } Reservation reservation = new Reservation(); reservation.setSeatId(seatId); reservation.setMemberId(memberId); reservation.setStatus(0); reservation.setStartTime(LocalDateTime.now()); reservationMapper.insert(reservation); return reservation.getId(); } }

这段代码的核心逻辑是seatMapper.update里的eq(Seat::getStatus, 0):别人先更新成功,WHERE status=0就匹配不到,updated返回 0,直接抛业务异常。两个用户同时点同一个座位,数据库层面只有一个事务能拿到status=0的那一行,另一个必然失败。这就是用“条件更新”替代“查询再更新”的标准写法,也是这类系统里最重要的并发防线。

@Transactional(rollbackFor = Exception.class)要特别注意rollbackFor。Spring 默认只对RuntimeException回滚,如果你抛的是自定义的BizException,它继承的是Exception,不写rollbackFor的话,前面seatMapper.update已经把座位改成了占用,后面插入预约记录失败,座位状态却不会回滚,系统就会出现“座位占用但没有预约记录”的黑匣子。这个坑我在代码评审里见过不止一次。

3.3 签到与计时:把“开始时间”记在状态里,而不是记在脑子里

用户到店后要“签到”,签到的动作就是把预约状态从“已预约”改成“使用中”,同时正式记录开始计费的时间点。这里最容易犯的错误是:把开始时间存成字符串,或者干脆不存,等到结算时再用当前时间减预约时间。正确做法是状态里带时间戳,而且时间戳的更新也要用条件更新,不能先查后改。

@Transactional(rollbackFor = Exception.class) public void checkIn(Long reservationId) { LocalDateTime now = LocalDateTime.now(); int updated = reservationMapper.update(null, new LambdaUpdateWrapper<Reservation>() .set(Reservation::getStatus, 1) .set(Reservation::getStartTime, now) .eq(Reservation::getId, reservationId) .eq(Reservation::getStatus, 0)); if (updated == 0) { throw new BizException("预约状态已变化,无法签到,请刷新后重试"); } }

签到之后,计时就靠start_time和结算时的当前时间做差。Java 8 的LocalDateTime在跨天计算时比老的Date安全得多,但要注意Duration.between(start, end).toMinutes()得到的是“总分钟数”,不是“小时数的小数”。共享自习室按小时计费时,常见规则是“不足半小时按半小时,超过半小时按一小时”,这个取整逻辑要放在业务层,不要指望数据库算。

计费规则本身用策略模式更好扩展——有的店按小时,有的店按“首小时 + 续小时”,有的店有会员折扣。把计费规则抽成一个BillingStrategy接口,比在 Service 里写一长串if (type == 1)干净得多,也是这个项目最容易在面试里被追问的扩展点。支付回调要做好幂等:同一笔订单微信或支付宝可能回调多次,处理回调时先查订单状态,已经是“已支付”就直接返回成功,不再重复扣费。

4. 避坑与排查:共享自习室系统从解压到上线最常翻车的 5 个环节

这一章是我最想让你先看的部分。下面 5 个问题,每一个都是真实工程里反复出现的典型故障,按“现象 → 原因 → 解决”说清楚。

4.1 启动失败:端口占用、JDK 版本不匹配与 MySQL 8 时区乱码

现象:启动后控制台报Web server failed to start. Port 8080 was already in use,或者报Unrecognized VM option,或者出现开头说的时区乱码错误。

原因:共享自习室系统这类课程设计包经常是作者在自己机器上开发的,zip 里写的server.port或 JDK 版本未必适合你的环境。端口占用最常见的是本地开了别的 Web 服务;JDK 报错一般是项目的pom.xml编译目标高于你本机安装的 JDK;时区乱码则是 MySQL 8 的serverTimezone没配。

解决:先看谁占了端口,能关就关,不能关就在application.yml里把server.port改成8081;JDK 版本问题用java -version和mvn -version对比,保证pom.xml的java.version不高于本机版本;时区问题把serverTimezone=Asia/Shanghai加进 JDBC URL。如果机器上装了多个 JDK,记得检查JAVA_HOME指向的是不是项目要求的那一个,这是 Java 环境变量最常见的坑。

4.2 座位超卖:并发预约下“查了空闲”但“改的时候已经被占”

现象:线上运营时,两个用户同时点同一个座位,两个人都收到了“预约成功”的提示,但数据库里只有一条预约记录,另一个用户到店后发现座位被人坐了。

原因:代码写成了“先select查状态,再update改状态”,两个请求都查到status=0,然后都去更新,后一个更新覆盖了前一个。select和update之间没有原子性,MySQL 默认的REPEATABLE READ隔离级别也挡不住这种“读-改-写”竞态。

解决:用前文 3.2 里的条件更新写法,把状态判断放进UPDATE ... WHERE status=0,靠affected rows判断是否成功;同时保留t_reservation表上的唯一索引作为兜底。两个措施一个是业务层防,一个是数据库层防,缺一个都可能在极端场景下出问题。

4.3 计时与结算:跨天时长算错、免充值押金逻辑不清

现象:用户 23:50 签到,00:20 结算,系统显示用时 1430 分钟,金额算出一个离谱的数字。或者用户首次使用不需要充值,押金状态没标对,结算时直接扣费失败。

原因:老代码如果用的Date做减法,跨天时getTime()的毫秒差本身没问题,问题多半出在中间有一次时间被重置,或者把end_time误存成了日期字符串。押金问题的根源是“免押金体验”和“储值扣费”两套逻辑混在了一起,没有单独字段标记当前订单是否走了免押金通道。

解决:统一用LocalDateTime,结算时Duration.between(startTime, LocalDateTime.now()),必要时把业务规则改成“按分钟取整,每 30 分钟为一个计费单位”。押金逻辑在订单表上加deposit_status字段,0 表示未收押金、1 表示已收待退、2 表示已退,结算时先判这个字段再决定要不要发起退款。

4.4 中文乱码:zip 包解压、MySQL 建表、接口返回三处编码不一致

现象:解压出来的 Java 源文件里中文注释变成乱码,接口返回的 JSON 里中文变成???,或者数据库里存进去是好的、查出来是乱的。

原因:zip 包里的源文件是 UTF-8,Windows 下用默认 GBK 解压导致文件名和文件内容双重乱码;接口返回乱码是 Spring Boot 的server.servlet.encoding没配置;数据库乱码是建库时字符集不是utf8mb4。

解决:Windows 上解压用7z x 基于Java的共享自习室系统.zip或让 IDE 以 UTF-8 重新打开文件;接口层在application.yml里配server.servlet.encoding.charset: UTF-8;建库时用CREATE DATABASE study_room DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。注意utf8mb4和utf8的区别,utf8在 MySQL 里存不下四字节的 emoji,头像昵称里面有 emoji 就直接写入失败。

4.5 MyBatis Plus 自动生成的 SQL 边界:表名撞关键词、逻辑删除失效

现象:某张表名是order,MyBatis Plus 自动生成的查询直接报语法错误;或者配置了逻辑删除后,手写的 SQL 里delete还是把数据物理删了。

原因:order是 MySQL 保留字,自动生成的 SQL 没有自动加反引号;逻辑删除的全局配置只对BaseMapper的通用方法生效,XML 里手写的DELETE FROM自然绕过。

解决:表名避开保留字,实在避不开就在实体类上用@TableName("order")手动加反引号;手写 SQL 时把“逻辑删除”当成一种约定,写UPDATE t_order SET deleted = 1 WHERE ...,不要真的写DELETE。这个问题的本质是“自动能力有边界,边界之外的 SQL 要自己守规矩”,也是 MyBatis Plus 在团队协作里最容易因为“有人不知道边界”而埋雷的地方。

5. 进阶验证:给系统补一个日结报表,再用并发测试证明预约不超卖

系统能跑通基本流程之后,我习惯做两件事来验证它是不是真的能上线,而不是“能点通就完事”。第一件是补一个日结报表,第二件是拿并发工具打一下预约接口。

5.1 日结报表:一张 SQL 看清明天的座位利用率和收入

共享自习室老板最关心的是“今天卖了多少小时、赚了多少钱、哪些座位最闲”。这个需求用一张 SQL 就能出结果,不需要引入报表引擎:

SELECT DATE_FORMAT(o.pay_time, '%Y-%m-%d') AS biz_date, COUNT(DISTINCT o.id) AS order_count, SUM(o.amount) AS total_amount, COUNT(DISTINCT r.seat_id) AS used_seats, ROUND(SUM(TIMESTAMPDIFF(MINUTE, r.start_time, r.end_time)) / 60.0, 2) AS total_hours FROM t_order o JOIN t_reservation r ON o.reservation_id = r.id WHERE DATE_FORMAT(o.pay_time, '%Y-%m-%d') = CURDATE() GROUP BY DATE_FORMAT(o.pay_time, '%Y-%m-%d');

把这条 SQL 放进一个定时任务里每天凌晨跑一次,结果写入一张t_daily_report表,老板端和管理员端直接查这张表就行。注意TIMESTAMPDIFF计算小时数时要除以 60.0 而不是 60,否则整数除法会把小数点后全丢掉,报表里每小时都会少算。这是我踩过的真实坑。

5.2 用 JMeter 打 100 并发预约:验证条件更新真的挡住了超卖

代码写得好不好,不是靠“我觉得没问题”,是靠压测结果。JMeter 不需要 GUI,命令行就能跑,适合在 Linux 服务器上做验证。先把预约接口用相同参数各打 100 并发,看两个指标:成功率是否 100%,以及数据库里t_reservation对同一个seat_id是不是只有一条有效记录。

我用笔记本跑过一次 50 并发预约同一个座位,条件更新的写法下,只有 1 个请求成功,其余全部返回“座位已被占用”,数据库里也只有 1 条status=0的预约记录。这就是这套方案可信的证据。如果你把代码改回“先查后改”再跑一遍,会看到多个请求都“成功”了,但数据库里只有一条记录——那种“接口全绿但数据就是不对”的翻车,压测一打就原形毕露。

我个人的习惯是:任何涉及状态流转的接口,上线前必须压一次,压完再看日志里的 SQL。条件更新虽然写法上多了一行eq,但它省掉的是上线后半夜被叫起来看数据的血泪。日结报表、并发测试、状态机三件套做完,这个基于 Java 的共享自习室系统才算是真正可以交出去的工程,而不是一个“能启动的 demo”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询