news 2026/9/29 18:22:07

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+SSM体育场地预约系统实战:从数据库建模到并发避坑全解析

简介:基于Java+SSM的体育场地预约使用系统毕业设计项目,面向软件工程、计算机科学、人工智能等专业的在校生、教师及企业员工,尤其适合需要完成毕业设计、课程设计或初期项目演示的人群。项目包含完整源码、数据库脚本与使用文档,覆盖用户登录、场地信息展示、预约管理等核心业务,从前端页面到后端逻辑再到数据持久化,能帮助读者清晰理解SSM三大框架的协同工作方式。压缩包为zip格式,共1157个文件,以HTML、CSS、JavaScript等前端资源,JSP动态页面,Java程序及Jar依赖包为主,辅以SQL数据库脚本与XML配置,整体约19MB,目录结构清楚,方便按模块查阅和复用。已有68人浏览学习。项目属导师认可的高分优秀毕业设计,答辩评审95分,已在Windows和macOS下测试运行成功,可直接用于毕设、课设参考,也可在其上修改扩展,适合计算机相关专业学生进阶学习。

1. 基于 java+SSM 的体育场地预约系统:为什么这个老组合最能打

体育场地预约系统这类题目,在每年的 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 负责 SQL(Mapper 映射)。三者通过配置粘合。

一个预约请求的完整链路是:用户在前端点“提交预约”→ 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 id="countOverlapping" resultType="int"> 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}:场地 ID,MyBatis 的 #{} 是预编译占位符,由 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.0,SSM 常规配置都兼容
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-set=utf8mb4 < sports_venue.sql

注意 --default-character-set=utf8mb4 这个参数。如果 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。导入步骤三步:

  1. IDEA 里 File → New → Project from Existing Sources,选中解压后的目录
  2. 选择 Import project from external model → Maven
  3. 等待依赖下载完成后,打开 Project Structure 检查 JDK 版本

pom.xml 里的核心依赖大致长这样:

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!-- SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <!-- MyBatis 核心 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <!-- mybatis 和 spring 的桥接包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <!-- JSP 标准标签库 --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

这里三个版本的对应关系最容易踩坑:mybatis-spring 2.0.x 需要 mybatis 3.5+ 和 Spring 5.x;如果源码是 Spring 4.x,mybatis-spring 要用 1.3.x。配对关系:

Spring 版本mybatis-spring 版本说明
4.3.x1.3.2老组合,兼容 JDK 8
5.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 里直接配置 Tomcat:Run → Edit Configurations → 添加 Tomcat Server → Deploy 里选择 Artifact → Application context 填 /sports_venue。这种方式更适合调试,因为可以断点跟踪源码。

3.4 db.properties 与 Spring 配置:数据库连接参数改哪几处

部署前一定要改的是数据库连接配置。在 SSM 项目里通常是一个 jdbc.properties 或 db.properties 文件,放在 src/main/resources 下。典型内容:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/sports_venue?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的数据库密码 jdbc.initialSize=5 jdbc.maxActive=20

逐项说明:

  • jdbc.driver:MySQL 8.0 用 com.mysql.cj.jdbc.Driver;MySQL 5.7 可以用老驱动类
  • jdbc.url 里的 serverTimezone=Asia/Shanghai 是关键,MySQL 8.0 不写时区参数会报时区错误
  • useSSL=false:本地开发不需要 SSL 加密,省掉一堆告警日志
  • characterEncoding=utf8:保证中文正常读写
  • 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 对象放进 Session:

@PostMapping("/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 class="com.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 后台管理:场地上下架与订单状态流转

后台管理模块相对简单,但它是区分“及格”和“高分”的关键。一般来说后台至少需要两个功能:场地管理(上下架、改价、增删改查)和订单管理(查看、取消、标记完成)。

场地管理的 Controller:

@Controller @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.Driver;url 里的库名改成你真实导入的库名,并拼上 serverTimezone=Asia/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 -Dincludes=org.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,页面却 404,Tomcat 日志没有任何明显的 Java 异常。

原因:Controller 返回的逻辑视图名,经过 InternalResourceViewResolver 拼前缀后缀后找不到对应 JSP。比如逻辑视图名是 venue/list,配置的前缀是 /WEB-INF/views/,后缀是 .jsp,那真实路径必须是 /WEB-INF/views/venue/list.jsp。文件不在那里就 404。

解决:打开 spring-mvc.xml 检查视图解析器:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>

然后按这个规则反查 JSP 文件的实际位置。我的经验是:不要凭记忆放 JSP,把每个 Controller 里 return 出来的字符串挨个和 webapp 目录下的文件比对一遍,一分钟就能找出错位。另外注意:@Controller 方法返回字符串时,如果方法上没加 @ResponseBody,SpringMVC 会把它当视图名;如果接口想返回 JSON,忘加 @ResponseBody 的后果就是浏览器打开一个写满字符串的页面,这种问题用浏览器开发者工具看响应内容一眼就能确认。

5.5 中文乱码:从页面到数据库的链条

现象:页面输入中文提交后,数据库里存的是问号或乱码,或者页面显示乱码。

原因:中文乱码是链路问题,任何一个环节断了就乱。常见的三个断裂点:页面编码不是 UTF-8、SpringMVC 的 CharacterEncodingFilter 没配、数据库表字符集不是 utf8mb4。三者缺一不可。

解决:逐个排查。首先确认每个 JSP 顶部有:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

其次在 web.xml 里配过滤器,注意这段配置要放在过滤器链的最前面:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

forceEncoding=true 表示请求和响应都强制用 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 变成 ref,rows 是否从几百几千降到几条。这个验证过程在答辩时拿出来讲,比“我用了缓存”这种空话有说服力得多。

6.2 演示脚本:照着走一遍,别让评审等你操作

我自己做毕业设计答辩时吃过亏:现场打开系统,管理员密码忘了,页面卡在登录,最后手忙脚乱。之后我给自己定了规矩,每个系统备一份演示脚本,按顺序走:注册一个新账号,展示表单校验效果;用新账号登录,进入场地列表;选一个场地预约,提交后看预约成功提示;再约同一时段,看系统提示已被预约;退出登录,用 admin 账号进后台,找到该订单执行取消;回到前台确认该时段恢复可约;最后打开数据库执行一条 SELECT,展示订单表的记录——包含新增、取消的状态变化。

这个脚本的妙处在于每个步骤都对应一个业务点:注册展示用户模块,重复预约展示冲突检测,取消展示状态机。每走一步你可以停下来讲一句底层实现,答辩节奏完全在自己手里。还有一个细节:演示前把浏览器缩放调到 100%,数据库命令行窗口提前开好,不要现场输密码。

把这套系统的请求链路和并发边界都讲顺了,你再去翻 Java 面试题里关于 Spring 容器、MyBatis 映射和数据库连接池的题目,会发现都是同一套东西。这套 SSM 项目可能不是最新的选题,但它是把 Java Web 基础串得最完整的选题之一——从数据建模到请求分发,再到并发兜底,每层都有可讲的内容。把上面的坑提前踩一遍,你的调试速度会比光看文档快很多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:21:47

GitHub热榜深度观察:从Trending日榜到开源项目避坑指南

GitHub 热榜&#xff0c;也就是 Trending&#xff0c;几乎是开发者每天都会默认刷一下的地方。2026-09-25 的日榜拉下来&#xff0c;扫一眼项目名单&#xff0c;既有意料之中的 AI 工具链、Web 基础设施&#xff0c;也有几个第一次冒头、让人想点进去看看的新仓库。日榜这个东西…

作者头像 李华
网站建设 2026/9/29 18:21:07

GitHub Trending 技术选型与开源项目分析方法

我无法根据“GitHub 热榜项目&#xff1a;日榜&#xff08;2026-09-23&#xff09;”这一标题生成符合要求的博文。 原因如下&#xff1a; 该标题 不具备可拆解的实质性项目特征 ——它既不是具体的技术方案&#xff08;如“用 Python 实现 GitHub 仓库自动归档工具”&…

作者头像 李华
网站建设 2026/9/29 18:20:58

SRGAN超分重建实战:生成器设计、损失调整与训练避坑指南

简介&#xff1a;源自经典论文《Photo-Realistic Single Image Super-Resolution》的SRGAN超分辨率重建源码包&#xff0c;面向深度学习和计算机视觉研究者&#xff0c;提供从低分辨率到高分辨率图像的完整生成对抗网络实现&#xff0c;涵盖数据预处理、模型构建、感知/对抗损失…

作者头像 李华
网站建设 2026/9/29 18:20:57

多模态大模型:统一语义空间与跨模态推理实战指南

1. 这不是“又一个AI概念”&#xff0c;而是正在发生的生产力迁移“多模态大模型能干什么&#xff1f;”——这个问题最近在技术圈、产品会、甚至咖啡馆里被反复抛出&#xff0c;但多数回答还停留在“它能看图说话”“它能听懂语音”这种碎片化描述上。我从2022年Q4开始系统性地…

作者头像 李华
网站建设 2026/9/29 18:20:38

SSM+微信小程序全栈毕设实战:中国剪纸项目从联调到避坑

简介&#xff1a;基于Java、SSM、MySQL与微信小程序的中国剪纸小程序毕业设计包&#xff0c;定位为计算机专业毕业设计、课程设计与期末大作业的完整参考项目。压缩包共含815个文件&#xff0c;整体约22兆字节&#xff0c;涵盖后端Java源码、SSM框架配置、小程序前端页面、后台…

作者头像 李华
网站建设 2026/9/29 18:20:36

多模态知识库搭建实战:从RAG架构到Dify落地避坑指南

1. 传统知识库的“搜索天花板”&#xff1a;为什么关键词检索撑不起企业AI化先聊一个很多企业都有的困惑&#xff1a;我们已经上了知识库系统&#xff0c;员工每天也能搜到文档&#xff0c;为什么还是感觉“搜不到、用不上、答不准”&#xff1f;我接触过不少传统知识库项目&am…

作者头像 李华