ARTICLE DETAIL

资讯详情

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

基于SpringBoot的毕业设计选题系统:版本选型、业务实现与部署避坑指南

基于SpringBoot的毕业设计选题系统:版本选型、业务实现与部署避坑指南 简介基于 Spring Boot 的毕业设计选题系统完整源码包面向计算机专业毕业生、毕业设计指导教师以及后端学习者解决传统线下选题中题目信息分散、学生重复申报、教师审核反馈滞后等问题。系统实现从选题发布、学生申报、教师审核到最终定稿的完整业务闭环可运行于本地开发环境便于二次开发也适合作为课程设计或毕业设计参考项目。压缩包约 4.93MB共 135 个文件其中 49 个 Java 源文件承担后端核心逻辑11 个 HTML 页面、6 个 CSS 样式与 5 个 JS 脚本构成前端操作界面SQL 脚本用于初始化数据库表结构和示例数据xlsx 文件可辅助批量导入选题清单图片素材用于页面展示目录结构清晰。当前已有 5621 人学习下载。通过阅读源码可深入了解项目分层架构、数据访问层写法、登录与角色权限控制流程以及前后端数据交互方式同时可复用其中的选题流程设计、数据库表结构和后台管理页面布局显著减少从零搭建系统的时间成本。1. 基于SpringBoot的毕业设计选题系统.zip不是“解压交差”而是“二次开发地图”毕设季节一到答疑群里一天能看到好几遍「基于SpringBoot的毕业设计选题系统.zip」有人求源码有人问能不能跑。这个 zip 里通常是一整套课题——学生端选题报名与状态跟踪教师端出题、审核、查看被选情况管理员端管用户和名额后端 SpringBoot MyBatis前端 Vue 或 Layui数据库 MySQL。目标只有一个把「选题」从线下表格搬到线上作为毕业设计交付给学院验收。但我要先泼盆冷水这类 zip 不是双击就能跑的产品而是给你二次开发的半成品。让它跑通的关键不在业务代码在版本匹配——SpringBoot 版本、JDK 大版本、Maven 依赖、MySQL 驱动一对不上就把人劝退。下面按接手这类系统最常见的路径讲先选对版本再落地选题流程处理前后端联调最后给一份排查清单。应届生二次开发、在职工程师给学院搭系统都能照着走。2. 版本选型与项目结构SpringBoot 版本太高为什么是第一个翻车点拿到 ZIP 的第一件事不是点开 IDEA而是先确认这套源码用哪个 SpringBoot 版本写的。常见做法是看 pom.xml 的 parent 节点再看跟源码一起带过来的 mvnw。如果你拿到的是 SpringBoot 2.7.x 的源码把它锁在 JDK 8~11 上跑不要为了追新升到 JDK 17如果是 SpringBoot 3.x必须 JDK 17 起步同时 MyBatis 的 starter 要换成 3.x 版本。很多搜「springboot版本太高」的人就是拿 2.7 的源码去配 3.3 的依赖然后被一堆诡异报错淹没。2.1 三道版本锁JDK、SpringBoot 与依赖的匹配关系为什么版本高了就崩这跟 SpringBoot 自动装配原理直接相关。自动装配本质是条件装配Starter 把 XxxAutoConfiguration 注册进 META-INF 下的配置文件框架启动时读取这些配置再用 ConditionalOnClass、ConditionalOnProperty 判断当前环境是否满足条件满足才创建 Bean。SpringBoot 3.x 把注册文件从 spring.factories 换成了 AutoConfiguration.imports老项目里手工加的自动配置类在新版本下根本不会被加载。于是出现最折磨人的现象应用能启动Tomcat 也起来了但数据源、定时任务、参数校验全部静默失效。没有报错、没有日志纯粹的黑匣子。这个表是我每次接手源码项目都会先过一遍的组合SpringBootJDKMyBatis starter自动配置注册文件稳的老方案2.7.x8~112.3.xspring.factories新的官方方向3.2.x173.0.xAutoConfiguration.imports选题系统这种以 CRUD 为主的项目我一般会选 2.7.18 JDK8 Maven图的是教程多、答辩时能讲清楚、MySQL 驱动不踩坑。除非你明确要用 JDK17 的虚拟线程、Record 这类新特性才考虑 3.2。若依框架的后续分支已经往 SpringBoot 3 迁移但那是一整套工程规范的复杂度对一个选题系统来说偏重。顺便理一下 SpringBoot 项目结构。常见的 web 项目目录就四层controller 只做参数接收和结果包装service 写业务判断mapper 接口对应 XMLentity 对应数据库表。如果你拿到手的 zip 里有一个包叫 config记得进去看一眼数据源、CORS、拦截器一般都在这里。找不到类的时候别翻遍整个项目按 controller → service → mapper 这条调用链找效率高很多。2.2 pom.xml 最小骨架四组坐标与两个必须写的插件毕设选题系统的 pom 不需要花哨四组依赖就够了。我用过的最小骨架是这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ !-- lookup parent from repository -- /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- MyBatis 注意SpringBoot 3.x 要用 3.x 的 starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build逻辑说明parent 把大部分依赖版本统一管理starter-web 负责内嵌 Tomcat 和 MVCstarter-validation 做参数校验选题系统里校验学号、手机号格式会用到。MyBatis starter 必须显式写版本SpringBoot 的 BOM 并不管理它的坐标不写就会拉到不兼容版本或者下载失败。mysql-connector-java 在 2.7 下只要写 artifactId版本交给 parent如果依赖里出现 javax 包名对应 SpringBoot 2.x出现 jakarta 才是 3.x别搞反。spring-boot-maven-plugin 负责打成可执行 jar这决定你能不能直接 java -jar 启动。有人会问 gradle 项目怎么处理。对应关系是apply plugin implementation 引入依赖groupId、artifactId 都不变但 SpringBoot Gradle 工程没有 parent 统一管版本要另外配 dependencyManagement 或 platform 插件这是它和 Maven 最大的差别。拿到 zip 先确认是 Maven 还是 Gradle别按 Maven 习惯硬套。2.3 用 mvn spring-boot:run 验证自动装配而不是上来就点 Run# 先清理再启动避免 target 里的旧 class 干扰 mvn clean spring-boot:run -Dspring-boot.run.profilesdev \ -Dspring-boot.run.arguments--server.port8081逻辑说明第一条命令把旧构建产物清掉再用 spring-boot 插件启动。-Dspring-boot.run.profilesdev 指定加载 application-dev.yml-Dspring-boot.run.arguments 里的 server.port 会覆盖配置文件端口适合本机 8080 被占的应急。profiles 决定数据源等环境相关配置arguments 是运行期覆盖项优先级高于 application.yml。如果你在 IDEA 2026 的 Run Configuration 里配置等价做法是VM options 填 -Dspring.profiles.activedevProgram arguments 填 --server.port8081。新版 IDEA 这两个输入框是分开的不要把 -D 开头的参数写进 Program arguments否则数据源配置加载不生效启动日志里会看到 No active profile set——这不是网络问题是 profile 没加载最容易误判。启动日志看到 Started Application 还不算完再确认两行Tomcat started on port 8081以及 Mapper XML 的解析记录。第二个确认很关键它说明 MapperScan 路径扫到了、数据源也初始化成功。只看端口起来就去点接口第一次请求才暴露 Mapper 绑定异常等于把问题留到了测试阶段。3. 核心业务落地选题状态机、乐观锁与定时任务3.1 四态状态机把“选题申请”做成一张 status 字段选题系统最核心的抽象是申请记录的状态。不要用布尔字段 is_choose而是用状态枚举public enum SelectionStatus { PENDING(0, 待审核), APPROVED(1, 已通过), REJECTED(2, 已拒绝), WITHDRAWN(3, 已撤回); private final int code; private final String desc; // 构造方法与 getter 省略 }常见的表结构是三张题目表 topic、选题申请表 selection、学生表 student。申请表里冗余 topic_id、student_id、status、apply_time 等字段好处是查「某教师名下已选题目」一条 join 就出来不用统计两次。状态流转要写在一个 Service 方法里不要散落到 Controller。我一般这样约束学生提交申请时 status 只能是 PENDING教师审核时只有 PENDING 才能流转到 APPROVED 或 REJECTED学生在未审核前可撤回。每个 update 语句都带旧状态条件比如 update selection set status1 where id? and status0。这是防止重复审核最简单的办法代价只是多写一个条件。3.2 乐观锁扣名额并发抢最后一个名额的正确姿势两个学生同时点「选题」名额只剩 1 个千万别想用 synchronized 解决——集群部署下 synchronized 只对单个 JVM 有效部署两台就穿帮。正确做法是乐观锁 数据库唯一约束。-- 同一个题目只能被一个学生选中靠唯一索引兜底 ALTER TABLE selection ADD UNIQUE KEY uk_topic_student (topic_id, student_id); -- 扣减剩余名额把名额 0 写进 WHERE影响行数为 0 说明已被抢走 UPDATE topic SET remain_count remain_count - 1 WHERE id #{topicId} AND remain_count 0;逻辑说明第一条是兜底约束保证同一个题目不会出现两条申请。第二条是关键UPDATE 的 WHERE 里加 remain_count 0MySQL 会对命中行加行锁两个并发事务只有一个能更新成功影响行数为 0 的那方直接返回「名额已满」。Service 里把两步串起来顺序不能反Transactional(rollbackFor Exception.class) public void chooseTopic(Long studentId, Long topicId) { // 1. 先扣名额affected 为 0 说明没抢到 int affected topicMapper.decreaseRemain(topicId); if (affected 0) { throw new BusinessException(该题目名额已满换个方向选吧); } // 2. 再插选题申请 Selection s new Selection(); s.setStudentId(studentId); s.setTopicId(topicId); s.setStatus(SelectionStatus.PENDING); selectionMapper.insert(s); }参数说明affected 是 MyBatis 返回的影响行数等于 0 说明 UPDATE 没命中必须抛异常让事务回滚。Transactional 的 rollbackFor 写 Exception.class 是重点——Spring 默认只对 RuntimeException 回滚自定义 BusinessException 如果继承 Exception默认不回滚后果是名额扣了但申请没写入数据库里少一个名额怎么查都对不上。还在用 synchronized 的旧代码别急着删可以留着做对比单机单线程没问题用 JMeter 开 20 个线程并发请求名额会变负数。这个血泪经验建议你亲自复现一次答辩论据就有了。3.3 定时任务自动关闭选题与生成统计报表选题系统通常会配定时任务开放时间到了自动开放选题截止时间到了自动关闭每天凌晨统计教师选题数量。SpringBoot 里最省事的做法是 Scheduled EnableScheduling。EnableScheduling SpringBootApplication public class TopicSystemApplication { public static void main(String[] args) { SpringApplication.run(TopicSystemApplication.class, args); } } Service public class TopicCloseTask { // 每天凌晨 2:00 执行把过期选题置为关闭 Scheduled(cron 0 0 2 * * ?) public void closeExpiredSelection() { topicMapper.updateExpiredToClosed(LocalDateTime.now()); } }逻辑说明EnableScheduling 加在主启动类上Scheduled 才会生效。cron 表达式六个字段从秒开始0 0 2 * * ? 表示每天 02:00:00 执行一次。这里有三个细节容易出问题。第一别把 Scheduled 写在 Controller 里能跑但职责混乱建议独立成 task 包。第二默认调度器是单线程任务多了会排队异步化需要自己配 ThreadPoolTaskScheduler 并设置 pool-size。第三cron 表达式多空格、少最后一位启动直接报 CronExpression 解析失败这是定时任务「感觉没写」的常见来源看启动报错就能定位。4. 从 ZIP 到“你的系统”前后端联调与部署落地4.1 开发期跨域CORS 全局配置比代理省心这类 zip 大多前后端分离前端 Vue 跑在 5173后端 8081开发期第一个遇到的就是跨域。不要一上来就配代理先在 SpringBoot 做全局 CORS 配置评审时也能把跨域原理讲清楚。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(http://localhost:*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }参数说明addMapping(/api/**) 表示只对 /api 开头接口生效。allowedOriginPatterns 用通配符匹配 localhost 任意端口开发期最省事。千万别用 allowedOrigins(*) 配合 allowCredentials(true)浏览器在凭证模式下不允许通配源直接报错。前端部署到服务器后把 allowedOriginPatterns 改成实际域名。还要注意如果后端有登录拦截器自定义 Filter 里必须放行 OPTIONS 预检请求否则前端一样报跨域。拦截器里写 if (OPTIONS.equals(request.getMethod())) return true这个判断要放在 token 校验之前。4.2 把 Vue 打包放进 SpringBoot三个必改点答辩或交付时一台服务器起两个服务后端 Nginx能讲清架构但很多同学为了简单都搜过「vue打包放进springboot中」这个操作。做法本身不复杂前端 npm run build 生成 dist把 index.html 和 static 目录整体复制到后端 src/main/resources/static 下重新打包 jar访问 http://ip:8081 直接就是前端页面接口同源连跨域配置都能删掉。但有三个必改点少一个页面就打不开。第一axios 的 baseURL 改成相对路径 /api不能写 http://localhost:8080否则页面打开了请求全 404。第二Vue Router 用 createWebHashHistory不要用 createWebHistoryhistory 模式在 SpringBoot 静态资源下刷新会 404还得额外写 forward 控制器。第三重新打包前 mvn clean否则 target/classes/static 残留旧 dist改了半天页面还是老的——这是最常见也最气人的一个。4.3 IDEA 2026 配置 SpringBoot 启动服务端口与 profile 一次说清新装的 IDEA 2026 导入 maven 项目后Run/Debug Configurations 默认只有 Application 类型。建议新建一个 Spring Boot 类型的配置关键项如下JRE选 JDK 8 或 17和 pom 里 java.version 对齐选错编译期报 release version 不支持。VM options填 -Dspring.profiles.activedev对应加载 application-dev.yml。Program arguments填 --server.port8081优先级比 application.yml 里的 server.port 高。Environment variables数据库密码这类敏感项填 DB_PASSWORDxxx配合 yml 里的 ${DB_PASSWORD} 使用。一个我在新版 IDEA 里踩过的坑如果你通过 Idea 的 maven 面板「Reload All Maven Projects」把依赖刷新到了本地仓库的最新版而项目是 2.7 的会出现 dependency conflict。别急着点 Update先看 pom 里有没有依赖管理把版本锁住版本太高导致的冲突根因往往是某个传递依赖被新版拉了进来把冲突的坐标在 pom 里用排除标签去掉即可。4.4 宝塔面板 Docker 部署最小可行步骤如果服务器装的是宝塔面板部署 SpringBoot 选题系统最省心的是 Docker。网上「宝塔docker部署springboot」的文章套路都差不多但有两个坑要提前避开。FROM openjdk:8-jre-alpine COPY topic-system.jar /app/app.jar ENV TZAsia/Shanghai ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]第一步是写上面的 Dockerfile然后 docker build -t topic-system . 再 docker run -d -p 8081:8081 topic-system。数据库不要用容器直接连宝塔装的 MySQL连接串里的 localhost 要改成宿主机内网 IP这是容器部署最常见的坑。第二个坑是时区不加 ENV TZAsia/Shanghai容器内时区是 UTC定时任务会在凌晨 8 点而不是凌晨 2 点执行报表日期错位一天。这是真实发生过的血泪经验很多人以为定时任务没写对其实是时区错位。5. 避坑排查先看现象再对症下药下面这些坑是选题系统里出现频率最高的几类。每条按现象、原因、解决展开可以直接对号入座。定位问题前先跑三条命令# 1. 应用是否活着 curl http://localhost:8081/actuator/health # 2. 日志里有没有异常 tail -f logs/app.log | grep -i error # 3. 端口是否真的被监听 netstat -tlnp | grep 8081这三条能把「系统没启动」「启动报错」「端口被占」三种情况区分开避免在业务代码里瞎猜。5.1 现象能查数据不能写一写就报 1366 或乱码原因MySQL 连接串没指定 characterEncoding而且表结构是 utf8mb3存中文或生僻字直接报 Incorrect string value。解决连接串加 characterEncodingutf8mb4同时核对库、表、列三层字符集。最彻底的做法是 ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4。别只看连接串表和列的字符集不一致照样乱码。5.2 现象前端改了代码刷新还是老页面原因分两种。开发期是浏览器缓存了 index.html打包放进 SpringBoot 后是 target/classes/static 下残留旧 dist。解决开发期开无痕窗口绕过缓存部署期 mvn clean 后重新 package。如果用了 Nginx 做静态缓存再给 location / 加 add_header Cache-Control no-cache。5.3 现象定时任务没到点执行或者一天执行两次原因两个常见坑一是 EnableScheduling 忘加二是多实例部署时每个实例都在跑同一 cron同一个任务执行了两遍。解决先确认启动类上有 EnableScheduling。多实例场景要给任务加动态开关或用数据库锁表保证只有一个实例执行。毕设系统一般是单实例不会遇到第二个但要在设计文档里写清这个限制。5.4 现象两个学生同时抢到最后一个名额原因只做了 SELECT 判断没做条件扣减。两个并发事务 SELECT 出来的 remain_count 都是旧值判断通过后各自 insert名额超卖。解决用第三章的 UPDATE ... WHERE remain_count 0通过影响行数判断。加了唯一索引后重复申请会抛 DuplicateKeyException也要在全局异常处理里包成「你已选过该题目」的提示。5.5 现象端口被占用关掉程序还是占用原因上次进程没被杀干净或者内嵌 Tomcat 的端口被残留监听占着。解决先 netstat -tlnp | grep 8081 找到 PID 再 kill。如果是 IDEA 里运行的在 Services 面板做 Stop不要直接关窗口。再不行就用 --server.port 指定临时端口这一步能区分是代码占端口还是环境占端口。5.6 现象insert 不报错但库里就是没数据原因多半是事务回滚但异常被吞了。没写 rollbackFor 时自定义业务异常会让事务静默回滚看起来没报错其实数据被撤销。解决把 Service 方法上的 Transactional(rollbackFor Exception.class) 补齐再打断点确认是否真走入了 insert。用 MySQL general_log 开启 SQL 日志能在日志里看到事务提交前是否有回滚这是最有用的排查手段。6. 验证与进阶用 actuator 与一条 SQL 给系统上好保险6.1 用 actuator 证明“系统没坏”选题系统交付后离答辩还有一步证明它能跑、数据是对的。与其手动开页面点点点不如把验证做成可重复执行的命令。先加 spring-boot-starter-actuator 依赖再在 application.yml 里暴露端点management: endpoints: web: exposure: include: health,info加完后启动系统curl http://localhost:8081/actuator/health返回 {status:UP}说明应用和数据源都正常。这步可以写进验证文档答辩时现场执行。6.2 用一条聚合 SQL 证明“业务没炸”把关键数据一致性校验成 SQL 脚本是我这几年常用的「后悔药」-- 校验已通过的人数 剩余名额 应等于总名额结果集为空才算正常 SELECT t.id, t.total_count, t.remain_count, COUNT(CASE WHEN s.status 1 THEN 1 END) AS approved_count FROM topic t LEFT JOIN selection s ON s.topic_id t.id GROUP BY t.id, t.total_count, t.remain_count HAVING t.remain_count approved_count t.total_count;这条查询只返回对不上的题目正常情况结果集为空。配合健康检查基本覆盖「系统没坏、业务没炸」两个维度说服力足够。6.3 附件上传的路径选择从 MultipartFile 到文件系统如果系统做了学生上传选题申请表的功能后端收到 MultipartFile 后常见做法是转存到文件系统再在数据库存相对路径而不是把文件二进制塞进数据库。配置一个上传目录按日期分文件夹存附件路径存进 selection 表的附件字段即可。网上常搜「springboot中multipart如何用resttemplate传」那是服务间转发场景毕设里用不到但知道边界有好处RestTemplate 转发 multipart 时要包装 HttpEntity 和 MultiValueMap直接裸传会丢文件。选题系统按文件系统存储方案做迁移时记得把上传目录整个拷走否则换服务器附件全丢。最后说个我自己的翻车经历早先代码里把扣名额的 UPDATE 放在了 INSERT 后面并发一测库存直接变负数。后来改成先 update 后 insert才意识到「先扣后插」这个顺序本身就是业务约束。现在每搭一个和额度有关的系统都会先画一遍并发时间线再写代码。希望这篇能帮你在自己的选题系统里少踩几个坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表