基于Spring Boot+Vue的课程设计选题管理系统开发实践

基于Spring Boot+Vue的课程设计选题管理系统开发实践 1. 项目概述课程设计选题管理到底在管什么每年到课程设计季高校老师的工作台基本会被纸质选题表淹没。学生围着办公室问还有哪个题能选班长在群里发Excel表格让大家接龙助教收上来一堆格式五花八门的申报材料最后统计名单还要手动去重。这种场景我相信很多做过课设的同学和年轻老师都深有体会。这个基于Web的课程设计选题管理系统本质上就是把教师发布选题—学生在线申报—教师确认名单—管理员兜底管理这套流程搬到线上用Web页面替代纸质表格和口头沟通。项目包含完整文档和源码适合三类人参考一是正在准备课程设计或毕业设计的在校生可以用它作为自己的课设项目二是想快速搭一套轻量级管理后台的开发者三是确实有选题管理需求、想省掉重复劳动的老师或教学管理人员。从技术角度看这个系统并不复杂它具备典型Web应用的基础构成前端页面负责交互展示后端接口处理业务逻辑数据库存选题、学生、申报记录这些核心数据。难点在于业务流程的梳理和状态控制——什么时候允许学生选、什么时候锁定名额、学生退选之后名额怎么释放、老师打回申请后学生还能不能重选这些规则想清楚代码反而是水到渠成的事。先说清楚这个系统的功能边界。它不是一个大而全的教学管理平台不需要管成绩、考勤、实验报告它只聚焦一件事选题申报。做好这件事系统就已经完成了80%的价值。我给这个项目划分了三个角色每个角色看到的页面和能做的操作完全不同管理员维护教师账号、学生账号查看整体选题进度处理异常数据比如手动调换学生选题。教师发布课程设计题目设置人数上限和截止时间查看申报学生列表通过或驳回申请。学生浏览全部可选题目在线提交选题申请查看审核结果在未被审核前可以撤回申请。整个选题流程可以描述为一条清晰的状态链路选题从草稿变为申报中学生提交申报后记录变为待审核老师通过后变为已确认此时名额锁定直到课程结束归档。这套状态机的设计是后面所有功能实现的基础我强烈建议任何要做这类系统的同学先从这张状态流转图开始设计而不是急着写代码。2. 技术选型与整体架构设计2.1 前端技术线分离还是模板渲染课程设计类项目最忌讳为了技术而技术。我见过很多人一上来就拆微服务前端上Vue全家桶后端配Redis加消息队列最后写不完或者根本跑不起来。选技术栈的核心逻辑是团队几个人、熟悉什么、学校演示环境允许多大复杂度。如果项目成员前端基础一般建议直接用Spring Boot Thymeleaf模板引擎后端渲染页面一个应用搞定前后端部署时只需打一个jar包。这样做的好处是少一套前端工程、少跨域配置、少联调成本适合单人开发或两人小组。如果团队里有人比较熟悉Vue或React那可以做成前后端分离。前端Vue3 Element Plus负责页面后端Spring Boot提供Restful接口数据库用MySQL。这套组合是当前主流招聘市场上搜Web项目出来最多的也是这个方向做出来的项目演示效果更好看。我这个项目选择的是前后端分离方案前端Vue3后端Spring Boot 2.7数据库MySQL 8.0。这里的核心考量是可演示性——课设答辩时一个界面精美的单页应用比传统刷新式页面更容易拿到高分。而且Vue的响应式数据绑定在处理选题列表的实时状态变化时体验确实更好比如某个选题人数已满前端列表上的按钮立刻变成灰色这种交互用模板渲染也能实现但要配合局部刷新或写很多JavaScript麻烦不少。2.2 后端与数据库设计三张核心表搞定业务后端架构上我没有做复杂的微服务拆分就是一个单体应用按Controller、Service、Mapper三层划分。Controller只做参数接收和结果返回Service层写业务规则Mapper层用MyBatis-Plus操作数据库。这个分层方式几乎适合所有中小型管理系统条理清楚出了问题也好排查。数据库设计是这个项目真正的核心。我总共设计了五张表其中三张是绝对的主角用户表user、选题表project、申报记录表selection_record另外两张分别是课程表course和通知表notice。用户表存三类账号用role字段区分1表示管理员2表示教师3表示学生。教师和学生虽然都是用户但基本信息差异较大如果字段合并会让表变得很宽比如学生有学号、班级教师有工号、职称。实际做的时候可以把公共字段用户名、密码、姓名放user表学号班级工号这些用profile表扩展关联。这个设计能让表结构更清爽尤其以后要加学院、专业这层维度时不用大改。选题表的核心字段我列出几个字段名类型说明idbigint主键titlevarchar(100)选题标题teacher_idbigint发布教师course_idbigint所属课程descriptiontext选题内容描述max_studentsint人数上限selected_countint当前已选人数statustinyint0草稿 1申报中 2已截止deadlinedatetime申报截止时间申报记录表是业务发生最频繁的表每次学生选课、退选、教师审核都会在这张表上产生操作。字段包括id、project_id、student_id、status0待审核、1已通过、2已驳回、remark教师备注比如驳回原因、create_time。这里专门提醒一点很多初学者喜欢把已选人数实时count出来而不是存一个selected_count字段。理论上count更准确但每次查询都要聚合数据量大时会拖慢列表页。我的方案是维护一个冗余字段selected_count每次申报成功或退选成功时同步加减。要注意在事务里操作防止计数和明细不一致。2.3 权限控制拦截器加自定义注解就够了课程设计系统的角色只有三种权限控制完全不需要引入Spring Security这种重型框架一个HandlerInterceptor加两个注解就能搞定。我在代码里定义了一个RequireRole注解标注在Controller方法上比如教师发布选题的接口标注RequireRole(teacher)学生申报选题的接口标注RequireRole(student)。拦截器拦截所有接口请求先从Session或Token里解析出当前登录用户再检查用户角色是否匹配注解标注的角色不匹配直接返回401。这样做有个好处后端接口做了真正的权限校验而不是只靠前端隐藏按钮。很多课设项目只在页面上判断如果是老师就显示审核按钮接口调一下照样能操作这就是明显的安全漏洞。我见过有同学在演示时被老师当场试出来场面非常尴尬。前后端都需要校验前端为了体验后端才是安全的底线。3. 核心模块流程与实现要点3.1 教师端选题发布与审核闭环教师端是整个系统内容的源头核心操作有两个发布选题和审核学生。发布选题的页面有表单校验标题必填、人数上限要设正整数、截止时间不能早于当前时间。后端保存时会把status置为0草稿只有点发布按钮之后状态才会变成1申报中学生才看得到。这个设计是想给教师一个缓冲期防止字段还没填完学生就开始抢。审核学生的流程要注意一个细节——名额和状态的联动。教师点通过时系统先检查选题当前selected_count是否小于max_students小于才能通过同时给申报记录状态置为1点驳回时必须填写原因学生端看到驳回结果才能知道问题出在哪里。这个通过时校验人数的逻辑非常关键因为学生提交申请和老师审核有一定时间差审核时名额可能早就已经被其他人占满。3.2 学生端申报、撤回与状态感知学生端的体验设计直接决定了整个系统的口碑。选题列表页我做了四个维度区分让学生一眼就明白哪些题能选、哪些不能选状态为申报中且未满员且未截止显示可选的彩色按钮。已满员按钮灰色标注已满点击时提示该选题人数已达上限。已截止整行变灰标注已截止。学生自己已经申报但还在待审核显示已提交等待审核同时提供撤回按钮。撤回逻辑需要特别处理权限问题。只有记录状态还是待审核时才能撤回一旦教师已经点了通过或驳回撤回按钮就必须消失。这个判断我同时在前端按状态渲染也在后端接口里做了二次校验防止有人伪造请求直接调撤回接口。另外还有一个场景容易被忽略退选。当学生的申报已通过、名额已锁定之后如果因为特殊原因想退出怎么办我预留了一个退选功能但加了条件限制——必须联系教师由教师在后台操作释放名额学生不能自行退选。原因是锁定名额后再自行退出会影响其他同学的公平也给教学管理造成混乱。这个功能上线后实际使用频率不高但存在就是兜底。3.3 管理员端进度总览与异常处理管理员端是整个系统的上帝视角页面我用了一个简单的数据看板来呈现总选题数、申报中数量、已满员数量、已提交申报学生数、尚未申报学生数每个数字都是一条统计SQL的事。看板下方是未申报学生列表管理员可以照着名单去催学生。异常处理功能是管理员端隐藏的价值点。比如学生A误选了题目X实际想去题目Y但X已经被教师确认走正常流程无法修改。管理员在后台可以直接调整申报记录把A在X下的记录改为已驳回释放名额再帮A新增一条Y题的申报记录Y题的selected_count同步加一。整个过程就是两次update加一次insert但权限上只有管理员能操作教师端都不开放这个入口。3.4 通知机制让每个状态变更都有迹可循课程设计选题最怕的就是学生不知道老师审核了没有或者老师不知道学生为啥不选。我在系统里加了一个轻量的站内通知模块教师发布选题时系统自动向全班学生推送消息新选题《XXX》已发布可前往申报教师通过或驳回申报时通知对应学生学生撤回申请时通知对应教师。这个功能技术上就是往notice表插一条数据学生登录后在页面右上角看到一个红点数字点击进去是通知列表。做的时候不用搞WebSocket实时推送登录后拉一次接口页面轮询两分钟一次也完全够用。真做WebSocket反而要处理连接管理、断线重连对课设项目来说性价比很低。4. 实操过程从建表到部署的全流程记录4.1 初始化数据库与核心建表SQL实际动手第一步是建库建表。我直接贴出核心的建表语句大家可以对照着调整字段。这里用MySQL语法但整套结构换到PostgreSQL或SQLite成本也不高。CREATE DATABASE IF NOT EXISTS course_design DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_design; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, name VARCHAR(50) NOT NULL, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, teacher_id BIGINT NOT NULL, course_id BIGINT, description TEXT, max_students INT NOT NULL DEFAULT 3, selected_count INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1申报中 2已截止, deadline DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE selection_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_id BIGINT NOT NULL, student_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已驳回, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_project_student (project_id, student_id) );这里要特别解释selection_record表上那个唯一键uk_project_student。它的作用是保证同一个学生对同一个选题只能产生一条申报记录从数据库层面杜绝重复申报。光靠后端代码判断是不够的万一两个请求同时打到接口并发情况下代码里的判断可能失效但数据库唯一键一定挡得住。4.2 后端选课接口并发控制与事务处理选课是最容易出现并发问题的接口。课程设计虽然不像电商秒杀那样规模大但热门选题在上线那一刻被全班同学同时刷新点申报按钮的时间差可能就在几百毫秒内。如果实现不严谨很容易出现名额超卖——明明只剩1个名额却有3个学生都提示申报成功。我处理这个问题的方式是两板斧事务加乐观锁式的条件更新。先看核心代码Transactional public synchronized boolean selectProject(Long studentId, Long projectId) { // 1. 查询选题当前状态 Project project projectMapper.selectById(projectId); if (project null) { throw new BusinessException(选题不存在); } if (project.getStatus() ! 1) { throw new BusinessException(该选题不在申报期); } if (project.getDeadline() ! null project.getDeadline().before(new Date())) { throw new BusinessException(该选题已截止); } // 2. 校验当前用户是否已申报过 Integer count selectionRecordMapper.countByStudentAndProject(studentId, projectId); if (count 0) { throw new BusinessException(你已申报过该选题请勿重复提交); } // 3. 条件更新用受影响行数判断名额是否抢占成功 int updated projectMapper.updateSelectedCountIfNotFull(projectId); if (updated 0) { throw new BusinessException(该选题名额已满); } // 4. 插入申报记录 SelectionRecord record new SelectionRecord(); record.setProjectId(projectId); record.setStudentId(studentId); record.setStatus(0); selectionRecordMapper.insert(record); return true; }关键在第三步的updateSelectedCountIfNotFull对应的SQL是UPDATE project SET selected_count selected_count 1 WHERE id #{projectId} AND selected_count max_students AND status 1这条SQL自带了一个原子性保障MySQL的update操作默认行锁两个并发请求同时执行时第二个请求必须等第一个commit之后才能执行而那时selected_count已经加了1如果选满它的where条件selected_count max_students不成立受影响行数就是0代码就能立刻判定名额已满。这就避免了先查后用再更新的传统写法在高并发下超卖的问题。4.3 前端核心页面选题列表与状态渲染前端我用的是Vue3加Element Plus。选题列表页的结构大体是页面加载时调用getProjectList接口拿回选题数组通过计算属性对每个item的状态做分类再动态决定操作按钮的渲染方式。核心逻辑都在状态判断里。我在后端返回的数据结构中统一加了几个冗余字段isFull表示是否满员isSelected表示当前学生是否已申报该题canSelect表示是否允许申报。这些字段由后端在接口层计算好返回前端只做展示不自己推算。这样设计的原因是能否申报的规则属于业务逻辑比如管理员可能临时关闭某个题目的申报这种状态变化在前端没法准确感知必须由后端统一判定。页面模板中按钮的渲染类似这样el-button v-ifrow.canSelect typeprimary sizesmall clickhandleSelect(row) 立即申报/el-button el-button v-else-ifrow.isSelected sizesmall disabled 已申报/el-button el-button v-else-ifrow.isFull sizesmall disabled 已满员/el-button注意这里只是前端展示层面的控制真正的权限校验在后端。我见过很多项目前端一套判断写得天花乱坠后端的申报接口却谁都能调这是本末倒置。记住一个原则前端判断是为了用户体验后端判断才是安全边界。4.4 部署发布最简单的jar加nginx方案项目做完之后要能演示、能交给老师检查。我建议的部署方式非常简单后端打成jar包运行在服务器任意空闲端口前端构建后生成dist目录由Nginx托管静态文件同时把/api路径反向代理到后端端口。后端启动命令java -jar course-design-server.jar --server.port8080Nginx配置的核心部分server { listen 80; server_name your-domain.com; root /var/www/course-design/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }前端使用Vue Router的history模式时location /里的try_files配置是必需的否则刷新非根路径页面会报404。这个坑我踩过当时演示时点浏览器刷新直接白屏排查了半小时才发现是Nginx没有配置fallback到index.html。如果要更省事后端也可以直接把前端build后的静态文件放进src/main/resources/static目录打成一个jar包一条命令启动所有服务。这种方式写文档时一步启动的描述会很加分但实际工程中前后端分开部署更灵活。5. 常见问题与排查技巧实录5.1 并发操作导致名额超卖这是选课系统最经典的问题。前面说的条件更新方案能解决大部分超卖场景但如果项目里用了事务要特别注意MySQL默认的隔离级别是REPEATABLE READ。在这种隔离级别下事务内先select再update两个并发事务可能同时读到selected_count2上限3然后都执行update。不过因为我用的update语句自带where条件selected_count max_students数据库行锁依然能保证只有一个update成功所以即使前面的select读到了旧值最终写库时依然安全。如果你用的是其他方案比如先select判断再update那绝对要加锁或者用悲观锁select ... for update。我一开始就踩过这个坑演示时两个同学同时抢最后一个名额两个人都提示成功查数据库才发现selected_count变成了4超过max_students的3。这就是没做并发控制的典型症状。5.2 学生退选后名额不释放的疑问有人问学生撤回申报之后project表里的selected_count要不要减回去要减但不是所有撤回都减。只看一条规则申请状态从待审核变成已撤回时count不变因为之前count1只是为了锁定报名资格不代表真正占用名额只有当状态从已通过变成已退选时count才需要减一因为这时是真的占用了名额。我在实现时把撤回申请和退选拆分成了两个不同接口就是为了避开这种逻辑混乱。如果混在一个接口里每次都要传一个type字段再分支处理过三个月自己看代码都容易犯迷糊。5.3 中文乱码问题中文乱码在这个项目里出现过两次一次是MySQL连接串没配置编码参数解决方法是jdbc连接URL加上characterEncodingutf8另一次是服务端向前端返回JSON时中文变成了问号解决方法是Spring Boot的server.servlet.encoding配置为UTF-8。现在Spring Boot 2.x默认请求和响应编码已经是UTF-8但老项目迁移时经常漏掉。这个坑浪费时间且无技术含量我建议在写第一篇文档时就把编码规范写进去趁早统一。5.4 修改密码与数据初始化系统上线初期最大的麻烦是测试数据怎么造。我的做法是写一个CommandLineRunner项目启动时自动检查数据库是否为空为空就插入预设的管理员账号、几个教师账号、十几个学生账号和一批选题数据。这样每次把数据库drop掉重新启动系统也能恢复到一个可演示的状态省去了手工录数据的痛苦。初始化数据的代码位置很关键要在Spring Boot启动流程的最后阶段执行避免依赖的Bean还未创建。CommandLineRunner接口的run方法在容器启动完成后回调正好满足需求。5.5 权限绕过风险排查课程设计项目容易在权限上翻车。我排查了一遍发现典型的漏洞有直接访问未登录接口没有拦截、学生调用教师接口没有校验角色、修改申报记录时没有校验记录归属人。前两个用前面说的拦截器加注解解决第三个需要在Service层加一条判断只有当申报记录里的student_id等于当前登录用户的id时才允许执行撤回操作。有些代码在Controller层只校验了是否登录却没校验是不是本人这是在写文档时都要写清楚的安全底线。6. 文档配套与再扩展建议这套系统虽然定位是课设项目但麻雀虽小五脏俱全。我随源码一起整理的文档包含需求分析、数据库设计说明、接口文档、部署手册四部分总共写了四十多页。写文档的经验是需求分析画用例图数据库设计画ER图接口文档用表格列出参数和返回字段部署手册给出每一步的命令和截图位置标注。文档不是给老师凑字数的而是自己三个月后回来看项目时最快的索引。如果时间充裕系统还可以在三个方向扩展一是给选题增加志愿维度学生可以对多个选题排序系统根据优先级和名额自动分配二是增加Excel导入导出老师可以批量导入学生名单、导出最终选题汇总表三是接入消息推送选题审核通过后通过邮件或微信模板消息通知学生。这些扩展点都在现有表结构上做增量修改不会推倒重来。最后再分享一个小技巧给整个项目写一个README.md放在代码仓库根目录第一屏用最短的篇幅展示项目截图、技术栈、启动步骤、默认账号。答辩时老师第一眼就会看这个文件你把自己当成从未见过这个项目的新人看看这份说明能不能让你在十分钟内跑起来。能就说明文档合格了。