ARTICLE DETAIL

资讯详情

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

Java+SSM体育场地预约系统实战:从数据库建模到并发避坑全解析

Java+SSM体育场地预约系统实战:从数据库建模到并发避坑全解析 简介基于JavaSSM的体育场地预约使用系统毕业设计项目面向软件工程、计算机科学、人工智能等专业的在校生、教师及企业员工尤其适合需要完成毕业设计、课程设计或初期项目演示的人群。项目包含完整源码、数据库脚本与使用文档覆盖用户登录、场地信息展示、预约管理等核心业务从前端页面到后端逻辑再到数据持久化能帮助读者清晰理解SSM三大框架的协同工作方式。压缩包为zip格式共1157个文件以HTML、CSS、JavaScript等前端资源JSP动态页面Java程序及Jar依赖包为主辅以SQL数据库脚本与XML配置整体约19MB目录结构清楚方便按模块查阅和复用。已有68人浏览学习。项目属导师认可的高分优秀毕业设计答辩评审95分已在Windows和macOS下测试运行成功可直接用于毕设、课设参考也可在其上修改扩展适合计算机相关专业学生进阶学习。1. 基于 javaSSM 的体育场地预约系统为什么这个老组合最能打体育场地预约系统这类题目在每年的 Java 毕业设计里几乎是“常青树”。但你仔细想一下人工管理场地确实痛点明显——电话预约记错时间、球场被重复订、收费靠手写台账。用 java SSM 做一套预约系统正好把 Spring 的依赖注入、SpringMVC 的请求分发、MyBatis 的数据库操作全部串进一个真实业务里。对答辩来说SSM 不是最新框架却是最稳的框架它不会哗众取宠每一个注解、每一段 SQL 都能讲出“为什么这样设计”。本文不聊大概念直接按源码、数据库、使用文档这条线讲清楚这套系统怎么做、坑在哪、怎么拿高分。2. 拆开 SSM 三层架构预约流程在代码里到底怎么流转2.1 先把数据库表定下来场地、用户、预约订单怎么建模做这种系统最忌讳一上来写代码。我一般先花半天把表结构定清楚因为后面所有 Service 逻辑、Mapper 查询都围绕表转。体育场地预约系统至少需要三张核心表用户表user、场地表venue、预约订单表reservation。如果还要做后台管理可以再加一张管理员表或者直接在 user 表里加 role 字段区分。先看用户表CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(30) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT 密码(MD5), real_name varchar(30) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 联系电话, role tinyint(4) DEFAULT 1 COMMENT 1-普通用户 2-管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;用户表里我故意只存了业务必需的字段。毕业设计里常见的毛病是字段冗余——比如把“年龄”“性别”硬塞进去但预约系统根本用不到。多余的字段只会让注册页面、实体类和参数校验都跟着变复杂不值得。场地表CREATE TABLE venue ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 场地ID, name varchar(50) NOT NULL COMMENT 场地名称, type varchar(20) DEFAULT 篮球场 COMMENT 场地类型: 篮球场/羽毛球场/网球场, location varchar(100) DEFAULT NULL COMMENT 位置描述, price_per_hour decimal(10,2) DEFAULT 0.00 COMMENT 每小时价格, open_time time DEFAULT 08:00:00 COMMENT 开放时间, close_time time DEFAULT 22:00:00 COMMENT 关闭时间, status tinyint(4) DEFAULT 1 COMMENT 1-可预约 0-停止预约, description varchar(255) DEFAULT NULL COMMENT 场地介绍, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场地表;这里有个设计点open_time / close_time 是 time 类型不是字符串。做场地筛选时可以直接用 BETWEEN 判断不用做字符串比较SQL 也能走到索引。预约订单表是三张表里最关键的一张CREATE TABLE reservation ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 订单ID, user_id int(11) NOT NULL COMMENT 下单用户, venue_id int(11) NOT NULL COMMENT 预约场地, reserve_date date NOT NULL COMMENT 预约日期, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, total_price decimal(10,2) DEFAULT 0.00 COMMENT 订单金额, status tinyint(4) DEFAULT 1 COMMENT 1-已预约 2-已完成 3-已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;订单表必须加 status 字段这是订单状态机的起点已预约、已完成、已取消。很多初次做这个题目的同学把订单删掉来实现“取消预约”这是错的——订单是业务记录只能改状态不能删删了报表和后台统计就全乱了。三张表之间的外键关系我不建议在数据库层面真的建 FOREIGN KEY。MyBatis 体系里外键约束会拖慢写入速度而且删除顺序错了会报外键错误。用逻辑外键——也就是在实体类里存 user_id、venue_id靠应用层保证一致性。这是 MyBatis 项目的常见做法。2.2 请求怎么走完一圈从 DispatcherServlet 到 Mapper 的完整链路表定好了接下来看请求链路。SSM 就是 Spring Spring MVC MyBatis 三方协作。很多同学的误区是把它当成一个大框架其实它是三个独立组件拼装起来的Spring 负责管理对象IoC 容器SpringMVC 负责接 HTTP 请求Controller 分发MyBatis 负责 SQLMapper 映射。三者通过配置粘合。一个预约请求的完整链路是用户在前端点“提交预约”→ Tomcat 收到 HTTP 请求 → DispatcherServlet 根据 RequestMapping 找到对应的 Controller 方法 → Controller 调用 Service 接口 → Service 实现类里通过 Spring 注入的 Mapper 执行 SQL → MyBatis 把结果集映射成 Java 对象返回 → Service 把业务结果返回给 Controller → Controller 用 ModelAndView 或 ResponseBody 把结果写回页面。写成代码是这样的Controller RequestMapping(/reservation) public class ReservationController { Autowired private ReservationService reservationService; PostMapping(/book) public String book(RequestParam(venueId) int venueId, RequestParam(reserveDate) String reserveDate, RequestParam(startTime) String startTime, RequestParam(endTime) String endTime, HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/login.jsp; } try { reservationService.createReservation(user.getId(), venueId, reserveDate, startTime, endTime); model.addAttribute(msg, 预约成功); } catch (BusinessException e) { model.addAttribute(msg, e.getMessage()); } return reservation_result; } }这里的 Autowired 就是 Spring 的依赖注入Controller 不需要 new Service而是由容器把 Bean 送进来。答辩时这个点经常被追问“为什么用接口不用实现类”答案是方便替换实现和做单元测试——ServiceImpl 换掉Controller 一行不用改。注意 PostMapping 是 Spring 4.3 引入的注解老源码里如果用的是 RequestMapping(method RequestMethod.POST)两种写法等价但你别在同一个项目里混着用风格统一也是评分点。2.3 为什么这里选 MyBatis 而不是 JPA半自动 SQL 的掌控感我自己做这类系统会优先 MyBatis理由很实际预约系统的核心业务是“时段冲突检测”“场地费用计算”这两种都离不开手写 SQL。JPA/Hibernate 虽然能帮你生成大部分 CRUD但一旦出现“同一场地同一日期时间段重叠”这种查询HQL 写起来绕性能调优还得回头去看生成的 SQL中间隔了一层黑匣子。MyBatis 的好处是 SQL 完全在自己手里。比如判断一个场地在某个时段是否已被预约我可以写出精确的区间重叠判断select idcountOverlapping resultTypeint SELECT COUNT(*) FROM reservation WHERE venue_id #{venueId} AND reserve_date #{reserveDate} AND status 1 AND start_time lt; #{endTime} AND end_time gt; #{startTime} /select这段 SQL 的核心是区间重叠判断两条预约记录时间重叠的条件是“对方的开始时间早于我的结束时间且对方的结束时间晚于我的开始时间”。用数学表达就是 (start_time, end_time) 与 (start, end) 两个区间有交集。参数说明#{venueId}场地 IDMyBatis 的 #{} 是预编译占位符由 JDBC 的 PreparedStatement 处理能防 SQL 注入不要用 ${} 直接拼接#{endTime}本次预约的结束时间用于判断已有预约是否已经开始但尚未结束#{startTime}本次预约的开始时间status 1只统计“已预约”状态的订单已取消的订单不参与冲突这就是 MyBatis 的掌控感——SQL 怎么写、走哪个索引、能不能命中你一眼就能看明白不用去猜 ORM 替你做了什么。3. 从 ZIP 到本地跑通MySQL、Tomcat 与 Maven 的最小配置集3.1 版本配对是第一步JDK、MySQL、Tomcat、Spring 怎么选这类毕业设计源码包打开后第一个坑是环境版本不匹配。ZIP 里的 pom.xml 是作者当时的版本你用新版 JDK 或新版 MySQL 硬跑大概率直接翻车。我建议按下面这个配对来组件推荐版本说明JDK1.8绝大多数 SSM 毕业设计基于 JDK 8最稳Tomcat8.5 / 9.0对应 Servlet 3.1 / 4.0SSM 常规配置都兼容MySQL5.7 或 8.08.0 需要换驱动类并加时区参数Maven3.6.x3.8 以上也能用注意仓库镜像配置Spring4.3.x 或 5.x取决于源码注解写法统一版本MyBatis3.4.x / 3.5.x配套 mybatis-spring 版本连接池Druid 或 C3P0推荐 Druid自带监控页这个表里最容易出问题的是 MySQL。如果你本机装的是 MySQL 8.0驱动类名要写 com.mysql.cj.jdbc.Driver而老源码里写的是 com.mysql.jdbc.Driver启动时直接报 ClassNotFoundException——在 8.0 驱动包里老类被移除了。Spring 版本的坑更隐蔽。有的源码用的是 Spring 4.3 反射创建 Bean 的方式你在 JDK 17 上跑会报模块访问错误因为 JDK 17 对反射做了强限制。所以环境首选 JDK 1.8别拿新 JDK 去挑战老源码。3.2 建库建数据用 SQL 脚本初始化数据库ZIP 包里通常带一份 xxx.sql 数据库脚本有的叫 db_sport.sql有的叫 sports_venue.sql。拿到后先别急着执行用文本编辑器打开看编码。很多老机器上导出的是 GBK 编码直接导入 MySQL 会出现中文乱码表能建出来页面显示却全乱。我一般的导入步骤mysql -uroot -p # 进入 MySQL 命令行后 source /path/to/sports_venue.sql;或者干脆用命令行重定向mysql -uroot -p --default-character-setutf8mb4 sports_venue.sql注意 --default-character-setutf8mb4 这个参数。如果 SQL 文件本身就是 UTF-8 编码指定这个参数能保证所有中文注释和初始数据都按 UTF-8 存进去避免建表后插入中文变成问号。提示导入前先备份一份原始 SQL 文件执行出错时还能对照原文排查这是最便宜的后悔药。导入后验证USE sports_venue; SHOW TABLES; SELECT COUNT(*) FROM user;至少能看到 user、venue、reservation 三张表且用户表里有初始数据。如果 SQL 文件里已经插入了管理员账号比如 admin/123456后续后台登录就直接用它。如果没有初始数据需要自己补一条管理员记录INSERT INTO user (username, password, real_name, role) VALUES (admin, MD5(123456), 管理员, 2);密码用 MD5 加密存储是毕业设计里最常见的做法。但你要在答辩时主动说一句“实际生产环境应该用 BCrypt”这句话能让你从及格变成加分。3.3 Maven 构建与部署IDEA 里的三步操作接下来把源码导入 IDEA。常见的源码结构是 Maven 项目根目录下有 pom.xml。导入步骤三步IDEA 里 File → New → Project from Existing Sources选中解压后的目录选择 Import project from external model → Maven等待依赖下载完成后打开 Project Structure 检查 JDK 版本pom.xml 里的核心依赖大致长这样properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency !-- MyBatis 核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.10/version /dependency !-- mybatis 和 spring 的桥接包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.29/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency !-- JSP 标准标签库 -- dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies这里三个版本的对应关系最容易踩坑mybatis-spring 2.0.x 需要 mybatis 3.5 和 Spring 5.x如果源码是 Spring 4.xmybatis-spring 要用 1.3.x。配对关系Spring 版本mybatis-spring 版本说明4.3.x1.3.2老组合兼容 JDK 85.x2.0.x新组合需要 mybatis 3.5改完 pom.xml 后在 IDEA 右侧 Maven 面板执行# 清理并打包成 war mvn clean package -DskipTests参数说明clean删除 target 目录避免旧编译产物干扰package执行打包生成 .war 文件-DskipTests跳过单元测试很多毕业设计源码里测试类不完整跑测试反而报错打包完成后target 目录下会生成 xxx.war。把这个 war 文件拷贝到 Tomcat 的 webapps 目录下启动 Tomcat$CATALINA_HOME/bin/startup.sh # Linux/Mac或者 Windows 下双击 bin/startup.bat。Tomcat 启动后会自动解压 war 包。浏览器访问 http://localhost:8080/你的war包名/路径取决于 war 包文件名。也有源码不是 Maven 项目而是纯 webapp 目录结构。那就在 IDEA 里直接配置 TomcatRun → Edit Configurations → 添加 Tomcat Server → Deploy 里选择 Artifact → Application context 填 /sports_venue。这种方式更适合调试因为可以断点跟踪源码。3.4 db.properties 与 Spring 配置数据库连接参数改哪几处部署前一定要改的是数据库连接配置。在 SSM 项目里通常是一个 jdbc.properties 或 db.properties 文件放在 src/main/resources 下。典型内容jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/sports_venue?useSSLfalseuseUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的数据库密码 jdbc.initialSize5 jdbc.maxActive20逐项说明jdbc.driverMySQL 8.0 用 com.mysql.cj.jdbc.DriverMySQL 5.7 可以用老驱动类jdbc.url 里的 serverTimezoneAsia/Shanghai 是关键MySQL 8.0 不写时区参数会报时区错误useSSLfalse本地开发不需要 SSL 加密省掉一堆告警日志characterEncodingutf8保证中文正常读写jdbc.password改成你自己的 MySQL 密码这是几乎 100% 会被忽略的一处Spring 配置文件里通过属性占位符读取这个 properties 文件再把参数注入到 Druid 数据源。如果启动时日志报 Cannot load driver class先去检查 mysql-connector-java 是不是真的下载下来了——Maven 依赖下载失败很常见排查方法是看 IDEA 的 Maven 面板 Dependencies 下有没有那个 jar没有就右键项目 → Maven → Reload Project。4. 预约与后台管理这样落地登录态、时段冲突与订单状态机4.1 用户登录Session 怎么存、拦截器怎么拦登录是每个系统的第一个模块也是答辩必问的“你怎么保证未登录用户不能访问”。SSM 项目的常见做法是用 Session 保持登录态再用 SpringMVC 的拦截器做统一校验。登录的 Service 层逻辑Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public User login(String username, String password) { // 密码存的是 MD5比对前先加密 String md5Pwd MD5Util.md5(password); User user userMapper.findByUsernameAndPassword(username, md5Pwd); return user; } }Controller 里登录成功后把 user 对象放进 SessionPostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); // 管理员进后台普通用户进首页 if (user.getRole() 2) { return redirect:/admin/index; } return redirect:/index; }这里有一个设计取舍把 role 塞进 Session 的 User 对象里每个请求都能直接读不用每次查库。缺点是改角色后要重新登录才生效。对毕业设计来说这个取舍值得省掉一次数据库查询代码也简洁。拦截器配置在 spring-mvc.xml 里mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ mvc:exclude-mapping path/admin/login/ bean classcom.example.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptors拦截器实现类public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user (User) request.getSession().getAttribute(loginUser); if (user null || user.getRole() ! 2) { response.sendRedirect(request.getContextPath() /admin/login); return false; } return true; } }这段代码的逻辑说明preHandle 返回 false 表示请求被拦截不能继续向后走。这里拦截了所有 /admin/** 路径但放行了 /admin/login 本身否则管理员永远登录不进去。判断条件先判空再取 role——Java 里 user 为 null 时直接调 getRole() 会抛 NullPointerException这个顺序不能反。4.2 预约下单事务 冲突检测的完整代码预约功能是整个系统的核心。前面 2.3 写了冲突检测的 SQL这里把它接进 Service 层。关键点有两个事务和逻辑顺序。Service public class ReservationServiceImpl implements ReservationService { Autowired private ReservationMapper reservationMapper; Autowired private VenueMapper venueMapper; Override Transactional(rollbackFor Exception.class) public Reservation createReservation(int userId, int venueId, String reserveDate, String startTime, String endTime) { // 1. 校验场地是否有效 Venue venue venueMapper.findById(venueId); if (venue null || venue.getStatus() ! 1) { throw new BusinessException(场地不存在或已停止预约); } // 2. 校验预约时间是否在开放时间内 if (!isWithinOpenTime(venue, startTime, endTime)) { throw new BusinessException(预约时间不在场地开放范围内); } // 3. 校验时段冲突 int count reservationMapper.countOverlapping(venueId, reserveDate, startTime, endTime); if (count 0) { throw new BusinessException(该时段已被预约请选择其他时间); } // 4. 计算价格并插入订单 double price calculatePrice(venue.getPricePerHour(), startTime, endTime); Reservation reservation new Reservation(); reservation.setUserId(userId); reservation.setVenueId(venueId); reservation.setReserveDate(DateUtil.parseDate(reserveDate)); reservation.setStartTime(DateUtil.parseTime(startTime)); reservation.setEndTime(DateUtil.parseTime(endTime)); reservation.setStatus(1); reservation.setTotalPrice(price); reservationMapper.insert(reservation); return reservation; } }逻辑说明Transactional(rollbackFor Exception.class) 表示方法内任何一个异常都会回滚整个事务。如果插入订单失败前面做的任何操作都不会留下半截数据。这里的 rollbackFor Exception.class 值得记住默认的 Transactional 只对 RuntimeException 回滚对普通 Exception 不回滚这是很多同学忽略的坑。步骤 3 的冲突检测是 SELECT步骤 4 的插入是 INSERT并发场景下两者之间存在时间差。两个用户同时提交时可能都查到 count 0然后都插入成功——这就是超卖问题。完整的解法是用数据库行锁或唯一索引毕业设计层面常见的兜底方案我放到第 5 章的避坑里展开。4.3 后台管理场地上下架与订单状态流转后台管理模块相对简单但它是区分“及格”和“高分”的关键。一般来说后台至少需要两个功能场地管理上下架、改价、增删改查和订单管理查看、取消、标记完成。场地管理的 ControllerController RequestMapping(/admin/venue) public class AdminVenueController { Autowired private VenueService venueService; PostMapping(/updateStatus) public String updateStatus(RequestParam(id) int id, RequestParam(status) int status) { venueService.updateStatus(id, status); return redirect:/admin/venue/list; } PostMapping(/save) public String save(Venue venue) { venueService.save(venue); return redirect:/admin/venue/list; } }两个方法都返回 redirect:/admin/venue/list这是 PRG 模式Post/Redirect/Get防止表单重复提交。如果不重定向用户按 F5 刷新会重复提交同一个请求导致同一条数据被插入两次。订单的状态机是重点。预约订单的三个状态1 已预约、2 已完成、3 已取消。状态流转只有两种情况已预约 → 已完成管理员点“完成”、已预约 → 已取消用户或管理员取消。不允许从“已完成”改回“已预约”也不允许“已取消”改成任何状态。这个流转规则要在 Service 层写死Transactional public void cancelReservation(int id) { Reservation r reservationMapper.findById(id); if (r null || r.getStatus() ! 1) { throw new BusinessException(订单不存在或当前状态不可取消); } reservationMapper.updateStatus(id, 3); }这里判断 status ! 1 才能取消就是为了挡住“把已完成的订单取消掉”这种非法操作。状态机的好处是数据永远处于合法状态报表统计也不会被“删除掉的历史订单”干扰——订单记录永远存在只是状态变了。5. 避坑这套系统最常见的 5 个翻车点和排查步骤5.1 MySQL 8 连接失败ClassNotFoundException 和 Unknown database现象Tomcat 启动后访问页面日志报 ClassNotFoundException: com.mysql.jdbc.Driver或者报 Unknown database sports_venue。原因MySQL 8.0 把老驱动类 com.mysql.jdbc.Driver 移到了新的包名下正确类名是 com.mysql.cj.jdbc.Driver且连接 URL 需要追加时区参数。Unknown database 则是数据库名和代码里 jdbc.url 写的不一致——这个问题很蠢但非常常见ZIP 里的 SQL 文件名和脚本里的库名经常对不上。解决打开 jdbc.properties改成driver 用 com.mysql.cj.jdbc.Driverurl 里的库名改成你真实导入的库名并拼上 serverTimezoneAsia/Shanghai。改完务必重启 Tomcat不要只刷新页面——数据库驱动类是在 Spring 容器启动时加载的热刷新不生效。5.2 Maven 依赖冲突Bean 创建失败 / NoSuchMethodError现象Tomcat 启动时 Spring 容器创建 Bean 报错日志堆栈里有 NoSuchMethodError 或 ClassNotFoundException指向某个 Spring 或 MyBatis 类。原因SSM 项目里 spring-webmvc、spring-jdbc、mybatis-spring 三者内部依赖的同类库版本打架。最典型的是 mybatis-spring 2.0.x 搭配 Spring 4.x启动时直接 NoSuchMethodError。另一个场景是 pom.xml 里同时依赖了老版本的 spring-jdbc 和新版 spring-webmvc不同版本的 spring-core 被同时加载进类路径。解决先确认 pom.xml 里 Spring 统一用 5.x或统一 4.3.x然后跑依赖树检查mvn dependency:tree -Dincludesorg.springframework看输出的依赖树里 Spring 组件是不是同一个版本。如果出现多个版本在 pom.xml 里对老版本统一加 exclusion只保留一个 Spring 主版本。mybatis-spring 的版本参照前面 3.3 的配对表。5.3 预约并发超卖同一时段被两个人同时订走现象验收时开两个浏览器登录两个账号对同一场地同一时段同时点预约两个都提示成功后台出现两条重叠订单。原因Service 里“查冲突 → 插入订单”是两个步骤中间存在时间窗口。两个请求都先执行 SELECT countOverlapping都返回 0然后都执行 INSERT就产生了重叠数据。单靠应用层判断防不住并发。解决毕业设计里至少要做到两步。第一步在 reservation 表的 (venue_id, reserve_date, start_time) 上建唯一索引虽然 start_time 相同才算冲突的设计不完全精确但至少挡住绝大多数同起点的重复预约ALTER TABLE reservation ADD UNIQUE KEY uk_venue_date_start (venue_id, reserve_date, start_time);第二步在 Service 里把冲突检测和插入包进事务插入时捕获 DuplicateKeyException 并转成友好提示try { reservationMapper.insert(reservation); } catch (DuplicateKeyException e) { throw new BusinessException(该时段刚被预约请刷新后重试); }这两步做完并发窗口从“无保护”缩小到“只剩 start_time 不同但时间段重叠”的边角场景。答辩时主动说清楚这个方案的边界评审反而会认可你对并发问题的理解深度。5.4 JSP 页面 404视图解析器路径写错现象点击“查看场地列表”后浏览器地址栏变成 xxx/listVenue.do页面却 404Tomcat 日志没有任何明显的 Java 异常。原因Controller 返回的逻辑视图名经过 InternalResourceViewResolver 拼前缀后缀后找不到对应 JSP。比如逻辑视图名是 venue/list配置的前缀是 /WEB-INF/views/后缀是 .jsp那真实路径必须是 /WEB-INF/views/venue/list.jsp。文件不在那里就 404。解决打开 spring-mvc.xml 检查视图解析器bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean然后按这个规则反查 JSP 文件的实际位置。我的经验是不要凭记忆放 JSP把每个 Controller 里 return 出来的字符串挨个和 webapp 目录下的文件比对一遍一分钟就能找出错位。另外注意Controller 方法返回字符串时如果方法上没加 ResponseBodySpringMVC 会把它当视图名如果接口想返回 JSON忘加 ResponseBody 的后果就是浏览器打开一个写满字符串的页面这种问题用浏览器开发者工具看响应内容一眼就能确认。5.5 中文乱码从页面到数据库的链条现象页面输入中文提交后数据库里存的是问号或乱码或者页面显示乱码。原因中文乱码是链路问题任何一个环节断了就乱。常见的三个断裂点页面编码不是 UTF-8、SpringMVC 的 CharacterEncodingFilter 没配、数据库表字符集不是 utf8mb4。三者缺一不可。解决逐个排查。首先确认每个 JSP 顶部有% page contentTypetext/html;charsetUTF-8 languagejava %其次在 web.xml 里配过滤器注意这段配置要放在过滤器链的最前面filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncodingtrue 表示请求和响应都强制用 UTF-8 编码。只配 encoding 不配 forceEncoding有时候 POST 请求能处理但对 GET 请求无效。最后确认建表时用的是 utf8mb4——utf8 在 MySQL 里是 utf8mb3存 emoji 会丢字符。这条链路我建议按“页面 → 过滤器 → 数据库”从前往后测每一环用已知中文字符串单独验证别整个链路一起猜。6. 答辩前还能做的三件事索引优化、慢查询验证与演示脚本6.1 两个索引让预约查询从全表扫描变成索引命中系统能跑和能拿高分之间隔着一件小事为什么你的页面比别人快。最常见的优化是给 reservation 表加组合索引ALTER TABLE reservation ADD INDEX idx_venue_date (venue_id, reserve_date, status);这个索引直接服务于前台“查某天某场地有没有空”的场景。加了之后用 EXPLAIN 验证EXPLAIN SELECT COUNT(*) FROM reservation WHERE venue_id 1 AND reserve_date 2024-06-01 AND status 1;看 type 列是否从 ALL 变成 refrows 是否从几百几千降到几条。这个验证过程在答辩时拿出来讲比“我用了缓存”这种空话有说服力得多。6.2 演示脚本照着走一遍别让评审等你操作我自己做毕业设计答辩时吃过亏现场打开系统管理员密码忘了页面卡在登录最后手忙脚乱。之后我给自己定了规矩每个系统备一份演示脚本按顺序走注册一个新账号展示表单校验效果用新账号登录进入场地列表选一个场地预约提交后看预约成功提示再约同一时段看系统提示已被预约退出登录用 admin 账号进后台找到该订单执行取消回到前台确认该时段恢复可约最后打开数据库执行一条 SELECT展示订单表的记录——包含新增、取消的状态变化。这个脚本的妙处在于每个步骤都对应一个业务点注册展示用户模块重复预约展示冲突检测取消展示状态机。每走一步你可以停下来讲一句底层实现答辩节奏完全在自己手里。还有一个细节演示前把浏览器缩放调到 100%数据库命令行窗口提前开好不要现场输密码。把这套系统的请求链路和并发边界都讲顺了你再去翻 Java 面试题里关于 Spring 容器、MyBatis 映射和数据库连接池的题目会发现都是同一套东西。这套 SSM 项目可能不是最新的选题但它是把 Java Web 基础串得最完整的选题之一——从数据建模到请求分发再到并发兜底每层都有可讲的内容。把上面的坑提前踩一遍你的调试速度会比光看文档快很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表