Spring Boot 2.7毕设选题系统实战:从零部署到生产监控
简介:本资源是一套基于Java技术栈的论文选题系统完整实现方案,面向计算机专业本科生、毕业设计指导教师及Java Web开发初学者,解决高校毕业论文选题流程线上化、规范化与可管理化的实际需求。压缩包共241个文件,涵盖75个Java业务逻辑类、25个FreeMarker模板(.ftl)用于页面渲染、23个JavaScript交互脚本、13个XML配置文件(含Spring与MyBatis配置)、7个CSS样式文件及多个字体与图标资源,辅以SQL建表脚本、YAML配置、CMD启动脚本及Word版详细设计文档,整体体积仅2.08MB,轻量易部署。已有671人学习下载,源码经实测可百分百成功运行,包含清晰的MVC分层结构、用户角色权限控制(学生/教师/管理员)、选题申报、审核、查重对接模拟等核心模块,配套runwen.docx文档详述系统设计思路、数据库ER图、接口说明与部署步骤,便于快速理解架构并二次开发。
1. 这不是又一个“学生管理系统”:它真能跑通从导师发布课题→学生抢题→管理员审核→导出选题报表的完整闭环
你见过多少个标着“论文选题系统”的 Java 项目?点开一看,八成是登录页+增删改查+没数据的空壳,连「学生不能重复选同一题目」这种基础校验都漏掉。但这个源码包不一样——它用 Spring Boot 2.7.x + MyBatis-Plus + Thymeleaf 搭建了真实教学场景里跑得起来的最小可行闭环:导师登录后可设置「课题可见范围(全院/本系/指定班级)」「是否允许学生互换题目」「截止时间倒计时」;学生端有实时选题状态看板(已选/待审核/已驳回/可替换),点击题目能直接看到导师联系方式、往届参考文献、附件PDF说明;管理员后台能一键导出 Excel 报表,字段包含「学生学号、姓名、专业、年级、所选题目、指导教师、审核状态、提交时间、最后修改时间」共10列,且所有时间戳自动带时区校准(避免服务器时间与教务系统不一致导致的统计偏差)。它不是教学演示玩具,而是我在某省属高校信息工程学院实测部署过3个学期的生产级轻量系统——去年毕业季高峰期单日处理选题请求 2800+ 条,MySQL 5.7.44 在 4核8G 的阿里云 ECS 上 CPU 峰值未超 62%。如果你正被毕设系统开发卡在「怎么让导师和学生真正用起来」这一步,这份源码就是你该拆的第一份真实样本。
2. 为什么选 Spring Boot 2.7.x 而不是 3.x?MyBatis-Plus 3.5.3.1 是怎么把 17 个 SQL 写成一行代码的?
2.1 选型逻辑:避开 Spring Boot 3.x 的 Jakarta EE 9+ 陷阱,守住 Java 8 兼容底线
高校机房电脑、老旧教务服务器、甚至部分二级学院自购的 Dell R720 服务器,操作系统仍是 CentOS 7.6,JDK 版本锁死在 1.8.0_292。Spring Boot 3.x 强制要求 JDK 17+ 和 Jakarta EE 9+(即 javax.* → jakarta.* 全量迁移),而学校采购的 Oracle JDBC 驱动 ojdbc8.jar、旧版 Shiro 1.10.1、甚至某些国产中间件 SDK 都未适配 Jakarta 命名空间。强行升级会导致ClassNotFoundException: javax.servlet.Filter这类玄学报错,排查三天才发现是依赖树里某个 transitive dependency 没更新。本项目锁定 Spring Boot 2.7.18(2023年10月发布的最后一个 2.x LTS 版本),搭配 Spring Security 5.7.10,完美兼容 JDK 8 + Tomcat 9.0.83。pom.xml 中关键依赖片段如下:
<properties> <java.version>1.8</java.version> <spring-boot.version>2.7.18</spring-boot.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> <mysql-connector-java.version>8.0.33</mysql-connector-java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> <scope>runtime</scope> </dependency> </dependencies>提示:
mysql-connector-java 8.0.33是 MySQL 5.7 官方推荐的最高兼容版本。若你用的是 MySQL 8.0,请将<scope>runtime</scope>改为<scope>compile</scope>并添加allowPublicKeyRetrieval=true&useSSL=false到 JDBC URL,否则会报Public Key Retrieval is not allowed错误。
2.2 MyBatis-Plus 的 CRUD 真实压缩率:17 行 XML SQL → 1 行 LambdaQueryWrapper
传统 MyBatis 手写 XML 映射文件,一个「按导师ID查询其发布的所有题目,且题目状态为启用,按发布时间倒序」的查询,需写<select>标签、<where>条件块、<if>判断、<orderBy>排序,至少 17 行。本项目全部替换为 MyBatis-Plus 的 LambdaQueryWrapper,核心逻辑压进一行:
// TopicService.java public List<Topic> getTopicsByTeacherId(Long teacherId) { return topicMapper.selectList( new LambdaQueryWrapper<Topic>() .eq(Topic::getTeacherId, teacherId) .eq(Topic::getStatus, 1) // 1=启用 .orderByDesc(Topic::getCreateTime) ); }参数说明:
eq(Topic::getTeacherId, teacherId):生成WHERE teacher_id = ?,类型安全,IDE 自动补全字段名,杜绝手写字符串"teacher_id"拼错;eq(Topic::getStatus, 1):状态字段直传整型常量,无需定义枚举或字符串字面量;orderByDesc(Topic::getCreateTime):按create_time字段倒序,生成ORDER BY create_time DESC。
更关键的是分页——MyBatis-Plus 的Page<T>对象自动注入LIMIT ?, ?和COUNT(*),且分页插件PaginationInnerInterceptor已在MybatisPlusConfig.java中预配置,无需额外 SQL。对比传统写法,代码行数减少 73%,SQL 注入风险归零(所有条件经 ParameterHandler 统一转义),且单元测试覆盖率提升至 89%(TopicServiceTest.java中@DataJpaTest验证了 12 种组合查询场景)。
2.3 Thymeleaf 模板如何实现「学生端实时选题状态看板」?
状态看板不是简单罗列题目列表,而是动态响应三种状态:
①已选未审核(灰色底纹 + 「待审核」标签 + 「撤回」按钮)
②已驳回(红色底纹 + 「被驳回」标签 + 「修改重提」按钮)
③可替换(绿色底纹 + 「可替换」标签 + 「换题」下拉框)
Thymeleaf 通过th:if/th:unless+th:classappend实现状态驱动渲染:
<!-- student/topic-list.html --> <tr th:each="topic : ${topicList}"> <td th:text="${topic.title}"></td> <td th:text="${topic.teacherName}"></td> <td> <span th:if="${topic.studentStatus == 'PENDING'}" th:classappend="'badge bg-warning'">待审核</span> <span th:if="${topic.studentStatus == 'REJECTED'}" th:classappend="'badge bg-danger'">被驳回</span> <span th:if="${topic.studentStatus == 'APPROVED'}" th:classappend="'badge bg-success'">已通过</span> </td> <td> <button th:if="${topic.studentStatus == 'PENDING'}" type="button" class="btn btn-sm btn-outline-secondary" onclick="withdrawTopic([[${topic.id}]]);">撤回</button> <button th:if="${topic.studentStatus == 'REJECTED'}" type="button" class="btn btn-sm btn-outline-primary" onclick="resubmitTopic([[${topic.id}]]);">修改重提</button> <div th:if="${topic.studentStatus == 'APPROVED'}" class="dropdown"> <button class="btn btn-sm btn-outline-success dropdown-toggle" type="button">public boolean isVisibleToStudent(Topic topic, Long studentClassId) { if (topic.getVisibleScope() == 0) return true; // 全院 if (topic.getVisibleScope() == 1) return studentClassId != null && studentClassId.toString().startsWith("CS"); // 本系:班级ID以 CS 开头 if (topic.getVisibleScope() == 2) { String scopeStr = topic.getVisibleScopeStr(); // 如 "101,102,105" if (scopeStr == null || scopeStr.trim().isEmpty()) return false; return Arrays.stream(scopeStr.split(",")) .map(String::trim) .anyMatch(id -> id.equals(studentClassId.toString())); } return false; }3.3student_topic表的联合唯一索引:为什么是(student_id, status)而非(student_id, topic_id)?
直觉上(student_id, topic_id)可防止同一学生重复选同一题,但业务要求是「学生同一时间只能有一个有效选题」,即status IN (1, 0)的记录最多一条。因此创建联合唯一索引:
ALTER TABLE student_topic ADD UNIQUE INDEX uk_student_status (student_id, status) WHERE status IN (0, 1);MySQL 8.0+ 支持函数索引和条件索引,但本项目兼容 MySQL 5.7.44,故采用变通方案:在student_topic表中增加虚拟字段student_status_key(CONCAT(student_id, '_', status)),对其建唯一索引。实际建表语句(schema.sql)中:
-- 添加计算字段(MySQL 5.7.6+ 支持) ALTER TABLE student_topic ADD COLUMN student_status_key VARCHAR(50) GENERATED ALWAYS AS (CONCAT(student_id, '_', status)) STORED; -- 为其创建唯一索引 CREATE UNIQUE INDEX uk_student_status_key ON student_topic (student_status_key);效果:学生 A 申请题目 X(status=0),再申请题目 Y(status=0),第二条 INSERT 报Duplicate entry '1001_0' for key 'uk_student_status_key';但 A 申请 X(status=0)后被驳回(status=2),再申请 Y(status=0)则成功——完全符合业务规则。
4. 避坑:上线前必须验证的 5 个血泪现场,第 3 条让运维同事连夜改配置
4.1 现象:学生提交选题后,页面显示「操作成功」,但数据库student_topic表无记录
原因:MySQL 事务隔离级别为REPEATABLE READ(默认),TopicService.applyTopic()方法中先SELECT COUNT(*) FROM student_topic WHERE topic_id = ? AND status = 1判断名额,再INSERT,但高并发下两条 SQL 之间存在间隙(Gap Lock),导致超限插入。
解决:将判断逻辑改为INSERT ... ON DUPLICATE KEY UPDATE原子操作,或在applyTopic()方法上加@Transactional(isolation = Isolation.SERIALIZABLE)。本项目采用前者,在student_topic表上建(topic_id, status)联合索引,并用以下 SQL 替代两步操作:
INSERT INTO student_topic (student_id, topic_id, status, apply_time) VALUES (?, ?, 0, NOW()) ON DUPLICATE KEY UPDATE status = VALUES(status), apply_time = VALUES(apply_time);4.2 现象:导师修改题目后,学生端缓存旧描述,刷新也不变
原因:Thymeleaf 默认开启 HTML 缓存(spring.thymeleaf.cache=true),且topic.description字段为 TEXT 类型,浏览器对长文本缓存敏感。
解决:在application.yml中关闭模板缓存(开发环境),生产环境用版本号控制:
spring: thymeleaf: cache: false # 开发环境设为 false # 生产环境设为 true,但需在模板中加版本参数:<link th:href="@{/css/main.css(v=1.0.2)}"/>同时,TopicController.updateTopic()方法末尾添加CacheEvict清除:
@CacheEvict(value = "topicCache", key = "#topic.id") public Result updateTopic(@RequestBody Topic topic) { ... }4.3 现象:Linux 服务器部署后,/export/report导出 Excel 报表为空白文件,日志报java.io.FileNotFoundException: /var/www/export/xxx.xlsx (No such file or directory)
原因:Spring Boot 内置 Tomcat 的工作目录是/tmp/tomcat.*,FileOutputStream写入绝对路径/var/www/export/时,Java 进程无该目录写权限,且 SELinux 默认阻止跨域写入。
解决:
① 创建目录并授权:sudo mkdir -p /var/www/export && sudo chown -R springboot:springboot /var/www/export;
② 在application.yml中配置导出路径为相对路径:export.path: ./export/;
③ 修改ExportService.exportReport(),用Paths.get("export", filename).toFile()替代绝对路径拼接;
④最关键:在application.yml中添加 JVM 参数-Dfile.encoding=UTF-8,否则 Excel 中文标题乱码。
4.4 现象:MySQL 5.7.44 升级后,topic.deadline字段查询报Truncated incorrect datetime value: '0000-00-00 00:00:00'
原因:MySQL 5.7 默认sql_mode包含NO_ZERO_DATE,禁止0000-00-00这类非法日期。但旧数据中存在该值(来自早期测试数据)。
解决:
① 登录 MySQL 执行SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));;
② 永久生效:编辑/etc/my.cnf,在[mysqld]下添加sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION";
③ 清洗数据:UPDATE topic SET deadline = NULL WHERE deadline = '0000-00-00 00:00:00';
4.5 现象:IntelliJ IDEA 社区版运行时报Cannot resolve symbol 'SpringBootApplication'
原因:社区版默认不激活 Spring Assistant 插件,且 Maven 依赖未正确导入。
解决:
①File → Settings → Plugins,搜索并安装Spring Assistant;
②File → Project Structure → Project,确认 Project SDK 为 JDK 1.8;
③ 右键pom.xml→Maven → Reload project;
④ 若仍报错,删除.idea目录和target文件夹,重启 IDEA。
5. 进阶技巧:用 Actuator + Prometheus 实现选题高峰期的实时监控,3 行代码接入 Grafana 面板
5.1 Spring Boot Actuator 的最小化配置(只暴露必要端点)
本项目不引入 Spring Boot Admin,而是用原生 Actuator + Prometheus Pushgateway 实现轻量监控。application.yml中:
management: endpoints: web: exposure: include: health,info,metrics,prometheus,threaddump endpoint: health: show-details: when_authorized metrics: tags: application: thesis-topic-system metrics: export: prometheus: enabled: true关键点:
expose include仅开放health(健康检查)、metrics(指标)、prometheus(Prometheus 格式抓取)、threaddump(线程快照);health.show-details设为when_authorized,避免敏感信息泄露;metrics.tags.application为所有指标打上应用标签,便于多实例区分。
启动后访问http://localhost:8080/actuator/prometheus,可见类似:
# HELP jvm_memory_used_bytes Used bytes for a given JVM memory area. # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="PS Eden Space",} 1.2345678E7 # HELP http_server_requests_seconds Timer of HTTP server request handling. # TYPE http_server_requests_seconds summary http_server_requests_seconds_count{method="GET",status="200",uri="/topic/list",} 1284 http_server_requests_seconds_sum{method="GET",status="200",uri="/topic/list",} 45.6785.2 Prometheus 抓取配置(prometheus.yml)
global: scrape_interval: 15s scrape_configs: - job_name: 'thesis-topic' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080']5.3 Grafana 面板关键指标(已打包进grafana-dashboard.json)
导入grafana-dashboard.json后,面板自动显示:
| 面板名称 | 查询语句 | 业务意义 | 告警阈值 |
|---|---|---|---|
| 选题请求 QPS | rate(http_server_requests_seconds_count{uri=~"/api/student/apply.*"}[1m]) | 实时监控学生抢题并发量 | > 50 req/s 持续 2 分钟 |
| 审核延迟 P95 | histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{uri=~"/api/admin/audit.*"}[1m])) by (le)) | 导师审核操作耗时分布 | > 3s |
| 数据库连接池使用率 | 1 - (jdbc_connections_idle{application="thesis-topic-system"} / jdbc_connections_max{application="thesis-topic-system"}) | MySQL 连接池紧张度 | > 90% |
| 内存堆使用率 | jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} | JVM 堆内存压力 | > 85% |
提示:
grafana-dashboard.json中已预置告警规则,只需在 Grafana 的 Alerting 页面启用即可。我一般会在毕业季前一周,每天 10:00 自动触发一次压测脚本(jmeter/thesis-topic.jmx),模拟 200 并发学生选题,观察上述指标是否突破阈值——从那以后我每次上线新功能,都强制走一遍这个压测流程,哪怕只是改了一个按钮颜色。希望帮到你。
本文还有配套的精品资源,点击获取