简介:这是一套面向高校计算机相关专业毕业设计的完整项目资料,基于Spring Boot框架开发大学生体质测试管理系统,采用B/S架构与MySQL数据库,适合正在准备毕业设计或需要Java Web实战案例的学生与开发者参考。资源包共797个文件,约53.23MB,涵盖99个Java源码文件、41个Vue组件、34个HTML页面、53个CSS样式及164个JavaScript脚本,另含SQL建表脚本、项目说明文档与演示视频,完整呈现前后端分离的工程结构。系统划分首页、体质测试、公告资讯、留言板、个人中心与后台管理等模块,管理员可管理用户、教师、体质测试、测试报告与成绩,用户和教师则按权限查看对应数据,功能边界清晰。已有285人学习下载,可帮助读者快速理解Spring Boot项目分层设计、权限控制与数据库表结构,并借助开发说明与录屏完成环境搭建和二次开发,是毕业设计选题与答辩准备的实用参考。
1. 从一份体测成绩单说起:这套 SpringBoot 管理系统到底解决什么问题
每年九月,高校体育部最头疼的事不是组织测试,而是测完之后那堆数据。50 米跑、坐位体前屈、800 米、1000 米、引体向上、肺活量,几千名学生的成绩分散在十几张 Excel 里,辅导员催着要汇总,体育老师要算达标率,教务处要按《国家学生体质健康标准》出报表。手工合并表格的后果就是:学号对不上、成绩重复录入、某个学院漏交一批数据,最后谁也不知道哪份才是最终版。
基于 SpringBoot 框架的大学生体质测试管理系统,本质就是把这套流程从 Excel 搬到 Web 端:学生能查自己的成绩和历年变化,体育老师能批量录入和审核,管理员能按学院、年级、项目维度出统计报表。它属于典型的计算机毕业设计选题——业务边界清晰、技术栈主流、工作量可控,同时又能覆盖 SpringBoot 后端开发、MyBatis 持久化、前后端分离、权限控制这些面试常问的点。如果你正在找计算机毕业设计题目,或者想拿一个真实业务场景练 SpringBoot 全栈,这个方向值得认真做一遍。下面我按实际开发顺序,把选型、建表、接口、踩坑和验收一条条讲清楚。
2. 技术选型与工程骨架:为什么这套组合最适合毕业设计落地
2.1 后端为什么锁定 SpringBoot + MyBatis 而不是别的
毕业设计的时间窗口通常只有两三个月,还要同时写论文、准备答辩。技术选型的核心原则不是“用最新最炫的”,而是“生态成熟、出问题能搜到答案、代码量可控”。SpringBoot 在这个场景下的优势很直接:内嵌 Tomcat,一个main方法就能跑起来,不用单独配 Web 服务器;starter 依赖把版本冲突问题基本抹平;application.yml集中管理数据源、端口、日志,改配置不用翻 XML。
持久层选 MyBatis 而不是 JPA,理由更实际。体测系统里有大量统计查询——按学院算平均分、按项目算达标率、按学年做同比——这些用 MyBatis 手写 SQL 反而比 JPA 的 Criteria API 更直观,也更容易在答辩时讲清楚“这条 SQL 干了什么”。分页用 PageHelper 插件,一行代码搞定物理分页,这是 MyBatis 生态里最成熟的方案。
前端如果时间紧,用 Thymeleaf 做服务端渲染最省事;如果想让简历好看一点,就上 Vue + Axios 做前后端分离,后端只返回 JSON。两种都能跑通,我一般建议基础一般的同学先用 Thymeleaf 把业务跑通,再考虑拆前后端。
2.2 用 Spring Initializr 生成骨架并跑通第一个接口
不要手动建 Maven 工程,直接用 IDEA 的 Spring Initializr 或者 start.spring.io 生成,省去写pom.xml的功夫。勾选依赖时把下面这几个选上:Spring Web、MyBatis Framework、MySQL Driver、Lombok。
生成后先改application.yml,把数据源配好:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fitness_test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml # XML 映射文件位置 type-aliases-package: com.example.fitness.entity configuration: map-underscore-to-camel-case: true # 数据库下划线自动映射驼峰字段map-underscore-to-camel-case这个配置一定要开,否则student_id映射不到studentId,查出来全是 null,这是新手最常见的翻车点之一。serverTimezone也必须带,MySQL 8 不配时区会直接报连接异常。
然后写一个最简单的健康检查接口验证工程能跑:
@RestController @RequestMapping("/api/health") public class HealthController { @GetMapping public Map<String, Object> check() { Map<String, Object> result = new HashMap<>(); result.put("status", "UP"); result.put("timestamp", System.currentTimeMillis()); return result; } }启动类跑起来,浏览器访问http://localhost:8080/api/health,能看到 JSON 就说明骨架没问题。这一步别跳过,很多后面排查半天的“接口 404”,根源就是启动类包路径不对导致组件扫描不到。
2.3 分层结构怎么切才不会被答辩老师追问
标准分层是 controller / service / mapper / entity,但毕业设计里我建议再加一层 DTO 和 VO。DTO 接前端传参,VO 返回给前端,entity 只对应数据库表。这样做的好处是:体测成绩表里有敏感字段(比如身份证号),直接返回 entity 会泄露;用 VO 可以只挑需要的字段。答辩时老师问“为什么不用 entity 直接返回”,你能答出字段隔离和安全考虑,就是加分项。
包结构建议按业务模块分,而不是按技术层分:
com.example.fitness ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo └── common # 统一返回结果、异常处理、工具类common包里放一个Result<T>统一返回格式,前端处理响应会省很多事。这个习惯从毕业设计就养成,工作后直接受益。
3. 数据库设计与核心表:体测业务的数据模型怎么建才不返工
3.1 五张核心表撑起整个系统
体测系统的数据关系其实不复杂,抓住“谁、测了什么、什么时候测的、成绩多少、达没达标”这条主线就够了。核心表我一般设计成五张:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| student | 学生信息 | id, student_no, name, gender, college_id, class_name |
| college | 学院信息 | id, college_name |
| test_item | 测试项目 | id, item_name, unit, sort_order |
| test_record | 测试记录(核心) | id, student_id, item_id, score, test_year, test_date |
| sys_user | 系统用户 | id, username, password, role, student_id |
test_record是整张系统的核心,student_id + item_id + test_year建议加联合唯一索引,防止同一个学生同一年同一个项目被录入两次。这个约束在数据库层面挡住,比在代码里判断靠谱得多。
性别字段用tinyint(1 男 2 女)而不是字符串,省空间也方便统计。测试项目表里unit存单位(秒、厘米、毫升),前端展示时直接拼上去,不用硬编码。
3.2 建表 SQL 与初始化数据
CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL COMMENT '1男2女', college_id BIGINT NOT NULL, class_name VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE test_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, item_id BIGINT NOT NULL, score DECIMAL(6,2) NOT NULL COMMENT '成绩数值', test_year VARCHAR(10) NOT NULL COMMENT '如2024-2025', test_date DATE, UNIQUE KEY uk_student_item_year (student_id, item_id, test_year), INDEX idx_year (test_year) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字符集一定用utf8mb4,不要用utf8。学生姓名里偶尔有生僻字,utf8存不进去会直接报错,这个坑我在真实项目里踩过。score用DECIMAL不用FLOAT,成绩要精确到小数点后一位,浮点数会有精度问题。
初始化数据里,test_item按国家标准把项目录进去:身高体重、肺活量、50 米跑、坐位体前屈、立定跳远、引体向上(男)/仰卧起坐(女)、1000 米(男)/800 米(女)。sort_order控制前端展示顺序,别依赖 id 排序。
3.3 达标判定逻辑放在哪一层
达标标准是分性别、分年级、分项目的,比如大一男生 50 米跑 7.1 秒是优秀,大三大四标准会放宽。这套规则如果写死在代码里,改一次标准就要重新发版。我的做法是单独建一张score_standard表,存gender + grade + item_id + level + min_score + max_score,判定时查表比对。
public String judgeLevel(Long itemId, Integer gender, String grade, BigDecimal score) { // 根据项目、性别、年级查出该项目的所有等级区间 List<ScoreStandard> standards = standardMapper.selectByItem(itemId, gender, grade); for (ScoreStandard std : standards) { // 注意:跑步类项目成绩越小越好,逻辑要区分 if (std.getIsAsc() == 1) { if (score.compareTo(std.getMinScore()) >= 0 && score.compareTo(std.getMaxScore()) <= 0) { return std.getLevel(); } } else { if (score.compareTo(std.getMinScore()) <= 0 && score.compareTo(std.getMaxScore()) >= 0) { return std.getLevel(); } } } return "不及格"; }这里的关键参数是isAsc——标记这个项目是“越大越好”还是“越小越好”。跑步、50 米这类是越小越好,肺活量、跳远是越大越好。如果不区分,跑步成绩 7 秒会被判成不及格,这是逻辑上的硬伤。把判定规则做成配置表,答辩时老师问“标准变了怎么办”,你答“改数据不改代码”,这就是工程思维的体现。
4. 核心功能实现:成绩录入、批量导入与统计报表怎么写
4.1 单个成绩录入接口与参数校验
成绩录入看着简单,但校验规则不少:学生必须存在、项目必须存在、成绩必须在合理范围、同一年同一项目不能重复。用 SpringBoot 的@Valid注解做参数校验,比在方法里写一堆 if 清爽得多。
@PostMapping("/record") public Result<Void> addRecord(@RequestBody @Valid TestRecordDTO dto) { // 1. 校验学生是否存在 Student student = studentMapper.selectById(dto.getStudentId()); if (student == null) { return Result.fail("学生不存在"); } // 2. 校验成绩范围(防止录入 999 这种脏数据) if (dto.getScore().compareTo(BigDecimal.ZERO) < 0 || dto.getScore().compareTo(new BigDecimal("9999")) > 0) { return Result.fail("成绩数值超出合理范围"); } // 3. 唯一索引兜底,捕获重复插入异常 try { testRecordMapper.insert(dto.toEntity()); } catch (DuplicateKeyException e) { return Result.fail("该学生本学年此项目成绩已存在"); } return Result.success(); }DuplicateKeyException是 Spring 对数据库唯一约束冲突的封装,捕获它比先查再插更高效,也避免了并发下的竞态问题。参数校验的边界值(0 和 9999)根据实际项目调整,肺活量可能上万,所以上限要放宽。
4.2 用 EasyExcel 做批量导入,避开 POI 的内存坑
体测数据最大的来源是 Excel。用 Apache POI 直接读,几千行数据就可能 OOM,因为 POI 会把整个文件加载到内存。阿里开源的 EasyExcel 用 SAX 模式逐行读,内存占用极低,是毕业设计里处理 Excel 的首选。
@PostMapping("/import") public Result<ImportResult> importExcel(@RequestParam("file") MultipartFile file) { List<TestRecordImportDTO> failList = new ArrayList<>(); int[] successCount = {0}; try { EasyExcel.read(file.getInputStream(), TestRecordImportDTO.class, new AnalysisEventListener<TestRecordImportDTO>() { @Override public void invoke(TestRecordImportDTO data, AnalysisContext context) { // 逐行处理,每行独立事务,失败不影响其他行 try { recordService.saveOne(data); successCount[0]++; } catch (Exception e) { data.setFailReason(e.getMessage()); failList.add(data); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 全部解析完成后的回调 } }).sheet().doRead(); } catch (IOException e) { return Result.fail("文件读取失败"); } ImportResult result = new ImportResult(successCount[0], failList); return Result.success(result); }关键点是逐行独立处理,某一行学号不存在或成绩格式错误,只记录这一行的失败原因,不影响其他行导入。返回结果里带上成功条数和失败明细,前端可以导出失败清单让老师修正后重新导入。这个设计比“全部成功或全部回滚”更实用,因为 Excel 数据脏是常态。
4.3 统计报表的 SQL 怎么写才不慢
统计报表是体测系统的门面功能。按学院算达标率、按项目算平均分、按学年做趋势对比,这些查询如果写不好,几千条数据就能让页面卡住。
按学院统计达标率的 SQL:
SELECT c.college_name, COUNT(*) AS total, SUM(CASE WHEN r.level != '不及格' THEN 1 ELSE 0 END) AS pass_count, ROUND(SUM(CASE WHEN r.level != '不及格' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM test_record r JOIN student s ON r.student_id = s.id JOIN college c ON s.college_id = c.id WHERE r.test_year = #{year} GROUP BY c.college_name ORDER BY pass_rate DESC;这里有个性能细节:level字段如果是在录入时就算好存进test_record的,统计时直接查就行;如果每次统计都实时判定,SQL 会变得很复杂且慢。我的做法是在录入和导入时就计算好level并落库,统计只做聚合。代价是标准表变更后需要重算历史数据,但体测标准几年才改一次,这个代价可以接受。
test_year上建了索引,student_id和college_id是外键关联字段,也要确保有索引。几千条数据可能感觉不出来,但答辩演示时如果数据量上万,没索引的查询会明显卡顿,老师一眼就能看出来。
5. 避坑与排查:这套系统开发中最容易翻车的五个地方
5.1 中文乱码:从数据库到前端一条链都要查
现象:学生姓名在数据库里显示正常,但接口返回给前端变成问号或乱码。
原因:乱码可能出现在三个环节——数据库连接字符集、表字符集、HTTP 响应编码。MySQL 连接 URL 里如果没带characterEncoding=utf8,驱动会用默认编码;表如果建成了latin1,存进去就是错的;SpringBoot 返回 JSON 默认是 UTF-8,但如果手动设置了produces且写错,也会乱。
解决:连接 URL 加useUnicode=true&characterEncoding=utf8;建表统一utf8mb4;检查server.servlet.encoding.charset=UTF-8和force=true。三个地方都确认一遍,基本能根治。
5.2 分页查询总数不对:PageHelper 的 count 查询被 JOIN 干扰
现象:列表分页显示每页 10 条,但总页数算出来偏大或偏小。
原因:PageHelper 会自动生成 count 查询,但如果你的 SQL 里有JOIN且GROUP BY,自动生成的 count 可能不准。比如按学院分组统计时,count 算的是分组前的行数。
解决:在 mapper 方法上手动指定 count 查询,或者用PageHelper.startPage(pageNum, pageSize, false)关闭自动 count,自己写一个 count 方法。这个坑在带 JOIN 的列表查询里特别常见。
5.3 批量导入时事务失效:同类内部调用不经过代理
现象:导入方法上加了@Transactional,但某一行失败后,前面成功的行也被回滚了,或者反过来该回滚的没回滚。
原因:Spring 的事务是基于 AOP 代理的,如果在同一个类里 A 方法直接调用本类的 B 方法(B 上有@Transactional),调用不经过代理,事务注解不生效。
解决:把需要独立事务的方法抽到另一个 Service 类里,通过注入调用;或者用AopContext.currentProxy()拿到代理对象再调。毕业设计里更简单的做法是逐行处理时不加事务,靠唯一索引和手动补偿保证一致性。
5.4 前端跨域:开发阶段 403 但 Postman 正常
现象:用 Postman 调接口一切正常,浏览器里前端请求报 CORS 错误。
原因:浏览器同源策略拦截,Postman 不受此限制。前后端分离时前端跑在 5173 或 8081,后端跑在 8080,端口不同就是跨域。
解决:后端加全局 CORS 配置,或者用 Vite 的server.proxy做代理。生产环境建议用 Nginx 反向代理,把前端和后端放同一个域名下,从根上避免跨域。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") // 生产环境要改成具体域名 .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别:当allowCredentials(true)时,allowedOrigins("*")会报错,必须用allowedOriginPatterns。这个细节很多人卡半天。
5.5 打包部署后接口 404:jar 包里 mapper XML 没打进去
现象:IDEA 里跑得好好的,mvn package之后java -jar启动,访问接口报 404 或 mapper 绑定异常。
原因:MyBatis 的 XML 文件默认放在src/main/java下的 mapper 包里,Maven 打包时只编译.java文件,.xml不会被复制到target/classes。
解决:在pom.xml的<build>里加资源目录配置,把src/main/java下的 XML 也纳入打包:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources>或者更规范的做法是把 XML 统一放到src/main/resources/mapper下,mapper-locations配成classpath:mapper/*.xml。两种都行,关键是打包后确认target/classes里能看到 XML 文件。
6. 从能跑到能答辩:验收清单与一个提升演示效果的小技巧
系统能跑起来只是及格线,答辩时老师看的不是“功能有没有”,而是“边界处理得怎么样、数据一致性有没有考虑、异常情况怎么兜底”。我整理了一份验收清单,按这个过一遍,基本能挡住大部分追问。
| 验收项 | 检查方法 | 合格标准 |
|---|---|---|
| 成绩重复录入 | 同一学生同一项目同一年录两次 | 第二次提示已存在,不产生脏数据 |
| 批量导入脏数据 | Excel 里混入不存在的学号 | 该行失败并返回原因,其他行正常导入 |
| 达标判定边界 | 录入刚好等于及格线的成绩 | 判定结果与标准表一致 |
| 分页总数 | 带 JOIN 的列表翻到最后一页 | 总页数与实际数据量吻合 |
| 权限隔离 | 学生账号访问管理接口 | 返回 403 或跳转登录 |
| 打包部署 | java -jar启动后访问所有接口 | 无 404、无 mapper 绑定异常 |
最后分享一个提升演示效果的小技巧:准备一份 200 条左右的模拟数据,覆盖不同学院、不同年级、不同达标等级。答辩演示时,先展示一个学生的成绩趋势图(用 ECharts 画折线),再切到管理端展示学院达标率排行。数据量够大,统计图才有说服力,老师也能直观看到系统的实际价值。
模拟数据不要手动录,写一个DataInitRunner实现CommandLineRunner,项目启动时自动灌入。这样换台电脑演示,数据环境一键还原,不用带着 SQL 文件到处跑。
@Component public class DataInitRunner implements CommandLineRunner { @Autowired private MockDataService mockDataService; @Override public void run(String... args) { // 只在数据为空时初始化,避免重复灌数据 if (studentMapper.count() == 0) { mockDataService.generate(200); } } }这个CommandLineRunner在开发阶段特别实用,但记得加数据量判断,否则每次重启都插一遍,唯一索引会直接报错。上线前把这段逻辑注释掉或者加个配置开关。
我自己做毕业设计时最大的教训是:功能写完就以为万事大吉,结果答辩前一周发现打包部署跑不起来,熬夜排查 mapper XML 的打包问题。后来养成习惯,每完成一个模块就mvn package跑一次,确认 jar 包能独立启动。这个习惯比任何技巧都值钱。希望帮到你。
本文还有配套的精品资源,点击获取