基于SSM和MySQL的乒乓球比赛管理系统设计与实现 📅 发布时间:2026/9/12 14:25:18 👁 浏览次数: 简介基于SSMSpringSpringMVCMyBatis和MySQL搭建的乒乓球比赛管理系统源码面向Java Web初学者、毕业设计或课程设计人群。系统覆盖赛事创建、选手报名、赛程编排、比分记录、成绩统计与权限控制等完整业务链路体现依赖注入、AOP事务处理、MVC分层和持久层映射等核心开发思路可直接导入IDE运行学习。压缩包共719个文件大小5.64MB包含170个Java源码、139个JS脚本、107个GIF动图、96个PNG图片、85个CSS样式、32个HTML页面及XML、SQL等配置与数据库初始化文件前端界面、后端逻辑和数据库脚本一应俱全便于对照理解。已有200人学习下载配置有清晰的目录结构和Maven/Git等工程管理痕迹适合用来复盘SSM整合流程、练习MySQL表设计及实现一个可运行的管理系统。1. 基于 SSM 和 MySQL 的乒乓球比赛管理系统架构定位与落地路径单位组织乒乓球比赛裁判和工作人员要处理报名名单、分组抽签、赛程排布、比分登记、晋级关系Excel 管到 32 人以上基本失控。标题中的 SSM 指 Spring、SpringMVC、MyBatis 的组合配上 MySQL 做持久化这是 Java Web 管理系统最经典的落地方式之一。这套组合分层明确Spring 管理对象和事务SpringMVC 定义接口路由MyBatis 保留 SQL 控制权MySQL 负责存储业务数据。SSM 需要手动维护 XML 配置过程能暴露框架协作原理因此仍被大量毕业设计和内部管理系统采用。这篇文章不依赖所谓源码包而是直接带你从数据库表结构开始完成后端接口、前端联调、打包部署和线上排错。可以直接照抄建表 SQL、Mapper XML、Service 方法和部署命令适合正在做毕业设计的学生、需要快速交付赛事系统的开发者以及复习 SSM 细节的求职者。2. 数据库设计乒乓球比赛管理系统的 MySQL 表结构与核心参数2.1 先理清业务实体赛事、报名、赛程、比分如何关联一个可用的乒乓球比赛管理系统业务上至少要覆盖四个环节赛事管理创建“2025 春季职工乒乓球赛”这样的赛事记录比赛类型、开始结束时间、当前状态。报名管理运动员报名参赛一个赛事下有多名运动员同一运动员也可报多个赛事所以需要一张报名关联表。赛程管理赛程是系统的核心每场比赛归属于某个赛事记录对阵双方、轮次、球台号、比赛时间和比赛状态。比分管理乒乓球比赛按局记录比赛可能打五局三胜所以比分需要记录局号和双方得分最终汇总出获胜方。从实体关系看运动员与赛事是多对多通过 entry 表解除赛程与赛事是多对一比分与赛程是一对多。大部分管理系统都是这种套路主体表、关联表、明细表。设计时最需要避免的是把比分字段直接堆在赛程表里比如加一行 score_a、score_b 只存总比分。这样做看起来省事但一旦要细分“每局的小分”就只好用逗号拼接字符串后续统计净胜局和胜负关系会非常痛苦。所以哪怕是一个小型系统也要把比分拆成独立明细表。这里有一个经验凡是将来要做“聚合统计”的字段尽量拆成行而不是用分隔符合并进一个字段。MySQL 对行数其实很宽容千级赛事打成几十万条比分记录完全没压力。权限方面通常需要一个 sys_user 表存登录账号和角色。运动员信息可以单独建 player 表也可以用 user_id 和 sys_user 关联。角色设计成管理员、裁判、普通用户三个档位就够用管理员能创建赛事和修改所有数据裁判负责录比分普通用户只能查看和报名。别做太细的权限矩阵那会让登录校验代码膨胀对这类内部系统没有实际收益。2.2 MySQL 建表 DDL 与字段选择说明下面这套 DDL 可以直接在 MySQL 8.0 上执行字符集统一用 utf8mb4。选择 utf8mb4 而不是 utf8 的原因后面会单独讲。先看建库和建表语句。CREATE DATABASE IF NOT EXISTS pingpong DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE pingpong; -- 1. 系统用户表管理登录账号、密码、角色 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, real_name VARCHAR(32) DEFAULT , role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2裁判 3普通用户, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT系统用户; -- 2. 运动员表非登录用户也能成为运动员故独立建表 CREATE TABLE player ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1男 2女, phone VARCHAR(20) DEFAULT , user_id INT DEFAULT NULL COMMENT 可关联登录账号, KEY idx_user (user_id) ) ENGINEInnoDB COMMENT运动员; -- 3. 赛事表 CREATE TABLE tournament ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(64) NOT NULL, type TINYINT NOT NULL DEFAULT 1 COMMENT 1单打 2双打 3团体, start_date DATE DEFAULT NULL, end_date DATE DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0报名中 1进行中 2已结束, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT赛事; -- 4. 报名表运动员与赛事的多对多关联 CREATE TABLE entry ( id INT PRIMARY KEY AUTO_INCREMENT, tournament_id INT NOT NULL, player_id INT NOT NULL, seed TINYINT NOT NULL DEFAULT 0 COMMENT 是否种子选手, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_entry_tournament_player (tournament_id, player_id), KEY idx_entry_player (player_id) ) ENGINEInnoDB COMMENT报名记录; -- 5. 赛程表一场比赛一行双打时 player_a/player_b 存组合编号 CREATE TABLE match_schedule ( id INT PRIMARY KEY AUTO_INCREMENT, tournament_id INT NOT NULL, round_no INT NOT NULL DEFAULT 1, player_a INT NOT NULL, player_b INT NOT NULL, match_time DATETIME DEFAULT NULL, table_no INT DEFAULT 0 COMMENT 球台号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未开始 1进行中 2已结束, winner INT DEFAULT NULL COMMENT 胜者player或组合id, KEY idx_match_tournament (tournament_id), KEY idx_match_status (status) ) ENGINEInnoDB COMMENT赛程; -- 6. 比分表一局一条记录比赛结束前允许反复修改 CREATE TABLE match_score ( id INT PRIMARY KEY AUTO_INCREMENT, match_id INT NOT NULL, set_no TINYINT NOT NULL COMMENT 第几局, score_a TINYINT NOT NULL DEFAULT 0, score_b TINYINT NOT NULL DEFAULT 0, UNIQUE KEY uk_score_match_set (match_id, set_no) ) ENGINEInnoDB COMMENT每局比分;对字段选择有几个要注意的地方第一所有业务表都用 InnoDB 且不设外键约束关联只通过索引在业务层保证这样导入数据、删改数据时不会被外键卡住是现在大多数 MySQL 业务表的设计习惯但唯一键不能省比如 entry 表的 (tournament_id, player_id) 唯一键可以拦住同一运动员重复报名。第二status 这类状态字段用 TINYINT 而不是字符枚举因为后续加状态不需要改字段类型Java 侧用常量或枚举类解释数字即可。第三双打比赛的 player_a/player_b 直接存组合编号组合编号可以用“运动员 id 拼接”生成比如 12_34这样赛程表不需要为双打单独建组合表。时间字段这里只使用了 DATETIME没有用 TIMESTAMP。原因很简单TIMESTAMP 有 2038 年上限DATETIME 的范围是 1000 到 9999 年对于记录比赛时间DATETIME 更稳妥也避免不同时区下的显示偏移。MySQL 8.0 里 DATETIME 默认支持小数秒但业务上用不到。注意建表时没写外键如果将来要做数据清理直接 TRUNCATE 子表再清主表即可省去外键检查的麻烦。2.3 MySQL 连接参数、字符集与连接池配置建表完成之后先别急着写代码把 MySQL 连接层参数定下来能避开很多坑。第一个坑是字符集。从 MySQL 5.7 开始 utf8mb4 才是完整 UTF-8utf8 只是 utf8mb3无法存放四字节 emoji 字符。虽然乒乓球报名信息里很少出现 emoji但运动员备注、日志字段里一旦出现中文生僻字加特殊符号utf8 就可能报“Incorrect string value”。所以建库、建表、JDBC URL 三处都要统一 utf8mb4。第二个坑是时区。MySQL 8.0 默认时区是 UTC而大多数服务器和本地机器是东八区。如果 JDBC URL 不指定 serverTimezone或者指定错了查询时间字段会刚好慢 8 小时甚至抛异常。下列配置是项目里最常用的一组参数推荐值说明jdbcUrl 字符集useUnicodetruecharacterEncodingutf8mb4与建库字符集保持一致jdbcUrl 时区serverTimezoneAsia/Shanghai消除 8 小时时差jdbcUrl 加密useSSLfalse内网环境省掉证书告警Druid 初始连接数5冷启动后连接池预热Druid 最大连接数20内部系统并发有限20 足够这里用 Alibaba Druid 连接池来举例因为它自带监控页面在管理系统里查看 SQL 执行情况很方便。下面给出 spring-context.xml 里数据源和事务管理器的核心配置后面会在这个基础上装配 MyBatis。context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/pingpong?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghaiamp;useSSLfalse/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property nameminIdle value5/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.pingpong.dao/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这个配置里的关键参数前面表格已经说过。补充一点XML 中符号必须写成amp;JDBC URL 里的多个参数用连接写进 properties 文件时不用转义写进 XML 时必须转义。这个细节常常让人卡上半小时。MapperScannerConfigurer 的 basePackage 要填写 Mapper 接口所在的包它会把该包下所有接口动态代理成 MyBatis 的 Mapper 对象Service 里才能直接通过Autowired注入。注意事务管理器默认回滚运行时异常而不回滚受检异常后面在写比分更新逻辑时我会用RuntimeException来触发回滚。3. SSM 后端实现与核心业务逻辑Mapper、事务与赛程编排3.1 Maven 项目结构与依赖清单SSM 项目通常打包成 war 部署在 Tomcat 上所以 maven 坐标的 packaging 是 war。项目结构上分成 Controller、Service、Dao 三层外加 resources 目录放配置和 Mapper XML。下面是一个最小可运行的项目骨架。src/main/java com.pingpong controller/ TournamentController.java MatchController.java service/ TournamentService.java MatchService.java dao/ MatchScheduleMapper.java MatchScoreMapper.java entity/ Tournament.java MatchSchedule.java MatchScore.java User.java common/ Result.java 统一返回体 src/main/resources jdbc.properties spring-context.xml spring-mvc.xml mybatis-config.xml mapper/ MatchScheduleMapper.xml MatchScoreMapper.xml src/main/webapp WEB-INF/views/ index.jsp match.jsp player.jsp这里关键点有三个一是统一返回体 Result 类包含 code、message、data 三个字段所有 Controller 都返回这个对象前端用 jQuery 判断 code 是否为 0比返回字符串再解析要清晰得多二是 Dao 层接口和 resources/mapper 下的 XML 文件名要一一对应MyBatis 通过命名空间和接口全限定名绑定三是视图放在 WEB-INF 下不允许浏览器直接访问 jsp只能通过 Controller 转发进入页面。pom.xml 里需要引入 spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-j、druid、jackson-databind、javax.servlet-apiprovided 作用域。这几个坐标缺一不可少了 Jackson 前端就无法解析 JSON 响应少了 mybatis-spring 则 MyBatis 无法被 Spring 容器管理。3.2 用 MyBatis 的 Mapper 接口和 XML 实现 CRUD 与联表查询先定义 Mapper 接口方法名和 XML 里的语句 id 一一对应。public interface MatchScheduleMapper { int insert(MatchSchedule ms); int deleteByTournamentId(Integer tournamentId); MatchSchedule selectById(Integer id); ListMatchSchedule selectByTournamentId(Integer tournamentId); ListMatchSchedule selectFinishedByTournament(Integer tournamentId); int updateWinner(Param(id) Integer id, Param(winner) Integer winner); int batchInsert(Param(list) ListMatchSchedule list); }对应的 XML 里最关键的是 resultMap 和批量插入。mapper namespacecom.pingpong.dao.MatchScheduleMapper resultMap idscheduleMap typecom.pingpong.entity.MatchSchedule id propertyid columnid/ result propertytournamentId columntournament_id/ result propertyroundNo columnround_no/ result propertyplayerA columnplayer_a/ result propertyplayerB columnplayer_b/ result propertymatchTime columnmatch_time/ result propertytableNo columntable_no/ result propertystatus columnstatus/ result propertywinner columnwinner/ /resultMap insert idinsert parameterTypecom.pingpong.entity.MatchSchedule useGeneratedKeystrue keyPropertyid INSERT INTO match_schedule(tournament_id, round_no, player_a, player_b, match_time, table_no, status) VALUES(#{tournamentId}, #{roundNo}, #{playerA}, #{playerB}, #{matchTime}, #{tableNo}, 0) /insert insert idbatchInsert INSERT INTO match_schedule(tournament_id, round_no, player_a, player_b, status) VALUES foreach collectionlist itemitem separator, (#{item.tournamentId}, #{item.roundNo}, #{item.playerA}, #{item.playerB}, 0) /foreach /insert update idupdateWinner UPDATE match_schedule SET winner #{winner}, status 2 WHERE id #{id} /update select idselectByTournamentId resultMapscheduleMap SELECT * FROM match_schedule WHERE tournament_id #{tournamentId} ORDER BY round_no, table_no /select /mapper写 MyBatis XML 时最容易踩的坑是 Java 属性和数据库下划线字段的映射。上面用了 resultMap 显式列出映射关系这是最稳妥的方式。如果不想写 resultMap可以在 mybatis-config.xml 里开启mapUnderscoreToCamelCase让 player_a 自动映射成 playerA。两种方式选一种不要混用否则排查字段为 null 的时候要花很多时间。batchInsert 用 foreach 循环拼接 values适合赛程一键生成场景。提示如果查询结果里某个 Java 属性一直是 null优先检查 resultMap 是否漏了字段而不是怀疑 SQL 本身。MyBatis 的字段映射错误不会报异常只会静默返回 null这种问题在管理系统开发里出现频率最高。3.3 用 Spring 事务编排完整业务生成赛程、录入比分、推进晋级赛程编排是乒乓球比赛管理系统里最值得写清楚的一块。网上很多源码包用的是最朴素的单败淘汰报名人数确定后把运动员随机两两配对胜者进入下一轮。我们不做复杂算法只提供这种最常见的轮次生成逻辑把事务边界和代码结构讲清楚。下面这段 Service 代码展示两个核心方法录比分和生成下一轮。Service public class MatchServiceImpl implements MatchService { Autowired private MatchScheduleMapper scheduleMapper; Autowired private MatchScoreMapper scoreMapper; Override Transactional(rollbackFor Exception.class) public void completeMatch(Integer matchId, ListScoreItem scores, Integer winner) { if (scores null || scores.isEmpty()) { throw new RuntimeException(比分记录不能为空); } // 1. 更新赛程的胜者和状态 scheduleMapper.updateWinner(matchId, winner); // 2. 删除该场比赛旧比分比分可能被修正后重新提交 scoreMapper.deleteByMatchId(matchId); // 3. 批量写入每局比分 for (ScoreItem item : scores) { MatchScore ms new MatchScore(); ms.setMatchId(matchId); ms.setSetNo(item.getSetNo()); ms.setScoreA(item.getScoreA()); ms.setScoreB(item.getScoreB()); scoreMapper.insert(ms); } } Override Transactional(rollbackFor Exception.class) public void generateNextRound(Integer tournamentId) { ListMatchSchedule finishedMatches scheduleMapper.selectFinishedByTournament(tournamentId); // 取每场比赛的胜者按顺序两两配对 ListInteger winners finishedMatches.stream() .map(MatchSchedule::getWinner) .collect(Collectors.toList()); ListMatchSchedule nextRound buildPairings(winners, tournamentId); scheduleMapper.batchInsert(nextRound); } private ListMatchSchedule buildPairings(ListInteger ids, Integer tournamentId) { ListMatchSchedule result new ArrayList(); for (int i 0; i 1 ids.size(); i 2) { MatchSchedule ms new MatchSchedule(); ms.setTournamentId(tournamentId); ms.setRoundNo(1); ms.setPlayerA(ids.get(i)); ms.setPlayerB(ids.get(i 1)); ms.setStatus(0); result.add(ms); } // 人数为奇数时最后一人轮空player_b 存 0 表示轮空 if (ids.size() % 2 1) { MatchSchedule bye new MatchSchedule(); bye.setTournamentId(tournamentId); bye.setRoundNo(1); bye.setPlayerA(ids.get(ids.size() - 1)); bye.setPlayerB(0); bye.setStatus(0); result.add(bye); } return result; } }这段代码里有几个决定系统健壮性的点第一completeMatch 方法必须用事务包住因为更新赛程、删除旧比分、插入新比分三个操作不能拆开一旦第 3 步插入失败第 1 步已经把比赛标记成已结束就会出现“比赛结束了却没有比分”的脏数据。注意注解写的是 rollbackFor Exception.class因为 Spring 默认只回滚 RuntimeException如果把异常处理成自定义的 checked exception事务可能不会回滚。第二比分的修改规则用了“先删后插”而不是逐条 update。因为前端提交时可能调整局数比如从三局改成五局逐条 update 会留下多余的局记录先删再插可以保证数据始终和页面提交的完全一致。代价是比分表的自增 id 会跳号这对业务没有影响。如果不想跳号可以改成先查再删多出的局但代码复杂度会上升我个人倾向于先删后插。第三赛程生成里的轮空处理player_b 为 0 表示轮空。业务上轮空比赛不需要录比分前端要隐藏这一场的录比分入口否则裁判会困惑。这里的 buildPairings 只做了最基础的随机配对真正的系统可以在报名阶段抽签排序后固定对阵表原理一致只是调整选手列表的顺序。4. 前端页面与系统联调JSP、Ajax 与比分提交4.1 JSP 页面、统一返回体和接口约定SSM 项目的前端常见做法是 JSP jQuery不需要引入 Vue 或 React。原因很简单页面数量少逻辑集中在几个列表和表单页jQuery 的 Ajax 写法足够直白而且 JSP 可以直接用 JSTL 渲染服务端数据减少一次接口请求。这个系统的前端页面可以分为四块登录页、赛事管理页、报名管理页、赛程与比分页。为了不给联调制造麻烦所有 Ajax 接口统一返回 Result JSON 结构code 为 0 表示业务成功非 0 表示失败message 里放可直接展示的提示文字。前后端接口约定如下功能接口方法参数返回登录/user/loginPOSTusername, passwordcode/message创建赛事/tournament/addPOSTtitle, type, startDate, endDatecode/data报名/tournament/entryPOSTtournamentId, playerIdcode/message赛程列表/match/listGETtournamentIdcode/data比分提交/match/scorePOSTmatchId, winner, scorescode/messageController 接收参数时尽量用 RequestParam 绑定简单字段而不是创建大量 DTO。对象参数在参数多、字段嵌套时自然用 POJO 接收比如录比分时通常传一个包含局分数组的对象。写 Controller 时要保持简洁只解析参数、调用 Service、返回 Result具体的业务判断放到 Service 层这样后续排查问题只需要看关键层。下面给出录比分接口的 Controller 写法。Controller RequestMapping(/match) public class MatchController { Autowired private MatchService matchService; ResponseBody PostMapping(/score) public Result submitScore(RequestBody ScoreSubmitVO vo) { if (vo.getMatchId() null || vo.getScores().isEmpty()) { return Result.error(缺少比分参数); } try { matchService.completeMatch(vo.getMatchId(), vo.getScores(), vo.getWinner()); return Result.ok(); } catch (Exception e) { return Result.error(e.getMessage()); } } }这里有两个细节容易忽略。第一接口方法签名用了 RequestBody表示请求体的 JSON 字符串会反序列化成 ScoreSubmitVO 对象。为此需要在 spring-mvc.xml 里配置 MappingJackson2HttpMessageConverter并注册注解驱动。很多 SSM 项目出现“前端传了 JSON 但后台收到 null”的问题根源就是只写了 Controller 而没有配置注解驱动消息转换器没有生效。第二Service 抛出的异常在 Controller 里被捕获并转成 Result.error这样异常信息可以直接回显给前端用户而不是让浏览器看到一片堆栈。事务回滚仍然有效因为异常是在 Service 内部抛出并终结的捕获点在外面。4.2 赛程与比分页面的 Ajax 实现录比分页面是前后端联调里最容易出错的场景。页面加载时要先查赛程列表点击某场比赛后弹出一个表单表单里有若干局比分输入框提交时把所有局分组装成数组发给后端。下面给一个最小可用的录比分页面片段。% page contentTypetext/html;charsetUTF-8 languagejava % html head title赛程管理/title script src${pageContext.request.contextPath}/static/js/jquery-3.6.0.min.js/script /head body h3赛程列表/h3 table idmatchTable border1 thead trth轮次/thth选手A/thth选手B/thth状态/thth操作/th/tr /thead tbody/tbody /table script var ctx ${pageContext.request.contextPath}; $(function () { loadMatches(); }); function loadMatches() { $.get(ctx /match/list, {tournamentId: 1}, function (res) { if (res.code ! 0) return; var rows ; $.each(res.data, function (idx, m) { rows tr td m.roundNo /td td m.playerAName /td td m.playerBName /td td (m.status 2 ? 已结束 : 未开始) /td tdbutton onclicksubmitScore( m.id )录比分/button/td /tr; }); $(#matchTable tbody).html(rows); }); } function submitScore(matchId) { var scores []; // 实际页面会动态生成若干局输入框这里简化为固定三局 for (var i 1; i 3; i) { scores.push({setNo: i, scoreA: 11, scoreB: 8}); } var payload {matchId: matchId, winner: 12, scores: scores}; $.ajax({ url: ctx /match/score, type: POST, contentType: application/json;charsetUTF-8, data: JSON.stringify(payload), success: function (res) { alert(res.message); } }); } /script /body /html这个片段里的 m.playerAName 是前端展示时需要的一个额外字段它本身不在 match_schedule 表里。常见做法是 Mapper 联查 player 表得到姓名组装到返回的实体中而不是让前端拿着 player_id 再去发一次请求。一次查询尽量把展示需要的字段都带出来这是前后端联调的基本默契。录比分的提交函数把每局比分构造成数组通过 JSON.stringify 发送。注意 Ajax 的 content-type 要设成 application/json并且把 ResponseBody 和 RequestBody 配对使用后端用 RequestBody 接收 JSON就必须发送原始 JSON 字符串而不是 form 表单格式。这两个配置经常被写反前端用默认的表单序列化后端却等着 JSON 对象结果就是报 415 错误。5. 源码打包、Tomcat 部署与线上排错技巧5.1 用 Maven 打 war 包并在 Tomcat 上部署代码写完、本地测试通过后交付的方式是打成 war 包部署到 Tomcat。常见做法是用 Maven 的 package 命令构建。mvn clean package -DskipTests cp target/pingpong.war /opt/tomcat/webapps/ /opt/tomcat/bin/startup.sh tail -f /opt/tomcat/logs/catalina.out打包前先检查 maven-compiler-plugin 的 source/target 是否匹配本机 JDK 版本。如果代码里用了 Java 8 的 lambda 和 stream编译版本低于 1.8 会直接失败。war 包部署到 Tomcat 的 webapps 目录后Tomcat 会自动解压。访问路径是http://服务器IP:8080/pingpong/如果你的项目名变了这个上下文路径也会跟着变。如果不想用 war 包解压后的目录名可以把 war 改名为 ROOT.war这样直接用根路径访问。部署后最先看 catalina.out 的日志绝大多数启动失败在里面都能找到线索。5.2 线上最常见的几类问题及对策报错或现象根本原因对策启动报 NoClassDefFoundErrorJava 依赖没有打进 war 包检查 pom.xml 依赖 scopeMySQL 驱动不要用 provided查询中文乱码数据库、连接 URL、页面编码不一致统一 utf8mb4JSP 顶部加 pageEncodingUTF-8Mapped Statements collection does not contain valueMapper XML 的 namespace 或文件名写错检查 namespace 与接口全限定名是否一致接口返回 415前端 content-type 不对或没配消息转换器前端用 application/jsonspring-mvc.xml 开启注解驱动Too many connections连接池配置过大或连接泄漏压测后调小 maxActive检查慢查询释放连接时间字段差 8 小时JDBC URL 缺少 serverTimezone 参数补上 serverTimezoneAsia/Shanghai5.3 验证数据正确性的几条 SQL 和后续优化方向部署完成并录入几场比赛后需要验证系统数据是否可靠。下面几条 SQL 可以直接在 mysql 命令行里跑用来检查赛程和比分是否一致。-- 查某赛事已结束但没录比分的比赛应该是空 SELECT sa.id, sa.round_no FROM match_schedule sa LEFT JOIN (SELECT DISTINCT match_id FROM match_score) sc ON sa.id sc.match_id WHERE sa.tournament_id 1 AND sa.status 2 AND sc.match_id IS NULL; -- 统计每位运动员的胜场数和积分排名页做核对 SELECT winner, COUNT(*) AS win_count FROM match_schedule WHERE tournament_id 1 AND status 2 GROUP BY winner ORDER BY win_count DESC;第一条利用左连接查缺口把“已结束但没比分”的脏数据暴露出来这类数据在录比分接口缺少事务保护时最容易出现。第二条统计胜场数可以核对比赛结果和积分排名是否合理。再往深走可以在 Service 层加一个定时任务每晚扫描一遍有没有“状态为已结束但没有比分”的记录把异常数据写进告警表这样系统就有了自检能力。比分分析上通过 match_score 按局统计双方净胜分可以计算小分排名这部分用一条 group by 就能完成直接利用前面建的 match_score 表即可不需要额外加列。本文还有配套的精品资源点击获取