资讯动态

Spring Boot教学管理系统源码:登录鉴权与作业批阅

发布时间:2026/10/29 20:16:41 来源:尧图企业网站定制
简介这套源码对应一个基于Web的操作系统课程教学网站面向计算机相关专业的毕业设计与课程设计也适合刚接触Java Web、需要完整项目练手的开发者。系统涵盖课程讲义、视频教程、在线测试、答疑解惑、作业提交与成绩查询等模块并配有成绩录入、查询与统计的管理逻辑服务端用Java处理请求并与数据库交互前端由JSP页面承载可据此理清教学平台的业务链路。压缩包共502个文件、约8.57MB以125个jsp页面为主要视图183个gif与10个jpg提供界面素材另有java与class文件、css与js脚本、db数据库、jar依赖以及11个doc文档bak备份保留了改动痕迹。目前已有38人学习。文档部分说明了设计思路、系统架构、功能模块与使用方法配合源码便于对照理解分层结构与数据表设计可作为答辩演示、二次开发或功能扩展的基础。1. 从三十份作业压缩包说起这套源码到底能干什么期末周收操作系统课的实验报告邮箱里躺着三十多份压缩包命名从「实验三-final-真的最终版」到「新建文件夹(2)」应有尽有批阅记录散在聊天窗口和 Excel 里想查谁没交得翻三个地方。这套 Java 毕业设计源码解决的就是这件事——把课程通知、课件资料、章节实验、作业提交与批阅搬到同一个 Web 站点上学生登录看资料交作业教师登录发通知打分。技术上是标准的 Spring Boot 单体工程MVC 三层分得清楚表结构围绕「课程—章节—资料—作业」四条主线铺开没有微服务、没有中间件依赖一台装了 JDK 和 MySQL 的机器就能跑起来。适合两类人一类是要交课程设计、需要一份结构完整可讲的底稿另一类是想拿现成骨架改成自己学院真在用的教学辅助站。2. 技术栈与目录结构一套 Spring Boot 单体工程的骨架拿到源码先别急着点运行先看 pom 和包结构。这类课程教学站的体量决定了它不需要花哨架构一个进程扛住全部请求是最省心的选择——真按微服务拆学生端、教师端、文件服务各起一个进程答辩时讲不清收益部署时还要多开三个端口属于典型的过度设计。2.1 依赖与版本组合核心依赖就六七个全部在pom.xml里能一眼看完。Spring Boot 2.7.x 是这类毕设的主流选择它对 JDK 8 和 11 都友好网上排错资料也最多。ORM 层用的是 MyBatis-Plus省略了单表 CRUD 的 XML复杂查询仍然可以回退到 XML 里写属于「够用且不锁死」。模板引擎选 Thymeleaf 而不是 JSP原因很实在JSP 在 Spring Boot 内嵌 Tomcat 下打包成 jar 会有路径问题很多同学在这上面栽过跟头。!-- pom.xml 关键依赖节选 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 容器 MVC站点的主体 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 渲染服务端页面直接放 templates 目录即可 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis-Plus 启动器单表查询不用写 XML -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency !-- MySQL 8 驱动注意版本要和本地服务端对齐 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok省掉 getter/setter 样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 密码加密只用到 BCrypt 一段spring-security-crypto 是轻量引入方式 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependency /dependencies这里有个取舍值得说清楚项目没有引入spring-boot-starter-security全家桶。整套安全框架一旦开起来默认会拦截所有请求并跳一个自带登录页对毕设这种「自己写登录页 自己写拦截器」的模式反而是负担。只引spring-security-crypto拿 BCrypt 做密码哈希是常见做法既能避免明文存库这个硬伤又不会让请求链路变复杂。版本组合上还有一点mysql-connector-j是 MySQL 8 之后的新坐标老代码里写的是mysql:mysql-connector-java。如果你的 IDE 报「找不到依赖」八成是坐标和驱动版本对不上把 URL 里的serverTimezone一并配上就能解决。2.2 包结构与分层工程是标准三层controller收请求、service放业务、mapper碰数据库entity与表一一对应config和interceptor承载横切逻辑。这个划分不新鲜但它的好处是答辩时能顺着一条请求讲完整条链路改功能时也知道该动哪一层。src/main/java/com/example/course ├── CourseApplication.java # 启动类MapperScan 在这里声明 ├── config │ ├── WebConfig.java # 注册拦截器、静态资源映射 │ └── MybatisPlusConfig.java # 分页插件 ├── interceptor │ └── LoginInterceptor.java # 登录态 角色校验 ├── controller │ ├── LoginController.java # 登录、注销、注册 │ ├── CourseController.java # 课程列表、详情 │ ├── MaterialController.java # 课件上传下载 │ ├── HomeworkController.java # 作业发布、提交、批阅 │ └── NoticeController.java # 通知公告 ├── service │ ├── impl # 业务实现接口与实现分离 ├── mapper │ └── *.java # 继承 BaseMapper复杂 SQL 走 resources/mapper/*.xml ├── entity │ └── User.java / Course.java / Homework.java ... └── common ├── Result.java # 统一返回体 └── RoleEnum.java # STUDENT / TEACHER / ADMIN src/main/resources ├── application.yml # 数据源、上传路径、模板配置 ├── mapper/*.xml # 手写 SQL ├── templates # Thymeleaf 页面按角色分目录 │ ├── login.html │ ├── student/ teacher/ admin/ └── static # css / js / img以及本地上传目录templates下按角色分目录这一点很实用。学生端和教师端页面布局差异大混在一个目录里后期加功能会互相影响分开放之后同一个 Controller 方法返回student/homework-list还是teacher/homework-list视图路径一目了然。common/Result.java是统一返回体前后端约定code / msg / data三字段。页面渲染走 Thymeleaf 时用不到它但项目里那些异步接口比如上传进度、作业列表刷新全靠它保持结构一致能省掉大量前端判断。2.3 数据库表设计表不多八张就能把业务闭环撑起来。设计上有两个地方值得注意作业和提交记录必须拆成两张表一张存教师布置的题目与截止时间一张存学生每次提交的文件与得分课件资料和课程之间用外键式的course_id关联不做级联删除删除课程时手动标记资料失效避免误删文件。-- 用户表一个账号一个角色够用且简单 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名/学号, password VARCHAR(100) NOT NULL COMMENT BCrypt 哈希不存明文, real_name VARCHAR(50) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT STUDENT COMMENT STUDENT/TEACHER/ADMIN, class_name VARCHAR(50) DEFAULT NULL COMMENT 学生所在班级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id BIGINT NOT NULL COMMENT 关联 sys_user.id, term VARCHAR(30) NOT NULL COMMENT 如 2024-2025-1, intro VARCHAR(500) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 章节表操作系统课的「进程管理」「内存管理」「文件系统」都挂这里 CREATE TABLE chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, sort_no INT NOT NULL DEFAULT 0 COMMENT 排序号越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 作业表教师发布带截止时间 CREATE TABLE homework ( id BIGINT PRIMARY KEY AUTO_INCREMENT, chapter_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT COMMENT 题目描述, deadline DATETIME NOT NULL, create_by BIGINT NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 提交表一个学生对一份作业只保留一条有效记录重复提交覆盖 CREATE TABLE homework_submit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, homework_id BIGINT NOT NULL, student_id BIGINT NOT NULL, file_path VARCHAR(255) NOT NULL COMMENT 相对路径不存绝对路径, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名下载时还原, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) NOT NULL DEFAULT SUBMITTED COMMENT SUBMITTED/GRADED, score INT DEFAULT NULL, comment VARCHAR(500) DEFAULT NULL, UNIQUE KEY uk_hw_stu (homework_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;file_path只存相对路径是个血泪经验。早期版本存的是绝对路径本地开发时是D:/upload/xxx.zip换到另一台机器直接失效部署到服务器更是全军覆没。改成存2025/06/uuid.zip这种相对路径读取时再拼上配置里的根目录迁移只要改一行配置。uk_hw_stu这个唯一索引也值得留意。它让「重复提交」在数据库层面就变成了一次 upsert不会出现同一个学生交了三份、教师批阅时挑花了眼的情况。3. 核心链路的代码拆解从登录鉴权到作业批阅看懂表结构之后接下来顺着一条真实请求走一遍。教学站的链路其实就三条主干登录进来、把课件传上去、把作业交上去再打分。这三条串起来整个项目的代码量也就消化掉一半了。3.1 登录态与角色拦截器登录逻辑本身简单难的是「谁在什么地方能被拦住」。项目用的是 Session HandlerInterceptor 的方案没有引 JWT原因很直白这是个服务端渲染的多页应用页面跳转靠的是浏览器跳转而不是 ajaxSession 天然合适用 Token 反而要在每个模板里塞隐藏字段。登录接口把用户放进 Session密码校验走 BCryptPostMapping(/doLogin) public String doLogin(String username, String password, HttpSession session, Model model) { User user userService.lambdaQuery() .eq(User::getUsername, username) .one(); // 用户不存在和密码错误统一提示避免被枚举出哪些学号已注册 if (user null || !passwordEncoder.matches(password, user.getPassword())) { model.addAttribute(error, 账号或密码错误); return login; } // 只放必要字段进 Session整个 User 对象含密码哈希能不放就不放 session.setAttribute(loginUser, user); session.setMaxInactiveInterval(60 * 60 * 2); // 两小时无操作自动失效 return redirect:/index; }passwordEncoder.matches()是 BCrypt 的匹配方法入参顺序是「明文、哈希」写反了永远返回 false这个坑不少人踩过。另外把整个User放进 Session 时记得在toString里屏蔽密码字段否则打日志时会原样打印出来。拦截器负责两件事未登录打回登录页以及管理员路径的角色校验。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { // 带上原始地址登录后能跳回用户本来想去的页面 String target URLEncoder.encode(request.getRequestURI(), UTF-8); response.sendRedirect(request.getContextPath() /login?redirect target); return false; } String uri request.getRequestURI(); String ctx request.getContextPath(); // 路径前缀做角色隔离/admin 只给管理员/teacher 只给教师及以上 if (uri.startsWith(ctx /admin) !RoleEnum.ADMIN.name().equals(loginUser.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无权限访问该页面); return false; } if (uri.startsWith(ctx /teacher) RoleEnum.STUDENT.name().equals(loginUser.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无权限访问该页面); return false; } return true; } }注册拦截器时要把登录页、静态资源和错误页排除掉否则会形成死循环——登录页自己被拦截重定向到登录页再被拦截Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /login, /doLogin, /register, /doRegister, /css/**, /js/**, /img/**, /favicon.ico, /error ); } Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 上传的课件不在 classpath 里要单独映射成可访问的 URL 前缀 registry.addResourceHandler(/upload/**) .addResourceLocations(file: UploadProperties.getRootPath() /); } }addResourceLocations里的file:前缀是关键指向磁盘绝对目录少了它Spring 会当成 classpath 相对路径去找上传成功的文件用浏览器打开就是 404。3.2 课件上传与文件落盘上传接口的复杂度集中在三件事文件名防冲突、大小与类型限制、路径拼接的跨平台问题。原始文件名不能直接用同名文件会互相覆盖还可能带上路径穿越字符常见做法是用 UUID 重命名把原始名单独存进数据库下载时再用Content-Disposition还原回来。PostMapping(/material/upload) public String upload(RequestParam(file) MultipartFile file, RequestParam(chapterId) Long chapterId, HttpSession session, RedirectAttributes ra) throws IOException { if (file.isEmpty()) { ra.addFlashAttribute(msg, 请选择要上传的文件); return redirect:/teacher/material?chapterId chapterId; } // 扩展名白名单比黑名单可靠后缀统一转小写避免 .ZIP 绕过 String original file.getOriginalFilename(); String ext original null ? : original.substring(original.lastIndexOf(.) 1).toLowerCase(); if (!Set.of(pdf, ppt, pptx, doc, docx, zip, txt).contains(ext)) { ra.addFlashAttribute(msg, 不支持的文件类型 ext); return redirect:/teacher/material?chapterId chapterId; } // 按月分目录单目录文件数不会膨胀到几万个 String monthDir LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM)); Path dir Paths.get(uploadRoot, monthDir); Files.createDirectories(dir); // UUID 做文件名原始名存库路径分隔符交给 Paths 处理别手拼 / String storeName UUID.randomUUID().toString().replace(-, ) . ext; Path target dir.resolve(storeName); file.transferTo(target.toFile()); Material m new Material(); m.setChapterId(chapterId); m.setFileName(original); m.setFilePath(monthDir / storeName); // 相对路径入库 m.setFileSize(file.getSize()); m.setUploadBy(((User) session.getAttribute(loginUser)).getId()); materialService.save(m); ra.addFlashAttribute(msg, 上传成功); return redirect:/teacher/material?chapterId chapterId; }参数说明一下uploadRoot来自配置文件app.upload.root本地一般写./upload或D:/upload服务器上写/data/course/uploadfile.transferTo()要求目标文件父目录已存在所以前面必须先createDirectories这也是新手最常漏的一行。下载接口则反过来从库里取相对路径拼根目录并把原始文件名做一次 URL 编码GetMapping(/material/download/{id}) public void download(PathVariable Long id, HttpServletResponse response) throws IOException { Material m materialService.getById(id); if (m null) { response.sendError(404, 文件不存在); return; } Path path Paths.get(uploadRoot).resolve(m.getFilePath()); if (!Files.exists(path)) { response.sendError(404, 文件已被清理请联系教师重新上传); return; } // 中文文件名要编码否则部分浏览器会截断或乱码 String encoded URLEncoder.encode(m.getFileName(), UTF-8).replace(, %20); response.setContentType(application/octet-stream); response.setHeader(Content-Disposition, attachment; filename*UTF-8 encoded); Files.copy(path, response.getOutputStream()); }filename*UTF-8是 RFC 5987 的写法比单写filename兼容性好得多中文名不会变成一串问号。3.3 作业提交与批阅状态机作业这块的状态流转很轻只有三种状态未提交、已提交待批阅、已批阅。把这三种状态固定成枚举比散落的字符串判断靠谱改需求时也不用满项目搜已批改。public enum SubmitStatus { SUBMITTED, // 学生已交等教师打分 GRADED // 教师已给分 }提交时要做两道校验截止时间已过则拒绝重复提交则覆盖。覆盖用 MyBatis-Plus 的「先查再 insertOrUpdate」写法配合前面那个唯一索引兜底Transactional public void submit(Long homeworkId, Long studentId, MultipartFile file) throws IOException { Homework hw homeworkService.getById(homeworkId); // 截止时间之后仍然允许补交但要标记为迟交方便教师区分 boolean late LocalDateTime.now().isAfter(hw.getDeadline()); String storeName UUID.randomUUID().toString().replace(-, ) file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.)); Path dir Paths.get(uploadRoot, homework, String.valueOf(homeworkId)); Files.createDirectories(dir); file.transferTo(dir.resolve(storeName).toFile()); HomeworkSubmit exist submitService.lambdaQuery() .eq(HomeworkSubmit::getHomeworkId, homeworkId) .eq(HomeworkSubmit::getStudentId, studentId) .one(); HomeworkSubmit record exist null ? new HomeworkSubmit() : exist; record.setHomeworkId(homeworkId); record.setStudentId(studentId); record.setFilePath(homework/ homeworkId / storeName); record.setFileName(file.getOriginalFilename()); record.setSubmitTime(LocalDateTime.now()); record.setStatus(SubmitStatus.SUBMITTED.name()); // 重新提交后状态回退需要教师重新批阅 record.setScore(null); record.setComment(late ? 迟交 : null); submitService.saveOrUpdate(record); }注意重新提交时把score和status一起重置了。这一点如果不做会出现「学生改了作业但分数还是上次的」属于教师端最容易引发争议的 bug。批阅接口相对简单教师填分数和评语状态从SUBMITTED推到GRADEDPostMapping(/teacher/homework/grade) public String grade(Long submitId, Integer score, String comment) { if (score null || score 0 || score 100) { throw new IllegalArgumentException(分数必须在 0-100 之间); } HomeworkSubmit s new HomeworkSubmit(); s.setId(submitId); s.setScore(score); s.setComment(comment); s.setStatus(SubmitStatus.GRADED.name()); submitService.updateById(s); // 只更新非 null 字段不会覆盖文件路径 return redirect:/teacher/homework/submits?homeworkId submitService.getById(submitId).getHomeworkId(); }用「构造一个只带 id 和待改字段的实体再updateById」是 MyBatis-Plus 的常用手法它默认只更新非空字段不会把没传的字段写成 null。4. 从零跑起来本地环境、建库、启动与验证代码看明白了剩下就是让它在自己机器上活过来。这套工程的启动门槛不高真正容易卡住的是数据库连接参数和上传目录这两处按顺序走一遍基本不会翻车。4.1 环境准备与数据库导入需要三样东西JDK 8 或 11、Maven 3.6、MySQL 5.7 或 8.0。JDK 别上 17Spring Boot 2.7 虽然能跑但部分老依赖在模块化限制下会抛InaccessibleObjectException排查起来很折腾。建库和导入数据# 登录 MySQL新建库并指定字符集utf8mb4 才能存住 emoji 和生僻字 mysql -uroot -p -e CREATE DATABASE course_site DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入源码里的 sql 文件一般在 src/main/resources/sql/ 或项目根目录 mysql -uroot -p course_site course_site.sql导入完检查一下表数量和初始账号USE course_site; SHOW TABLES; -- 初始账号的密码通常存的是 BCrypt 哈希先确认有几条 SELECT id, username, role FROM sys_user LIMIT 10;提示如果 sql 文件里的建表语句带DEFINER导入时可能因为当前账号没权限而报错。全选替换掉DEFINER...那段或者直接用 Navicat 的可视化导入它会自动忽略。4.2 配置改写与启动application.yml是唯一需要动的地方四个参数必须和自己的环境对齐server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver # 关键时区必须写否则时间字段会差 8 小时 url: jdbc:mysql://localhost:3306/course_site?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 servlet: multipart: # 默认单文件 1MB课件和作业包很容易超调到 50MB max-file-size: 50MB max-request-size: 100MB thymeleaf: # 开发阶段关缓存改完页面刷新就生效上线记得改回 true cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true mapper-locations: classpath*:/mapper/**/*.xml app: upload: root: ./upload # 相对项目根目录服务器上换成绝对路径serverTimezoneAsia/Shanghai、allowPublicKeyRetrievaltrue这两个参数是 MySQL 8 的必备项。前者不写会导致deadline读出来比实际早八小时作业明明没到期却提示已截止后者不写会报Public Key Retrieval is not allowed属于新手最常见的报错之一。配置好后直接启动# 在项目根目录执行-Dfile.encoding 防止控制台中文乱码 mvn spring-boot:run -Dfile.encodingUTF-8看到Started CourseApplication in x.xxx seconds就说明容器起来了。首次启动时如果upload目录不存在程序里最好补一段初始化逻辑或者手动建一个空目录——部分上传代码在启动阶段不会主动创建根目录。4.3 端到端验证清单光看到启动日志不算跑通按下面的顺序点一遍能把大部分隐藏问题提前暴露第一用初始账号登录观察是否跳转到对应角色首页。如果一直停在登录页打开浏览器 F12 看/doLogin的响应多半是重定向路径少了 context-path。第二教师端建一门课、加两个章节再上传一个 PDF 课件。上传成功后点下载文件能正常打开才算链路完整。这一步失败通常是静态资源映射没配对。第三学生端登录进入作业列表交一份 zip。然后刷新页面看提交状态是否变成「已提交」再用教师账号进去打分学生端刷新能看到分数。第四故意在截止时间之后再交一次确认系统给出迟交标记而不是直接 500 报错。注意验证完记得把thymeleaf.cache和数据库连接池的调试日志关掉这两项在演示时会让首屏慢上几百毫秒投影出来很明显。5. 避坑与常见问题排查跑不起来多半卡在这几处同一份源码在不同机器上的表现差异基本集中在环境相关的几个点上。下面这几条是我在不同机器上反复遇到的按「现象 → 原因 → 解决」整理遇到问题时可以对号入座。5.1 启动报 Public Key Retrieval is not allowed现象控制台抛出java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed应用起不来。原因MySQL 8 默认使用caching_sha2_password认证插件而 JDBC 驱动在非 SSL 连接下需要显式允许获取公钥。MySQL 5.7 用的是mysql_native_password所以老项目换到新库时才突然报这个错。解决在 JDBC URL 末尾追加allowPublicKeyRetrievaltrueuseSSLfalse。如果不想改 URL也可以在 MySQL 里把账号的认证插件改回旧版但更推荐前者配置文件里一行的事。5.2 上传成功但下载 404现象教师端提示「上传成功」数据库里也有记录可点下载或者直接在浏览器打开/upload/xxx.pdf全是 404。原因上传目录不在 classpath 里Spring Boot 默认只对外暴露static、public这些类路径目录。磁盘上的目录必须通过addResourceHandlers手动映射。另一种可能是映射写了但addResourceLocations里漏了file:前缀被当成了类路径相对地址。解决确认WebConfig里有registry.addResourceHandler(/upload/**).addResourceLocations(file: 绝对路径 /)。路径末尾那个斜杠不能省少了它拼接出来的地址会变成/upload2025/06/xxx.pdf。5.3 时间差八小时作业提前显示已截止现象明明晚上十点才到截止时间下午学生端就提示「已截止」。原因JDBC URL 没配serverTimezone驱动按 UTC 解析数据库里的DATETIME取出来再转成本地时间就少了八小时。另一种情况是实体类用了java.util.Date而数据库是DATETIME序列化时又转了一次时区。解决URL 里补上serverTimezoneAsia/Shanghai实体类统一改成LocalDateTime模板里用#temporals.format(...)输出。改完重启重新导入一次测试数据。5.4 中文文件名下载后变成乱码或空文件现象下载的课件名变成%E5%AE%9E%E9%AA%8C.pdf这种或者文件能下但名字被截断。原因Content-Disposition头里直接放中文HTTP 头按 ISO-8859-1 编码浏览器解析时就乱了。用URLEncoder.encode时把空格编成了也会导致部分浏览器还原失败。解决用filename*UTF-8的 RFC 5987 写法并把替换回%20。同时确保响应头在写输出流之前设置response.getOutputStream()一调用头就固定了之后再加不生效。5.5 页面改动不生效必须重启现象改完templates/xxx.html浏览器刷新还是旧内容清缓存也没用。原因Thymeleaf 默认在application.yml里可能配置了cache: true模板编译一次后缓存在内存里文件变了也不重新加载。更隐蔽的一种是 IDE 编译输出目录和运行目录不一致改的是源码跑的是target/classes里的旧副本。解决开发阶段设spring.thymeleaf.cachefalse并确认 IDE 开启了自动编译。如果用的是 IDEABuild Project Automatically要勾上同时注意target目录别被手动清掉又没重新编译。6. 二次开发把「演示版」改成能真用的课程站这套骨架能演示但直接拿给真实教学用还有几处得补。第一处是权限现在的拦截器靠 URL 前缀判断学生把/teacher/homework/grade手动敲进地址栏照样能提交——补一层方法级校验更稳。// 自定义注解 AOP 或拦截器里读取比硬编码路径前缀灵活 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); // 允许访问的角色 } // 使用处一目了然加新接口不用回头改拦截器 RequireRole({TEACHER, ADMIN}) PostMapping(/teacher/homework/grade) public String grade(Long submitId, Integer score, String comment) { ... }第二处是文件存储。本地磁盘在单机够用但换服务器、做备份、多实例部署都很别扭。把transferTo换成对象存储的 SDK接口层保持「路径入库、下载时按路径取流」的抽象改动量其实就集中在MaterialService一个类里。第三处是成绩导出。教师批完一个班的作业总想导成 Excel 交给教务。用 EasyExcel 写十几行就能出注意表头字段与前端列一致导出前先按学号排序。改造点现状建议做法改动范围权限控制URL 前缀判断注解 拦截器校验interceptor、controller文件存储本地磁盘对象存储保留相对路径service 层成绩导出无EasyExcel 按班级导出新增 controller分页全量查询MyBatis-Plus 分页插件mapper、页面第四处容易被忽略学生列表和提交记录现在多半是全量查出来在内存里过滤几十人没问题一个年级几百人就明显卡。加上 MyBatis-Plus 的分页插件PageHomeworkSubmit page new Page(pageNo, 10)一行就能把 SQL 变成LIMIT页面再补个翻页条即可。验证改造是否生效有个笨办法但很可靠把浏览器窗口缩到手机宽度点一遍再把网络面板限速到 3G 刷一次。权限、分页、懒加载的问题在这两种极端情况下最容易露馅比正常环境点十遍都管用。从那以后我每次接这类毕设改造都强制先跑一遍「学生账号手动敲教师接口」这条路径再动任何功能代码。很多隐患不是写的时候没有而是当时没想到要去试。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑