资讯动态

SpringBoot+Vue+MyBatis课程设计选题管理系统实战

发布时间:2026/10/17 13:56:30 来源:尧图企业网站定制
每学期到了课程设计选题的时间点基本上都是管理员、教师、学生三方一起上火的时段。老师要建Excel表、发群里、等学生填写再手动汇总学生要反复刷新、找有没有空余题目、填了表格又怕被挤掉管理员最惨要来回核对人名、题目编号和重复选题一次统计能吃掉小半天。所以当某同学来找我讨论课程设计怎么做的时候我直接建议他把“课程设计选题管理”做成一个前后端分离的Web系统技术栈用SpringBootVueMyBatisMySQL也就是圈子里常说的ABO选题管理系统——前端负责交互和展示后端负责业务逻辑和数据数据库统一管理选题关系。这篇博文就围绕这个项目展开完整讲清楚系统的痛点、表结构、核心逻辑、联调细节和部署方案适合正在做类似课程设计、或者想完整跑通一套SpringBootVue全家桶项目的朋友参考。1. 为什么课程设计的选题环节值得做一个系统1.1 课程设计选题的三个老痛点第一个痛点是信息不同步。老师把题目清单发到群里学生下载、修改、再回传文件版本一多就彻底乱套。有的同学课设还没开始就选好了三个题有的同学等截止前一天才发现自己根本没在名单里。第二个痛点是并发冲突特别是热门题目多个学生同时填写Excel表单根本没法保证谁先谁后哪怕用在线文档也会出现覆盖编辑的情况。第三个痛点是审核状态不透明。学生提交选题之后老师到底看没看、同没同意全靠线下追问没有任何一个集中反馈的入口。如果你想把这个痛点讲清楚最好的方法不是口头描述而是直接罗列一份“没有系统时的选题流程”对比“有系统后的选题流程”。实际做下来这种对比放在课程设计报告的引言部分非常加分因为评委一眼就能看出项目解决的问题是真实的而不是为写代码硬造需求。1.2 系统的角色与功能范围这个系统的核心角色有三个管理员、教师、学生。对应的功能边界很清晰。管理员管理用户账号、管理教师开设的题目池、查看全局选题情况、导出选题统计表。教师发布题目、设置题目可选人数、审核学生选题申请、退回并填写原因。学生浏览可选题目、提交选题申请、查看审核结果、在未通过时重新选择。项目的主体框架就是围绕这三个角色展开的。后端用SpringBoot提供RESTful接口前端用Vue组件化页面数据统一落到MySQL。除了这三个角色的CRUD整个系统里最有价值的部分其实是“选题申请—教师审核—结果锁定”这个状态流转过程还有多位学生同时抢热门题目时的并发控制。这两个点在答辩环节非常容易被追问后面我会单独拆开细讲。2. 技术组合背后的选择逻辑与工程结构2.1 为什么是SpringBootVueMyBatis而不是其他组合很多人做课程设计时习惯选自己最熟的框架但这里我建议你把选择逻辑讲明白这比堆功能更能体现水平。选SpringBoot的原因很直接它内置了Tomcat、自动化配置多、起步依赖清晰能让你把主要精力放在业务代码而不是环境搭建上。更关键的是SpringBoot是当前中小型企业级Java项目最常用的基础框架之一找资料、调试、部署都有大量现成经验可参考。Vue这边我推荐用Vue 2或者Vue 3的选项式API写法配合Vue Router和Axios。Vue的组件化思想和这套系统的页面结构天然契合——每个角色都有自己的页面每个页面都能拆成独立的组件来开发和维护。这种前后端分离的开发模式也符合目前真实团队协作的基本形态。至于MyBatis我在这类课程设计项目里一般不建议直接用MyBatis-Plus除非你想省掉写SQL的功夫。理由有两个一是课程设计需要展示基本功能够手写SQL、控制查询条件在答辩时是一个加分点二是这个系统的核心逻辑里有一些带条件更新的SQL比如扣减题目剩余名额、校验重复选题用MyBatis的XML手写SQL反而表达得更清楚也更容易做调试。2.2 项目目录与包结构整个工程建议用一个根目录分四个部分backend、frontend、database、docs。course-selection-system/ ├── backend/ │ ├── src/main/java/com/course/selection/ │ │ ├── controller/ # 控制层接收HTTP请求 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Mapper接口 │ │ ├── entity/ # 数据库实体类 │ │ ├── dto/ # 前端交互的数据传输对象 │ │ ├── common/ # 统一响应、异常、工具类 │ │ └── config/ # 跨域、拦截器等配置 │ └── src/main/resources/ │ ├── mapper/ # MyBatis XML文件 │ └── application.yml ├── frontend/ │ ├── src/ │ │ ├── api/ # Axios接口封装 │ │ ├── router/ # 路由配置 │ │ ├── views/ # 页面组件 │ │ ├── components/ # 公共组件 │ │ └── store/ # 登录态管理 │ └── package.json ├── database/ │ └── init.sql # 建库建表与初始数据 └── docs/ ├── 需求说明书.md ├── 数据库设计.md └── 部署文档.md这个目录结构本身也是文档的一部分它把“前后端分离”体现得非常直白。后端包名用com.course.selection作为根避免和默认的com.example混淆。前端分开api、router、views、components四个目录让每个文件职责单一。我在实际带课设项目时一直强调这个结构比写花哨的代码更能体现工程意识因为很多同学的项目不是功能做不完而是结构太乱导致自己都找不到代码。2.3 数据库表设计四张核心表数据库设计是整个项目的地基我建议围绕用户、题目、选题记录、审核记录四个核心概念来建表初始表结构如下。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, role tinyint(4) NOT NULL COMMENT 1管理员 2教师 3学生, real_name varchar(50) DEFAULT NULL, student_no varchar(20) DEFAULT NULL COMMENT 学生学号, teacher_no varchar(20) DEFAULT NULL COMMENT 教师工号, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;题目表主要字段包括题目名称、题目描述、所属教师、总名额、已选名额、状态等。已选名额这个字段很关键它不是冗余字段而是为了快速展示“剩余名额”以及在业务层做判断。设计时要注意已选名额永远不能超过总名额这个约束要通过下面的业务逻辑保证。CREATE TABLE topic ( id bigint(20) NOT NULL AUTO_INCREMENT, topic_name varchar(100) NOT NULL, description text, teacher_id bigint(20) NOT NULL, total_count int(11) NOT NULL DEFAULT 1, selected_count int(11) NOT NULL DEFAULT 0, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1可选 2已满 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;选题记录表是业务逻辑的核心它记录学生和题目之间的申请关系同时要冗余存储当前状态方便列表查询。审核记录表则记录每一次审核动作的备注一旦发生争议或需要追溯可以直接查表。选题记录表建议加一个联合唯一索引UNIQUE KEY uk_stu_topic (student_id, topic_id)。这个索引不是可有可无的它是防止同一学生对同一题目重复申请的最后一道数据库保底防线。3. 选题状态机与并发控制的完整落地3.1 状态机设计申请、通过、拒绝、退回选题管理系统的核心业务逻辑不是增删改查而是状态流转。我建议把状态定义成一个枚举类不要用散落的魔法数字。public enum SelectionStatus { PENDING(0, 待审核), APPROVED(1, 已通过), REJECTED(2, 已拒绝), CANCELED(3, 已取消); }状态流转规则是这样的学生提交申请状态变为PENDING教师审核通过状态变为APPROVED同时题目表的selected_count加一教师审核拒绝状态变为REJECTED学生可以重新选其他题学生自己取消申请状态变为CANCELED释放名额。这里有一个很容易被忽略的细节通过审核后必须检查当前题目是否已经满员。很多人在审核阶段不做检查只检查“提交申请时有没有空位”结果就会出现名额超卖。正确做法是在提交申请和教师审核通过这两次操作时都要对剩余名额做校验。3.2 约束执行的三个层次我把这个系统的并发约束分成三层来设计数据库约束、业务层校验、面向用户的交互提示。第一层是数据库约束。除了联合唯一索引外还可以在申请表中只允许同一学生存在一条非取消状态的记录这个可以通过设计来保证比如新申请插入前检查该学生是否存在状态不为CANCELED的申请记录。第二层是业务层校验。在Service层的方法上加Transactional方法内部先查状态、再查剩余名额、最后执行更新。注意查和更新之间会有一个时间窗口这个窗口在高并发下可能产生问题所以要配合第三层解决。第三层是SQL层的原子更新。把“扣减名额”和“增加申请记录”变成一个带条件的更新语句这样能真正兜住并发问题。实现方式是在更新题目表时使用UPDATE topic SET selected_count selected_count 1 WHERE id ? AND selected_count total_count然后检查更新影响的行数如果影响行数为0说明名额已经不足直接抛出业务异常并回滚事务。3.3 一个典型的并发抢题场景我举一个最常见的场景某热门题目总名额是3已有2个人通过审核剩下1个名额。此时两个学生同时点击“申请选题”。如果只是先查询剩余名额再插入申请记录两个请求可能同时读到“剩余1个”同时通过校验最终在数据库里形成两条记录名额变成4实际只应有3个。使用上面的原子更新方法后两个请求同时执行更新语句时数据库的行锁会让其中一个更新成功、另一个更新失败。考研失败的那方接口返回“该题目已满员”前端弹出提示整个过程不需要手动加分布式锁。这也是这套系统里最值得讲给评委听的一个设计点。Transactional public void applySelection(Long studentId, Long topicId) { // 1. 校验学生当前是否已有未完成的选题申请 int count selectionMapper.countActiveByStudent(studentId); if (count 0) { throw new BusinessException(当前已有进行中的选题申请); } // 2. 原子扣减名额防止并发超卖 int rows topicMapper.increaseSelectedCount(topicId); if (rows 0) { throw new BusinessException(选题人数已满); } // 3. 插入选题申请记录 SelectionRecord record new SelectionRecord(); record.setStudentId(studentId); record.setTopicId(topicId); record.setStatus(SelectionStatus.PENDING.getCode()); selectionMapper.insert(record); // 4. 如果插入申请记录时发现唯一索引冲突回滚整个事务 }在Mapper的XML里increaseSelectedCount对应的SQL就是前面说的原子更新。这里还要提醒一点事务方法内部不要捕获所有异常又吞掉插入记录如果触发了唯一索引冲突要让异常向上抛事务才能正确回滚否则会出现名额扣了、申请记录没插入的数据不一致。4. 前后端分离联调中的五个关键细节4.1 统一响应结构让前端少写一百个判断前后端分离项目最怕的就是各写各的返回格式。后端一会在成功时返回数据对象一会在失败时返回错误字符串前端就没法玩了。我建议全项目统一使用一个泛型响应类。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }同时在全局异常处理器里把所有业务异常统一转换成Result返回前端只看code字段做判断。这样做的收益是前端所有接口的响应结构完全一致Axios封装可以只处理一种形态的数据少写无数个分支判断。4.2 登录态与权限拦截用JWT还是Session这个系统有三种角色接口必须做权限控制。我建议使用JWT方式用户登录成功后后端返回一个带角色信息的Token前端把Token存储在本地每次请求在请求头里携带Authorization: Bearer xxx。后端写一个拦截器统一解析Token把用户ID和角色放进去请求上下文。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token将userId和role写入ThreadLocal LoginUser user JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(user); return true; } }在拦截器中校验完身份后Controller方法上使用自定义注解RequireRole(role RoleEnum.TEACHER)来标注哪些接口只允许教师访问再配合一个方法级拦截器做二次校验。这种设计比把权限判断散落在每个接口里干净得多也更容易在答辩时说明白。4.3 Axios封装与路由守卫前端的骨架前端这边我在api目录里封装了一个统一的request对象。核心是拦截器请求发出前从localStorage取Token加到请求头响应回来后如果code不是200统一弹出错误提示并跳转到登录页。import axios from axios import { Message } from element-ui const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use(response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) }) export default service如果前端项目是使用Vue CLI创建的开发环境下我一般通过vue.config.js里的devServer.proxy把/api代理到后端localhost:8080而不是在前端代码里写死后端地址。这样才能避免开发调试时频繁出现跨域问题。4.4 联调时容易翻车的三个小问题这三个问题别看小每个都能让人卡住半小时以上。第一个是跨域配置。即使开发环境做了代理后端也要配置Cors跨域支持因为一旦部署到Nginx或者直接访问后端IP时仍然会触发跨域。第二个是日期格式前后端不匹配。后端的LocalDateTime默认序列化格式是2025-06-01T10:30:00前端组件不一定认识所以我建议在application.yml里配置统一的日期格式。第三个是Long类型精度丢失。如果数据库主键是bigint且使用雪花算法前端JavaScript的Number类型会丢失精度最简单的处理是在JSON序列化时把Long类型转成String。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8长期实战下来这三个问题基本是每一届课程设计都会遇到的通病。提前写好配置参数联调阶段会顺畅得多。5. 本地到服务器部署全过程的复盘5.1 本地启动完整流程拿到完整源码之后第一步不是急着跑代码而是把数据库初始化好。我在项目的database/init.sql文件里已经包含了建库、建表和基础测试数据直接用Navicat或者命令行执行一遍即可。后端启动前在application.yml里确认数据源配置正确。然后执行下面的命令启动后端服务cd backend mvn clean package -DskipTests java -jar target/course-selection-0.0.1-SNAPSHOT.jar如果本地已有SpringBoot运行环境也可以直接用IDE启动。但用java -jar方式启动的好处是能更早暴露配置问题比如数据库账号密码错误、端口被占用等。前端启动则简单很多cd frontend npm install npm run serve这里有一个我在部署中最常踩的坑npm install在国内网络环境下容易卡在下载环节建议优先配置npm镜像源再执行安装。另外前端和后端要同时启动浏览器访问http://localhost:8080看到登录页就是联调成功的第一步。5.2 服务器部署方案后端Jar包加Nginx静态资源服务器部署我推荐一个很实用的组合前端打包成静态文件交给Nginx托管后端打成一个可运行的Jar包交给systemd管理。不建议在服务器上再跑一个Node服务那样浪费资源又没有必要。前端打包命令cd frontend npm run build打包后会在frontend/dist目录生成静态文件。把这些文件上传到服务器的/var/www/course-selection目录。Nginx配置如下server { listen 80; server_name your.server.domain; root /var/www/course-selection; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置的关键点有两个一是try_files把所有前端路由都转发到index.html避免刷新页面时出现404二是/api/开头的请求反向代理到后端的8080端口并且后端接口地址保持与本地一致不用在前端代码里改任何URL。如果你的系统里有文件上传功能Nginx端还需要调整client_max_body_size否则上传稍大的附件就会报413错误。后端的systemd服务配置也很简单。在/etc/systemd/system/course-selection.service写入配置[Unit] DescriptionCourse Selection Backend Afternetwork.target [Service] Userwww WorkingDirectory/opt/course-selection ExecStart/usr/bin/java -jar /opt/course-selection/course-selection.jar Restartalways RestartSec10 [Install] WantedBymulti-user.target配置好后执行systemctl daemon-reload systemctl enable course-selection systemctl start course-selection后端服务就托管给systemd了。它的好处是进程意外退出后会自动重启而且可以用journalctl -u course-selection -f查看实时日志排查异常非常方便。5.3 部署后的检查清单每次部署完成后我都建议按下面的清单检查一遍而不是急着关电脑。首先确认后端Jar包正常启动日志中没有红色ERROR其次确认MySQL时区设置正确特别是如果服务器时间显示正常但写入数据库的时间差了8小时多半是JDBC连接串里缺少serverTimezoneAsia/Shanghai再次确认Nginx配置语法正确并重新加载最后在浏览器里完整走一遍登录、选题、审核、导出的核心流程看看前端有没有404和由于跨域引起的控制台报错。我还在部署文档里特意加了一条不要把数据库密码写在application.yml后直接传到服务器。建议用环境变量或配置中心的方式管理敏感信息哪怕只是课程设计项目这个习惯也能体现工程素养。6. 课程设计演示的加分项与常见扣分点6.1 演示前要准备的测试数据很多人在答辩前一天还在手工录入数据其实正确的做法是在初始化SQL里直接准备一套完整的演示数据。比如准备三个账号管理员、教师、学生五个题目其中有两个名额已满、两个可选、一个未开放再准备几条不同状态的选题申请记录。这样一登录进去页面上什么状态都有演示的时候点几下就能展示全部核心功能不用现场造数据。我通常会额外准备一个“边界情况”的演示脚本比如让某个学生申请一个热门题目使名额恰好用完再到另一个账号尝试申请同一个题目前端弹出“已满员”提示。这个场景直观展示了系统里最复杂的并发控制逻辑演示效果远好于普通的增删改查流程。6.2 答辩时一定会被问到的“为什么”我有一次跟某同学模拟答辩发现评委的问题高度集中在几个点上为什么用MyBatis而不用MyBatis-Plus为什么前端要搞一套登录拦截并发抢题时怎么保证不出错这些问题如果没有准备现场很容易答不上来。我建议的应对方式是把答案组织成“场景方案效果”的结构。比如提到手写SQL就说“因为选题申请涉及余额扣减需要UPDATE语句里嵌套条件判断MyBatis的XML写起来最直观”提到并发就说“使用数据库行锁和原子更新的方式既保证了一致性也避免了复杂分布式锁”。把设计理由讲成一个个小场景远比念概念更能赢得认同。6.3 容易被忽略的细节软删除、日志与空状态还有三个细节很容易被忽略。第一是删除操作尽量用逻辑删除不要物理删除比如管理员错误创建了一个题目应让状态变为“下架”而不是从表里抹掉记录这样才能保留操作痕迹。第二是业务操作的关键节点要输出日志我用的是一个简单的Slf4j注解配合logback配置记录谁在什么时间做了选题或审核操作。第三是前端空状态设计列表页为空时不能只显示一片空白应该给一张“暂无数据”的插图和返回按钮这个小细节在演示时非常加分。说实话课程设计系统本身并不难写真正的难点在于把业务逻辑讲清楚、把数据结构规划合理、把边界情况处理好。这套ABO选题管理系统代码量不大但覆盖了SpringBoot、Vue、MyBatis、MySQL全家桶的完整使用链路也踩过了前后端分离项目里最常见的坑。我的经验是一旦你亲手把状态流转和并发控制跑通再去看其他管理系统类项目都会觉得轻松很多。

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

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

免费获取报价 →
↑