Java毕业设计学生成绩管理系统:从压缩包到简历项目的完整实战指南 简介这是一套面向高校计算机专业学生与Java初学者毕业设计参考的学生成绩管理系统完整资料包围绕学生信息、课程信息、成绩录入查询与统计分析等核心业务展开帮助读者理解从数据库设计到Web应用落地的全过程。压缩包共1073个文件约123.1MB以js、html、css等前端资源与gif、png图片素材为主另含java源码、class编译文件、sql建库脚本、xml配置、properties属性文件及mp4部署视频覆盖MVC分层、JDBC数据访问、前端交互与部署文档等环节。已有215人学习下载。读者可获得可运行的源码工程、数据库脚本、部署文档与可视化录屏对照Controller、Service、DAO分层结构梳理业务逻辑并借助排错思路与目录组织快速完成环境搭建与二次开发适合作为课程设计或毕业设计的实践范本。1. 从一份“学生成绩管理系统”压缩包说起Java 毕业设计到底该做成什么样每年到了毕设季计算机专业的学生手里都会拿到类似这样一个压缩包基于 Java 的学生成绩管理系统里面塞着源代码、数据库脚本、部署文档甚至还有一段部署视频。表面上看这就是一个增删改查的练手项目但真正动手跑起来的人会发现从解压到系统能登录、能录入成绩、能导出报表中间隔着一堆让人头大的细节。我带过几届学生的毕设也帮同事排查过他们公司新人的练手项目发现一个反直觉的结论这类系统翻车的地方往往不是业务逻辑写错了而是环境、数据库连接、字符编码和权限控制这些“边角料”没处理好。这篇文章就是写给正在做基于 Java 的毕业设计、或者想拿这套源码当 Spring Boot 入门实战的开发者我会把从拿到压缩包到系统稳定运行的完整路径拆开告诉你每一步该怎么做、参数怎么调、坑在哪里。读完你至少能判断这套东西值不值得投入时间以及怎么把它改成一个能写进简历的项目。2. 拆开压缩包先别急着跑Java 成绩管理系统的技术栈与选型逻辑2.1 为什么这类系统普遍是 Spring Boot MyBatis MySQL拿到源码后第一件事是看pom.xml或者build.gradle确认技术栈。绝大多数基于 Java 的学生成绩管理系统后端骨架都是 Spring Boot 加 MyBatis数据库用 MySQL前端要么是 Thymeleaf 服务端渲染要么是 Vue 加 Element UI 的前后端分离。这个组合不是随便选的它对应了毕业设计场景下的三个硬需求第一Spring Boot 的自动配置能让一个没接触过企业级框架的学生在两天内把项目跑起来第二MyBatis 的 XML 映射文件把 SQL 和 Java 代码分开方便答辩时展示“我懂数据库”第三MySQL 免费、资料多学校机房和导师电脑上大概率已经装好了。从选型角度看如果你打算把这个项目改造成能写进简历的版本我建议保留 Spring Boot 和 MySQL但把 MyBatis 换成 MyBatis-Plus或者至少加上 PageHelper 分页插件。原因很简单原始代码里的分页查询往往是手写的LIMIT拼接参数一多就容易出 SQL 注入和索引失效的问题。MyBatis-Plus 的Page对象能帮你省掉大量重复代码而且面试官看到你用了主流增强工具会默认你了解过实际项目里的开发效率问题。另一个需要确认的点是 JDK 版本。压缩包里的代码如果写于两三年前大概率用的是 JDK 8。你本地如果装了 JDK 17 或 21直接跑可能会遇到javax.annotation包找不到、或者 Lombok 版本不兼容的问题。稳妥的做法是先用java -version看一眼然后决定是切回 JDK 8还是把源码里的javax.*批量替换成jakarta.*。这一步没有捷径但替换之前记得先提交一次 Git不然改崩了连后悔药都没得吃。2.2 数据库表结构里藏着的三个设计信号打开数据库脚本文件通常叫db.sql或者schema.sql你会看到几张核心表学生表、课程表、成绩表、用户表。别急着执行先看三个地方。第一看成绩表的字段设计。如果成绩是直接存在score字段里类型是decimal(5,2)那说明作者考虑了小数成绩。但如果用的是varchar你就要小心了后面做统计平均分、最高分的时候数据库层面没法直接算得在 Java 里转BigDecimal性能差不说还容易出精度问题。遇到这种情况我一般会先把字段改成decimal(5,2)再写一个数据迁移脚本把旧数据转过去。第二看学生表和用户表是不是分开的。有些简易版本会把登录账号直接塞进学生表用学号当用户名。这种设计在单角色场景下能跑但一旦你要加“教师录入成绩”的功能权限就乱了。正确的做法是用户表独立通过role字段区分管理员、教师、学生学生表里只存学号、姓名、班级这些业务信息用user_id做外键关联。第三看有没有建索引。成绩表上如果经常按student_id和course_id联合查询那这两个字段上必须有联合索引。很多毕设源码为了省事只建了主键索引数据量一到几千条查询就明显变慢。你可以用EXPLAIN命令看一眼SELECT * FROM score WHERE student_id ? AND course_id ?的执行计划如果type是ALL那就得手动加索引了。-- 检查现有索引 SHOW INDEX FROM score; -- 如果缺少联合索引用这条语句补上 ALTER TABLE score ADD INDEX idx_student_course (student_id, course_id); -- 用 EXPLAIN 验证索引是否生效 EXPLAIN SELECT * FROM score WHERE student_id 1001 AND course_id 2001;上面这段 SQL 的逻辑很直接先看score表上已经有哪些索引然后补一个student_id和course_id的联合索引最后用EXPLAIN确认查询走了索引而不是全表扫描。参数说明方面idx_student_course是索引名你可以按自己的命名习惯改但建议保持idx_前缀方便后续维护。注意如果成绩表已经有大量数据加索引会锁表最好在业务低峰期操作或者用ALGORITHMINPLACE参数减少锁的时间。2.3 从压缩包到本地运行的最小命令集假设你已经确认了技术栈和表结构接下来就是把项目跑起来。我一般会按这个顺序操作先建数据库再改配置文件然后启动后端最后验证接口。# 1. 登录 MySQL创建数据库 mysql -u root -p -e CREATE DATABASE student_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 2. 导入表结构和初始数据 mysql -u root -p student_score db.sql # 3. 进入项目目录修改 application.yml 里的数据库连接信息 # 重点改这几项url、username、password # url 示例jdbc:mysql://localhost:3306/student_score?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai # 4. 用 Maven 打包并启动 mvn clean package -DskipTests java -jar target/student-score-0.0.1-SNAPSHOT.jar这段命令里第一步创建数据库时指定了utf8mb4字符集这是为了支持中文姓名和特殊符号。第二步导入 SQL 文件注意db.sql的路径要写对如果文件在压缩包解压后的sql目录里就先cd进去。第三步改配置文件是新手最容易翻车的地方serverTimezone必须设成Asia/Shanghai否则插入时间会差 8 小时characterEncoding要写utf8不是utf8mb4这是 MySQL 连接器的一个历史遗留问题写错了会报“Unknown character set”错误。第四步打包时加了-DskipTests因为毕设源码里的单元测试往往跑不通先跳过能节省时间等系统跑起来再回头修测试。启动成功后打开浏览器访问http://localhost:8080如果看到登录页说明后端和前端已经通了。默认账号密码通常在db.sql的user表里用SELECT * FROM user;查一下就能看到。如果登录报 500 错误先看控制台日志大概率是数据库连接池配置不对或者mapper文件路径没扫描到。3. 把增删改查写出生产味成绩录入、查询与导出的代码级实现3.1 成绩录入接口的四个必调参数成绩录入看起来简单不就是往score表插一条记录吗但实际写的时候有四个参数必须处理好否则系统上线第一天就会被用户骂。第一个是student_id和course_id的合法性校验。不能直接拿前端传过来的 ID 去插库要先查学生表和课程表确认这两个 ID 存在。第二个是成绩范围校验通常要求 0 到 100 之间但有些学校允许 0 到 150 的卷面分所以这个范围最好做成配置项别硬编码。第三个是重复录入的处理同一个学生同一门课只能有一条成绩记录插入前要先SELECT一次或者给student_id和course_id建唯一索引让数据库来兜底。第四个是事务控制如果一次录入多条成绩要么全成功要么全回滚不能插一半留一半。Service public class ScoreService { Autowired private ScoreMapper scoreMapper; Autowired private StudentMapper studentMapper; Autowired private CourseMapper courseMapper; Transactional(rollbackFor Exception.class) public void addScore(Long studentId, Long courseId, BigDecimal scoreValue) { // 1. 校验学生和课程是否存在 Student student studentMapper.selectById(studentId); Course course courseMapper.selectById(courseId); if (student null || course null) { throw new BusinessException(学生或课程不存在); } // 2. 校验成绩范围从配置读取上限 BigDecimal maxScore new BigDecimal(100); if (scoreValue.compareTo(BigDecimal.ZERO) 0 || scoreValue.compareTo(maxScore) 0) { throw new BusinessException(成绩必须在 0 到 maxScore 之间); } // 3. 检查是否重复录入 Score existing scoreMapper.selectByStudentAndCourse(studentId, courseId); if (existing ! null) { throw new BusinessException(该学生这门课已有成绩请使用修改功能); } // 4. 插入记录 Score score new Score(); score.setStudentId(studentId); score.setCourseId(courseId); score.setScoreValue(scoreValue); score.setCreateTime(new Date()); scoreMapper.insert(score); } }这段代码的逻辑是先做存在性校验再做范围校验然后查重最后插入。Transactional注解保证了如果插入过程中抛异常前面的查询操作不会留下脏数据。参数方面scoreValue用BigDecimal而不是double是为了避免浮点数精度问题比如 89.99 存进去变成 89.98999999。maxScore从配置读取是个好习惯你可以在application.yml里加一行score.max: 100然后用Value注入这样不同学校的不同评分标准就不用改代码了。3.2 多条件分页查询的 SQL 与索引配合成绩查询页面通常会有多个筛选条件按班级、按课程、按分数段、按学号模糊搜索。如果每个条件都写一个if拼 SQL代码会变得又长又容易出注入漏洞。我一般用 MyBatis 的动态 SQL 标签来处理配合分页插件既能保证安全又能让数据库用上索引。select idselectByCondition resultTypecom.example.entity.ScoreVO SELECT s.id, st.name AS studentName, c.name AS courseName, s.score_value, s.create_time FROM score s LEFT JOIN student st ON s.student_id st.id LEFT JOIN course c ON s.course_id c.id where if teststudentId ! null AND s.student_id #{studentId} /if if testcourseId ! null AND s.course_id #{courseId} /if if testminScore ! null AND s.score_value gt; #{minScore} /if if testmaxScore ! null AND s.score_value lt; #{maxScore} /if if teststudentName ! null and studentName ! AND st.name LIKE CONCAT(%, #{studentName}, %) /if /where ORDER BY s.create_time DESC /select这段 XML 的关键在于where标签会自动处理第一个AND避免 SQL 语法错误。LIKE CONCAT(%, #{studentName}, %)用的是参数绑定不是字符串拼接所以没有 SQL 注入风险。但要注意LIKE %xxx%这种前置模糊查询用不上索引如果学生表数据量大查询会变慢。优化方案是把模糊搜索改成前缀匹配LIKE CONCAT(#{studentName}, %)或者引入 Elasticsearch 做全文检索。对于毕设项目数据量通常不大前置模糊可以接受但你要知道这个边界在哪里。分页参数方面MyBatis-Plus 的Page对象需要传current和size两个参数。current是当前页码从 1 开始size是每页条数建议默认 10 或 20不要设太大否则前端渲染会卡。如果你用的是原生 MyBatis那就得自己写LIMIT子句记得LIMIT offset, size里的offset是(current - 1) * size算错了会漏数据。3.3 成绩导出 Excel 的三种方案与选型建议导出功能是毕业设计的加分项但也是容易翻车的地方。常见方案有三种Apache POI、EasyExcel、还有直接生成 CSV。Apache POI 功能最全但内存占用高导出几千条数据就可能 OOMEasyExcel 是阿里开源的基于 SAX 解析内存友好代码也简洁CSV 最简单但没法设置单元格样式而且中文用 Excel 打开容易乱码。我一般推荐 EasyExcel尤其是数据量超过 1000 条的场景。下面是一个导出成绩单的示例public void exportScores(HttpServletResponse response, ScoreQuery query) throws IOException { ListScoreVO list scoreMapper.selectByCondition(query); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(成绩单, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment;filename*UTF-8 fileName .xlsx); EasyExcel.write(response.getOutputStream(), ScoreVO.class) .sheet(成绩明细) .doWrite(list); }这段代码里response.setContentType指定了 xlsx 的 MIME 类型Content-Disposition里的filename*UTF-8是为了让中文文件名在浏览器里正常显示不会变成乱码。EasyExcel.write的第一个参数是输出流第二个参数是数据类ScoreVO上的字段需要用ExcelProperty注解标记列名。注意导出操作最好放在独立的线程里或者用异步任务否则数据量大时会阻塞 HTTP 请求前端一直转圈。如果你坚持用 Apache POI记得用SXSSFWorkbook而不是XSSFWorkbook前者会在内存里只保留一定行数超出部分写到临时文件能有效防止 OOM。但SXSSFWorkbook不支持一些高级样式比如合并单元格后的边框用之前要确认需求。4. 部署文档和视频里不会告诉你的五个坑4.1 坑一MySQL 8 的时区与驱动类名现象项目在本地跑得好好的一部署到服务器就报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8 的 JDBC 驱动默认用 UTC 时区而服务器上的 MySQL 配置的是中国标准时间驱动解析不了中文时区名。解决在application.yml的 JDBC URL 里加上serverTimezoneAsia/Shanghai同时把驱动类名从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。这两个改动缺一不可只改一个还是会报错。4.2 坑二静态资源 404 与拦截器顺序现象登录页能打开但 CSS 和 JS 文件全部 404页面样式全乱。原因Spring Boot 的拦截器把静态资源请求也拦截了或者static目录下的文件没有被正确映射。解决在拦截器的preHandle方法里放行/static/**、/css/**、/js/**这些路径或者用WebMvcConfigurer的addResourceHandlers手动指定静态资源位置。注意如果你用的是前后端分离架构前端打包后的dist目录要放到resources/static下并且配置index.html的转发规则。4.3 坑三中文乱码的三层排查现象录入学生姓名后数据库里显示正常但页面上显示成问号。原因字符编码问题可能出现在三个地方——数据库连接、数据库表、HTTP 响应。解决第一步检查 JDBC URL 里有没有characterEncodingutf8第二步用SHOW CREATE TABLE student;看表的字符集是不是utf8mb4第三步在application.yml里加server.servlet.encoding.charsetutf-8和server.servlet.encoding.forcetrue。三层都确认一遍基本能解决 99% 的乱码问题。4.4 坑四事务不生效的三种写法现象在 Service 方法上加了Transactional但插入失败后数据还是留在了库里。原因第一种可能是方法不是public的Spring 的 AOP 代理只拦截public方法第二种可能是同类内部调用比如this.addScore()调用了另一个带事务的方法代理没生效第三种可能是异常被try-catch吞掉了没有抛出去事务管理器以为一切正常。解决把方法改成public把内部调用改成通过注入的代理对象调用或者在catch块里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。4.5 坑五部署视频里的路径和你的不一样现象跟着部署视频一步步操作但视频里用的是D:\project\student-score你的是C:\Users\xxx\Desktop\毕设\student-score结果命令执行失败。原因视频录制者的环境路径和你的不同而且 Windows 路径里有空格和中文时命令行解析会出问题。解决把项目放到一个纯英文、无空格的路径下比如D:\workspace\student-score。如果必须用中文路径记得在命令行里给路径加双引号。另外视频里如果用了mvn命令你要先确认本地装了 Maven 并配好了MAVEN_HOME环境变量否则会报“mvn 不是内部或外部命令”。5. 从能跑到能写进简历三个进阶改造方向5.1 用 Spring Security 替换手写登录拦截原始代码里的登录校验通常是在拦截器里查 Session或者用一个ThreadLocal存用户信息。这种写法在单机环境下能跑但一旦你要做权限细分比如“教师只能录入自己教的课程成绩”就会变得很乱。我建议花半天时间把 Spring Security 集成进来用UserDetailsService加载用户用PreAuthorize注解控制接口权限。改造完之后你的项目在简历上就能写“基于 Spring Security 实现 RBAC 权限模型”比“手写拦截器”有分量得多。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/login, /css/**, /js/**).permitAll() .requestMatchers(/score/add).hasRole(TEACHER) .requestMatchers(/score/export).hasAnyRole(TEACHER, ADMIN) .anyRequest().authenticated() ) .formLogin(form - form.loginPage(/login).defaultSuccessUrl(/index)) .logout(logout - logout.logoutSuccessUrl(/login)); return http.build(); } }这段配置里requestMatchers定义了不同路径的访问权限hasRole(TEACHER)表示只有教师角色能访问成绩录入接口。formLogin指定了自定义登录页defaultSuccessUrl是登录成功后的跳转地址。注意Spring Security 6 的配置方式和 5 有较大差异如果你用的是 Spring Boot 3.xauthorizeHttpRequests和requestMatchers是新写法老教程里的authorizeRequests和antMatchers已经废弃了。5.2 加一层 Redis 缓存高频查询成绩查询页面如果每次刷新都查数据库并发一高数据库就扛不住。我一般会加一层 Redis 缓存把“按课程查成绩”这种高频查询的结果缓存起来设置 5 分钟过期。用 Spring Cache 注解实现最简单Cacheable(value scoreByCourse, key #courseId, unless #result null) public ListScoreVO getScoresByCourse(Long courseId) { return scoreMapper.selectByCourseId(courseId); } CacheEvict(value scoreByCourse, key #courseId) public void updateScore(Long courseId, Long studentId, BigDecimal newScore) { // 更新逻辑 }Cacheable表示第一次查询走数据库结果存入 Redis后续相同courseId的请求直接读缓存。unless #result null是防止空结果被缓存否则一旦缓存了空列表后面即使有数据也查不出来。CacheEvict在更新成绩时清除对应课程的缓存保证数据一致性。注意Redis 的序列化方式要配好默认的 JDK 序列化要求实体类实现Serializable接口而且存出来的值在 Redis 客户端里看是乱码建议换成GenericJackson2JsonRedisSerializer。5.3 用 Docker Compose 一键拉起整个环境部署文档里如果写的是“先装 JDK再装 MySQL再改配置”那这份文档的复用价值就很低。我习惯用 Docker Compose 把 MySQL、Redis 和应用打包在一起换台机器只需要docker-compose up -d就能跑起来。version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: student_score ports: - 3306:3306 volumes: - ./db.sql:/docker-entrypoint-initdb.d/db.sql redis: image: redis:7-alpine ports: - 6379:6379 app: build: . ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/student_score?serverTimezoneAsia/Shanghai SPRING_REDIS_HOST: redis这份 Compose 文件里mysql服务通过volumes把db.sql挂载到初始化目录容器第一次启动时会自动执行建表和插数据。app服务用depends_on确保 MySQL 和 Redis 先启动但depends_on只保证启动顺序不保证服务就绪所以应用里最好加一个重试连接逻辑。SPRING_DATASOURCE_URL里的主机名是mysql不是localhost因为容器之间通过服务名通信。这套配置跑通之后你的项目就具备了“一键部署”的能力面试时聊起来也比“我手动配了环境”更有说服力。最后说一个我自己的习惯每次改完代码不管多小的改动都先git commit一次。毕设期间你可能同时改数据库、改后端、改前端一旦某个改动导致系统跑不起来没有版本控制就只能靠记忆回滚那才是真正的血泪经验。希望帮到你。本文还有配套的精品资源点击获取