ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot + Thymeleaf + Layui 学生成绩管理系统实战与避坑指南

SpringBoot + Thymeleaf + Layui 学生成绩管理系统实战与避坑指南 简介基于Spring Boot、Layui与Thymeleaf整合开发的学生成绩管理系统以学生成绩管理为核心业务适合Java Web课程设计、毕业设计也是初学Spring Boot整合Thymeleaf开发者不错的参考项目。源码已在本机编译通过下载后按配套文档配置环境即可直接运行项目难度适中内容经助教老师审定适合用来理解MVC分层、模板渲染与前端组件库的配合方式。压缩包共332个文件体积约6.67MB核心以Java源码、HTML页面、JavaScript脚本、CSS样式及SQL数据库文件为主同时包含较多GIF图片、JSON配置数据、字体图标和maven wrapper构建脚本目录结构清晰便于按模块阅读。目前已有85人学习使用。资源中还保留了application.properties、ViewRoute等配置文件的.bak备份和基础工具类在排查环境异常或进行二次开发时可快速对照还原适合作为毕业设计基础框架进一步扩展。1. 这套成绩管理系统不是简单 CRUD而是 SpringBoot Thymeleaf Layui 的完整配合每到毕业设计季总有人问我“学生成绩管理系统”这类选题是不是太简单、没含金量。我的看法正好相反这类课题看起来是增删改查实际上把 SpringBoot 后端、Thymeleaf 模板渲染、Layui 前端组件这三层都串起来了做得好不好差别非常大。这套基于 SpringBoot Layui Thymeleaf 的学生成绩管理系统适合两类人一是正在做 Java 毕业设计或课程作业、需要一个能跑通前后端完整链路的同学二是想快速了解 Thymeleaf 服务端渲染怎么和 Layui 组件共处而不是一上来就前后端分离的从业者。它解决的不是“能不能跑”而是“结构清不清楚、成绩统计怎么算、权限怎么控、部署时有哪些坑”。2. 项目结构拆解三个技术栈各自边界与目录组织2.1 SpringBoot 负责什么Thymeleaf 和 Layui 又各自管什么这个系统最值得先讲清楚的一点是它的技术栈分工。SpringBoot 是后端底座负责依赖管理、接口路由、数据库操作和业务逻辑Thymeleaf 是服务端模板引擎负责把后端的数据填充到 HTML 页面里再返回给浏览器Layui 是前端 UI 组件库负责表格、弹窗、按钮、分页这些交互层的东西。也就是说这个项目没有做前后端分离不存在前端发 ajax 拿 JSON、后端返回 JSON、前端再渲染这种典型 Vue SpringBoot 分工。页面的渲染主体是 ThymeleafLayui 在页面加载完成后接管交互效果。很多做熟了前后端分离的人第一次看这种项目会不习惯但服务端渲染方案在毕设和中小型内部系统中其实完全够用而且省掉了跨域、token 传递、前端构建这类麻烦事。我一般会建议读者先看 pom.xml确认 Thymeleaf 和 MyBatis 的依赖是否齐全再看 resources 目录下的 templates 文件夹和 static 文件夹各自放了什么。templates 下是 Thymeleaf 的页面模板static 下放 Layui 的 css、js 和字体文件。这个划分一旦理解后面改页面的时候就不会找不到文件。2.2 目录结构对照Controller 和模板如何对应典型的 SpringBoot Thymeleaf 项目目录是这样组织的下载下来的源码里也应该是这个结构src/main/java/com/example/grade/ ├── controller/ │ ├── LoginController.java │ ├── StudentController.java │ ├── ScoreController.java │ └── AdminController.java ├── service/ │ ├── StudentService.java │ └── ScoreService.java ├── mapper/ │ ├── StudentMapper.java │ └── ScoreMapper.java ├── entity/ │ ├── Student.java │ └── Score.java └── config/ └── LoginInterceptor.java src/main/resources/ ├── templates/ │ ├── login.html │ ├── student/list.html │ ├── score/list.html │ └── score/add.html ├── static/ │ └── layui/ └── application.ymlController 层的方法名和 templates 下的页面文件名基本是一一对应的。比如 StudentController 里有一个跳转列表页的方法它 return 的字符串就是student/listThymeleaf 会自动去 templates/student/list.html 找模板。这个约定一旦打破页面就会直接报 404 或 Whitelabel Error Page。2.3 数据库表设计学生表、成绩表、用户表怎么关联成绩管理系统的核心表通常是三张用户表存登录账号和密码、学生表存学号、姓名、班级、专业、成绩表存课程名称、分数、考试类型。学生表和成绩表通过学号关联用户表独立用于登录。建表脚本在源码里一般会带一份 SQL 文件表结构类似这样CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, student_name VARCHAR(50) NOT NULL, class_name VARCHAR(50), major VARCHAR(50) ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, course_name VARCHAR(50) NOT NULL, score DECIMAL(5,2), exam_type VARCHAR(20), FOREIGN KEY (student_no) REFERENCES student(student_no) ); CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, role VARCHAR(20) );参数上要注意两点一是 score 字段用DECIMAL(5,2)能存 999.99 分以内的分数有些同学用INT存成绩最后统计平均分时被整除了才有问题二是 sys_user 的 password 字段长度建议至少 64 位因为后面如果做了 MD5 或 BCrypt 加密长度不够会报数据截断错误。这块表设计看着简单但外键使用student_no而不是id关联是为了查询时直接显示学号少一次 JOIN。3. 后端核心实现登录鉴权与成绩 CRUD 怎么落地3.1 登录拦截器没有 Spring Security 的轻量方案这个系统没有引入 Spring Security而是用 SpringBoot 拦截器做登录验证。很多毕设都是这种做法因为 Spring Security 的过滤器链和权限表达式对于这类 CRUD 系统来说配置成本偏高。 LoginInterceptor 的核心代码是这样的public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }这段逻辑很直接每次请求进入 Controller 之前先检查 Session 里有没有currentUser这个 attribute。没有就重定向到 /login有就放行。然后把它注册到拦截器配置里Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /layui/**); } }这里有个细节值得注意excludePathPatterns里必须放行静态资源路径。如果不放行/layui/**、/css/**、/js/**那么登录页本身的样式和脚本都会被拦截打开页面就是一堆没有样式的裸 HTML。我第一次写这种拦截器时漏了静态资源放行折腾了快两小时才意识到问题。登录 Controller 的做法也简单查询 sys_user 表比对用户名和密码。密码比对时建议用Objects.equals或StringUtils.equals来避免空指针。3.2 成绩列表的分页查询MyBatis 手写 Limit 还是 PageHelper成绩列表页是整个系统的核心页面需要支持分页、按学号或姓名搜索、按课程过滤。分页方案有两种引入 PageHelper或者手写 LIMIT 参数。这个项目如果只依赖 MyBatis 且没有引入 PageHelper 依赖那就是后者。手写分页的 Mapper 大概是这样的Select(SELECT s.student_no, s.student_name, s.class_name, sc.course_name, sc.score FROM score sc LEFT JOIN student s ON sc.student_no s.student_no WHERE #{keyword} IS NULL OR s.student_name LIKE CONCAT(%, #{keyword}, %) LIMIT #{offset}, #{pageSize}) ListScoreVO selectScorePage(Param(keyword) String keyword, Param(offset) Integer offset, Param(pageSize) Integer pageSize);参数说明offset是起始位置pageSize是每页条数。前端 Layui 表格的page和limit参数传过来后Controller 需要计算 offset 为(page - 1) * limit。SQL 里#{keyword} IS NULL这个写法是为了支持不传搜索条件时查全部比较偷懒但有效。有一个坑如果用 MySQLLIMIT后不能直接写#{page}做运算必须在 Java 端算好 offset 再传参。有些人写成LIMIT #{offset}, #{limit}没问题是标准写法但写成LIMIT (#{page}-1)*#{limit}就会报语法错误因为 MyBatis 的 PreparedStatement 不支持这种表达式。3.3 成绩添加与修改事务处理和参数校验成绩添加和修改的表单字段基本一样学号、课程、分数、考试类型。Controller 里需要区分新增和更新常见做法是这样PostMapping(/score/save) public String saveScore(Score score, RedirectAttributes attributes) { if (score.getScore() null || score.getScore() 0 || score.getScore() 100) { attributes.addFlashAttribute(errorMsg, 成绩必须在0-100之间); return redirect:/score/add; } if (score.getId() null) { scoreService.insertScore(score); } else { scoreService.updateScore(score); } return redirect:/score/list; }逻辑说明先做分数范围校验0 到 100 之外直接打回然后根据id是否为空判断是新增还是更新。RedirectAttributes的addFlashAttribute会把提示信息带到重定向后的页面Thymeleaf 里用${errorMsg}取出来展示。成绩修改时容易忽略一个场景同一个人同一门课能不能重复录入如果业务上不允许重复应该在插入前先查一次 score 表有没有同 student_no 和 course_name 的记录。源码里如果没做这个校验你可以在 Service 层加上去这也是答辩时能讲的加分点。4. 前端渲染Thymeleaf 与 Layui 从页面模板到表格交互4.1 模板布局用 replace 抽取公共头部和导航栏如果大量页面都有同一个导航栏复制粘贴是最差的做法。Thymeleaf 提供了布局方式可以在 templates 下建一个layout.html存放公共部分然后其他页面通过th:replace引用。公共导航片段示例nav th:fragmentnavbar classlayui-nav layui-bg-blue div classlayui-container a classlayui-nav-item th:href{/student/list}学生管理/a a classlayui-nav-item th:href{/score/list}成绩管理/a a classlayui-nav-item th:href{/logout}退出登录/a /div /nav其他页面引用时只需要一行div th:replace~{layout :: navbar}/div参数说明~{layout :: navbar}是 Thymeleaf 的片段表达式含义是获取 layout.html 中定义名为 navbar 的th:fragment。这个语法初看有点绕但它是服务端渲染页面复用的核心手段改一次导航栏所有页面同步更新。4.2 Layui 表格与 Thymeleaf 的组合方式Layui 的表格有两种用法一种是用table.render()请求后端 JSON 数据渲染另一种是直接渲染页面已有的 HTML 表格。这个项目如果用的是服务端渲染那么 Layui 表格多半是第二种——后端已经用 Thymeleaf 的th:each把数据行输出到表格里了Layui 只是给表格加上分页工具和样式。Thymeleaf 输出成绩列表的片段table classlayui-table idscoreTable thead tr th学号/th th姓名/th th课程/th th分数/th th操作/th /tr /thead tbody tr th:eachrow : ${pageInfo.list} td th:text${row.studentNo}/td td th:text${row.studentName}/td td th:text${row.courseName}/td td th:text${row.score}/td td a th:href{/score/edit/{id}(id${row.id})} classlayui-btn layui-btn-sm编辑/a a th:href{/score/delete/{id}(id${row.id})} classlayui-btn layui-btn-danger layui-btn-sm删除/a /td /tr /tbody /table要点说明th:each是 Thymeleaf 的循环语法类似 Java 的 for-each{/score/edit/{id}(id${row.id})}是 URL 表达式会生成/score/edit/3这样的链接。这种写法比字符串拼接参数安全Thymeleaf 会自动处理 URL 编码。Layui 在这一页只负责把表格样式处理好不需要初始化数据加载。但如果用户要求支持排序和列筛选那就要把数据源切换到 JSON 模式让 Layui 的table.render去请求一个/score/json接口。需要根据源码实际实现来判断若源码已经是 JSON 接口模式则 Controller 返回的是Result包装的 Page 数据。4.3 表单页面与 Layui 弹窗新增和编辑共用一套表单新增和编辑页面可以共用一个form.html通过id是否为空判断是新增还是回显。Thymeleaf 回显的方式很简单input typehidden nameid th:value${score null ? null : score.id} input typetext namestudentNo classlayui-input th:value${score null ? : score.studentNo} placeholder请输入学号 input typetext namecourseName classlayui-input th:value${score null ? : score.courseName} placeholder请输入课程名称 input typenumber namescore classlayui-input th:value${score null ? : score.score} placeholder请输入分数这里的关键是每次进入页面时 Controller 要往 Model 里塞一个score对象即使是新新增的情况也塞一个空对象。如果不塞Thymeleaf 模板中${score null}的判断分支会报错。很多人从前后端分离切过来习惯用 JS 判断 edit 还是 add 模式服务端渲染这套就是直接看score这个对象存不存在。Layui 的表单校验模块form.on(submit())可以拦截提交做前端校验。不过要注意前端校验只是体验优化后端仍然要校验否则绕过浏览器直接 POST 请求就可以把非法数据写进数据库。5. 避坑指南本地能跑、一部署就挂的五个常见问题5.1 现象页面能打开但样式全乱Layui 图标显示为方块原因Thymeleaf 布局里引用了 Layui 的静态资源路径但路径写错了或者被登录拦截器拦截了静态资源。解决方法是先看浏览器 console 的 404 列表。如果报 404 的是/layui/css/layui.css这类路径去检查 static 目录下是否有对应文件以及拦截器 excludePathPatterns 里是否放行了/layui/**。我见过一个项目把layui.css误放到了 templates 下页面引用的也是/layui/css/layui.css结果始终 404。静态文件不能放在 templates 目录必须放 static 或 public 目录。5.2 现象刚启动时一切正常刷新几次后页面报模板解析错误原因Thymeleaf 默认会缓存解析后的模板开发环境下如果改了 HTML 不重启或没开热部署会看到旧内容。如果是模板解析错误多半是th:each或${...}写错了。常见的情况是th:eachrow : ${list}里list对象没有传给 Model导致 SpEL 表达式解析失败。解决方法是看错误日志里提示的是哪个表达式、哪个行号把对应的 th 属性删掉再试。另一个经验是修改模板后强制刷新浏览器确认不是本地缓存问题。5.3 现象登录成功后跳转到了 /login 死循环原因拦截器配置里放行了 /login 的 GET 请求但登录的 POST 提交地址写的不是 /login比如写成了/user/login这个地址被拦截器拦截后重定向到 /login 页面而 /login 页面里又用 ajax 请求了/static下的资源这些资源没有放行。解决思路把所有静态资源路径和登录相关路径都放进 excludePathPatterns不要只放一个 /login 就完事。排查时可以在拦截器 preHandle 里加一段System.out.println(request.getRequestURI())看哪些请求被拦了这个方法比猜有效得多。5.4 现象MySQL 连接报 Public Key Retrieval is not allowed原因MySQL 8 以上版本默认的驱动连接配置需要显式指定allowPublicKeyRetrievaltrue和useSSLfalse。SpringBoot 2.x 自带的是 MySQL 8 驱动旧的 JDBC 连接串不加这两个参数就会出现这个报错。在 application.yml 里加一下即可spring: datasource: url: jdbc:mysql://localhost:3306/grade_system?useUnicodetruecharacterEncodingutf8useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai参数说明useSSLfalse关闭 SSL 减少连接耗时serverTimezoneAsia/Shanghai是避免时区报错allowPublicKeyRetrievaltrue配合 MySQL 8 的 sha256_password 认证方式使用。这三个参数属于 MySQL 8 连接的基本配置缺一个都可能启动失败或连接超时。5.5 现象IDEA 里点 Run 启动正常打包成 jar 运行后页面模板找不到原因Thymeleaf 模板默认从 classpath:/templates 下加载如果打包时把 templates 目录漏掉了或者项目资源没有正确打进 jar就会出问题。另外 SpringBoot 打包后访问的是 jar 内部的模板不是外部文件路径所以不能再用 IDE 运行时那种项目绝对路径方式引用资源。解决说穿了就两步一是确认 pom.xml 里没有把 src/main/resources 排除掉二是打 jar 包后先解压看一眼 BOOT-INF/classes/templates 是否存在对应模板文件。如果存在仍然报错就检查某个模板文件是否引用了一个不存在的片段。我就有过一次教训改了个公共片段的路径忘了改引用页面结果每个页面都模板解析报错一查全是同一个原因。6. 进阶玩法成绩统计、Excel 导出与答辩亮点6.1 平均分与及格率统计在列表页直接展示基础版系统只做成绩录入和查询论文答辩时如果有统计功能会好很多。可以在列表页加一个顶部统计卡片区总人数、平均分、最高分、最低分、及格率。查询 SQL 可以单独写一个聚合查询Select(SELECT COUNT(*) AS totalCount, AVG(score) AS avgScore, MAX(score) AS maxScore, MIN(score) AS minScore, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS passRate FROM score WHERE course_name #{courseName}) MapString, Object selectScoreStatistics(Param(courseName) String courseName);逻辑说明AVG(score)在 MySQL 里保留的是小数但如果 score 字段是INT类型AVG 结果会是四舍五入后的整数所以建表时用DECIMAL在这里就体现出优势了。SUM(CASE WHEN ...)是 MySQL 里条件计数的标准写法。6.2 成绩导出 Excel用 Apache POI 还是 EasyExcel毕设里做成绩导出最常见的是 Apache POI代码量适中、依赖单一。核心逻辑是创建 XSSFWorkbook创建 Sheet 和 Row遍历成绩列表填充单元格最后把 workbook 写入 HttpServletResponse 的输出流。需要注意响应的 Content-Type 是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet同时设置Content-Disposition让浏览器以附件方式下载。try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(成绩表); Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(课程); header.createCell(3).setCellValue(分数); int rowNum 1; for (ScoreVO item : scoreList) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(item.getStudentNo()); row.createCell(1).setCellValue(item.getStudentName()); row.createCell(2).setCellValue(item.getCourseName()); row.createCell(3).setCellValue(item.getScore()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamescores.xlsx); workbook.write(response.getOutputStream()); }这段代码有两个细节容易出错一是try-with-resources写法可以把 workbook 的关闭交给 JVM避免文件句柄泄漏二是设置Content-Disposition后如果文件名带了中文需要做 URL 编码否则下载时文件名会乱码。Excel 导出这个功能论文里能写答辩时能演示实用性和展示效果兼备。但做的时候别在一个 Controller 里塞全部导出逻辑把导出方法单独抽到一个 Service 类里看起来会更专业。6.3 把项目从“能跑”变成“能讲”日志、异常处理和注释最后一个建议不涉及新功能而是改代码习惯。我把这套系统从本地跑通到提交答辩之间花时间最多的地方不是功能而是可读性。Controller 里每个接口加一句注释说明“这个方法是干什么的返回什么页面”Service 里对成绩的删除操作加一个”删除前校验成绩是否存在“的防御逻辑application.yml 里加上 MyBatis 的 SQL 日志输出mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开这个开关之后后端控制台会打印 MyBatis 执行的 SQL 语句和参数排查列表页数据不对、条件查询查不出结果这类问题会很直观。毕业设计答辩的时候评委问数据库查询你能直接说我用日志看到 SQL 是这样的效果比背概念好得多。从那以后我拿到任何这类 SpringBoot Thymeleaf Layui 的项目第一件事都是先把 SQL 日志打开模板路径和静态资源放行过一遍再谈功能改造。希望帮到你。本文还有配套的精品资源点击获取
返回列表