ARTICLE DETAIL

资讯详情

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

SSM框架实验室预约系统:从ER图到并发冲突校验的完整实践

SSM框架实验室预约系统:从ER图到并发冲突校验的完整实践 简介基于Java SSM的实验室预约系统完整项目包面向Java Web学习者与毕业设计开发者提供可直接运行的源码与数据库覆盖实验室资源管理、预约冲突处理、用户权限控制等典型业务场景。资源共787个文件压缩包约56.26MB包含png/gif界面截图与流程演示、jar依赖库、class编译文件、jsp页面、java源码、css/js前端资源及sql数据库脚本目录结构完整便于对照学习。核心功能包括用户注册登录、实验室信息维护、空闲时段查询与预约、历史记录检索及预约提醒通过Spring DI/AOP解耦业务、SpringMVC处理请求映射、MyBatis灵活操作MySQL可清晰理解SSM整合流程与Web分层开发思路。当前已有356人学习下载适合用于课程设计、项目实训以及SSM框架综合实践。整体资料从数据库脚本到前端页面均有覆盖对提升真实项目开发能力有直接帮助。1. 实验室预约系统为什么选 SSM先想清楚再写代码实验室预约系统看起来是一个标准的“数据库增删改查”项目但真正决定它好不好用的不是新增、删除、修改这几个基础操作而是“同一间实验室在同一时段不能被两个人约走”这件事。很多课程设计和简历项目都倒在这一步页面上选好时间、点提交数据确实写进去了可并发场景下两个浏览器窗口同时提交系统照样生成了两条冲突预约而且没有任何报错。选择 SSM 而不是 Spring Boot不是因为 Boot 做不了而是 SSM 把 Controller、Service、Dao 三层边界逼着你亲手搭一遍。Spring 管理对象和事务Spring MVC 负责请求分发MyBatis 负责 SQL 与 Java 对象的映射这种“每层都看得见”的结构恰恰是 Java 面试里被反复追问的底层逻辑。对刚学完 Java 基础和数据库原理的在校生或者要接手老项目的工程师来说跑通这套源码只是第一步能把三层依赖关系和事务边界讲清楚才算真正掌握了标题背后要交付的东西。2. 拆解实验室预约系统的核心模型从 ER 图到 MyBatis 映射2.1 用户、实验室与预约单三张表如何避免冗余进入编码前先画 ER 图是值得养成的习惯。实验室预约系统最精简的设计就是三张实体表用户表保存学生和教师账号实验室表保存房间、容量与位置信息预约单表记录“谁在什么时间用了哪间实验室”。实体关系上一个用户可以发起多条预约一间实验室也会被多次预约所以预约单与用户、实验室都是多对一关系通过外键 user_id 和 lab_id 关联。表名主要字段关系说明sys_userid, username, password, role被 reservation.user_id 引用labid, lab_name, capacity, location被 reservation.lab_id 引用reservationid, user_id, lab_id, start_time, end_time, status, purpose分别关联用户和实验室这里有一个新手经常搞反的设计预约单里直接冗余“用户名”和“实验室名”两个字符串字段。这样做表面上查询方便但一旦用户在系统里改了显示名或者实验室编号规则调整历史预约数据就全部变脏。用外键关联查询时 JOIN 一下就能拿到名称写入时只存 id数据一致性由数据库保证。2.1.1 时间字段与状态字段的选型预约时间建议用 datetime 而不是字符串。MyBatis 可以直接把 datetime 映射成 java.util.Date后续在 Java 端做区间比较、计算时长都不需要手动解析字符串。状态字段用 tinyint 比 varchar 更好维护因为“待审核、已通过、已使用、已取消”是有限枚举用 0、1、2、3 表示可以让 Service 层按状态机做流转校验。下面是可直接执行的建表脚本。CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, role tinyint(4) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE lab ( id int(11) NOT NULL AUTO_INCREMENT, lab_name varchar(100) NOT NULL, capacity int(11) NOT NULL, location varchar(100) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reservation ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, lab_id int(11) NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, status tinyint(4) NOT NULL DEFAULT 0, purpose varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_lab_time (lab_id, start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本里最值得关注的是 reservation 表末尾的联合索引 idx_lab_time。预约系统的查询条件几乎总是“指定实验室 指定时间区间”把 lab_id 放在联合索引最左侧才能让后续的冲突检测 SQL 走索引而不是全表扫描。password 字段用 varchar(64) 是为了匹配 MD5/SHA 摘要的定长输出早期项目里直接存明文的做法不仅不好看还是实打实的安全债。2.2 写一个可执行的预约核心 SQL 与 Mapper 接口建表之后Dao 层最核心的方法不是 insert而是“查询某实验室在某个时间区间是否已被占用”。这个查询用到了 SQL 区间重叠判断已有的预约时间段是 [start_old, end_old]新请求是 [start_new, end_new]两者冲突的条件是 start_new end_old 并且 end_new start_old。也就是新预约的开始早于已有预约的结束且新预约的结束晚于已有预约的开始时间线上必然有交叠。public interface ReservationMapper { ListReservation selectByLabAndTime(Param(labId) Integer labId, Param(start) Date start, Param(end) Date end); }对应 XML 里写的是select idselectByLabAndTime resultTypecn.example.entity.Reservation SELECT id, user_id, lab_id, start_time, end_time, status, purpose FROM reservation WHERE lab_id #{labId} AND status IN (0, 1) AND start_time lt; #{end} AND end_time gt; #{start} /select这里有两个易错点。第一status IN (0, 1) 是为了跳过“已取消/已拒绝”的旧预约否则历史记录会把后续申请全部挡掉。第二XML 里的小于号和大于号必须写成 和 否则 MyBatis 解析 XML 时报错提示内容字符不合法。这段查询返回的是所有重叠预约Service 层只要判断列表不为空就知道这个时段已经不能再约。2.3 MyBatis 动态 SQL 处理多条件查询后台管理页面经常要按实验室名称、预约日期、状态做组合筛选。如果每种组合都写一条独立 SQLMapper 会膨胀到没法维护。MyBatis 的where加if组合就是为这种场景设计的它可以按传入参数动态拼接查询条件。select idsearchReservation resultTypecn.example.entity.Reservation SELECT * FROM reservation where if testlabId ! null AND lab_id #{labId} /if if testdate ! null AND DATE(start_time) #{date} /if if teststatus ! null AND status #{status} /if /where ORDER BY start_time DESC /selectwhere标签会自动去掉第一个多余的 AND这是最容易被忽略的细节如果自己手写 WHERE 11 也能跑但可读性差数据量上来后 11 对优化器也不友好。DATE(start_time) 这种写法有个副作用对字段做函数运算后索引会失效预约记录多时查询会很慢。更稳妥的做法是让前端传 startTime 和 endTime直接用 start_time #{startTime} AND start_time #{endTime} 做范围匹配让函数运算离开查询条件。3. 在 Spring MVC 里把预约流程跑通Controller、Service 与事务边界3.1 预约业务的状态机从提交到审核再到使用写业务代码前先定义清楚状态机比直接写 CRUD 重要得多。预约单的状态流转可以分为三条线学生提交后进入待审核管理员通过后变为已通过使用时间到达后学生确认使用或者管理员拒绝、学生取消进入终态。状态字段用 tinyint 存储Java 端不要到处写魔法数字否则代码里全是 if (status 0) 这种看不懂的硬编码。状态码含义允许迁移到的状态0待审核1 通过 / 3 拒绝1已通过2 已使用 / 3 取消2已使用无3已取消/拒绝无这个状态表给 Service 层提供的约束是每次更新预约状态前先取当前状态再判断“当前状态是否能迁移到目标状态”。很多 SSM 项目出问题就是因为 Controller 直接把前端传来的 status 覆盖到数据库学生可以绕过管理员审核把自己提交的预约改成已通过整个预约流程的审批意义就消失了。3.2 Service 层防止重复预约的两种做法先看一个入门项目最常见的实现。在 ReservationServiceImpl 的创建方法里先调用 Mapper 查冲突记录有冲突就抛异常没有冲突才执行插入。这个实现逻辑直接也容易向别人解释。Service public class ReservationServiceImpl implements ReservationService { Autowired private ReservationMapper reservationMapper; Override Transactional(rollbackFor Exception.class) public void createReservation(Reservation reservation) { ListReservation conflicts reservationMapper.selectByLabAndTime( reservation.getLabId(), reservation.getStartTime(), reservation.getEndTime()); if (conflicts ! null !conflicts.isEmpty()) { throw new BusinessException(该实验室在所选时段已被预约); } reservation.setStatus(0); reservationMapper.insert(reservation); } }这个写法能解决单机、低并发下的重复预约但存在一个竞态窗口两个请求同时执行查询都发现没有冲突记录然后双双执行 insert最终数据库里出现了两条重叠预约。要根治并发问题数据库层面必须加约束或锁。一个相对简单的兜底做法是在插入前用 SELECT ... FOR UPDATE 锁住实验室所在行把对这个实验室的预约操作串行化。Transactional(rollbackFor Exception.class) public void createReservation(Reservation reservation) { Lab lab labMapper.selectByIdForUpdate(reservation.getLabId()); if (lab null) { throw new BusinessException(实验室不存在); } int count reservationMapper.countConflict(reservation); if (count 0) { throw new BusinessException(该实验室在所选时段已被预约); } reservationMapper.insert(reservation); }selectByIdForUpdate 对应的 SQL 是SELECT * FROM lab WHERE id #{id} FOR UPDATE。锁住实验室主键后同一时间对这个实验室的预约请求会排队执行后续请求必须等前一个事务提交后才能读到最新数据此时 countConflict 就能看到刚插入的重叠记录从而正确拒绝请求。这个方法比“只在应用层判断”可靠得多。注意SELECT ... FOR UPDATE 必须在事务内才生效。如果所在方法没有加 Transactional数据库连接会在查询后自动提交锁立即释放并发问题照旧出现。另外锁持有时间跟事务耗时正相关事务里不要夹杂写日志、发通知这类无关操作。3.3 用 Spring 声明式事务保证并发下不超约上面两个方法都加了Transactional(rollbackFor Exception.class)。这里有一个 Java 面试常问的细节为什么必须指定 rollbackFor Exception.class因为 Spring 默认只在抛出 RuntimeException 时才回滚事务。很多 SSM 项目里自定义的 BusinessException 继承自 Exception属于受检异常如果方法上只写 Transactional 而不指定 rollbackFor业务方法抛出异常后事务不会回滚前面的数据修改全部保留冲突检测就白做了。事务边界同样值得注意。把 Transactional 加到 Controller 方法是反模式Controller 要做参数绑定、调用 Service、返回视图事务范围过大会让数据库连接被占用过久并发能力显著下降。正确位置是 Service 的业务方法。创建预约就只放冲突检测和 insert不要把发通知、写操作日志都塞进同一个事务否则非核心步骤失败会导致预约记录一起回滚使用者只会感觉系统莫名其妙就丢单。4. 把源码数据库跑起来Maven 配置、初始化脚本与常见报错4.1 导入项目前要确认的 JDK、Tomcat 与 MySQL 版本拿到压缩包后别急着双击打开先翻到根目录看 pom.xml再决定本机环境怎么配。多数 SSM 课程设计项目基于 JDK 8 和 Tomcat 8.5/9数据库用 MySQL 5.7 或 8.0。如果你本机装的是 JDK 17直接编译会报“无效的目标发行版”因为新版 JDK 不再支持 pom.xml 里指定的旧编译级别。先执行 java -version 和 mvn -v 确认环境比一路点“运行”按钮更省时间。如果 java -version 显示正常但 mvn 提示找不到 JAVA_HOME多半是环境变量配置指向了错误的 JDK 安装路径。再看 Maven 依赖能不能下下来公司内网环境经常需要把仓库地址改成阿里云镜像SSM 依赖的 spring-webmvc、mybatis、mysql-connector-java 都能在中央仓库找到镜像只影响下载速度。下面是一段可用的 pom 配置注意 servlet-api 的 scope 必须声明为 provided。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version5.3.23/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies这里列出的版本不是唯一答案只要 Spring 与 MyBatis 主版本兼容即可。servlet-api 使用 provided 是为了避免和 Tomcat 自带的 Servlet 实现冲突否则运行时会报 NoSuchMethodError 这类诡异异常因为同一个类被加载了两遍。4.2 数据库初始化脚本执行顺序与连接池参数压缩包名里的“数据库”通常指一份 SQL 脚本可能以 .sql 或 .txt 格式存在。执行顺序不能乱先建库再建表最后插入初始化数据。如果直接把整个文件复制到 Navicat 执行大概率会报“未选择数据库”。脚本开头一般有 CREATE DATABASE 和 USE 语句但要注意执行账号是否有建库权限没有的话需要先手动创建库再单独执行建表语句。连接池参数决定了系统能同时支撑多少数据库连接。SSM 项目常用 Druid三个核心参数是 initialSize、maxActive、maxWait。实验室预约属于轻量系统maxActive 设 20、maxWait 设 60000 毫秒足够。如果运行中频繁打印“获取连接超时”不一定是连接池太小也可能是 MySQL 的 max_connections 默认值被占满先执行 show processlist; 看有没有连接未被释放再决定调哪边。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/lab_reserve?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordroot druid.initialSize5 druid.maxActive20 druid.maxWait600004.3 运行期常见异常的排查清单报错现象起因排查方向Table lab_reserve.reservation doesnt exist表名大小写或建表脚本未执行检查 MySQL 是否区分表名大小写Linux 下建议统一小写Access denied for user rootlocalhost数据库账号密码与配置不一致先改 jdbc.properties再检查 MySQL 用户授权Invalid bound statement (not found)Mapper XML 的 namespace 或 id 与接口不匹配确认 namespace 为接口全限定名id 为方法名Too many connections连接数与 MySQL 上限冲突show processlist 检查未释放连接调整连接池参数Error creating bean with name sqlSessionFactoryMyBatis 配置文件路径错误检查 mapperLocations 指向的目录是否存在且包含 XMLUnknown character set连接串缺少 characterEncoding统一使用 utf8mb4 字符集这张表列的每一条都能对应到具体位置第一条查数据库第二条查配置第三条查 namespace。遇到报错先看异常栈第一行而不是把整个异常信息复制去搜索第一行通常已经直接指出是连接失败、SQL 映射缺失还是 SQL 语法错误。5. 从能跑到能用给系统补上时间冲突校验与统计报表5.1 预约时间冲突的区间判重 SQL前面的 selectByLabAndTime 能挡住创建预约时的冲突但管理员手动录入、导入历史数据这两条路径往往绕过了 Service 层。把冲突判断抽成一个独立 SQL供所有入口复用比在每个方法里复制条件更可靠。判重的核心条件只有一条SELECT COUNT(*) FROM reservation WHERE lab_id #{labId} AND status IN (0, 1) AND start_time #{newEnd} AND end_time #{newStart}边界条件要特别小心。如果把第一个 改成 那么新预约的结束时间等于已有预约的开始时间时也会被判为冲突导致两个时段无法无缝衔接。同样的第二个 改成 后新预约开始时间等于已有预约结束时间同样会被拦截。实验室使用场景里前一场结束后立刻开始下一场是正常需求所以应该用开区间判断。5.2 用拦截器做权限校验的最小实现预约系统里学生、教师、管理员三类角色权限不同。很多源码把角色判断写死在每个 Controller 里代码重复而且容易漏掉某个入口。用 Spring MVC 的 HandlerInterceptor 统一收口是最小改动方案。下面这个拦截器拦截后台管理路径非管理员一律跳回登录页。public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Integer role (Integer) request.getSession().getAttribute(role); String uri request.getRequestURI(); boolean isAdminOp uri.startsWith(/admin/); if (isAdminOp (role null || role ! 2)) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }在 spring-mvc.xml 中对 /admin/** 路径应用这个拦截器并单独排除静态资源路径否则 CSS、JS 文件也会被挡在登录页外。拦截器用来做“是否已登录、角色够不够”这种粗粒度校验细粒度的“这条预约是否属于当前用户”仍然要放在 Service 层判断。验证时打开两个浏览器标签页同时提交同一间实验室同一时段的预约请求看第二次请求是否被正确拒绝。这个操作能同时验证锁、事务和冲突 SQL 是否真实生效比单纯读代码更容易暴露问题。本文还有配套的精品资源点击获取
返回列表