ARTICLE DETAIL

资讯详情

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

SSM+JSP场馆预约管理系统:源码部署与并发防超卖实战解析

SSM+JSP场馆预约管理系统:源码部署与并发防超卖实战解析 简介基于SSMJSP构建的场馆预约管理系统提供完整源码与数据库是面向计算机专业毕业设计的Java Web项目资源。系统以后端SpringMVC、Spring、MyBatis为框架前端采用JSP与jQuery按管理员、用户两类角色划分权限集成场地管理、轮播图管理、通知管理、场馆预定等实用功能可作为课程设计或毕业设计的参考原型。压缩包共1021个文件包括68个Java源文件、67个JSP页面、239个JS脚本、CSS与字体资源、76个Jar依赖包以及1个SQL数据库脚本资源整体大小22.43MB目录结构覆盖前端静态资源、后端业务逻辑、数据库初始化脚本与运行依赖下载后可在IntelliJ IDEA 2021.3中结合MySQL 5.7.26、Tomcat 7.0.73、JDK 1.8导入运行。目前已有47人学习读者可获得可直接部署的运行工程、清晰目录结构和完整数据表设计便于对照源码理解SSM框架整合、角色权限控制与场馆预约核心流程也能为后续功能扩展或论文撰写提供参考。1. 为什么 SSMJSP 的场馆预约管理系统到今天还有人做一提“基于 SSMJSP 的场馆预约管理系统源码数据库”很多新入行的同学第一反应是“JSP 不是过时了吗”。但实际交付场景里这类项目恰恰是课程设计、毕业设计、中小型体育馆内部预约、实验室机房预约里最常见的形态内网使用、用户量不大、权限简单、要快速交付源码加数据库脚本SSM 加 JSP 这套组合依然是很多团队的第一选择。这个标题背后解决的是三类问题场地时段怎么排、用户怎么预约、后台怎么确认订单核心是把“一个场馆一天 24 小时分成哪些时段、谁约了哪段、状态怎么流转”用一张关系型数据库理清楚。本文适合手里刚拿到一套这类源码、或者在选型阶段想评估这套方案值不值得投入的人。2. 拆开技术栈SSM 三层、JSP 页面与 MySQL 表结构设计2.1 SSM 三层在预约业务里如何分工Controller、Service、Mapper 各管哪一段这套系统的分层非常固定SpringMVC 负责请求路由Controller 层只接收参数、调用 Service、返回页面Spring 负责对象管理和事务Service 层写预约规则MyBatis 把 Java 方法与 SQL 绑定Mapper 层做数据访问。JSP 则承担视图渲染用 EL 表达式和 JSTL 标签把后端放进 request/session 里的数据打印出来。为什么这三层不能混着写我在维护这类源码时最常见的问题就是某个同学把查询逻辑写在 Controller 里SQL 拼在 Java 字符串里结果是改一个预约状态要动四个文件还要担心拼接引号出错。SSM 的价值是把“取参数”“算业务”“查数据库”三者隔开排查问题时先看异常在哪一层进来就报 400 是参数问题业务方法里抛异常是规则问题Mapper 层报 SQLException 才是数据库问题。选型理由也要说清楚这个标题里的系统是给“管理员维护场馆 普通用户按时间段抢约”这种中等复杂度业务用的。用户并发不高场地数量几十个以内不需要微服务不需要 Redis 队列MySQL 一张表加一个事务就够。JSP 在这个场景里的优势是自带 Session 操作登录状态直接挂在内置 session 上不需要写 Token 鉴权内网系统完全够用。2.2 数据库怎么建模用户、场馆、时段、预约订单四张表的字段与约束表设计是整个项目的地基我一般先画四张表用户表、场馆表、时段表、预约订单表。场馆和时段分开而不是在订单表里直接存“周一下午 3 点到 4 点”这种字符串。原因是时段要允许管理员提前批量生成同一个场馆同一天可以切出早中晚多个时段分开存之后改场馆信息不用动订单查冲突也只需要对时段表做时间区间判断。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT 密码建议存加盐后的哈希, real_name varchar(32) DEFAULT NULL COMMENT 真实姓名, phone varchar(16) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 1普通用户 2管理员, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE venue ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL, location varchar(128) DEFAULT NULL, price_per_hour decimal(10,2) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场馆表; CREATE TABLE slot ( id int(11) NOT NULL AUTO_INCREMENT, venue_id int(11) NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0空闲 1已占用 2关闭, PRIMARY KEY (id), UNIQUE KEY uk_venue_time (venue_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT时段表; CREATE TABLE booking ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, venue_id int(11) NOT NULL, slot_id int(11) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_slot (user_id, slot_id), KEY idx_slot (slot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;这套 SQL 里有几个点需要重点看字符集用了 utf8mb4 而不是 utf8因为用户填备注时可能带 emoji 或生僻字utf8 在 MySQL 5.7 里存不了这四个字节的字符时段的唯一键是venue_id start_time意思是同一个场馆同一开始时间只能有一个时段防止管理员后台重复生成订单表里放uk_user_slot唯一索引这是拦截“同一用户反复抢同一时段”的兜底手段后面第 6 章还会展开。字段类型方面价格用 decimal(10,2) 而不是 floatfloat 算钱在累计金额时会有二进制误差。表之间不建物理外键也是这类源码常见的做法。我说一下原因物理外键会导致后续删场馆、改用户时被约束卡住团队里如果有人直接操作数据库删数据外键会变成一颗定时炸弹。逻辑外键靠应用层保证代码里 SQL 多写一个 join 条件就行。2.3 预约状态机从可预约到已取消的流转与页面按钮联动预约系统的业务复杂度基本都集中在状态上。时段表有“空闲、已占用、关闭”三个状态订单表有“待确认、已确认、已完成、已取消”四个状态。正常的流转路径是用户选定空闲时段提交订单订单进入待确认管理员在后台确认时段从空闲变成已占用使用时间结束后管理员手动或定时任务把订单改为已完成时段恢复空闲。这个状态机必须前后端配合JSP 页面里的按钮不是写死的。待确认状态要显示“取消预约”按钮已确认状态要显示“确认到场/标记完成”已取消和已完成状态则不能显示任何操作按钮。用 JSTL 的c:if判断状态值比在 Java 代码里拼 HTML 干净得多。如果状态枚举值不一致比如数据库里存 1 表示已确认页面注释里写的是 0排查起来会非常费劲这是这个项目里最容易翻车的地方。3. 把源码跑起来从 IDEA 导入到 Tomcat 部署的最小步骤3.1 先把运行环境对齐JDK、Maven、Tomcat、MySQL 的版本组合跑 SSM 老项目的第一关不是代码是环境。很多同学卡在启动报错其实是 JDK 版本太新或者 MySQL 驱动不匹配。我常用的版本组合是下表这套拿到的源码如果 pom.xml 里指定了版本以 pom 为准。组件推荐版本说明JDK1.8这类项目大多按 javac 8 编译高版本可能遇到反射报错Maven3.6.x3.8 对仓库下载限制更严老仓库可能拉不下来Tomcat8.5 或 9.0对应 Servlet 3.1 / 4.0SSM 项目通用MySQL5.7 或 8.0两个版本驱动类名不一样后面有单独避坑IDEACommunity 版即可导入 Maven 工程用不到收费功能安装时注意JDK 环境变量JAVA_HOME必须指向 JDK 而不是 JRETomcat 不要解压到带空格的路径比如C:\Program Files这种路径在 IDEA 里偶发部署失败我一般放在D:\dev\apache-tomcat这种纯英文路径下能少很多玄学问题。3.2 初始化数据库导入 SQL 脚本与修改 jdbc 配置两个必做动作拿到手的源码包里一般会带一个db.sql或venue.sql文件这就是标题里“数据库”那部分的实物。把这套源码跑起来之前先建一个空库再把 SQL 文件导入进去。库名要和配置里一致否则后面改配置多一步。mysql -uroot -p CREATE DATABASE venue_booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; exit;mysql -uroot -p venue_booking db.sql如果源码里已经带了建库语句直接用mysql -uroot -p db.sql也可以但要注意 SQL 文件里是否写死了库名。导入后用mysql -uroot -p进去执行show tables;确认四张表都建出来了再说下一步。接着改jdbc.properties注意这个文件名可能是db.properties或application.properties内容大同小异jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/venue_booking?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordroot密码改成你自己数据库的密码serverTimezoneAsia/Shanghai在 MySQL 8.x 下不加必报错5.7 可以不加useSSLfalse是避免本地连接时警告刷屏。如果数据库名不是venue_booking这里对应改。这一步是整套系统的命门配置写错启动时就会报数据库连接失败。3.3 启动并验收打包部署到 Tomcat 后的三条验收路径导入 IDEA 的方式不多说解压源码包IDEA 里File - Open选中根目录下的pom.xml等 Maven 依赖下载完。启动方式有两种一种是在 IDEA 里配置 Tomcat Server 添加war exploded运行另一种是 Maven 打包后丢进 Tomcat 的 webapps 目录mvn clean package -DskipTestscp target/venue-booking.war /path/to/tomcat/webapps/ cd /path/to/tomcat/bin ./startup.sh打包后访问http://localhost:8080/venue-booking/注意最后的上下文路径是 war 包名改过包名这里也要改。验收动作按三条路径走先看登录页面能不能打开再用管理员账号登录后台能新增场馆并生成时段然后退出用普通用户注册登录选择一个空闲时段提交预约最后回到后台确认订单此时时段状态从空闲变成已占用。整条链路走通了说明这套源码在你机器上真正活了。4. 核心模块拆解预约提交、冲突检测与用户权限控制4.1 提交预约的核心逻辑先查后插的并发漏洞与事务处理预约系统的核心方法是“提交预约”几乎所有安全性和并发问题都集中在这。最朴素的写法是先查询时段是否空闲再插入订单但仔细想一下如果两个人同时提交同一个时段两个查询都看到空闲两个插入都成功就超卖了一晚。源码项目里的做法一般是在事务里对时段行加锁查询再更新状态和插入订单。Override Transactional(rollbackFor Exception.class) public boolean book(Integer userId, Integer slotId) { Slot slot slotMapper.selectByIdForUpdate(slotId); if (slot null || slot.getStatus() ! 0) { return false; } slot.setStatus(1); slotMapper.updateStatus(slot); Booking booking new Booking(); booking.setUserId(userId); booking.setSlotId(slotId); booking.setVenueId(slot.getVenueId()); booking.setStatus(0); booking.setCreateTime(new Date()); bookingMapper.insert(booking); return true; }这段代码的关键在selectByIdForUpdate对应的 Mapper XML 是SELECT * FROM slot WHERE id #{id} FOR UPDATE。FOR UPDATE是悲观锁事务开启后这一行数据被当前事务锁住其他事务的查询会阻塞到本事务提交或回滚。配合Transactional(rollbackFor Exception.class)如果中间任何一步抛异常状态更新和订单插入一起回滚不会出现时段标记占用但订单没生成的情况。参数说明rollbackFor里写Exception.class而不是默认的RuntimeException.class是把受检异常也纳入回滚比如主键冲突产生的DuplicateKeyException虽然是运行时异常但显式声明这个参数更保险。slotMapper.updateStatus(slot)更新的是整个对象的所有字段如果想防误改可以在 XML 里只写UPDATE slot SET status 1 WHERE id #{id} AND status 0这样影响行数为 0 时就说明时段已被抢。4.2 登录与权限拦截Filter 如何挡住未登录访问SSMJSP 项目里权限控制不需要 Spring Security一个 Filter 就够了。登录成功后把用户对象放进 session之后每个请求进 Filter检查 session 里有没有 user没有就重定向到登录页。这个是 Web 工程里最经典的做法好理解也好维护。public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); Object loginUser session null ? null : session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }注意request.getSession(false)里的false意思是拿不到 session 就返回 null而不是新建一个。如果写成getSession()每个未登录请求都会强制创建 session相当于拦截器白写了。Filter 配置在 web.xmlfilter filter-nameloginFilter/filter-name filter-classcom.venue.web.filter.LoginFilter/filter-class /filter filter-mapping filter-nameloginFilter/filter-name url-pattern/*/url-pattern /filter-mapping/*会拦截所有请求包括登录页、静态资源 JS/CSS 图片和登录接口。所以 Filter 内部要放行这些路径常见做法是判断请求 URI包含login.jsp、login.do、/static/、.css、.js、.png的直接放行。很多源码包已经写好了这部分如果没有你加上时一定要把静态资源排除掉否则登录页的样式会全部加载不出来。4.3 后台管理与个人信息页面场馆时段维护和数据回显后台管理的功能比较直白管理员维护场馆列表维护某场馆未来一段时间的时段查看所有预约订单并变更状态。这类页面的难点不在 SQL而在 JSP 数据回显。比如个人信息展示页面就是典型的“把 user 表字段显示在 JSP 表格里”。以 jsp 个人信息展示页面为例页面要显示当前登录用户的姓名、手机号、角色那么 Controller 里把用户对象放到 session 里即可table tr td姓名/td td${sessionScope.loginUser.realName}/td /tr tr td手机号/td td${sessionScope.loginUser.phone}/td /tr tr td角色/td td${sessionScope.loginUser.role 2 ? 管理员 : 普通用户}/td /tr /table${sessionScope.loginUser.realName}是 EL 表达式直接从 session 作用域里取对象的属性比 Java 代码里写session.getAttribute()再强转简洁得多。如果页面显示空白先看 Controller 里放的是session还是request用错了作用域数据就到不了页面。还有一种常见需求是在 JSP 页面上做图片坐标定位来点选场地这类交互不要指望 JSP 模板去实现JSP 只负责输出 HTML点击定位应该交给前端 JavaScript 处理后端接收坐标参数存库就行。后台管理页面的表格会做得比较宽场馆名称、时段起止、订单状态、操作按钮我用c:forEach遍历列表输出每行操作按钮里的id都是从列表对象里动态取出来的比如onclickconfirmOrder(${booking.id})。这种页面在 jQuery 时代很常见放到现在依然能跑前提是数据列表不能一次全查出来需要接分页先LIMIT offset, pageSize用着数据量大再上 PageHelper。5. SSMJSP 场馆预约系统的 5 个高频踩坑记录5.1 MySQL 8 连不上驱动类名与时区参数的翻车现场现象Tomcat 启动时报ClassNotFoundException: com.mysql.jdbc.Driver或者连接数据库时报Communications link failure再或者报The server time zone value йʱ is unrecognized。原因拿到的源码包里 pom 依赖可能还是老旧的mysql-connector-java 5.xMySQL 8.x 里驱动类名从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver同时要求连接串带serverTimezone参数否则驱动无法解析系统时区报错信息里还会出现中文乱码。解决把 pom 里 MySQL 驱动版本升到 8.0.xjdbc.properties里的jdbc.driver改为com.mysql.cj.jdbc.Driver连接串末尾加上serverTimezoneAsia/ShanghaiuseSSLfalse。如果不想升驱动也可以把 MySQL 库降级到 5.7但新环境装 5.7 反而更麻烦所以升级驱动才是更干净的路子。5.2 JSP 页面用 EL 取不到数据作用域和视图解析器的坑现象页面打开正常表格也渲染出来了但$(list}这种位置全是空控制台没有报错。这是这套系统里最隐蔽的坑之一问题不是数据没查到而是数据放的位置和 JSP 取的位置不一致。原因Controller 返回 ModelAndView 时把数据放在了model里这对应的是 request 作用域如果拦截器或过滤器把请求转发到了另一个 JSP数据还在原 request 里转发后新页面拿不到旧 request 的属性。还有一种情况是 project 里装了isELIgnored配置或者 web.xml 用的 Servlet 规范版本太低导致 EL 默认不启用。解决先看 JSP 顶部是否有% page isELIgnoredfalse %再检查 Controller 返回视图名时是否用redirect:重定向会新建 request需要把参数拼到 URL 后面最后在 JSP 页面里临时用(request.getAttribute(list)打印一下能确认是作用域问题还是 EL 开关问题。5.3 MyBatis update 返回 0状态改不动的三个常见原因现象后台点“确认订单”页面提示成功但刷新后订单状态还是待确认数据库里 status 也没变。原因第一Service 方法没有加TransactionalMyBatis 的 SqlSession 自动提交被关闭时更新会回滚第二Mapper XML 里的 update 语句 where 条件写错比如更新条件带了and status 0但当前订单状态不是 0第三Java 实体类属性名与表字段驼峰映射不上realName映射成real_name失败导致更新语句的 set 片段为空。解决先看控制台有没有 SQL 日志直接把 MyBatis 打印的 update 语句复制到数据库客户端里执行能判断是 SQL 条件问题还是事务问题。然后在 Service 方法上显式加Transactionalmybatis-config.xml里确认启用了map-underscore-to-camel-case否则就要在 resultMap 里逐一映射字段名。5.4 图片路径 404换了项目名就找不到资源的血泪经验现象场馆列表的图片在上个项目里好好的换个机器部署后全部裂开控制台报 404路径里显示的是/venue-booking/upload/xxx.jpg但浏览器请求的是/upload/xxx.jpg。原因JSP 页面里写了相对路径srcupload/xxx.jpg本地开发时项目上下文路径恰好是根路径没暴露问题部署时 war 包名带了前缀页面 URL 变了相对路径解析出来的根路径对不上图片就全丢了。这是 JSP 页面里最容易犯的路径问题改 Tomcat 端口或改项目名都会踩。解决页面上所有静态资源路径都用绝对路径拼接src\${pageContext.request.contextPath}/upload/xxx.jpg。上传的图片要存在服务器磁盘目录而不是项目部署目录因为 Tomcat 解压 war 后每次更新部署会清空 webapps 下的上传文件。常见的做法是单独配一个file.upload-dirSpringMVC 里用addResourceHandlers把磁盘目录映射到/upload/**访问路径。另外有人想在 JSP 页面上做图片坐标定位来标记场馆座位这种需求请交给前端用绝对定位或 canvas 实现模板引擎不负责坐标计算后端只存坐标值即可。5.5 数据库时间从页面变成英文Date 序列化与格式化处理现象场馆时段列表里的开始时间显示成Tue May 17 18:00:00 CST 2023用户看得一头雾水。原因数据库字段是 datetimeMyBatis 查出后映射成java.util.DateJSP 里直接输出对象调用的是toString()输出就是一长串英文格式。解决JSP 页面用 JSTL 的fmt:formatDate标签格式化fmt:formatDate value${slot.startTime} patternyyyy-MM-dd HH:mm/页头记得引入% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %。后端返回 JSON 接口时在实体类的 getter 上加JsonFormat(patternyyyy-MM-dd HH:mm:ss, timezoneGMT8)避免前端拿到的是 UTC 时间。数据库里存 datetime 比 timestamp 更省心因为 timestamp 有 2038 年上限且受数据库时区影响排错要少一层。6. 给预约系统加一道保命锁唯一索引、乐观锁与事务怎么配合这类系统真正上线后最容易出的事故不是功能缺失而是并发预约把同一时段卖给了两个人。我在前面第 4 章里用了悲观锁FOR UPDATE解决冲突但悲观锁在用户量上来后会有锁等待所以更稳妥的做法是“索引兜底、乐观锁控制、事务回滚”三层配合。第一层是数据库唯一索引也就是 booking 表里那个uk_user_slot唯一键。这层是最终防线即使代码逻辑全错了数据库也会拦住同一个人对同一个时段的重复预约插入时抛出DuplicateKeyException。第二层是乐观锁在更新时段状态时带上条件WHERE id #{slotId} AND status 0update 影响行数为 0 说明时段已经被人占了Service 直接返回失败提示不再继续插入订单。第三层是事务把“更新时段状态”和“插入订单”放在同一个Transactional方法里任何一步失败都回滚。验证方法很直观启动项目后用两个浏览器无痕窗口同时登录两个账号对同一个时段毫秒级前后点击提交最终库里只能出现一条 booking 记录slot 状态为 1。想看压力效果可以写一段并发脚本在命令行循环发起请求然后去查slot和booking两张表确认没有状态漂移。如果不用锁这个测试十次能复现两三次超卖。我现在的习惯是接任何一套预约类源码先翻它的 booking 表有没有唯一索引再翻 Service 有没有事务注解两个都没有就直接提醒对方上线前必须补否则高峰期迟早出事。这个三层方案已经把血泪经验压进去了希望帮到你。本文还有配套的精品资源点击获取
返回列表