简介:本资源是一份面向计算机专业本科生与Java初学者的毕业设计类实践文档,聚焦校园博客系统的设计与实现全过程,解决高校师生知识分享、学习记录与互动交流的实际需求。文档以Word格式(.docx)完整呈现,共1个文件,大小4.91MB,内容涵盖技术选型(Java+Spring Boot+MySQL+Tomcat)、系统可行性分析、功能与业务流程梳理、模块化功能结构设计、数据库E-R图及数据表字段设计、前后台核心功能实现细节(含管理员、博主、前台首页)以及系统测试方案与结论,结构严谨、章节完整,具备直接参考与复用价值。目前已有62人学习下载,适合用于课程设计参考、毕设开题与技术方案借鉴,尤其利于理解Web应用从需求分析到部署落地的全周期开发逻辑。
1. 为什么一个“校园博客系统”值得用 Java 重做一遍?——不是为了炫技,而是解决真实教学场景里的三类断层
你见过学生交的博客作业:Word 文档里贴截图、手写 URL、用邮箱附件传.zip;老师批改靠 Excel 打分表,评论写在 Word 批注里,反馈周期动辄一周;课程群每天刷屏“老师我的博客打不开”“数据库连不上”“登录后跳回首页”,没人知道是前端路由错、Session 失效,还是 MySQL 8.0 默认认证插件不兼容。这不是开发能力问题,是教学系统与工程实践之间存在三道断层:需求断层(学生要交作业,不是建 SaaS)、部署断层(机房只装 JDK 17 + Tomcat 9,不配 Docker)、维护断层(毕设答辩完代码就进回收站,没人管半年后的 Spring Boot 版本升级)。这个“基于 Java 的校园博客系统”不是又一个 CRUD 演示项目,它是把 Java 工程能力压进教学闭环的最小可行载体:用Spring Boot 3.2做骨架,MyBatis-Plus 4.3自动生成建表 SQL 和基础 CRUD,Thymeleaf 3.1渲染纯 HTML 页面(避开 Vue/React 学习成本),所有功能跑在单台 Windows Server 上,学生 clone 后改两行配置就能本地启动,老师导出 WAR 包扔进机房 Tomcat 就能开课。它不追求高并发,但必须扛住 200 人同时提交作业的瞬时压力;它不玩微服务,但每个模块(用户、文章、评论、权限)都按真实项目分包;它甚至预留了“教师后台一键清空学生数据”的按钮——因为每学期初,真有老师需要重置整个班级的博客空间。如果你正带 Java 课程设计、指导毕业设计,或想给实训平台加个可交付的练手项目,这个系统就是那块“垫脚石”:不高,但刚好够到工程落地的门槛。
2. 从零搭起骨架:用 Spring Boot 3.2 + MyBatis-Plus 4.3 初始化项目并生成基础表结构
2.1 创建项目并确认 JDK 与依赖版本的硬性约束
校园环境对运行时版本极其敏感。机房服务器普遍为 Windows Server 2016,预装 JDK 17(LTS),而部分老教师电脑仍用 JDK 11。Spring Boot 3.x 要求最低 JDK 17,因此必须放弃 JDK 11 兼容性,这是第一个技术取舍。创建项目时,使用 start.spring.io (注意:选 Spring Boot 3.2.x,非 3.3+),勾选以下依赖:
- Spring Web
- Spring Data JDBC(非 JPA,因 MyBatis-Plus 更适配教学场景的 SQL 可视化)
- Lombok(减少样板代码,学生易理解)
- Thymeleaf(服务端渲染,避免前端框架学习负担)
- Validation(校验学生注册邮箱、密码格式)
提示:不要勾选 Spring Security。校园系统需“教师账号拥有全站权限,学生仅能管理自己博客”,用自定义拦截器比 Security 配置更直观,也方便学生 debug 权限逻辑。
生成 ZIP 后解压,用 IDEA 打开。关键一步:检查pom.xml中 MyBatis-Plus 版本是否为4.3.0(2023 年底发布,支持 Spring Boot 3.2,且@TableName注解默认开启驼峰转下划线,省去手动配置)。若版本不符,强制覆盖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>4.3.0</version> </dependency>2.2 用 MyBatis-Plus 逆向工程生成实体类与建表 SQL:三步完成数据库初始化
学生最怕“建表语句写错导致整个项目崩”。MyBatis-Plus 的AutoGenerator可根据 Java 实体类反向生成建表 SQL,这才是教学友好型方案。我们先定义核心实体类(以BlogPost为例):
// src/main/java/com/example/campusblog/entity/BlogPost.java import com.baomidou.mybatisplus.annotation.*; @TableName("blog_post") // 显式指定表名,避免自动转 blog_post public class BlogPost { @TableId(type = IdType.AUTO) // 主键自增,MySQL 用 AUTO_INCREMENT private Long id; @TableField("user_id") private Long userId; // 关联学生 ID @TableField("title") private String title; @TableField("content") private String content; @TableField("create_time") private LocalDateTime createTime; // 使用 LocalDateTime,非 Date @TableField("status") // 0-草稿, 1-已发布, 2-已删除 private Integer status; // getter/setter 省略,Lombok @Data 已覆盖 }关键点在于@TableField显式标注字段名,禁止依赖驼峰自动转换——因为学生常把createTime写成create_time或createtime,显式声明能杜绝歧义。
接下来,编写代码生成器(放在src/test/java下,仅用于初始化):
// src/test/java/com/example/campusblog/generator/CodeGenerator.java import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.Collections; public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://localhost:3306/campus_blog?serverTimezone=Asia/Shanghai&useSSL=false", "root", "123456") .globalConfig(builder -> { builder.author("teacher") // 作者名,生成代码时写入注释 .outputDir("src/main/java"); // 生成到 src/main/java }) .packageConfig(builder -> { builder.parent("com.example.campusblog") // 包名前缀 .moduleName("entity") // 模块名,影响子包路径 .pathInfo(Collections.singletonMap(OutputFile.xml, "src/main/resources/mapper")); // XML 映射文件位置 }) .strategyConfig(builder -> { builder.addInclude("blog_user", "blog_post", "blog_comment") // 只生成这三张表 .entityBuilder() .enableLombok() // 启用 Lombok .naming(com.baomidou.mybatisplus.generator.config.NamingStrategy.underline_to_camel) // 数据库下划线转驼峰 .controllerBuilder() .enableRestStyle(); // 生成 RESTful 风格 Controller }) .templateEngine(new FreemarkerTemplateEngine()) // 使用 Freemarker 模板 .execute(); } }执行main()方法后,自动生成:
BlogUser.java,BlogPost.java,BlogComment.java实体类BlogUserMapper.java,BlogPostMapper.java接口(含insert,selectById等方法)BlogUserMapper.xml,BlogPostMapper.xml(含<insert><select>标签)BlogUserController.java,BlogPostController.java(带@RestController的空壳)
参数说明:
serverTimezone=Asia/Shanghai是必须项,否则LocalDateTime字段插入 MySQL 会报时区错误;useSSL=false因校园内网 MySQL 通常未配 SSL,强行开启会导致连接失败;addInclude明确限定表名,避免生成无关表(如flyway_schema_history)。
2.3 运行生成器后,获取建表 SQL 并手动执行:为什么不能直接schema.sql?
MyBatis-Plus 不提供“一键建库建表”功能,但生成器输出的 XML 文件中,<insert>标签内的 SQL 是可读的。更可靠的做法是:用 HikariCP 连接池 +spring.sql.init.mode=always加载初始化脚本。在src/main/resources/application.yml中添加:
spring: sql: init: mode: always schema-locations: classpath:schema.sql >-- src/main/resources/schema.sql CREATE TABLE IF NOT EXISTS blog_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT '0-学生, 1-教师', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS blog_post ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, status TINYINT DEFAULT 1 COMMENT '0-草稿,1-已发布,2-已删除', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_id (user_id) ); CREATE TABLE IF NOT EXISTS blog_comment ( id BIGINT AUTO_INCREMENT PRIMARY KEY, post_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_post_id (post_id) );为什么不用
@MapperScan自动建表?
因为@MapperScan只扫描 Mapper 接口,不执行 DDL。而schema.sql在应用启动时由 Spring JDBC 自动执行,且IF NOT EXISTS保证重复启动不报错——这对学生反复重启项目至关重要。
3. 让学生真正“交作业”:实现博客发布流程与教师后台审核链路
3.1 学生端:从登录到发布,三步完成一篇博客(含富文本与图片上传)
校园博客不需复杂编辑器。用summernote(轻量级 jQuery 富文本)即可,它支持图片拖拽上传,且 JS 文件仅 300KB。在src/main/resources/templates/user/dashboard.html中引入:
<!-- src/main/resources/templates/user/dashboard.html --> <link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script> <div class="form-group"> <label>博客内容</label> <textarea id="content-editor" name="content" class="form-control"></textarea> </div> <script> $(document).ready(function() { $('#content-editor').summernote({ height: 300, toolbar: [ ['style', ['bold', 'italic', 'underline']], ['insert', ['picture', 'link']] ], callbacks: { onImageUpload: function(files) { sendFile(files[0]); // 调用自定义上传函数 } } }); }); function sendFile(file) { var formData = new FormData(); formData.append("file", file); $.ajax({ url: "/api/upload/image", type: "POST", data: formData, processData: false, contentType: false, success: function(url) { $('#content-editor').summernote('insertImage', url); // 插入图片 URL } }); } </script>后端接收图片的 Controller 必须处理MultipartFile,并保存到src/main/resources/static/upload/目录(静态资源目录,图片可直接通过 HTTP 访问):
// src/main/java/com/example/campusblog/controller/UploadController.java import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController @RequestMapping("/api/upload") public class UploadController { private final Path rootLocation = Paths.get("src/main/resources/static/upload"); @PostMapping("/image") public String handleFileUpload(@RequestParam("file") MultipartFile file) { try { if (file.isEmpty()) { return ""; } // 生成唯一文件名:时间戳 + 原文件名 String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String filename = timestamp + "_" + file.getOriginalFilename(); Path destinationFile = this.rootLocation.resolve( Paths.get(filename)) .normalize().toAbsolutePath(); // 确保目录存在 if (!this.rootLocation.toFile().exists()) { Files.createDirectories(this.rootLocation); } Files.copy(file.getInputStream(), destinationFile); // 返回可访问的 URL(注意:static 目录下文件路径为 /upload/xxx.jpg) return "/upload/" + filename; } catch (Exception e) { e.printStackTrace(); return ""; } } }关键细节:
return "/upload/" + filename是硬编码路径,因为src/main/resources/static/是 Spring Boot 默认静态资源目录,无需额外配置即可通过http://localhost:8080/upload/xxx.jpg访问。学生写博客时插入的图片 URL 就是这个地址,避免使用file://协议(浏览器安全限制)或绝对路径(部署后失效)。
3.2 教师端:审核列表与一键操作,用 Thymeleaf 渲染动态表格
教师后台首页/admin/posts展示待审核博客列表。MyBatis-Plus 分页查询 + Thymeleaf 表格渲染,代码简洁且易调试:
// src/main/java/com/example/campusblog/controller/AdminController.java @GetMapping("/admin/posts") public String listPendingPosts(Model model) { Page<BlogPost> page = new Page<>(1, 10); // 第 1 页,每页 10 条 QueryWrapper<BlogPost> wrapper = new QueryWrapper<>(); wrapper.eq("status", 0); // 只查草稿状态 wrapper.orderByDesc("create_time"); Page<BlogPost> resultPage = blogPostService.page(page, wrapper); model.addAttribute("posts", resultPage.getRecords()); model.addAttribute("page", resultPage); return "admin/post-list"; // 对应 templates/admin/post-list.html }post-list.html中用 Thymeleaf 渲染表格:
<!-- src/main/resources/templates/admin/post-list.html --> <table class="table table-striped"> <thead> <tr> <th>标题</th> <th>学生</th> <th>提交时间</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="post : ${posts}"> <td th:text="${post.title}">示例标题</td> <td th:text="${post.user.username}">张三</td> <td th:text="${#dates.format(post.createTime, 'yyyy-MM-dd HH:mm')}">2024-05-01 10:20</td> <td> <a th:href="@{/admin/post/{id}/publish(id=${post.id})}" class="btn btn-success btn-sm">发布</a> <a th:href="@{/admin/post/{id}/reject(id=${post.id})}" class="btn btn-danger btn-sm">退回</a> </td> </tr> </tbody> </table>发布操作只需更新status字段:
// src/main/java/com/example/campusblog/controller/AdminController.java @PostMapping("/admin/post/{id}/publish") public String publishPost(@PathVariable Long id) { BlogPost post = blogPostService.getById(id); if (post != null && post.getStatus() == 0) { post.setStatus(1); // 1-已发布 blogPostService.updateById(post); } return "redirect:/admin/posts"; }为什么不用 REST API + AJAX?
因为教师可能用老旧 IE 浏览器(机房常见),AJAX 兼容性差;而redirect是最稳妥的页面跳转方式,且 Thymeleaf 模板天然支持服务端渲染,学生 debug 时直接看 HTML 源码就能理解数据流向。
3.3 权限控制:用拦截器实现“学生只能看自己的博客,教师看全部”
不引入 Spring Security,改用HandlerInterceptor实现细粒度控制。创建AuthInterceptor:
// src/main/java/com/example/campusblog/interceptor/AuthInterceptor.java import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } String uri = request.getRequestURI(); BlogUser user = (BlogUser) session.getAttribute("user"); // 学生不能访问 /admin/ 开头的路径 if (uri.startsWith("/admin/") && user.getRole() == 0) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问管理员页面"); return false; } // 学生访问 /post/{id} 时,校验该博客是否属于本人 if (uri.matches("/post/\\d+") && user.getRole() == 0) { Long postId = Long.parseLong(uri.substring(uri.lastIndexOf("/") + 1)); BlogPost post = blogPostService.getById(postId); if (post == null || !post.getUserId().equals(user.getId())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权查看他人博客"); return false; } } return true; } }在WebMvcConfigurer中注册:
// src/main/java/com/example/campusblog/config/WebConfig.java import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/images/**"); } }拦截器 vs Filter:Filter 在 Servlet 容器层面,无法直接获取 Spring Bean(如
blogPostService);而HandlerInterceptor在 Spring MVC 拦截链中,可注入 Service,适合业务逻辑判断。这是教学场景下的合理降级——用更易懂的机制替代复杂框架。
4. 避坑指南:学生部署时最常遇到的 4 个致命问题与血泪解决方案
4.1 现象:本地启动成功,但部署到机房 Tomcat 后页面空白,控制台无报错
原因:Spring Boot 3.x 默认打包为jar,而机房 Tomcat 要求war包;且jar包内嵌 Tomcat 与外部 Tomcat 冲突。
解决:
- 修改
pom.xml,将打包方式改为war:<packaging>war</packaging> - 排除内嵌 Tomcat:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> <!-- 交给外部 Tomcat 提供 --> </dependency> - 主类继承
SpringBootServletInitializer:@SpringBootApplication public class CampusBlogApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(CampusBlogApplication.class); } public static void main(String[] args) { SpringApplication.run(CampusBlogApplication.class, args); } } - 用
mvn clean package -DskipTests生成target/campus-blog-0.0.1-SNAPSHOT.war,丢进Tomcat/webapps/即可。
4.2 现象:学生注册时提示“邮箱格式错误”,但输入student@school.edu.cn仍失败
原因:@Email注解默认正则过于严格,不支持中文域名或长后缀(如.edu.cn)。
解决:自定义校验注解,或改用@Pattern:
// 在 BlogUser 实体类中 @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确,请输入如 student@school.edu.cn") private String email;玄学经验:正则末尾
{2,}允许任意长度顶级域名,比
4.3 现象:教师后台点击“发布”后,博客状态没变,刷新页面仍是草稿
原因:blogPostService.updateById(post)更新时,MyBatis-Plus 默认只更新非null字段;而post对象中userId,title等字段为null(因只设置了id和status),导致 SQL 中SET status=?被忽略。
解决:两种方案任选其一:
- 方案 A(推荐):用
LambdaUpdateWrapper显式指定更新字段:LambdaUpdateWrapper<BlogPost> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(BlogPost::getId, id).set(BlogPost::getStatus, 1); blogPostService.update(wrapper); - 方案 B:在
BlogPost实体类中,为所有字段添加@TableField(fill = FieldFill.UPDATE),并在MetaObjectHandler中填充,但教学场景过于复杂,不推荐。
4.4 现象:图片上传后显示 404,URL 是/upload/20240501102030_xxx.jpg,但浏览器打不开
原因:src/main/resources/static/upload/目录在打包成war后,路径变为WEB-INF/classes/static/upload/,而 Tomcat 默认不将WEB-INF下的资源设为静态可访问。
解决:在application.yml中显式配置静态资源路径:
spring: web: resources: static-locations: classpath:/static/,file:./upload/然后修改上传代码,将图片保存到项目根目录的upload/文件夹(而非src/main/resources/static/upload/):
private final Path rootLocation = Paths.get("./upload"); // 改为相对路径这样./upload/位于 Tomcat 的webapps/ROOT/同级,天然可被 HTTP 访问。
5. 让系统“活”过一个学期:教师后台的批量操作与数据快照技巧
5.1 教师一键清空全班博客:用 MyBatis-Plus 的remove方法安全擦除
每学期初,教师需要重置学生博客空间。与其手动删库,不如在后台加个按钮。在AdminController中添加:
@PostMapping("/admin/clear-all-posts") public String clearAllPosts() { // 删除所有博客(软删除:status=2) UpdateWrapper<BlogPost> wrapper = new UpdateWrapper<>(); wrapper.set("status", 2) // 标记为已删除 .ne("status", 2); // 避免重复操作 blogPostService.update(wrapper); // 清空评论 UpdateWrapper<BlogComment> commentWrapper = new UpdateWrapper<>(); commentWrapper.set("status", 2) .ne("status", 2); blogCommentService.update(commentWrapper); return "redirect:/admin/dashboard"; }对应前端按钮:
<!-- 在 admin/dashboard.html 中 --> <form action="/admin/clear-all-posts" method="post"> <button type="submit" class="btn btn-danger" onclick="return confirm('确定清空所有博客和评论?此操作不可撤销!')"> 清空全班博客 </button> </form>为什么用软删除(status=2)而非物理删除?
因为教师可能误点,物理删除后无法恢复;而软删除只是标记状态,SQL 查询时加WHERE status != 2即可过滤,且保留数据用于期末统计(如“本学期共提交博客 1200 篇”)。
5.2 生成学期数据快照:用SELECT ... INTO OUTFILE导出学生博客为 CSV
期末归档时,教师需要导出所有已发布博客为 Excel。MySQL 原生命令SELECT ... INTO OUTFILE可直接生成 CSV,比 Java 导出更高效:
-- 在 MySQL 客户端执行(需 FILE 权限) SELECT u.username AS '学生姓名', p.title AS '博客标题', p.content AS '博客内容', p.create_time AS '发布时间' INTO OUTFILE '/var/lib/mysql-files/campus_blog_export.csv' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' FROM blog_post p JOIN blog_user u ON p.user_id = u.id WHERE p.status = 1;注意:
/var/lib/mysql-files/是 MySQLsecure_file_priv参数指定的安全目录,不能随意改路径;导出后,教师用scp或远程桌面复制该文件到本地,用 Excel 打开即为标准 CSV。
5.3 防止学生“刷分”:用 Redis 记录每日发布上限
有学生为拿高分,一天发 20 篇博客。在BlogPostController的发布接口中加入限流:
// src/main/java/com/example/campusblog/controller/BlogPostController.java @Autowired private RedisTemplate<String, Object> redisTemplate; @PostMapping("/post/publish") public String publishPost(@RequestBody BlogPost post, HttpSession session) { BlogUser user = (BlogUser) session.getAttribute("user"); // 限流:每天最多 3 篇 String key = "post_limit:" + user.getId() + ":" + LocalDate.now(); Long count = redisTemplate.opsForValue().increment(key, 1); if (count > 3) { model.addAttribute("error", "今日发布次数已达上限(3 篇)"); return "user/dashboard"; } redisTemplate.expire(key, 1, TimeUnit.DAYS); // 设置 1 天过期 // 正常发布逻辑... post.setUserId(user.getId()); post.setStatus(0); // 先存草稿 blogPostService.save(post); return "redirect:/user/dashboard"; }Redis 部署建议:校园机房可安装 Windows 版 Redis(微软维护的
redis-windows),内存占用仅 10MB,比数据库计数更轻量。若无 Redis,退化为数据库计数(SELECT COUNT(*) FROM blog_post WHERE user_id=? AND DATE(create_time)=CURDATE()),但性能稍差。
5.4 我的习惯:每次课程更新前,用 Git Tag 锁定教学版本
我带的 Java 课程每年迭代,但不同届学生用的系统版本必须一致。我的做法是:
- 每学期初,在 GitHub 仓库打 Tag,如
v2024-spring; - 给学生文档明确写:“请 clone
git clone -b v2024-spring https://github.com/yourname/campus-blog.git”; - 所有
application.yml中的数据库密码、Redis 地址等敏感配置,用application-dev.yml单独存放,并加入.gitignore; - 最后,把
v2024-springTag 对应的 ZIP 包上传到学校教学平台,供学生直接下载。
这看似多一步,却避免了“学生用最新版代码,结果 Spring Boot 升级到 3.3,JDK 17 不兼容”的翻车现场。工程能力的第一课,不是写多炫的算法,而是让代码在别人机器上稳稳跑起来。
希望帮到你。
本文还有配套的精品资源,点击获取