☰
基于SSM+JSP+MySQL的羽毛球交流平台毕业设计解析
2026/10/27 16:11:56 网站建设 项目流程

简介:这是一份面向Java初学者与正在完成毕业设计、课程设计学生的SSM框架实战项目,基于SSM+JSP+MySQL实现羽毛球交流平台,覆盖前台用户与后台管理两种角色的完整业务闭环。前台包含网站首页、用户注册登录、站内新闻详情、场地信息查询与在线预约、留言板互动、个人资料维护等模块;管理员端则负责系统后台的整体管理,可对预约、留言与新闻等数据进行维护。资源压缩包约48.8MB,内含项目源码、数据库SQL脚本、开发说明文档及常用软件工具,解压后按文档指引即可导入并运行调试。目前已有132人学习下载。对正在筹备毕设答辩或期末大作业的学生来说,可以借此完整走通SSM分层架构、JSP页面渲染与MySQL数据操作的串联流程,拿到的是一套可直接演示、可继续扩展改写的项目底稿。

1. 从“羽毛球交流平台”说起:这个 SSM 毕设到底值不值得你照着做

如果你正在 Java 课程设计或者毕业设计的选题列表里反复横跳,大概率会撞见这个近乎标配的标题:基于 SSM+JSP+MySQL 的羽毛球交流平台。它没有大数据、人工智能那么玄乎,却正好踩在高校《Java Web 应用开发》课程的主干知识点上:Spring 管对象、SpringMVC 收请求、MyBatis 写 SQL、JSP 出页面、MySQL 存数据。平台本身是一个带用户登录、场地预约、球友交流和帖子发布功能的业务系统,业务不复杂,却把 Web 开发的主线完整走了一遍。

这个方向的真正价值在于:既有能现场演示给老师看的页面,又有能写进论文的完整分层;哪怕你之前没写过 SSM 整合,照着源码和数据库也能在两小时内跑起来。适合三类人:准备答辩的毕业生、想低成本积累项目经验的自学者,以及刚把 Java 语法读完、需要一个综合项目练手的入门者。下面我按“选型、落地、实现、避坑、增值”的顺序把它拆透,让你不光拿到能运行的代码,还能讲明白每一步为什么要这么做。

2. SSM+JSP+MySQL 选型拆解:这套组合为什么成了毕设常青树

2.1 Spring 负责“容器”与“解耦”:不是背概念,是看它怎么管对象

我辅导学弟学妹时发现,很多人能背出“IoC 控制反转、DI 依赖注入”,但一到项目里就不知道该把哪个对象交给 Spring 管理。其实在这个羽毛球平台里,规矩可以压缩成一句话:凡是需要被多个地方复用的 Java 对象,都交给 Spring 容器。比如 UserService、ReserveService,你不去手动 new,而是在类上标一个 @Service,再把它注入到 Controller 里,由容器统一创建和销毁。

@Service public class UserService { @Autowired private UserMapper userMapper; public User login(String username, String password) { return userMapper.selectByUsernameAndPassword(username, password); } }

这段代码的逻辑很直白:UserService 自己不 new UserMapper,而是通过 @Autowired 让 Spring 把 MyBatis 生成的动态代理对象塞进来。好处是,如果哪一天你想把 MySQL 换成 PostgreSQL,只需要改数据源配置,UserService 一行代码都不用动,这正是“解耦”在毕业设计论文里最容易被答辩老师追问的落点。

参数说明:@Service 是 Spring 的组件注解,表示这个类要被容器管理;@Autowired 默认按类型注入,如果项目里配了同类型多个实现类,建议配合 @Qualifier 指定名称。另外,这部分内容在 Java 面试题里也高频出现,比如“Spring 的 Bean 作用域”“为什么用接口注入而不是实现类”,你答辩时能接住“容器”和“依赖查找”这两个词,通常印象分会更高。

2.2 SpringMVC 的请求分发:从前端 JSP 到 Controller 的路由规则

SpringMVC 的核心是 DispatcherServlet,它像一个前台:所有请求先进来,再由它根据 URL 找到对应的 Controller 方法。在这个羽毛球平台里,典型的一个登录请求是:JSP 表单把 username 和 password POST 给 /user/login,SpringMVC 的 HandlerMapping 找到 UserController 里的 login 方法,把参数绑定到方法形参上。

@Controller @RequestMapping("/user") public class UserController { @RequestMapping(value = "/login", method = RequestMethod.POST) public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) { User user = userService.login(username, password); ModelAndView mav = new ModelAndView(); if (user != null) { session.setAttribute("loginUser", user); mav.setViewName("redirect:/reserve/list"); } else { mav.addObject("error", "用户名或密码错误"); mav.setViewName("login"); } return mav; } }

逻辑说明:这个方法的返回值是 ModelAndView,既带数据又带视图。登录成功后重定向到 /reserve/list,通过 redirect 前缀强制浏览器发起新请求,避免表单重复提交;失败则回到 login 页面,把 error 放进 model,JSP 里用 ${error} 回显。

参数说明:@RequestParam 默认要求前端必须传同名参数,如果 JSP 里的 input name 写错,SpringMVC 会直接报 400,这是新手最容易翻车的点。另外,@RequestMapping 不加 method 时 GET 和 POST 都会匹配,安全性不严谨,毕业设计中我建议显式写明 method,这条可以在论文的可扩展性小节里当优化点写。

2.3 MyBatis 与 MySQL:SQL 写在 XML 里,参数怎么映射

MySQL 负责真相,MyBatis 负责把 Java 方法调用翻译成 SQL。为什么毕业设计里几乎都用 MyBatis 而不是 JdbcTemplate 或裸 JDBC?因为它把结果映射这件事做得足够省事:你只需要在 XML 里写一条 select,MyBatis 就能把结果集装进 User 对象,不用自己写 while (rs.next()) 的循环。

<select id="selectByUsernameAndPassword" resultType="com.example.entity.User"> SELECT * FROM user WHERE username = #{username} AND password = #{password} </select>

逻辑说明:resultType 告诉 MyBatis 把每行数据映射到哪个类;#{} 是预编译占位符,运行时会变成 ?,然后用 PreparedStatement 传参,能从源头挡住 SQL 注入。如果你手滑写成 ${username},那就是直接拼字符串,等于把数据库裸露给构造恶意输入的人。

参数说明:如果实体字段和数据库列名不一致,用 resultType 会映射失败,返回的字段全是 null。常见做法是写 resultMap,把列名显式对应到属性名,或者打开 mapUnderscoreToCamelCase 驼峰映射。这条在后面的避坑章节还会再提,因为它在 MySQL 命令行里看不到,只在 Java 侧暴露,属于典型的黑匣子问题。

2.4 JSP 在 SSM 里的真实定位:渲染层还是“过时”技术?

每次有人一看到 JSP 就说“这技术过时了”,但在毕业设计这个场景里,JSP 反而是最快出效果的选择。它能在页面里直接用 EL 表达式和 JSTL 标签输出数据库数据,不需要像前后端分离那样额外写 JSON 接口。比如“jsp 个人信息展示页面”这类需求,用 c:forEach 十几行就能把场地列表渲染出来。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <div class="court-list"> <c:forEach items="${courtList}" var="court"> <div class="court-item"> <h3>${court.name}</h3> <p>${court.location} · ${court.pricePerHour} 元/小时</p> </div> </c:forEach> </div>

逻辑说明:JSP 在 SSM 里只做渲染,不做业务。Controller 把查好的 List 放进 Model,JSP 再从 model 里取出来循环输出。这样做的最大好处是答辩演示时刷新页面就能看到数据变化,坏处是页面逻辑和 Java 代码耦合在同一个文件里,不适合大型系统维护,但这对单体毕设来说完全可以接受。

顺带回应一个搜索热词:“jsp 图片如何对坐标定位”这种需求,本质不是 JSP 技术,而是前端 JavaScript 在做图片裁剪时计算坐标。SSM 后端只需要接收裁剪后的坐标参数,把它和图片路径一起提交,坐标定位实际由 canvas 完成。后面在做上传功能时我会给一个简化版方案。

3. 把项目跑起来:从 JDK 到 Tomcat 的完整落地路径

3.1 环境清单与版本配对:JDK 8 / MySQL 5.7 或 8.0 / Tomcat 8.5

先说结论:如果你拿到的 zip 解压后自带源码加数据库和文档,那么不要贸然升级环境。JDK 用 1.8,Tomcat 用 8.5,MySQL 可以用 5.7 或 8.0。很多人问“MySQL 5.7 是不是停在 5.7.44 了”,是的,官方把维护重心放到了 8.0,但 5.7.44 跑这种规模的表结构完全够用;如果你非要用 8.0,JDBC 驱动要换成 mysql-connector-java 8.x,并且连接 URL 要处理时区和公钥问题。

组件推荐版本说明
JDK1.8(8u202 或相近)SSM 项目最常见的编译目标版本
Tomcat8.5.x与 JDK 8 兼容,支持 Servlet 3.1 / JSP 2.3
MySQL5.7.44 或 8.0.x5.7 安装省事;8.0 需要处理时区和公钥
Maven3.6.x管理 jar 依赖,不是必须但强烈建议

版本配错是不少人的“玄学翻车点”。我一般会让学生优先找 MySQL 5.7 的 ZIP 包,解压后初始化 data 目录,再用 net start mysql 启动服务。MySQL 8.0 的安装过程多几步,网上搜“mysql安装教程8.0”能看到大量踩坑记录,主要是初始密码获取和认证插件变化。如果你的机器上已经装了 8.0,也不用卸载,只需要在连接 URL 里多补两个参数,后面会讲。

提示:环境越接近项目原生的版本,越不容易碰到依赖兼容问题。毕业设计的核心是完成闭环,不是研究极限版本。

3.2 建库与初始化数据:SQL 脚本手动执行还是自动执行?

解压 zip 后,数据库文件夹里通常有一个 .sql 文件,比如 badminton.sql。常见做法是手动执行,因为你可以清楚看到建了哪些表、插了哪些初始数据,答辩时被问到“数据库怎么设计的”能答得出来。用 MySQL 命令行导入:

mysql -uroot -p123456 < badminton.sql

逻辑说明:这里没有指定数据库名,所以 SQL 文件内部必须有 CREATE DATABASE IF NOT EXISTS badminton; 和 USE badminton;,否则会导入到默认库。执行后如果报 “No database selected”,说明脚本里漏了 USE,你需要先手动建库,再用 -D 参数指定库名重跑。

参数说明:-p123456 表示密码与 p 连写,注意没有空格。如果密码里有特殊字符,建议只写 -uroot -p,回车后再输入密码,避免命令历史记录里泄露。导入完成后,用 mysql -uroot -p -e "use badminton; show tables;" 查看表是否都在,这是最常用的 MySQL 数据库常用命令之一,务必熟悉。

3.3 修改 jdbc.properties:连接池、时区、SSL 三个参数别偷懒

数据库连接配置在 src/main/resources/jdbc.properties(或 db.properties)里。这是把项目跑起来最容易出错的文件,MySQL 版本不同,参数差很多。我见过不少项目明明代码没问题,却在这里折腾一晚上的情况。

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/badminton?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

逻辑说明:driver 如果填 com.mysql.jdbc.Driver,对应 MySQL 5.x 的 JDBC 驱动;换 MySQL 8.0 后需要改成 com.mysql.cj.jdbc.Driver,否则启动时报 ClassNotFoundException。url 里的 useUnicode 和 characterEncoding 负责中文不乱码,useSSL=false 省去证书握手,serverTimezone 指定时区,避免 JDBC 取不到时区报错。

参数说明:serverTimezone=Asia/Shanghai 在 MySQL 8.0 下几乎必须加,不加会报 “The server time zone value is unrecognized”。如果你的数据库装在 Windows 10 上并且改了 MySQL 服务端口,url 里的 3306 也要同步改。再有就是连接池,比如 druid 的配置会覆盖一部分行为,但 dataSource 的 driver 和 url 始终由这份文件控制,改 pool 配置不能替代这里的修正。

3.4 部署到 Tomcat:IDEA 配置 Artifact 与本地启动验证

新手容易在 IDEA 的部署配置里迷路。先说最稳的做法:打开 IDEA 后进入 File -> Project Structure -> Artifacts,新建 Web Application Exploded,然后配置 Tomcat Server 的 Deployment,把 Artifact 添加进去,Application context 填 /badminton。启动后浏览器访问 http://localhost:8080/badminton/ 即可。

# 如果你习惯命令行,可以在 Windows 下用 Tomcat 的 catalina.bat 启动 cd D:\apache-tomcat-8.5.xx\bin catalina.bat run

逻辑说明:Exploded 表示不打成 war 包,直接把编译后的 classes 和 webapp 资源放到 Tomcat 的 webapps 下,改代码后重新 build 就能生效,调试速度比打包 war 快很多。访问路径里必须带 Application context,否则 404。

参数说明:开发阶段 Application context 可以填 /,这样 URL 最干净;但要注意页面里的静态资源路径如果写成绝对路径 /css/style.css,会和你项目的 context 冲突。启动后按 F12 打开开发者工具的 Network 面板,确认 CSS、JS 状态是 200,而不是 404,这一步能看到很多“页面白板但控制台没报错”背后的真相。

4. 核心功能实现:羽毛球交流平台的“场地预约”闭环

4.1 数据库表设计:用户、场地、预约、帖子四张表怎么关联

平台最核心的是预约闭环。最少需要四张表:用户表 user、场地表 court、预约记录表 reservation、交流帖子表 post。我见过一些毕设把所有信息塞进一张大表,答辩时被老师问“第三范式”就卡壳,所以这边宁可多建几张表,把关联关系讲清楚。

CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, court_id INT NOT NULL, reserve_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT DEFAULT 1, UNIQUE KEY uk_court_time (court_id, reserve_date, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:reservation 表通过 user_id 和 court_id 与用户表、场地表建立逻辑关联。注意删除预约不要物理 DELETE,而是用 status 字段做逻辑删除,论文里可以写“保留历史预约记录,便于后续统计分析”。UNIQUE 约束是防止同一场地同一时间段被重复预约的第一道保险。

参数说明:start_time 和 end_time 用 TIME 类型,比用 VARCHAR 存“09:00”更规范;reserve_date 用 DATE,但要注意 Java 侧的 LocalDate 映射。如果使用 MySQL 5.7,utf8mb4 比 utf8 多占一点空间,但能存下 emoji,交流帖子里带表情不会变问号。

4.2 登录与拦截器:Session 校验的 Controller 写法

登录是门面,但如果只做登录不拦未登录用户,预约接口形同虚设。因此需要写一个 HandlerInterceptor,在进入 Controller 之前检查 session 里有没有 loginUser。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/user/login"); return false; } return true; } }

逻辑说明:preHandle 返回 false 表示请求被拦截,直接重定向到登录页;返回 true 才继续往后走。HandlerInterceptor 是 SpringMVC 提供的扩展点,不需要额外引包。随后需要在 springmvc.xml 里注册该拦截器,并配置 exclude-mapping 放行登录页和静态资源。

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/user/login"/> <mvc:exclude-mapping path="/css/**"/> <mvc:exclude-mapping path="/js/**"/> <mvc:bean class="com.example.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

参数说明:path="/**" 表示拦截所有请求;exclude 放行登录接口和静态资源,这是网上教程最容易漏的一步,漏掉的后果是样式全丢、页面变成白板。另外,Session 默认存活时间 30 分钟,如果答辩演示时嫌频繁重新登录,可以在 web.xml 里配置 session-config,把时间调长,比如 60 分钟。

4.3 场地预约的并发控制:MyBatis 乐观锁怎么用

预约并发是这套毕设的加分点。如果不上锁,两个用户同时提交同一场地同一时段,很可能两个请求都判断“可预约”,然后各插一条记录。UNIQUE 约束会挡住第二个,但它会直接抛异常,用户看到 500 页面,体验很差。更好的做法是使用乐观锁:预约前判断当前时段,然后更新场地表的 version 字段,以影响行数判断是否冲突。

@Transactional(rollbackFor = Exception.class) public boolean reserve(int userId, int courtId, String date, String time) { Court court = courtMapper.selectById(courtId); if (court == null) { return false; } int rows = courtMapper.incrementVersion(courtId, court.getVersion()); if (rows == 0) { return false; // 版本号不匹配,说明已被其他人抢先修改 } Reservation r = new Reservation(userId, courtId, date, time); reservationMapper.insert(r); return true; }
<update id="incrementVersion"> UPDATE court SET version = version + 1, last_reserve_time = NOW() WHERE id = #{id} AND version = #{version} </update>

逻辑说明:先查出当前 version,执行 update 时把 version 作为条件,如果影响行数为 0,说明条件不成立,已被其他事务改过,直接返回 false,让用户换时段。这里没有用悲观锁的 SELECT FOR UPDATE,是为了减少锁竞争,也更容易在论文里解释“乐观锁利用版本号避免更新丢失”。

血泪经验:@Transactional(rollbackFor = Exception.class) 必须写,否则 update 成功之后 insert 失败,场地次数被扣了但预约记录没有生成,属于数据不一致,答辩时被追问会很被动。还要注意 MySQL 事务隔离级别默认是可重复读,两个并发事务之间版本号的可见性在这里反而帮了忙,不用额外调隔离级别。

4.4 交流帖子的图片上传:JSP 提交 multipart 表单的完整链路

交流帖子的图片上传,在 JSP 里就是一个 enctype 为 multipart/form-data 的表单,Spring 侧用 MultipartFile 接收。这里实现一个简化版方案:把文件存到本地目录,用时间戳重命名,再把文件名存到数据库。

<form action="${pageContext.request.contextPath}/post/add" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="标题"/> <textarea name="content" placeholder="内容"></textarea> <input type="file" name="image" accept="image/*"/> <button type="submit">发布</button> </form>
@RequestMapping(value = "/post/add", method = RequestMethod.POST) public String addPost(@RequestParam("title") String title, @RequestParam("content") String content, @RequestParam(value = "image", required = false) MultipartFile image, HttpServletRequest request) throws IOException { Post post = new Post(); post.setTitle(title); post.setContent(content); if (image != null && !image.isEmpty()) { String realPath = request.getServletContext().getRealPath("/uploads/"); String fileName = System.currentTimeMillis() + "_" + image.getOriginalFilename(); image.transferTo(new File(realPath, fileName)); post.setImagePath("/uploads/" + fileName); } postService.insert(post); return "redirect:/post/list"; }

逻辑说明:图片不是存进数据库,而是存到 Tomcat 的 webapps 下的 uploads 目录,数据库只存相对路径。这样页面上用就能直接访问,数据库表体量也不会因为二进制内容而膨胀。时间戳前缀是避免多用户上传同名文件互相覆盖的标准做法。

参数说明:accept="image/*" 只是前端过滤,后端一定要再判断文件类型和大小,否则有人上传一个 .jsp 文件,Tomcat 有可能把它当作脚本执行,这是非常现实的安全隐患,毕业设计里容易被忽略。对于“jsp 图片如何对坐标定位”的困惑,头像裁剪可以在前端用 canvas 计算坐标,再通过隐藏域提交,后端只需要存图片路径和坐标参数,不需要在后端处理图像算法。

5. 避坑/常见问题:SSM 项目从“能运行”到“稳运行”的五个坑

5.1 现象:Tomcat 启动报 ClassNotFoundException:org.springframework.web.context.ContextLoaderListener

原因:IDEA 的 Artifact 没有把 Maven 依赖打包进去。具体来说,Project Structure -> Artifacts 里 Output Layout 缺少 libs 目录,导致 Tomcat 启动时找不到 Spring 的类。这种情况最容易出现在导入 zip 源码后沿用原配置,但 Maven 本地仓库路径变了的时候。

解决:打开 File -> Project Structure -> Artifacts,选中当前 Artifact,在 Output Layout 里点击右键选择 Put into Output Root,把依赖的库加入;然后 Rebuild Project 再重启 Tomcat。另外确认 Maven 本地仓库路径不要放在中文目录下,否则解析依赖也容易莫名其妙失败。

5.2 现象:页面访问 404,但控制台没有一条异常日志

原因:URL 路径与 Controller 的 RequestMapping 值不一致,或者项目 context path 没拼对。常见误用是把 DispatcherServlet 的 url-pattern 配成 /*,导致所有请求都进入 SpringMVC,JSP 页面和静态资源无法访问,前端就想当然认为是后端写错接口。

解决:先把 url-pattern 改成 /(斜杠),而不是 /*;再检查 springmvc.xml 里是否配置了 mvc:default-servlet-handler 或 mvc:resources 放行静态资源。页面里的链接用 ${pageContext.request.contextPath} 自动拼上 context path,避免写死绝对路径。改完重启,F12 看 Network,404 会明显减少。

5.3 现象:连接 MySQL 8.0 启动报错 Public Key Retrieval is not allowed

原因:MySQL 8.0 默认使用 caching_sha2_password 认证,首次连接需要从服务器获取公钥来加密密码,JDBC 驱动默认不允许这个操作。这个是“mysql安装教程8.0”和“mysql安装配置教程”下面最常见的后续问题之一,很多人安装阶段没问题,一接 Java 就翻车。

解决:在 jdbc.url 末尾加 allowPublicKeyRetrieval=true,并把驱动换成 mysql-connector-java 8.0.33 以上。如果项目里的驱动还是 5.1.x,直接换版本。如果你对数据库权限熟悉,也可以把用户认证方式改成 mysql_native_password,但为了不动数据库配置,我更建议在连接串里加参数。

5.4 现象:页面中文正常,但存入 MySQL 后变成 ?? 问号

原因:MySQL 表或字段字符集不是 utf8,或者 JDBC 连接里没有 characterEncoding=utf8。最常见的情况是建表时没写 DEFAULT CHARSET=utf8mb4,MySQL 用了默认的 latin1,中文自然存不进去。

解决:先改表字符集:ALTER TABLE court CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 再检查 jdbc.url 是否包含 characterEncoding=utf8,最后用 SHOW CREATE TABLE court 确认 DEFAULT CHARSET 已经是 utf8mb4。如果还乱码,可能是 JSP 页面本身的 page 指令编码不是 UTF-8,三个地方要一起看。

5.5 现象:查询结果里 Java 实体字段是 null,数据库里明明有值

原因:数据库列名是下划线风格(create_time),Java 属性是驼峰(createTime),MyBatis 自动映射时匹配不上。resultType 是把列名直接和属性名对比,create_time 对不上 createTime,于是那一列返回 null。

解决:在 mybatis-config.xml 里加 ,一行搞定。如果你需要精细控制,可以写 resultMap 显式映射,但毕业设计追求效率,我建议先用驼峰映射,后面如果出现个别特殊字段再单独写 resultMap。这个坑在 MySQL 命令里完全看不出问题,属于典型黑匣子问题。

6. 给羽毛球平台加上“积分签到”闭环:一个能写进论文的功能扩展

6.1 需求与表结构变更:签到表怎么设计

大部分羽毛球平台项目只做到“预约场地”,答辩演示千篇一律。一个低成本但能写进论文的扩展是“积分签到”:用户每天可签到一次,连续签到 7 天送一次免费预约机会。表结构只需要加一张 sign_in 表,并在 user 表里加一个 free_reserve_cnt 字段。

CREATE TABLE sign_in ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, sign_date DATE NOT NULL, UNIQUE KEY uk_user_date (user_id, sign_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6.2 实现要点:Service 事务与防重复签到

核心逻辑在 Service 层用事务包住,先查今天有没有签到记录,有就直接返回 false,没有就插入并将用户的可预约次数加一。

@Transactional(rollbackFor = Exception.class) public boolean signIn(int userId) { LocalDate today = LocalDate.now(); SignIn signIn = signInMapper.selectByUserIdAndDate(userId, today); if (signIn != null) { return false; // 今天已经签过,防止重复签到 } signInMapper.insert(userId, today); Integer count = signInMapper.countThisWeek(userId); if (count != null && count % 7 == 0) { userMapper.increaseFreeReserveCnt(userId); } return true; }

逻辑说明:today 由服务端生成,不要信任前端传过来的日期,否则用户可以随便改系统日期刷积分。签到表上的 UNIQUE 约束是数据库层面的最后防线,即使两个请求同时进来,也只有一个能插入成功。countThisWeek 统计的是从当前周一到今天的签到记录数,达到 7 的倍数就增加免费预约次数。

6.3 验证方法:用并发请求和数据库约束来验证

验证方式很简单:准备两个浏览器标签页,同时点击签到,观察是否只有一次成功。也可以直接写一个循环测试,模拟 20 个线程同时签到,看看最终 sign_in 表里该用户当天是否只有一条记录。如果出现两条,说明事务边界有问题,应该回查 @Transactional 是否生效。

我带学弟做这类毕设时,最大的一个教训是不要贪功能。把场地预约和签到这两个闭环做到完整,比堆十个半成品功能更容易拿高分。答辩时老师更关心的是你怎么处理并发、怎么保证数据一致、遇到乱码和 404 怎么排查,这些前面讲到的点,比项目本身花哨多少页面都重要。希望帮到你。

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

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

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

立即咨询