☰
SpringMVC实战:篮球俱乐部场地预约与器材管理系统全拆解
2026/10/5 4:30:25 网站建设 项目流程

前两天一个学弟把毕设题目发给我,标题写得挺长:Java基于SpringMVC的篮球俱乐部在线平台、基于Web的篮球俱乐部综合服务平台、篮球场地预约与器材管理系统。他说老师给了三个名字,自己越看越懵,不知道到底要做几个系统。我看完直接告诉他:这其实是一套系统,只是站在不同角度起了三个名,而真正能支撑你写完整个毕设的,就是“场地预约”和“器材管理”这两条核心业务线。

这篇文章我就把这个题目彻底拆开讲。从选题拆解、技术选型、数据库设计,到SpringMVC核心配置、预约防冲突、器材防超借,再到IDEA 2024创建Web项目、404排查、中文乱码这些必踩的坑,最后是答辩演示技巧。内容尽量按我做项目时的思路来写,不是步骤清单罗列,而是把每个选择背后的为什么也讲清楚。适合正在做Java Web毕设、准备期末大作业,或者想认真理解一遍SSM分层的人参考。

1. 选题拆解:三个标题其实就是一套系统

1.1 标题里到底藏了几个需求

先看这三个名字之间的关系。“基于SpringMVC的篮球俱乐部在线平台”点名了技术栈是Java、SpringMVC,视角偏用户端;“基于Web的篮球俱乐部综合服务平台”强调的是Web化和“综合服务”,也就是除了场地预约,还得有器材、公告、后台管理这类配套功能;“篮球场地预约与器材管理系统”则直接点出了核心业务——场地预约和器材租借管理。

所以别被标题迷惑,需求就两句话:给篮球俱乐部做一个网站,会员能在线预约场地、租借器材,管理员能审核预约、管理器材和发布公告。围绕这个需求,功能模块可以拆成六块:用户注册登录、场地信息展示与预约、器材展示与租借归还、公告管理、后台数据统计、以及贯穿全局的权限控制。

1.2 核心角色与业务流程

系统里有三种角色:游客、注册会员、管理员。游客能浏览场地和器材信息,但一旦要预约或租借,必须登录;会员可以提交场地预约、查询自己的预约记录、提交器材租借申请;管理员负责审核预约、维护场地和器材数据、处理器材归还,以及查看统计报表。

两条核心业务线要理清楚。第一条是场地预约:用户选场地、选日期和时间段,系统自动做冲突检测,提交预约后由管理员审核,审核通过才算是占用场地。第二条是器材租借:用户到场地后凭预约记录租器材,离场前归还,管理员在后台确认归还状态,归还超时要计算额外费用。这两条线不是完全独立的,器材租借可以关联到某条预约记录上,后面做数据库关系时我会细讲。

1.3 谁适合拿这个题目做项目

这个题目对基础的要求不高不低。如果你已经学过Java基础、Servlet和JSP,那做起来会非常顺;如果还没接触过Web框架,建议先花一周把SpringMVC的入门Demo走一遍,再开始搭这个项目。

我的建议是:不要试图在系统里堆砌大量功能模块。毕设看重的不是你做了多少个页面,而是你能不能把一个核心流程讲透。场地预约和器材管理这两个模块,已经足够把你对SpringMVC分层、事务、并发控制、权限拦截这些知识的理解全部串起来。

2. 技术选型与项目骨架:SpringMVC为什么还是毕设常青树

2.1 SpringMVC的定位与核心机制

SpringMVC是Spring框架的Web层解决方案,核心职责很清晰:接收HTTP请求,解析请求参数,调用业务逻辑,再把结果渲染成页面返回给浏览器。它的工作流程可以简单概括为:请求到达DispatcherServlet,由DispatcherServlet根据URL找到对应的Controller方法,方法执行完成后返回ModelAndView,再经ViewResolver解析成具体的JSP页面。

毕设选SpringMVC,不是因为它新,而是因为它在学校课程里还没淘汰,资料足够多,代码结构又足够规范。更重要的是,SpringMVC的很多设计思想,比如HandlerMapping(请求映射)、Interceptor(拦截器)、HandlerMethodArgumentResolver(参数解析器),放到Spring Boot里依然是同一套底层逻辑。就算你答辩时被问“为什么不直接用Spring Boot”,你也能从底层机制的角度回答出来。

2.2 完整技术栈与版本建议

直接给一套我实测过很稳的版本组合:

组件推荐版本说明
JDK8或11避免太高版本,Tomcat和IDE兼容最省事
Spring Framework5.2.x / 5.3.x对应javax.servlet命名空间
SpringMVC与Spring同版本统一使用Spring 5系
MyBatis3.5.x持久层框架,SQL可控性好
MySQL5.7或8.0两者都可,连接串略有差异
Tomcat9.x对应javax.servlet,别用Tomcat 10
Maven3.6+ / 3.8+管理依赖和构建
前端JSP + JSTL + BootstrapJSP和SpringMVC视图解析天然契合

这里特别提醒:千万不要用Spring 6和Tomcat 10的组合。Spring 6把javax.servlet迁移到了jakarta.servlet,包名全变了,网上大部分SpringMVC资料还是基于javax的,一旦混用,编译时会报一堆“找不到符号”的错。

2.3 项目工程结构与分包规范

标准Maven War工程结构长这样:

basketball-club ├── pom.xml └── src/main ├── java/com/club │ ├── controller // 控制层,接收请求、返回视图 │ ├── service // 业务层,事务边界都在这里 │ ├── mapper // MyBatis接口层 │ ├── entity // 实体类,对应数据库表 │ ├── interceptor // 拦截器 │ └── util // 工具类 ├── resources │ ├── jdbc.properties // 数据库连接配置 │ ├── mybatis-config.xml // MyBatis全局配置 │ └── springmvc.xml // SpringMVC配置 └── webapp ├── static // 静态资源:css/js/images ├── WEB-INF │ ├── views // JSP页面 │ └── web.xml └── index.jsp

分层的逻辑一定要给老师讲清楚:Controller只负责参数接收和页面跳转,不写任何业务SQL;Service负责业务规则和事务管理,比如预约冲突检测、器材库存扣减都在这一层;Mapper只做数据库访问。这样做的好处是,万一预约逻辑出了问题,你只需要排查Service层,不用在Controller里翻几百行代码。

3. 数据库设计:预约不冲突,从表结构就要想好

3.1 核心表与字段设计

我把核心表浓缩成七张,刚好覆盖全部业务场景。

用户表是基础,字段至少包括用户名、密码、昵称、手机号、角色、状态;密码建议MD5或加盐后存储,别存明文。场地表要区分场地类型(室内/室外、全场/半场),最重要的字段是每小时价格和开放时间。预约表是核心中的核心,字段包括预约场地、预约日期、开始时间、结束时间、总价、状态;状态我用0待审核、1已确认、2已取消、3已完成来区分。器材表要有器材名称、分类、总库存、可用库存、每小时租赁价格。器材租借记录表要关联用户和器材,记录租出时间、归还时间、数量、费用和状态。再补一张公告表,用于后台发布通知。

这里有两个设计经验值得单独说。第一个,预约表不要按小时拆成多条重复记录,很多新手喜欢把“9点到10点”拆成一条“9点-10点”的记录,导致同一天同一块场地要存十几条数据,查询冗余还容易混乱。直接在预约表里存start_time和end_time两个字段,用区间判断冲突,既简洁又准确。第二个,器材表里要有total_stock和available_stock两个字段,前者是原始总数,后者是当前可借数,两个字段一对比就能看出有多少器材在外面没归还。

3.2 预约冲突检测:一段人人都该抄的SQL

场地预约最核心的问题是:同一块场地、同一天、同一时间段,绝对不能出现两条有效预约。很多人上来就写“start_time等于新开始时间或者end_time等于新结束时间”,这是错的,因为它只判断了端点,漏掉了时间段重叠的情况。

正确的判断是区间重叠公式:新的开始时间小于已有预约的结束时间,且新的结束时间大于已有预约的开始时间。写成SQL如下:

SELECT COUNT(*) FROM appointment WHERE court_id = #{courtId} AND appoint_date = #{appointDate} AND status = 1 AND start_time < #{endTime} AND end_time > #{startTime}

如果返回结果大于0,说明这个时间段已经被占了。这就是毕设里最值得炫耀的一段SQL。解释的时候可以比喻成两个会议:你的会议四点半结束,别人四点开始,虽然结束时间不一样,但四点那场和四点半那场中间有重叠,同样会冲突。只有使用“前者开始时间小于对方结束时间、前者结束时间大于对方开始时间”这个条件,才能覆盖所有重叠情况。

3.3 器材库存防超借:条件UPDATE比先查再减安全

器材租借涉及库存扣减,如果库存只剩两件,同时有两个用户来借三件,就会出问题。最常见的错误是先SELECT查询可用库存,判断大于等于3,再UPDATE减库存。这种方式在单用户操作下没问题,但并发访问时会超借,因为两个请求可能同时读到库存等于3。

安全的做法是直接用一条条件UPDATE让数据库来保证原子性:

UPDATE equipment SET available_stock = available_stock - #{quantity} WHERE id = #{id} AND available_stock >= #{quantity}

这条SQL的意思是:只有库存大于等于你要借的数量时,才允许扣减。如果受影响行数为0,说明库存不足,Service层抛出异常即可。归还时反向执行加库存操作,同时把租借记录状态从未归还改成已归还。把“先查后改”变成“条件式更新”,是彻底解决超借的关键思路。

4. 从零搭建与核心功能实现:照着能跑起来的步骤

4.1 环境准备:JDK、Maven、MySQL

不管用什么IDE,JDK环境变量必须配好。JAVA_HOME指向JDK安装目录,PATH里加上%JAVA_HOME%\bin,配置好后在命令行输入java -version验证。IDEA里再把Maven指向本地仓库,建议用阿里云镜像加速依赖下载,不然Spring相关依赖够你等半天。MySQL安装时如果选择8.0,要记住root密码,后面连接数据库要用。

4.2 web.xml与springmvc.xml配置

web.xml是整个Web应用的总入口,也是最容易出错的地方。核心要做两件事:配置CharacterEncodingFilter解决POST中文乱码,配置DispatcherServlet接管所有请求。

关键配置如下:

<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> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

springmvc.xml里五个配置缺一不可:开启注解扫描、开启注解驱动、配置视图解析器、放行静态资源、配置拦截器。

视图解析器这样配:

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

Controller里写return "court/list",就对应了/WEB-INF/views/court/list.jsp,这样页面被统一保护在WEB-INF目录下,外部无法直接访问JSP文件。

4.3 登录、权限与SpringMVC拦截器

SpringMVC拦截器是很多人面试时会被问到的点,也是这个项目里控制权限的核心手段。我的做法是定义两个拦截器:LoginInterceptor负责检查用户是否登录,AdminInterceptor负责检查当前用户角色是否为管理员。

配置方式如下:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/register"/> <mvc:exclude-mapping path="/static/**"/> <mvc:exclude-mapping path="/court/list"/> <bean class="com.club.interceptor.LoginInterceptor"/> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/admin/**"/> <bean class="com.club.interceptor.AdminInterceptor"/> </mvc:interceptor> </mvc:interceptors>

LoginInterceptor里做的事情很单一:判断Session里有没有user对象,没有就重定向到登录页。这里的坑在于必须把登录请求、注册请求和静态资源全放行,否则会陷入“访问页面要登录、但登录页面本身也要登录”的死循环。

4.4 场地预约功能完整实现链

以提交预约为例,把一次完整请求串起来看。用户在JSP页面选择场地、日期、开始时间和结束时间,表单提交到/appointment/save。Controller接收参数后调用Service:

@Controller @RequestMapping("/appointment") public class AppointmentController { @Autowired private AppointmentService appointmentService; @PostMapping("/save") public String save(@RequestParam Integer courtId, @RequestParam String appointDate, @RequestParam String startTime, @RequestParam String endTime, HttpSession session) { User user = (User) session.getAttribute("user"); appointmentService.createAppointment(user.getId(), courtId, appointDate, startTime, endTime); return "redirect:/appointment/mine"; } }

Service层放事务注解和冲突检测:

@Transactional public void createAppointment(Integer userId, Integer courtId, String appointDate, String startTime, String endTime) { int count = appointmentMapper.countConflict(courtId, appointDate, startTime, endTime); if (count > 0) { throw new RuntimeException("该时段已被预约,请更换时间"); } // 计算价格,由场地单价乘小时数得到 BigDecimal price = courtMapper.getPricePerHour(courtId) .multiply(BigDecimal.valueOf(computeHours(startTime, endTime))); appointmentMapper.insert(userId, courtId, appointDate, startTime, endTime, price, 0); }

这里要特别注意@Transactional的位置。为什么事务要加在Service而不是Controller?因为一个业务操作往往包含多次数据库操作,比如这里的冲突检查加插入记录,必须保证要么都成功、要么都失败;Controller只管接收和返回,不负责业务规则。

4.5 器材租借与归还

器材租借的核心是库存扣减和状态流转。用户提交租借申请时,Service层调用那条条件UPDATE扣减可用库存;扣减成功后,插入器材租借记录,状态为0代表未归还。

归还流程稍微复杂一点。管理员或用户提交归还时,系统要根据租出时间和当前时间计算是否超时,未超时直接把租借记录状态改为1,可用库存加回数量;超时的话,在原租金基础上追加超时费用,再改状态为2。超时费用不建议写得太复杂,按小时计费就够了,毕设答辩时重点讲清楚“费用如何计算、状态如何流转”,这一块的完整度比算法复杂程度更能打动老师。

5. 常见问题与排查技巧实录:这些坑我全替你踩过

5.1 IDEA 2024 创建Web项目找不到Servlet或Artifacts

IDEA 2024新建项目如果不选Maven骨架模板,默认不会帮你生成Web模块配置。很多人写完代码,启动Tomcat时发现没有Artifacts可选,或者没有WEB-INF目录。我的建议是创建项目时直接选Maven骨架maven-archetype-webapp,这样webapp目录和web.xml自动生成。

如果你已经建了普通Maven项目,补救方法是:在Project Structure里给模块手动添加Web Facet,再把src/main/webapp设为Web资源目录,然后在Artifacts里新建Web Application: Exploded。这一步做完,Tomcat部署配置里才能看到可选择的war包。

5.2 启动Tomcat访问Controller总是404

这是所有SpringMVC新手都会遇见的痛。我的排查顺序严格执行五步:先看Tomcat控制台有没有输出“SpringMVC DispatcherServlet初始化成功”的日志,没有就说明web.xml或jar包有问题;再看Controller类有没有加@Controller注解,以及Controller所在的包有没有被<context:component-scan>扫描到;接着检查浏览器访问的URL和@RequestMapping的路径是否完全一致;然后是看target目录下有没有生成对应的class文件,IDEA里经常需要rebuild;最后再看DispatcherServlet的url-pattern是/还是/*。

顺便解释一下/和/*的区别:/不会拦截JSP请求,/*会拦截一切请求,包括JSP渲染后的请求,导致页面也404。记得用/。

5.3 POST请求中文乱码

普通页面提交中文数据出现乱码,原因基本都是没有配置CharacterEncodingFilter,或者配置顺序不对。这个过滤器必须在DispatcherServlet之前执行,否则请求参数已经被SpringMVC读取成ISO-8859-1了,再乱转码也救不回来。另外MySQL连接串也要带上字符集参数:jdbc:mysql://localhost:3306/club?useUnicode=true&characterEncoding=utf8&useSSL=false。

5.4 静态资源CSS/JS全挂了

明明HTML结构正确,页面却完全没有样式。几乎可以断定是DispatcherServlet把静态资源请求也拦截了。解决方案就是在springmvc.xml里放行static目录:

<mvc:resources mapping="/static/**" location="/static/"/>

同时页面里引入路径必须写项目上下文前缀,用${pageContext.request.contextPath}拼接,否则部署到带项目名路径的Tomcat时,静态资源会全部404。

5.5 MySQL 8驱动包与时区报错

项目用的MySQL是5.7,连接驱动和8.0完全一样吗?不完全一样。MySQL 8的驱动类名改成了com.mysql.cj.jdbc.Driver,而且连接串里必须加时区参数serverTimezone=Asia/Shanghai。如果启动时还报“Public Key Retrieval is not allowed”,在连接串加allowPublicKeyRetrieval=true就好。

5.6 javax.servlet和jakarta.servlet版本混用

这个问题在新手群里出现率很高。网上有些教程是基于Spring 6和Tomcat 10写的,依赖包里是jakarta.servlet;而你的项目用的是Tomcat 9,类名是javax.servlet。一旦混用,所有涉及HttpServletRequest、HttpSession的代码全部编译报错。毕设阶段认准Spring 5 + Tomcat 9这一套组合,不要把版本混搭当个性。

6. 答辩与演示:从“能跑”到“会讲”

6.1 演示脚本怎么排

演示环节最忌讳的是现场从头登录开始慢慢操作,既浪费时间又容易中途报错。我的做法是把测试数据提前准备好,演示顺序控制在六个步骤:先展示首页和场地信息,让老师看到系统是完整的;然后切换到一个已登录账号,演示场地预约,重点展示冲突检测的效果——选一个已经被占用的时间段,系统会弹出提示;再演示器材租借,让老师看到库存数量发生变化;接着切到管理员账号,演示预约审核通过后场地被占用;最后打开后台统计页面,用柱状图或表格展示近一周的预约量。

每一步停留时间不超过20秒,核心是让老师看到“业务闭环”,而不是看你怎么填表。

6.2 几个高概率提问的应答思路

有些问题基本是必问的,提前准备答案就好。问“为什么用SpringMVC”,回答重点放在MVC思想和控制层与业务层分离的优势上,顺便提一句Spring生态的整合能力。问“事务为什么加在Service层”,要说出Controller管参数、Mapper管SQL、Service管业务规则的分层逻辑,事务要覆盖一个完整业务操作中的所有数据库操作。问“多人同时预约同一块场地会怎样”,先讲条件查询加事务保证数据一致,再补充说明极端情况下可以使用数据库行锁SELECT ... FOR UPDATE来串行化,毕设层面能把这两层讲清楚已经很能说明问题。问“能不能改成Spring Boot”,回答是能,并且Controller、Service、Mapper分层完全不用动,Spring Boot只是把这些配置自动化了。

这些问题放到真实的Java面试场景里也一样高频,所以准备的过程其实也是给面试做积累。

6.3 代码走读时主动讲什么

老师让你展示代码时,千万不要从头到尾念一遍。我会主动带着老师走一个完整请求的三层链路:从index.jsp页面发起预约请求,进入AppointmentController,再到AppointmentService里的@Transactional方法和冲突检测SQL,最后落到MyBatis的mapper XML。中途停下来强调两处细节:一是那条判断时间段是否重叠的SQL,解释区间重叠条件;二是器材库存扣减用条件UPDATE而不是先查再减,说明原子性。

这两处细节是项目里最能体现专业度的位置,主动讲出来,老师大概率会认为你真正理解了自己写的代码。最后说点我个人的体会。像“篮球俱乐部在线平台”这类毕设题目,真正拉开差距的从来不是页面数量多不多、界面炫不炫,而是你有没有把一个核心业务闭环做完整、做扎实,能不能把数据一致性问题解释清楚。把场地预约和器材管理这两条线吃透,把事务、拦截器、并发控制这几个SpringMVC的知识点讲明白,答辩时比堆十个花哨模块都更扛得住追问。

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

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

立即咨询