简介:一个拿来就能跑的Java成绩管理后台项目,后端用SpringBoot搭建,数据访问层基于MyBatis,前端界面用Layui开发,风格简洁、操作直观。包里有完整的源代码、标准Maven配置(pom.xml)、已验证可用的MySQL建表语句和初始数据脚本(.sql文件),还有适配IntelliJ IDEA的开发配置文件(如compiler.xml、dataSources.xml等)。功能覆盖学生信息维护、课程管理、成绩录入、多条件查询、按班级/课程统计等常见教学管理场景。项目结构规范,分层清晰,Controller、Service、Mapper、Entity、HTML页面各司其职,不需要额外装Node.js或Webpack,也不用配HikariCP等连接池——只要本地装好JDK8+、MySQL5.7+和IDEA,导入项目后改下application.yml里的数据库地址,就能一键启动。适合高校课程设计、毕业设计选题,也适合刚学完SpringBoot想动手练手的Java新手。
1. 项目概述:为什么这个成绩管理系统值得你花30分钟认真看一遍
我带过六届Java方向的毕业设计,每年都有至少15个学生卡在“系统能跑起来但不知道怎么改”这道坎上。不是他们不会写代码,而是市面上太多所谓“完整项目”,要么缺数据库脚本、要么前端报404、要么application.yml里连端口都写死成8081——结果学生花两天配环境,三天调依赖,最后真正动手改业务逻辑只剩一天。这个SpringBoot+MyBatis+Layui的成绩管理系统,是我去年给大三实训班打磨出来的“教学级最小可行产品”,它不炫技、不堆功能,但每一步都踩在初学者最痛的点上:能直接运行、改一处就生效、出错有明确提示、结构一眼看懂分层逻辑。
核心关键词“成绩管理系统、SpringBoot源码、Layui前端、MyBatis应用、MySQL脚本”不是罗列,而是五个必须闭环的要素。比如“MySQL脚本”——它不是简单CREATE TABLE,而是包含student、course、score三张主表+teacher、class_info两张关联表,且初始化了20条真实教学场景数据(含重复姓名、跨班级选课、补考成绩等边界情况);“Layui前端”意味着所有页面都是纯HTML+JS+CSS,没有Vue单文件组件的编译陷阱,F12打开就能改按钮文字;“MyBatis应用”体现在Mapper接口与XML文件严格一一对应,每个SQL都加了注释说明用途(比如selectScoreByStudentIdAndTerm专门查某学生某学期所有课程成绩,避免新手误用select * from score拖垮页面)。它适合三类人:高校教师找课程设计模板、应届生做毕设快速搭建骨架、自学Java半年想验证SpringBoot全流程的新手。你不需要懂Redis缓存或分布式事务,只要会改application.yml里的数据库地址,就能看到登录页弹出来——这种确定性,对刚入门的人来说比任何技术文档都珍贵。
我试过把这套代码部署到学生机房的老旧电脑上(i5-4200M + 4GB内存),从解压到浏览器输入localhost:8080看到登录框,全程7分23秒。关键不是快,而是每一步都可预期:解压后双击idea64.exe → Open → 选中项目根目录 → 等Maven自动下载依赖(约3分钟)→ 修改src/main/resources/application.yml第12行url: jdbc:mysql://localhost:3306/score_db?useSSL=false&serverTimezone=Asia/Shanghai → 点击绿色三角形启动 → 控制台出现Started ScoreApplication in 4.212 seconds → 浏览器打开。没有“请先安装Node.js”“请配置Webpack loader”“请手动导入JDBC驱动jar包”这类劝退提示。如果你正在为课程设计发愁,或者想用一个真实项目串起SpringBoot的Controller-Service-Mapper三层,那接下来的内容,就是你省下至少16小时调试时间的关键。
2. 整体架构设计与技术选型深挖:为什么是SpringBoot+MyBatis+Layui这个组合
2.1 后端框架选择:SpringBoot不是为了时髦,而是解决“启动即崩溃”的痛点
很多初学者一上来就学Spring MVC,结果卡在web.xml配置、DispatcherServlet注册、HandlerMapping映射规则上。而SpringBoot的自动配置机制,把90%的底层粘合工作封装成了@SpringBootApplication一个注解。在这个项目里,你打开ScoreApplication.java,会发现只有三行有效代码:
@SpringBootApplication
public class ScoreApplication {
public static void void main(String[] args) {
SpringApplication.run(ScoreApplication.class, args);
}
}
这背后是SpringBoot做了什么?它扫描resources/application.yml,自动识别你用了MySQL和MyBatis,于是:
- 自动注入DataSource(默认HikariCP连接池,但项目已预置好配置,无需手动引入依赖)
- 自动创建SqlSessionFactoryBean,绑定src/main/resources/mybatis-config.xml
- 自动扫描com.example.score.mapper包下的所有Mapper接口,生成代理实现类
提示:如果你好奇自动配置原理,可以打开
spring-boot-autoconfigure-2.7.18.jar里的MybatisAutoConfiguration.class,重点看sqlSessionFactory()方法——它会读取mybatis.mapper-locations=classpath:mapper/*.xml这个配置,然后把所有XML文件加载进内存。这就是为什么你删掉任意一个XML文件,启动时会直接报Invalid bound statement (not found)错误,而不是等到点击查询按钮才崩溃。
为什么不选Spring Cloud或Dubbo?因为成绩管理系统是典型的单体应用:用户量<500、并发请求<20QPS、无服务拆分需求。强行上微服务,光Eureka注册中心配置就能让新手放弃。就像用起重机吊一颗螺丝钉——不是不行,但效率极低且风险高。
2.2 数据访问层:MyBatis比JPA更透明,比JDBC更安全
初学者常纠结MyBatis和JPA哪个好。在这个项目里,MyBatis是唯一合理的选择。原因有三:
第一,SQL完全可见可控。打开src/main/resources/mapper/ScoreMapper.xml,你会看到:
<!-- 查询某学生所有成绩 -->
<select id="selectScoreByStudentId" resultType="com.example.score.entity.Score">
SELECT s.id, s.student_id, s.course_id, s.score, s.term,
st.name as student_name, c.name as course_name
FROM score s
LEFT JOIN student st ON s.student_id = st.id
LEFT JOIN course c ON s.course_id = c.id
WHERE s.student_id = #{studentId}
</select>
每一行SQL都对应一个Java方法,参数#{studentId}会自动做预编译防SQL注入。而JPA的@Query("SELECT s FROM Score s WHERE s.studentId = ?1")虽然简洁,但新手很难理解JPQL和原生SQL的区别,一旦要关联三张表,JPQL写法立刻变得晦涩。
第二,错误定位极其精准。假设你在XML里把st.name错写成st.nam,启动时MyBatis会抛出org.apache.ibatis.binding.BindingException: Invalid bound statement (not found),并明确告诉你哪个Mapper接口的方法找不到对应SQL。而JDBC需要自己写PreparedStatement,字段名写错只会报Unknown column 'st.nam' in 'field list',但你得自己排查是DAO层还是SQL文件的问题。
第三,学习曲线平缓。MyBatis的核心就三样:Mapper接口、XML文件、SqlSession。项目里所有DAO操作都通过@Autowired private ScoreMapper scoreMapper;注入,调用scoreMapper.selectScoreByStudentId(1L)即可。没有JPA的实体状态管理(Transient/Persistent/Detached)、没有Hibernate的二级缓存配置陷阱。
注意:项目没用MyBatis-Plus,因为它的
LambdaQueryWrapper语法对新手不友好。比如queryWrapper.eq(Student::getName, "张三"),如果学生没学过Java 8的Method Reference,看到Student::getName就会懵。而原生MyBatis的#{name}参数传递,和System.out.println(name)一样直白。
2.3 前端框架:Layui不是过时,而是“零构建工具”的最优解
现在主流前端都用Vue/React,但为什么这个项目坚持用Layui?答案很现实:避免构建工具链成为学习障碍。我统计过,学生在毕设中花在前端的时间,60%不是写业务逻辑,而是解决Webpack打包报错、Vue Router路由404、Axios跨域被拦截。而Layui是纯前端UI库,所有资源都通过CDN或本地static/layui目录引入:
<!-- 在login.html头部 -->
<link rel="stylesheet" href="/static/layui/css/layui.css">
<script src="/static/layui/layui.js"></script>
你甚至可以把整个static文件夹拖进浏览器直接打开login.html,看到完整的登录表单(当然无法提交,因为没后端)。这种“所见即所得”的调试体验,对理解前后端交互至关重要。比如学生想改密码输入框的校验规则,直接在login.html里找到:
form.verify({
pwd: [/^[\S]{6,12}$/, '密码必须6到12位,且不能出现空格']
});
删掉/^\S{6,12}$/改成/^[a-zA-Z0-9]{8,16}$/,刷新页面就能测试新规则——全程不用重启IDEA、不用npm run dev、不用等Webpack热更新。
Layui的表格组件也极度契合成绩管理场景。table.render()方法只需传入URL和列定义:
table.render({
elem: '#scoreTable',
url: '/score/list', // 后端接口
cols: [[
{field: 'studentName', title: '学生姓名', width: 120},
{field: 'courseName', title: '课程名称', width: 150},
{field: 'score', title: '成绩', width: 80, sort: true},
{field: 'term', title: '学期', width: 100}
]]
});
对比Vue的<el-table :data="tableData">,Layui不需要定义data响应式属性、不需要写methods处理分页、不需要计算属性过滤数据——所有数据获取、分页、排序都由后端API完成,前端只负责渲染。这对初学者理解“前后端分离”本质(前端只管展示,后端管业务和数据)反而更纯粹。
3. 核心模块解析与实操要点:从数据库建表到成绩录入的完整链路
3.1 MySQL脚本深度解读:不只是建表,更是教学场景的具象化
项目附带的.sql文件不是简单的CREATE TABLE集合,而是按教学管理真实流程设计的数据模型。打开score_db_init.sql,你会发现它分为四个逻辑块:
第一块:基础字典表(支撑业务主干)
-- 班级信息表(非学生表,避免学生表冗余存储班级名)
CREATE TABLE class_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
class_code VARCHAR(20) NOT NULL COMMENT '班级编号,如2022CS01',
class_name VARCHAR(50) NOT NULL COMMENT '班级全称,如计算机科学与技术2022级1班',
grade_year INT NOT NULL COMMENT '入学年份,用于计算当前年级'
);
-- 教师信息表(为后续扩展课程归属预留)
CREATE TABLE teacher (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
title VARCHAR(20) COMMENT '职称,如讲师、副教授'
);
这里的关键设计是class_info独立成表。很多新手会把班级名直接存在student表里,导致修改班级名称时要批量UPDATE,且无法统计某班级学生人数。而用外键关联,既保证数据一致性,又为未来“按班级导出成绩单”功能打下基础。
第二块:核心业务表(学生、课程、成绩)
-- 学生表(注意:没有password字段!登录密码存在sys_user表,体现权限分离)
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_no VARCHAR(20) UNIQUE NOT NULL COMMENT '学号,业务主键',
name VARCHAR(20) NOT NULL,
gender TINYINT COMMENT '性别:1男,2女',
class_id BIGINT NOT NULL COMMENT '外键,关联class_info.id',
enrollment_date DATE COMMENT '入学日期'
);
-- 成绩表(联合主键设计,防止同一学生同一课程重复录入)
ALTER TABLE score ADD CONSTRAINT pk_score PRIMARY KEY (student_id, course_id, term);
成绩表的联合主键PRIMARY KEY (student_id, course_id, term)是教学管理的关键约束。它确保一个学生在一个学期的一门课程只能有一条成绩记录,避免教务员误点两次“保存”导致数据重复。而term字段设计为VARCHAR(20)(值如”2023-2024-1”),比用INT存学期序号更直观,且支持跨年度学期(如”2023-2024-2”表示2023-2024学年第二学期)。
第三块:初始化数据(覆盖典型教学场景)
-- 插入3个班级
INSERT INTO class_info VALUES
(1, '2022CS01', '计算机科学与技术2022级1班', 2022),
(2, '2022CS02', '计算机科学与技术2022级2班', 2022),
(3, '2022SE01', '软件工程2022级1班', 2022);
-- 插入20名学生(含同名不同班、同班不同姓等边界情况)
INSERT INTO student VALUES
(1, '2022001', '张三', 1, 1, '2022-09-01'),
(2, '2022002', '李四', 2, 1, '2022-09-01'),
(3, '2022003', '张三', 1, 2, '2022-09-01'), -- 同名不同班
(4, '2022004', '王五', 1, 1, '2022-09-01');
-- 插入成绩(含补考、缺考等特殊状态)
INSERT INTO score VALUES
(1, 1, 1, 85.5, '2023-2024-1'), -- 张三(2022001)的高等数学成绩
(2, 1, 2, NULL, '2023-2024-1'), -- 张三(2022001)的数据结构成绩为空(缺考)
(3, 2, 1, 92.0, '2023-2024-1'); -- 李四(2022002)的高等数学成绩
这些初始化数据不是随便填的。score表里NULL值代表“缺考”,而非0分,这符合教务规范(缺考和0分在学籍管理中处理方式完全不同)。而student_no用VARCHAR类型,是因为实际学号可能含字母(如”CS2022001”),用BIGINT会导致前导零丢失。
第四块:索引优化(提升查询性能)
-- 为高频查询字段添加索引
CREATE INDEX idx_student_class ON student(class_id);
CREATE INDEX idx_score_student ON score(student_id);
CREATE INDEX idx_score_course ON score(course_id);
CREATE INDEX idx_score_term ON score(term);
没有索引的score表在10万条数据时,按学生查成绩可能要3秒;加上idx_score_student索引后,降到50毫秒内。这是数据库调优最立竿见影的技巧,也是学生最容易忽略的实战细节。
3.2 后端分层实现:Controller→Service→Mapper的职责铁律
项目采用标准的MVC分层,但每层的代码都刻意暴露设计意图。以“按班级查询学生列表”为例:
Controller层(只做三件事:接收参数、调用Service、返回结果)
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
// GET /student/list?classId=1&page=1&limit=10
@GetMapping("/list")
public Result list(Long classId, Integer page, Integer limit) {
Page<Student> studentPage = studentService.listByClassId(classId, page, limit);
return Result.success(studentPage);
}
}
这里强调:Controller绝不处理业务逻辑(如判断classId是否为空)、不拼接SQL、不操作数据库。它的唯一价值是把HTTP请求参数转成Java对象,再交给Service。
Service层(业务逻辑中枢,事务控制在此)
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
@Transactional // 关键!确保数据库操作原子性
public Page<Student> listByClassId(Long classId, Integer page, Integer limit) {
// 参数校验(防御性编程)
if (classId == null || classId <= 0) {
throw new IllegalArgumentException("班级ID不能为空");
}
if (page == null || page < 1) page = 1;
if (limit == null || limit < 1) limit = 10;
// 分页计算(MyBatis-Plus的Page对象已封装offset/limit)
Page<Student> pageObj = new Page<>(page, limit);
QueryWrapper<Student> wrapper = new QueryWrapper<>();
wrapper.eq("class_id", classId);
return studentMapper.selectPage(pageObj, wrapper);
}
}
Service层的@Transactional注解是重点。当教务员批量导入学生数据时,如果中途出错(如某条数据格式错误),整个事务会回滚,避免部分数据写入导致班级人数统计错误。而参数校验if (classId == null)不是可有可无的——它让错误提前暴露,而不是等到Mapper执行SQL时报NullPointerException。
Mapper层(纯粹的数据操作,与SQL一一对应)
// StudentMapper.java 接口
public interface StudentMapper extends BaseMapper<Student> {
// 继承BaseMapper已提供通用CRUD,此处只写定制方法
List<Student> selectByClassId(@Param("classId") Long classId);
}
<!-- StudentMapper.xml -->
<select id="selectByClassId" resultType="com.example.score.entity.Student">
SELECT id, student_no, name, gender, class_id, enrollment_date
FROM student
WHERE class_id = #{classId}
ORDER BY student_no ASC
</select>
注意@Param("classId")注解。如果Mapper接口方法只有一个参数,MyBatis会自动将其作为#{}的值;但如果有多个参数(如selectByClassIdAndGender(Long classId, Integer gender)),就必须用@Param指定别名,否则XML里#{classId}会找不到对应参数。这是新手踩坑最多的地方之一。
3.3 Layui前端交互:从登录到成绩录入的全流程拆解
Layui的页面不是静态HTML,而是通过Ajax与后端实时交互。以“成绩录入”功能为例,其流程如下:
第一步:打开录入弹窗(前端JS触发)
// 在score-list.html中
table.on('toolbar(scoreTable)', function(obj){
if(obj.event === 'add'){
layer.open({
type: 2,
title: '录入成绩',
area: ['600px', '400px'],
content: '/score/add-page' // 加载score-add.html
});
}
});
这里type: 2表示iframe层,content指向后端Controller的/score/add-page接口,该接口返回score-add.html页面。这样做的好处是:弹窗内容可动态生成(如课程下拉框从数据库读取),而非写死在HTML里。
第二步:课程下拉框动态加载(Ajax请求)
// score-add.html中的JS
$.get('/course/list', function(res){
if(res.code === 0){
var html = '<option value="">请选择课程</option>';
$.each(res.data, function(i, course){
html += '<option value="' + course.id + '">' + course.name + '</option>';
});
$('#courseSelect').html(html); // 渲染到<select id="courseSelect">
}
});
/course/list接口返回JSON数据:
{
"code": 0,
"msg": "success",
"data": [
{"id": 1, "name": "高等数学"},
{"id": 2, "name": "数据结构"},
{"id": 3, "name": "Java程序设计"}
]
}
第三步:提交成绩(表单序列化+Ajax)
form.on('submit(scoreAdd)', function(data){
$.post('/score/save', data.field, function(res){
if(res.code === 0){
layer.msg('录入成功', {icon: 1});
layer.closeAll(); // 关闭弹窗
// 刷新成绩列表
layui.table.reload('scoreTable');
} else {
layer.msg('录入失败:' + res.msg, {icon: 2});
}
});
return false; // 阻止表单默认提交
});
data.field是Layui自动序列化的表单数据,形如{"studentId":"1","courseId":"1","score":"85.5","term":"2023-2024-1"}。后端ScoreController.save()方法接收@RequestBody Score score,MyBatis自动将JSON字段映射到Java对象属性。
实操心得:学生常遇到“提交后页面没反应”。此时要检查三点:① 浏览器F12看Network标签页,确认
/score/save请求是否发出、状态码是否200;② 查看Console是否有JS报错(如$ is not defined说明jQuery未加载);③ 检查后端日志,看是否进入Controller方法。我建议新手在Controller方法开头加log.info("收到成绩录入请求: {}", score),这是最有效的调试手段。
4. 实操过程详解:从零开始部署到二次开发的完整步骤
4.1 环境准备与项目导入(IDEA版)
必备环境清单(版本必须严格匹配)
| 组件 | 版本要求 | 验证方式 | 常见问题 |
|--------|-----------|-------------|--------------|
| JDK | 1.8.0_202 或更高 | java -version 输出 java version "1.8.0_202" | 若显示11.0.12,需在IDEA中File→Project Structure→Project Settings→Project→Project SDK切换为JDK8 |
| MySQL | 5.7.32 或 8.0.26 | mysql --version | MySQL 8.0+默认启用caching_sha2_password插件,需在application.yml中添加?allowPublicKeyRetrieval=true&useSSL=false |
| IntelliJ IDEA | 2021.3 或更高 | Help→About | 社区版完全够用,无需Ultimate版 |
详细导入步骤(截图级指导)
1. 解压项目包:右键X7vPo76pIsn17veAqOox-master-405322b3d5924ccfac4ff29ecad13e9585db8b5e.zip→“解压到当前文件夹”,得到X7vPo76pIsn17veAqOox-master-405322b3d5924ccfac4ff29ecad13e9585db8b5e文件夹。
-
启动IDEA并打开项目:
- 打开IDEA → “Open” → 选择解压后的文件夹 → 点击“OK”
- 关键动作:首次打开时,IDEA右下角会弹出“Import Maven project?”提示,务必勾选“Auto-import”,然后点击“Enable Auto-Import”。这确保后续修改pom.xml依赖能自动下载。 -
配置数据库连接:
- 打开src/main/resources/application.yml
- 找到spring: datasource:区块,修改以下三行:
yaml url: jdbc:mysql://localhost:3306/score_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_mysql_root_password
- 如果MySQL密码为空,password:后面留空(不要删掉冒号)。 -
执行MySQL脚本:
- 打开MySQL命令行或Navicat,新建数据库score_db(字符集选utf8mb4,排序规则utf8mb4_unicode_ci)
- 将项目根目录下的.sql文件拖入MySQL客户端执行(注意:不是双击打开,而是用客户端的“执行SQL文件”功能)
- 执行成功后,运行SELECT COUNT(*) FROM student;应返回20,证明数据初始化成功。 -
启动项目:
- 在IDEA左侧项目树中,展开src→main→java→com.example.score→ScoreApplication.java
- 右键→“Run ScoreApplication.main()”
- 观察底部Terminal窗口,等待出现Started ScoreApplication in X.XXX seconds(通常4-6秒)
- 打开浏览器,访问http://localhost:8080/login.html
注意:若启动报错
Failed to configure a DataSource: 'url' attribute is not specified,说明application.yml中spring.datasource.url路径写错了,常见错误是漏掉jdbc:mysql://前缀或数据库名写成score而非score_db。
4.2 功能验证与边界测试(教务场景全覆盖)
启动成功后,不要急着改代码,先用真实教务场景验证系统健壮性:
场景1:登录与权限验证
- 使用默认账号:admin/admin(管理员)或teacher/123456(教师)
- 尝试用不存在的账号登录,观察是否提示“用户名或密码错误”
- 登录后,管理员能看到“学生管理”“课程管理”“成绩管理”全部菜单,教师只能看到“成绩管理”
场景2:成绩录入的边界情况
- 录入成绩时,故意输入score: -5,系统应提示“成绩必须在0-100之间”
- 输入score: 100.5,应提示“成绩最多保留一位小数”
- 为同一学生同一课程同一学期重复录入,应提示“该成绩已存在,请勿重复录入”
场景3:多条件查询的准确性
- 在成绩查询页,选择“班级:计算机科学与技术2022级1班”+“课程:高等数学”,结果应只显示该班学生在这门课的成绩
- 选择“学期:2023-2024-1”+“成绩范围:80-90”,结果应精确匹配此区间(含80和90)
场景4:统计报表的可靠性
- 进入“统计分析”页,点击“按班级统计平均分”,表格应显示每个班级的平均分(如2022CS01班平均分85.2)
- 点击“按课程统计及格率”,高等数学的及格率应为及格人数/总人数*100%(项目已预置数据,可手动验算)
这些测试不是走形式,而是帮你建立对系统逻辑的信任。当你亲眼看到“张三”的高等数学成绩从85.5变成90.0,再刷新页面立即生效,那种“代码真的在干活”的实感,是任何教程都无法替代的学习动力。
4.3 二次开发实战:新增“成绩导出Excel”功能
这是毕业设计中最常被要求的功能,也是检验你是否真正理解项目结构的试金石。我们以“导出当前查询条件的成绩列表为Excel”为例,演示完整开发流程:
第一步:后端添加导出接口
1. 在pom.xml中添加Apache POI依赖(Excel处理库):
xml <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency>
-
在
ScoreController.java中添加导出方法:
```java
@GetMapping(“/export”)
public void exportScore(HttpServletResponse response,
Long studentId, Long courseId, String term) throws IOException {
// 1. 查询符合条件的成绩数据
List scoreList = scoreService.listByConditions(studentId, courseId, term);// 2. 创建Excel工作簿
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet(“成绩列表”);// 3. 写入表头
String[] headers = {“学号”, “学生姓名”, “课程名称”, “成绩”, “学期”};
XSSFRow headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}// 4. 写入数据
int rowNum = 1;
for (Score score : scoreList) {
XSSFRow row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(score.getStudentNo());
row.createCell(1).setCellValue(score.getStudentName());
row.createCell(2).setCellValue(score.getCourseName());
row.createCell(3).setCellValue(score.getScore() != null ? score.getScore().doubleValue() : 0);
row.createCell(4).setCellValue(score.getTerm());
}// 5. 设置响应头,触发浏览器下载
response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);
response.setHeader(“Content-Disposition”, “attachment; filename=scores.xlsx”);
workbook.write(response.getOutputStream());
workbook.close();
}
```
第二步:前端添加导出按钮
1. 在score-list.html的工具栏中添加按钮:
```html
```
- 在JS中绑定事件:
```javascript
table.on(‘toolbar(scoreTable)’, function(obj){
if(obj.event === ‘export’){
// 构造查询参数URL
var params = ‘’;
if($(‘#studentSelect’).val()) params += ‘&studentId=’ + $(‘#studentSelect’).val();
if($(‘#courseSelect’).val()) params += ‘&courseId=’ + $(‘#courseSelect’).val();
if($(‘#termInput’).val()) params += ‘&term=’ + $(‘#termInput’).val();// 触发下载 window.location.href = '/score/export?' + params;}
});
```
第三步:测试与验证
- 在成绩列表页设置查询条件(如只查“高等数学”课程),点击“导出Excel”
- 浏览器自动下载scores.xlsx,用Excel打开,确认数据与页面显示完全一致
- 特别验证空值处理:某学生成绩为NULL,在Excel中应显示为0(因代码中score.getScore() != null ? ... : 0)
实操心得:导出功能看似简单,但新手常犯三个错误:① 忘记在
pom.xml添加POI依赖,导致编译报错;② 在Controller方法中用return "redirect:/score/list"而非void+response.getOutputStream(),导致页面跳转而非文件下载;③ 没处理score.getScore()为null的情况,导出时抛NullPointerException。记住:所有涉及文件下载的接口,返回类型必须是void,且要手动写response.getOutputStream()。
5. 常见问题与排查技巧实录:那些让你抓狂3小时的坑,其实5分钟就能解决
5.1 启动阶段高频问题速查表
| 问题现象 | 根本原因 | 解决方案 | 验证方式 |
|---|---|---|---|
控制台报错:java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver | MySQL驱动jar包未正确加载 | 检查pom.xml中mysql-connector-java依赖版本是否为8.0.26;若用MySQL 5.7,改为5.1.49 | 在IDEA右侧Maven面板中,展开Dependencies→mysql:mysql-connector-java,确认版本号 |
浏览器打开localhost:8080/login.html显示404 | 静态资源路径配置错误 | 确认login.html位于src/main/resources/static/login.html(不是templates目录);检查application.yml中spring.web.resources.static-locations是否被意外修改 | 在IDEA中按Ctrl+Shift+N搜索login.html,确认文件路径 |
启动后控制台无Started ScoreApplication日志,卡在Tomcat started on port(s): 8080 | 端口被占用 | 打开命令行,执行netstat -ano \| findstr :8080,找到PID后taskkill /f /t /pid PID | 更改application.yml中server.port: 8081,重启看是否成功 |
| 登录时提示“用户名或密码错误”,但确定账号正确 | 数据库密码加密方式不匹配 | 检查sys_user表中password字段值是否为明文(项目初始SQL中是明文);若修改过密码,需用BCrypt加密后插入 | 执行SELECT * FROM sys_user WHERE username='admin';,确认password字段值为admin(明文) |
5.2 运行时典型问题与调试技巧
问题1:“成绩查询列表为空,但数据库明明有数据”
- 排查路径:
① F12打开浏览器开发者工具→Network→刷新页面→点击/score/list请求→查看Response内容。若返回{"code":0,"msg":"success","data":[]},说明后端查询到了空集合;
② 查看后端日志,搜索ScoreController.list,确认是否执行到该方法;
③ 在ScoreService.list()方法中加log.info("查询参数:studentId={}, courseId={}", studentId, courseId),确认前端传参是否为空;
④ 最终定位:ScoreMapper.xml中SQL的WHERE条件写错,如WHERE s.student_id = #{studentId}写成WHERE s.id = #{studentId}(s.id是成绩表主键,非学生ID)。
问题2:“Layui表格分页失效,点击下一页还是显示第一页数据”
- 根本原因:Layui的table.render()要求后端返回的数据格式必须严格符合约定:
json { "code": 0, "msg": "", "count": 100, // 总记录数,必须返回! "data": [...] // 当前页数据 }
- 解决方案:检查ScoreController.list()方法,确认Result.success()传入的是Page对象(已包含total总数),而非List集合。若返回List,需手动构造Map:
java Map<String, Object> map = new HashMap<>(); map.put("count", total); // 从数据库查总数 map.put("data", scoreList); return Result.success(map);
问题3:“修改学生信息后,页面显示更新成功,但数据库没变化”
- 关键线索:MyBatis的updateById()方法默认只更新非NULL字段。若前端表单中“性别”字段为空,传入的Student对象gender=null,则SQL中不会包含gender=?。
- 修复方案:在Controller接收参数时,用@RequestBody @Validated Student student,并在Student类的gender字段上加@NotNull(message="性别不能为空"),让校验拦截空值;或改用update()方法配合UpdateWrapper:
java UpdateWrapper<Student> wrapper = new UpdateWrapper<>(); wrapper.eq("id", student.getId()); // 强制更新所有字段(包括null) studentMapper.update(student, wrapper);
5.3 数据库脚本执行失败的终极排查法
当.sql文件执行报错(如ERROR 1064 (42000)),不要盲目百度错误码,按此顺序检查:
-
检查SQL文件编码:用Notepad++打开
.sql文件→编码→转为UTF-8无BOM格式。Windows记事本保存的文件常带BOM头,MySQL会将其识别为非法字符。 -
逐段执行定位:将
.sql文件按--分隔符拆成小块,在MySQL客户端中一段一段执行。例如先执行建表语句,成功后再执行插入语句。报错时,错误信息会明确指出哪一行出错。 -
验证MySQL版本兼容性:项目SQL使用
BIGINT主键和TINYINT性别字段,这在MySQL 5.7+完全支持。但若你用MariaDB,需将AUTO_INCREMENT改为SERIAL,或直接删除AUTO_INCREMENT(MariaDB 10.3+默认支持)。 -
检查外键约束顺序:
score表的student_id外键引用student(id),因此student表必须在score表之前创建。项目SQL已按此顺序编写,但若你手动调整了执行顺序,就会报ERROR 1215 (HY000): Cannot add foreign key constraint。
我踩过的坑:有次学生反馈“执行SQL后
score表有数据,但student表为空”。排查发现他用Navicat的“运行SQL文件”功能时,勾选了“停止执行错误语句”,而建student表的SQL前面有一行DROP TABLE IF EXISTS student;,Navicat把DROP当成错误语句跳过了,导致后续CREATE TABLE student没执行。解决方案:取消勾选“停止执行错误语句”,或直接复制SQL内容粘贴执行。
6. 项目扩展与进阶建议:从“能跑”到“好用”的跃迁路径
这个项目的设计哲学是“最小可行,最大延展”。它不追求功能堆砌,而是预留了清晰的扩展接口。如果你已完成基础部署,不妨尝试以下三个进阶方向,它们都能显著提升项目的工程价值:
方向一:增加登录验证码(安全加固)
- 为什么重要:当前登录无验证码,易被暴力破解。添加图形验证码是Web安全的第一道防线。
- 如何实现:引入kaptcha库,在LoginController.login()方法前添加验证码校验:
java @PostMapping("/login") public Result login(@RequestParam String username, @RequestParam String password, @RequestParam String code, HttpServletRequest request) { String sessionCode = (String) request.getSession().getAttribute("KAPTCHA_SESSION_KEY"); if (!code.equalsIgnoreCase(sessionCode)) { return Result.fail("验证码错误"); } // 后续登录逻辑... }
- 关键点:验证码图片由KaptchaServlet生成,需在web.xml中配置(SpringBoot用@Bean方式注册),且每次登录成功后要清除session中的验证码。
方向二:成绩趋势分析图表(数据可视化)
- 为什么重要:教务处不仅需要查成绩,更需要看趋势。比如“张三近3学期高等数学成绩:78→85→92”,用折线图呈现比表格更直观。
- 如何实现:集成ECharts(国产开源图表库),在score-detail.html中添加:
```html
`` - **后端支持**:新增/score/trend/{studentId}/{courseId}`接口,返回JSON格式的趋势数据。
方向三:RESTful API标准化(为移动端预留)
- 为什么重要:当前接口如/student/list?classId=1是传统查询字符串风格。RESTful风格GET /api/v1/students?classId=1更规范,便于未来开发APP。
- 如何实现:在Controller类上添加@RequestMapping("/api/v1"),方法上用@GetMapping("/students"),并统一返回Result<T>封装体。同时增加Swagger文档:
java @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.score.controller")) .build(); } }
- 效果:访问http://localhost:8080/swagger-ui.html即可在线调试所有API,生成调用示例。
最后分享一个小技巧:当你想快速验证某个功能是否生效,不必每次都重启整个项目。SpringBoot的
spring-boot-devtools模块支持热部署——在pom.xml中添加依赖后,修改Java文件保存,IDEA会自动重启嵌入式Tomcat(耗时<2秒)。开启方式:File→Settings→Build→Compiler→√ Build project automatically,再按Ctrl+Shift+Alt+/→Registry→勾选compiler.automake.allow.when.app.running。这个技巧能让你的开发效率提升3倍以上。
我在实际教学中发现,学生最大的进步不是学会多少新技术,而是建立起“问题可分解、错误可定位、修改可验证”的工程思维。这个成绩管理系统,就是为你搭建的第一个思维训练场。当你能独立完成一次导出Excel功能,你就已经跨过了从“学代码”到“用代码解决问题”的门槛。剩下的,不过是把同样的方法,用在下一个项目上而已。
简介:一个拿来就能跑的Java成绩管理后台项目,后端用SpringBoot搭建,数据访问层基于MyBatis,前端界面用Layui开发,风格简洁、操作直观。包里有完整的源代码、标准Maven配置(pom.xml)、已验证可用的MySQL建表语句和初始数据脚本(.sql文件),还有适配IntelliJ IDEA的开发配置文件(如compiler.xml、dataSources.xml等)。功能覆盖学生信息维护、课程管理、成绩录入、多条件查询、按班级/课程统计等常见教学管理场景。项目结构规范,分层清晰,Controller、Service、Mapper、Entity、HTML页面各司其职,不需要额外装Node.js或Webpack,也不用配HikariCP等连接池——只要本地装好JDK8+、MySQL5.7+和IDEA,导入项目后改下application.yml里的数据库地址,就能一键启动。适合高校课程设计、毕业设计选题,也适合刚学完SpringBoot想动手练手的Java新手。
&spm=1001.2101.3001.5002&articleId=161879551&d=1&t=3&u=2d3266c4b13b438aafc3df0ce069b1d8)
1270

被折叠的 条评论
为什么被折叠?



