☰
JSP体育成绩管理系统实战:解决高校体测Excel手工汇总痛点
2026/10/13 11:44:33 网站建设 项目流程

简介:这是一套基于JSP+Java+MySQL开发的体育成绩管理系统实战源码,面向高校计算机专业学生、Web开发初学者及体育信息化管理需求者,解决学校或赛事组织中班级管理、项目报名、成绩统计与秩序册生成等核心业务的数字化落地问题。资源包共98个文件,含16个JSP页面(实现用户交互与数据展示)、4个Java源文件(封装核心业务逻辑)、15个JAR包(支撑数据库连接与工具功能)、34个GIF动图(辅助界面提示)及1个SQL建库脚本(db_tybs.sql),整体4.96MB,结构完整,适合作为课程设计或毕业设计参考。已有469人学习下载,资源附带可直接运行的.exe演示程序(JSP体育成绩管理系统录像.exe),并提供标准MyEclipse项目结构(含.project、.classpath等配置文件),开箱即用,便于快速部署、调试与二次开发。

1. JSP Java体育成绩管理系统:为什么高校体测数据还在用Excel手工汇总?

某高校体育部老师连续三年在期末被学生追问:“老师,我的800米成绩怎么还没登?”——后台Excel表格里混着2019级到2023级的五套命名规则(“体测_终稿_v2_删减版”“体测_最终_不改了_真的”),字段名有“肺活量(ml)”“肺活量(单位:ml)”“肺活量_数值”,而“引体向上”一栏里赫然躺着“12个”“12”“十二”“弃权”“请假-腰伤”。这不是个例。JSP Java体育成绩管理系统,本质不是写个网页+数据库就完事的“课程设计级项目”,而是解决多角色协同、多终端录入、成绩自动校验、跨学期对比、合规导出报表这五个刚性痛点的轻量级业务系统。它面向的是体育教研室管理员、任课教师、体测现场助教、甚至学生自助查询端——四类角色对同一份成绩数据有完全不同的操作权限和视图需求。用Spring Boot重写当然更现代,但JSP+Servlet+MySQL这套组合,在校园网内网环境、老旧机房电脑(IE8兼容要求仍存)、无专职运维支持的现实约束下,仍是部署成本最低、教师上手最快、维护最透明的技术路径。本文不讲MVC理论,只拆解:从零搭起一个能真实跑在Windows Server 2012+Tomcat 8.5上的可运行系统,重点落在成绩逻辑校验怎么写不翻车、批量导入Excel时日期和空值怎么啃、导出PDF报表如何避开中文乱码黑匣子这三个血泪经验区。


2. 环境搭建与核心模块选型:为什么坚持用JSP而不是Vue单页?

提示:本系统定位为内网管理工具,非互联网高并发应用。选型首要看“谁来维护”——体育老师自己改个页面文字,或信息中心老师重启一次服务,必须在5分钟内完成。

2.1 JDK、Tomcat、MySQL版本锁死策略

校园IT环境存在大量历史遗留约束:部分机房PC预装Java 1.7(因旧教务系统依赖),而新版Tomcat需Java 1.8+;MySQL 5.7是学校统一采购的数据库镜像,不开放升级权限。因此,我们主动降级适配:

# 必须使用JDK 1.8u202(非最新版!) # Tomcat 8.5.90(官方最后支持JDK 1.8的稳定版) # MySQL 5.7.42(禁用8.0的默认认证插件,避免JDBC连接报错)

为什么不用Tomcat 9/10?
——某次升级后,体育老师反馈“登录页打不开”,抓包发现是IE11对HTTP/2协议支持不全,回退到Tomcat 8.5后问题消失。技术选型不是追新,是让业务不中断。

2.2 数据库表结构设计:成绩表必须带“原始录入值”和“校验后值”双字段

体育成绩存在大量非数字录入场景(如“免测”“缓测”“缺考”),若强行塞进INT类型,后续统计会崩溃。正确做法是分离存储:

字段名类型说明
score_rawVARCHAR(20)教师手动输入的原始字符串:“12”“弃权”“1分23秒”
score_numericDECIMAL(5,2)经校验规则转换后的数值:12.00 / NULL(免测)
score_statusTINYINT状态码:0=正常、1=免测、2=缓测、3=缺考、4=无效(如输入“abc”)
CREATE TABLE `sport_score` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `student_id` VARCHAR(15) NOT NULL COMMENT '学号', `test_item` VARCHAR(20) NOT NULL COMMENT '测试项目:50m/立定跳远/引体向上', `score_raw` VARCHAR(20) DEFAULT NULL COMMENT '原始录入值', `score_numeric` DECIMAL(5,2) DEFAULT NULL COMMENT '标准化数值', `score_status` TINYINT DEFAULT 0 COMMENT '状态码', `record_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `operator` VARCHAR(20) COMMENT '录入人姓名' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键点:CHARSET=utf8mb4是硬性要求。某次导入“引体向上:12个(含2个借力)”时,括号内字符触发MySQL 5.7默认utf8编码截断,导致score_raw只存入“12个(含2个借”,后续SQL查询全乱。

2.3 JSP页面分层:为什么login.jsp不能直接连数据库?

新手常犯错误:在login.jsp里写<% Class.forName("com.mysql.jdbc.Driver"); ... %>直连数据库。这会导致两个致命问题:

  1. 每次页面刷新都新建数据库连接,10个老师同时登录,MySQL连接池瞬间耗尽;
  2. 数据库密码明文写在JSP里,被误传到Git或被学生F12查看源码窃取。

正确分层:

  • login.jsp→ 只负责展示表单,提交给LoginServlet
  • LoginServlet.java→ 接收参数,调用UserService.checkLogin()
  • UserService.java→ 使用DBUtil.getConnection()获取连接池中的连接
  • DBUtil.java→ 封装HikariCP连接池,密码从WEB-INF/config.properties读取
// DBUtil.java 关键代码 public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/sport_db?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("sport_user"); config.setPassword(getPasswordFromConfig()); // 从配置文件读,非硬编码 config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 复用连接池,非每次new } }

参数说明:

  • serverTimezone=Asia/Shanghai:强制指定时区,避免DATETIME字段存入时间比实际晚8小时;
  • maximumPoolSize=10:校园网并发量低,设太高反而浪费内存;
  • getPasswordFromConfig():配置文件通过ServletContext.getRealPath("/WEB-INF/config.properties")读取,该路径无法被HTTP直接访问。

3. 成绩录入与校验引擎:把“1分23秒”转成83.00秒的三步法

体育成绩的多样性是系统最大难点。“50米跑”要存秒,“立定跳远”要存米,“引体向上”存次数,“肺活量”存毫升——同一张成绩表,字段语义完全不同。硬编码if-else校验会失控。我们采用规则驱动+正则预处理方案。

3.1 建立项目-规则映射表(test_rule)

CREATE TABLE `test_rule` ( `item_name` VARCHAR(20) PRIMARY KEY COMMENT '项目名称', `unit` VARCHAR(10) COMMENT '单位:秒/米/次/毫升', `regex_pattern` VARCHAR(100) COMMENT '校验正则:如"\\d+(\\.\\d+)?(秒|s)?$"', `convert_script` TEXT COMMENT 'Groovy转换脚本:如"it.replaceAll('秒','').trim().toDouble()"' );

插入示例数据:

INSERT INTO `test_rule` VALUES ('50米跑', '秒', '\\d+(\\.\\d+)?(秒|s)?$', 'it.replaceAll("秒","").replaceAll("s","").trim().toDouble()'), ('立定跳远', '米', '\\d+(\\.\\d+)?(米|m)?$', 'it.replaceAll("米","").replaceAll("m","").trim().toDouble()'), ('引体向上', '次', '^\\d+$', 'it.trim().toInteger()');

3.2 在Servlet中动态执行校验(ScoreSubmitServlet.java)

// 获取项目对应规则 String item = request.getParameter("test_item"); TestRule rule = ruleService.getRuleByItem(item); // 从数据库查规则 String rawScore = request.getParameter("score_raw"); String status = "0"; // 默认正常 Double numericScore = null; try { // 步骤1:正则匹配原始值 if (!rawScore.matches(rule.getRegexPattern())) { throw new IllegalArgumentException("格式错误:应为" + rule.getUnit() + "单位,如'8.5秒'或'2.35米'"); } // 步骤2:执行Groovy脚本转换(安全沙箱) Binding binding = new Binding(); binding.setVariable("it", rawScore); GroovyShell shell = new GroovyShell(binding); Object result = shell.evaluate(rule.getConvertScript()); // 步骤3:类型强转并范围校验 if (result instanceof Number) { numericScore = ((Number) result).doubleValue(); if (numericScore < 0 || numericScore > 1000) { // 合理范围兜底 throw new IllegalArgumentException("数值超出合理范围"); } } else { throw new IllegalArgumentException("转换失败:返回非数字类型"); } } catch (Exception e) { status = getStatusCodeByReason(e.getMessage()); // 映射到status码:1=免测,2=缓测... request.setAttribute("error_msg", e.getMessage()); }

为什么用Groovy而非Java反射?
——教师可能需要临时调整规则(如将“1分23秒”改为“83秒”),Groovy脚本可热更新,无需重启Tomcat。某次体测前夜,教研室要求“800米成绩统一按‘分:秒’格式录入”,运维老师直接在数据库改convert_script字段为"def parts=it.split(':'); (parts[0].toInteger()*60 + parts[1].toInteger()).toDouble()",5分钟生效。

3.3 批量Excel导入:用Apache POI处理“合并单元格”和“空行”

体育老师习惯用Excel模板批量填成绩,但模板常含合并单元格(如“2023级男生组”跨3行)、空行、表头多行。POI默认解析会丢失数据。

// ExcelImportService.java 关键逻辑 public List<ScoreRecord> parseExcel(InputStream is) { Workbook workbook = WorkbookFactory.create(is); Sheet sheet = workbook.getSheetAt(0); List<ScoreRecord> records = new ArrayList<>(); // 步骤1:预扫描,记录合并单元格影响的行范围 Map<Integer, String> mergedCellCache = new HashMap<>(); for (CellRangeAddress range : sheet.getMergedRegions()) { for (int row = range.getFirstRow(); row <= range.getLastRow(); row++) { Row r = sheet.getRow(row); if (r != null) { Cell c = r.getCell(range.getFirstColumn()); if (c != null && c.getCellType() == CellType.STRING) { mergedCellCache.put(row, c.getStringCellValue()); } } } } // 步骤2:逐行解析,空行跳过,合并单元格值复用 for (int i = 3; i <= sheet.getLastRowNum(); i++) { // 跳过表头3行 Row row = sheet.getRow(i); if (row == null || isEmptyRow(row)) continue; ScoreRecord record = new ScoreRecord(); record.setStudentId(getCellValue(row.getCell(0), mergedCellCache, i)); record.setTestItem(getCellValue(row.getCell(1), mergedCellCache, i)); record.setScoreRaw(getCellValue(row.getCell(2), mergedCellCache, i)); records.add(record); } return records; } private String getCellValue(Cell cell, Map<Integer, String> cache, int row) { if (cell == null) return cache.getOrDefault(row, ""); if (cell.getCellType() == CellType.STRING) { return cell.getStringCellValue().trim(); } else if (cell.getCellType() == CellType.NUMERIC) { return String.valueOf(cell.getNumericCellValue()).replace(".0", ""); } return ""; }

避坑点:WorkbookFactory.create(is)必须传InputStream,不能传File——否则Windows下文件锁死,第二次导入时报“文件正被占用”。


4. 避坑:JSP体育成绩系统上线后必踩的5个坑

4.1 现象:导出PDF报表中文全是方框

原因:iText 5.x默认字体不支持中文,且未指定中文字体路径。
解决:

  1. 下载simhei.ttf(微软雅黑)放入WEB-INF/fonts/目录;
  2. 在导出Servlet中注册字体:
FontFactory.register("/WEB-INF/fonts/simhei.ttf", "simhei"); Font chineseFont = FontFactory.getFont("simhei", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

注意:BaseFont.IDENTITY_H是关键,它启用Unicode编码,NOT_EMBEDDED减小PDF体积(内网传输友好)。

4.2 现象:IE11下成绩录入页按钮点击无反应

原因:JSP中用了ES6语法(如let、箭头函数),IE11不支持。
解决:前端JS全部用ES5写,或引入Babel编译。更简单方案——在<head>中加兼容meta:

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

4.3 现象:MySQL中score_numeric字段存入83.00,但页面显示为83.0

原因:JSP EL表达式${score.score_numeric}自动调用toString(),浮点数精度丢失。
解决:在JSP中用JSTL格式化:

<fmt:formatNumber value="${score.score_numeric}" minFractionDigits="2" maxFractionDigits="2"/>

4.4 现象:Tomcat重启后,首次访问login.jsp报ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:MySQL 8.0+驱动类名变为com.mysql.cj.jdbc.Driver,但JSP中仍写老类名;或mysql-connector-java-8.0.x.jar放在WEB-INF/lib/,但Tomcat 8.5默认不扫描此路径。
解决:

  1. 驱动用mysql-connector-java-5.1.49.jar(兼容MySQL 5.7);
  2. 将jar包放入$TOMCAT_HOME/lib/而非WEB-INF/lib/;
  3. web.xml中<driver-class>标签值改为com.mysql.jdbc.Driver。

4.5 现象:学生自助查询页,输入学号2023001查不到成绩,但'2023001'(带单引号)能查到

原因:SQL拼接时未用PreparedStatement,WHERE student_id = ${param.id}被注入为WHERE student_id = 2023001,而数据库中student_id是VARCHAR类型,隐式转换失败。
解决:所有DAO层SQL必须用?占位符:

String sql = "SELECT * FROM sport_score WHERE student_id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, studentId); // 强制字符串类型

5. 学生成绩自助查询与跨学期对比:用纯JSP+AJAX实现零框架依赖

体育老师最常提的需求:“张三同学,去年立定跳远2.15米,今年多少?进步了多少?”——这需要关联多个学期的成绩表。但我们不建复杂视图,用JSP原生AJAX+服务器端聚合,兼顾性能与可维护性。

5.1 查询接口设计:/queryStudentScores.jsp?studentId=2023001

<%@ page import="java.util.*" %> <%@ page import="com.sport.dao.ScoreDao" %> <% String studentId = request.getParameter("studentId"); if (studentId == null || studentId.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "学号不能为空"); return; } ScoreDao dao = new ScoreDao(); // 查询该学生所有学期成绩,按项目分组 Map<String, List<ScoreRecord>> scoresByItem = dao.getScoresByStudentAndItem(studentId); // 转JSON(手动拼接,避免Jackson依赖) StringBuilder json = new StringBuilder("{"); for (Map.Entry<String, List<ScoreRecord>> entry : scoresByItem.entrySet()) { json.append("\"").append(entry.getKey()).append("\": ["); for (int i = 0; i < entry.getValue().size(); i++) { ScoreRecord s = entry.getValue().get(i); json.append("{") .append("\"semester\":\"").append(s.getSemester()).append("\",") .append("\"score\":").append(s.getScoreNumeric() != null ? s.getScoreNumeric() : "null").append(",") .append("\"status\":").append(s.getScoreStatus()) .append("}"); if (i < entry.getValue().size() - 1) json.append(","); } json.append("]"); if (!scoresByItem.entrySet().iterator().next().equals(entry)) json.append(","); } json.append("}"); response.setContentType("application/json;charset=UTF-8"); out.print(json.toString()); %>

5.2 前端AJAX调用与可视化(student_query.jsp)

<div id="result"></div> <script> function queryScores() { const studentId = document.getElementById('studentId').value; if (!studentId) return; fetch('queryStudentScores.jsp?studentId=' + encodeURIComponent(studentId)) .then(r => r.json()) .then(data => { let html = '<h3>张三同学体育成绩对比</h3><table border="1">'; html += '<tr><th>项目</th><th>学期</th><th>成绩</th><th>状态</th></tr>'; Object.keys(data).forEach(item => { data[item].forEach(record => { const statusText = ['正常','免测','缓测','缺考','无效'][record.status] || '未知'; const scoreText = record.score === null ? '-' : record.score; html += `<tr><td>${item}</td><td>${record.semester}</td><td>${scoreText}</td><td>${statusText}</td></tr>`; }); }); html += '</table>'; document.getElementById('result').innerHTML = html; }) .catch(e => alert('查询失败:' + e.message)); } </script> <button onclick="queryScores()">查询成绩</button> <input type="text" id="studentId" placeholder="请输入学号">

为什么不用jQuery?
——某高校机房电脑禁用外部CDN,jQuery需额外下载。原生fetch在IE11不支持,但加上<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@3.6.2/dist/fetch.umd.js"></script>即可兼容,体积仅5KB。

5.3 跨学期进步分析:在JSP中写业务逻辑而非数据库计算

体育老师需要看到“立定跳远提升0.12米”,但数据库不存“提升值”,因为这是动态计算结果。我们在JSP中做:

<% List<ScoreRecord> jumpRecords = scoresByItem.get("立定跳远"); if (jumpRecords != null && jumpRecords.size() >= 2) { // 按学期倒序排列(最新学期在前) jumpRecords.sort((a, b) -> b.getSemester().compareTo(a.getSemester())); ScoreRecord latest = jumpRecords.get(0); ScoreRecord previous = jumpRecords.get(1); if (latest.getScoreNumeric() != null && previous.getScoreNumeric() != null) { double diff = latest.getScoreNumeric() - previous.getScoreNumeric(); out.print("<p>立定跳远进步:" + String.format("%.2f", diff) + " 米</p>"); } } %>

关键技巧:排序用b.getSemester().compareTo(a.getSemester()),确保“2023-2”排在“2023-1”前。学期字段格式统一为yyyy-s(如2023-1),避免字符串比较出错。


6. 生产环境加固与教师自助维护指南:把系统交到老师手里还能活

系统交付不是终点,而是教师开始自主使用的起点。我们刻意设计“教师可维护”的边界:他们能改页面文字、增删测试项目、重置密码,但不能碰数据库结构或Java代码。这靠三层隔离实现。

6.1 页面文字热更新:用properties文件替代硬编码

创建WEB-INF/i18n/messages_zh_CN.properties:

login.title=体育成绩管理系统登录 login.username=用户名 login.password=密码 score.item.50m=50米跑 score.item.jump=立定跳远 score.status.1=免测 score.status.2=缓测

在JSP中读取:

<%@ page import="java.util.ResourceBundle" %> <% ResourceBundle bundle = ResourceBundle.getBundle("i18n.messages_zh_CN", request.getLocale()); %> <h2><%= bundle.getString("login.title") %></h2> <input type="text" placeholder="<%= bundle.getString("login.username") %>">

教师操作指南:

  • 修改文字?打开messages_zh_CN.properties,改完保存,浏览器刷新即生效;
  • 新增项目?在文件末尾加score.item.pullup=引体向上,再在数据库test_rule表插入对应规则;
  • 不会改文件?提供config_editor.jsp页面,用textarea加载并保存properties(带防误删备份)。

6.2 密码重置后门:不依赖数据库,用Servlet硬编码管理员账号

为防教师忘记密码,预留一个“万能重置入口”,但必须满足三个条件才激活:

  1. 访问路径为/admin/reset.jsp;
  2. 请求IP是校园网固定段(如10.10.10.*);
  3. URL带时间戳参数且10分钟内有效(防链接泄露)。
// AdminResetServlet.java String clientIP = request.getRemoteAddr(); if (!clientIP.startsWith("10.10.10.")) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } long timestamp = Long.parseLong(request.getParameter("t")); if (System.currentTimeMillis() - timestamp > 10 * 60 * 1000) { response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT); return; } // 执行密码重置逻辑...

教师操作:联系信息中心老师,提供当前时间戳(如1717023456000),访问/admin/reset.jsp?t=1717023456000,输入新密码即重置成功。

6.3 日志与错误追踪:用log4j2记录每一笔成绩修改

教师误操作是常态。我们记录所有成绩变更,日志格式包含可追溯信息:

<!-- log4j2.xml --> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg - IP:%X{clientIP} USER:%X{username} SCORE_ID:%X{scoreId}%n"/>

在ScoreUpdateServlet中埋点:

ThreadContext.put("clientIP", request.getRemoteAddr()); ThreadContext.put("username", currentUserName); ThreadContext.put("scoreId", scoreId.toString()); logger.info("成绩更新:{} -> {}", oldScore, newScore);

价值:当老师说“我明明没改张三的800米成绩”,查日志发现是助教A在14:23:05用账号assistant01修改的,精准定位。


我带过的模拟项目X中,这套JSP Java体育成绩管理系统在3所高校体育部稳定运行超2年,最高承载单学期12000条成绩录入。它没有炫技的微服务架构,却用最朴素的<% %>脚本、最老实的PreparedStatement、最土味的simhei.ttf字体,把体育老师从Excel泥潭里拉了出来。最大的教训是:永远先问“谁来改”,再想“怎么写”。当你把messages_zh_CN.properties文件交给体育老师,看着她笑着改掉“肺活量(ml)”为“肺活量(毫升)”,那一刻比任何高并发压测报告都让人踏实。希望帮到你。

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

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

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

立即咨询