1. 项目整体设计思路与技术选型1.1 毕设级项目为什么要选前后端分离先说结论如果你准备拿一个“SpringBoot Vue 大学生选修选课系统”当毕业设计那大概率不是自己临时拍脑袋选的而是反复比较后觉得这个组合“足够主流、足够有区分度、又刚好能展示完整开发链路”。我拆这个项目时第一反应是看它是不是典型的“前后端分离 管理系统 业务服务端”三层结构。确实如此。前端用 Vue 2.x 配 Element UI后端用 SpringBoot 2.x 配 MyBatis Plus数据库用 MySQL 5.7鉴权用 JWT。这套组合在国内教学项目里几乎快成了默认模板但它能成为模板是有原因的每一层你都能讲出“为什么选它”而不是背一堆名词。SpringBoot 负责把繁琐的 Spring 配置自动搞定内置 Tomcat打一个 jar 就能跑对毕设演示来说非常省心。Vue Element UI 做管理后台和用户页面都很快组件成熟表格、表单、弹窗开箱即用短时间能出界面。MySQL 加 MyBatis Plus 写业务逻辑时CRUD 的体力活被大大简化把精力留给真正的核心业务——选课规则。这套组合解决的核心问题是如何让一个学生、教师、管理员三种角色共存的业务系统既做到界面清晰又保证后端接口安全可控。它适合两类人一类是准备做毕设、需要快速理解完整项目并二次开发的学生另一类是刚工作不久、想复刻一个小型前后端分离项目练手的开发者。1.2 功能模块拆解三种角色各司其职这个系统不是简简单单的“学生登录后点选课按钮”它必须回答三个角色各自关心的问题角色核心诉求对应功能学生我能选什么课、选了没、退课方便吗选课大厅、我的课表、退课、成绩查询教师我开了什么课、谁选了我的课、课容量够不够开课申请、我的课程、选课学生名单、成绩录入管理员谁在管这些事、基础数据从哪来学生管理、教师管理、课程管理、院系列表、选课开关控制很多初学者搞混“教师开课”和“管理员管理课程”的边界。现实中管理员负责维护院系、教师账号和基础课程信息教师是在自己名下创建具体的教学班指定时间、地点、容量、学分。项目里把这两个动作拆开数据上就清晰得多课程表是“元数据”开课记录表才是“可以被人选择的排课结果”。整个项目的核心业务逻辑集中在两块一是“选课冲突检测”二是“选课开关控制”。这两块写得好不好直接决定答辩时评委愿不愿意深聊。后面我会专门剖析。2. 数据库设计一张选课表如何撑起整个业务2.1 核心表结构设计与反直觉的地方拿到 SQL 脚本后我先看了表结构。一共有九张表左右sys_user、sys_role、student、teacher、course、course_selection、course_schedule、college、major。这种设计很常见但有两个地方值得细讲。第一用户表和角色表分离没有把“学生”“教师”直接做成两张带密码字段的表。所有登录入口统一走 sys_user用 role 字段区分身份。student 表和 teacher 表只存业务属性学号、工号、院系、职称。好处是登录逻辑只需要写一套坏处是你必须保证用户表和业务表的数据一致否则可能出现“用户存在但学生档案查不到”的脏数据。项目里用了手动维护关联字段的方式虽然简单但在演示时千万别乱删数据。第二course_selection 选课记录表是核心中的核心。它的唯一约束不是 id而是联合唯一索引 (student_id, course_id)。这个设计直接防住了同一个学生重复选同一门课。但真正重要的不是这行索引而是“选课状态”字段。我看到很多类似项目只存“已选”和“已退”这个项目把状态扩展为“已选、已退、已修”这样学生退课后历史记录还在不会因为退课让记录消失后续做成绩录入时也不用手动补记录。表结构设计中最容易被人忽略的是 n:m 关系如何拆。学生和课程是多对多中间表自然要有选课时间字段create_time但很多同学忽略“选课学期”字段。如果系统只有一学期那没问题但如果要演示多学期这个字段就是刚需。我看到的这个版本里选课表加了 semester 字段课程表也加了 semester 字段两条数据通过学期字段对齐算是比较负责的设计。2.2 为什么用逻辑删除而不是物理删除项目中所有核心表都带有 deleted 字段。学生删除、教师删除、课程删除都是逻辑删除MyBatis Plus 的 TableLogic 注解配合全局配置就能实现。第一次接触时可能会问反正删掉了留着干嘛这个问题在答辩时会很致命。你要能回答选课记录涉及历史成绩追溯学生虽然退学了但成绩单还要保留课程虽然停开但已选学生的名单需要留痕。用物理删除会把这种关联信息一起抹掉。项目里逻辑删除状态字段只是冰山一角真正的关键是在联表查询时MyBatis Plus 会自动过滤 deleted0但如果手写 SQL一定要记得加条件。日常开发里很多人抱怨逻辑删除导致“唯一索引失效”因为无论删除多少条id 还在累加。这个项目也踩了这个坑学生表设置逻辑删除后如果重新添加一个同名的学生理论上可以添加成功因为逻辑上删除了但如果你给学号做了唯一索引那就会冲突。解决方案有两种要么把学号也做成逻辑唯一查时过滤 deleted要么物理删除学生记录。项目采用的是后者窥探一下需求管理员删除学生时会弹确认框“此操作将不可恢复”实际走的是物理删除。这里不算工程最优解但符合毕设场景。3. 后端核心功能拆解选课那点事怎么才算优雅3.1 权限控制与登录鉴权的实现路径这个项目没有引入 Spring Security 这种重型框架而是用了拦截器 JWT 方案。我之前常反复强调毕设项目不一定非要往复杂了堆重点是你能说清楚每行代码是干什么的。JWT 方案的优势在于无状态前端拿到 token 后每次请求放在请求头里后端拦截器校验 token 是否合法再根据 token 里的角色字段决定接口是否放行。典型的实现路径是登录接口接收用户名密码校验通过后生成 JWT把 userId、role 放进去签名密钥写在 application.yml 里。前端拿到 token 存到 localStorage每次 axios 请求前在拦截器中携带 token。后端自定义一个拦截器继承 HandlerInterceptorAdapter在 preHandle 里解析 token如果解析失败直接返回 401。同时再定义角色拦截比如 “/api/admin/**” 必须要有管理员角色才能访问。很多开发者在调这种项目时会遇到一个问题登录成功了但请求别的接口还是 401。多半是前端没有正确携带 token或者 token 过期时间设置太短。项目里的 token 过期时间默认是 24 小时对于毕设演示足够了千万不要为了“省事”把过期时间设成一年答辩时有可能被问到“怎么处理 token 续签”。还有个小细节密码存储用的是 MD5 加盐。自己玩的项目可以接受但如果你在简历里写“安全经验丰富”这里最好改成 BCrypt。我在使用过程中习惯把这段代码换成 BCryptPasswordEncoder改造成本不大却能让技术亮点上一个台阶。如果时间紧张保留 MD5 也能跑通但要在接口文档里说明这是为了演示简化。3.2 选课与退课的事务边界选课接口不是简单的 insert 一条记录它至少要经过五步校验当前是否在选课开放时间内由管理员控制开关学生是否存在且状态正常课程是否存在、学期是否匹配课程是否已经选满当前选课人数 容量该学生是否已经选过这门课查中间表。这五步只要任何一步不通过接口就返回对应的错误提示。问题在于这五步是在“插入之前”执行的但并发情况下“检查人数未满”和“插入记录”之间可能出现时间差。两个学生同时选倒数第二个名额数据库可能插入两条记录导致超选。解决超选的经典办法有三种这个项目用的是第一种在代码层面使用 synchronized 或分布式锁单机可用在数据库层面使用 select ... for update 锁行利用数据库的唯一索引插入后根据异常判断冲突。单机项目用 synchronized 锁 service 方法是一个简洁方案但要注意锁的粒度。如果锁住整个选课方法一个学生一次只能选一门课如果锁住某个课程的剩余名额需要把课程 id 作为锁维度。建议的做法是定义一个 ConcurrentHashMap 来维护 courseId 对应的锁对象这样不同课程之间互不影响。这段代码写起来不难却是整个后端里最值得在答辩时展开的东西。退课的逻辑相对简单但有一个细节要注意退课接口不仅要删除或变更选课记录状态还要把课程表的 selected_count 减一。如果你只改了选课表课程列表页的人数统计就会对不上。项目里手动同步这两个数据没走数据库触发器所以在写单元测试时要注意验证。3.3 API 接口设计规范路径、参数与返回值接口文档是这批资料的重要组成部分。查看后发现接口路径很规整比如POST /api/auth/login 登录GET /api/student/courses 学生可选课程列表POST /api/student/select 选课POST /api/student/cancel 退课GET /api/teacher/students/{courseId} 查看选课学生名单POST /api/teacher/grade 录入成绩GET /api/admin/courses 课程管理列表POST /api/admin/toggle-select 开启关闭选课统一返回体使用 { code, message, data } 字段code 为 200 表示成功401 未登录403 无权限500 服务器异常。大家在做二次开发时可以沿用这个结构不要自己再换一套否则前端 axios 响应拦截器里的逻辑全部要改。这里要特别提醒接口文档中写的字段名是驼峰但数据库字段是下划线。MyBatis Plus 默认开启 map-underscore-to-camel-case所以大多数情况下没问题但手写多表联查 SQL 时要么给查询字段起别名要么就老老实实用下划线去接收。项目里部分手写 SQL 没有加别名如果你直接复制运行返回的 data 里可能看到 course_id 而不是 courseId前端就显示不出课程编号。这个坑几乎每一位实际操作者都会踩一遍。4. 前端 Vue 页面与交互设计体验4.1 路由权限拦截与动态菜单前端的目录结构是典型的 Vue Cli 项目src/router 下配置路由src/store 里管理用户状态src/api 下按模块拆分接口请求。页面包含登录页、学生端、教师端、管理端三大布局。学生在登录后根据 role 字段动态生成能看到的菜单而不是一次性把所有页面都注入。路由守卫用 beforeEach判断逻辑是没有 token 就跳转登录页有 token 但页面需要更高角色就跳转 403 页。菜单没有用后端动态渲染是前端根据角色写死的。这种方式对毕设足够但想在简历里吹“权限动态控制”时建议说明清楚——动态菜单和动态权限不是一回事。前端组件里比较出彩的部分是选课卡片的展示。课程列表不是简单表格而是以卡片形式展示课程名、教师、学分、容量、剩余名额、上课时间选课按钮在卡片右下角。这样视觉上比表格更容易吸引评委目光实际交互也简单。点击“选课”按钮后会弹出确认框确认后调用接口成功则卡片状态变为“已选”同时剩余名额减一。这个交互背后是响应式数据更新并不复杂但体验感很好。4.2 跨域与代理配置的实操细节前端项目默认在 localhost:8080 端口运行后端是 localhost:8081请求不同源必然有跨域问题。项目在前端 vue.config.js 里配置了开发代理devServer 的 proxy 把 /api 开头的请求转给 http://localhost:8081。这种方式的好处是浏览器只看到同源请求不会出现 CORS 报错。我实际运行时发现有的同学复制项目后不修改代理配置直接 npm run serve然后打开页面登录发现接口全部 404。原因是代理配置中的 target 地址写死了 8080而自己后端改了端口。解决办法就是把 vue.config.js 里的 target 改成实际后端地址。这一处是常见问题排查看板上的第一项。生产环境部署建议不要用前端占 8080 端口、后端占 8081 这种方式长期跑。可以前端 npm run build 打包后把 dist 目录放到后端项目的 src/main/resources/static 下SpringBoot 自己就能托管静态资源。这样只需要一个 8081 端口部署省事也避免跨域。4.3 Element UI 表单校验与状态反馈前后端分离项目最容易出现的问题是“前端校验过了后端照样报错”。比如学生选课时前端只判断了“是否已登录”却没有下拉框校验“选择身份”。管理员添加学生表单里姓名字段不能为空、手机号格式要合法这部分使用 Element UI 的 rules 校验。但后端在 Controller 层也写了部分参数校验例如必填参数通过 RequestParam 接收如果没传会直接抛异常。我在实操中一个小发现前端下拉框的选项值如果和后端字典值不一致会引发“看似正常但按条件查不到数据”。例如角色字段前端用 “0、1、2” 表示管理员、教师、学生后端用 “ADMIN, TEACHER, STUDENT”一旦没对上登录后的菜单就全部乱掉。这个项目里统一用的是数字枚举并且放在了接口文档的枚举说明里但很多初学者不会先看文档再测试导致浪费大量时间。建议拿到项目后先看 src/utils/auth.js 里 token 的 key再看 store 里 role 是怎么存的最后再看登录接口返回值三个地方对齐了再往后测。5. 部署运行全流程记录5.1 SQL 脚本导入与初始化数据项目自带的 sql 目录通常含一个 database.sql 文件里面建了库、建了表、插入了基础数据。第一次导入时不要用图形化工具一键执行全部建议使用命令行先创建数据库再指定字符集。顺序是启动 MySQL 服务创建数据库course_system字符集 utf8mb4执行 sql 脚本。如果是 Windows 下用 cmd 执行先 cd 到 mysql 的 bin 目录然后执行 mysql -uroot -p path/to/database.sql。macOS/Linux 下命令一致。如果脚本包含“DROP TABLE IF EXISTS”执行后原有数据会被清空所以想保留旧数据先备份。导入成功后重点检查三张表是否有关联数据admin 账号是否默认存在学生表是否有测试学号课程表是否有教师对应。项目默认账号通常是 admin / 123456学生和教师账号各自对应测试账号。如果登录失败先排查是不是脚本里的加密方式和你运行时的 MD5 算法不一致。5.2 后端启动参数与必要配置修改后端项目用 IDEA 打开后需要先等 Maven 依赖下载完成。默认需要的依赖较多第一次加载可能卡住。建议检查 settings.xml 里的镜像源国内网络环境下载失败时配置阿里云镜像。application.yml 里需要修改的核心配置只有四处数据源地址、数据库账号密码、Redis 地址如果有、JWT 密钥。我这个版本没用到 Redis所以少了缓存这一层。如果你看到某些版本自带 Redis 配置但你没装 Redis启动会失败要么安装 Redis要么直接注释掉相关依赖和代码。我在运行项目中发现一个小问题Tomcat 端口默认 8081如果同时跑了其他服务会端口冲突。解决方式很简单改 server.port 即可但别忘了同步修改前端 vue.config.js 里的代理目标端口。后端启动成功的标志不是看到 Spring 图标而是看到控制台出现“启动成功”或者 Tomcat started on port(s) 8081。如果日志里出现了数据库连接失败先看密码有没有空格再看数据库服务是否启动。5.3 前端启动与联调测试前端项目 node_modules 通常不会随源码提供需要执行 npm install 重新安装依赖。这里有个实操经验如果有 package-lock.json最好用 npm ci 安装它比 npm install 更能保证版本一致。启动命令是 npm run serve默认自动打开浏览器。这里再提一个常见坑如果 npm install 版本过高或过低编译时可能出现各种奇怪报错。项目要求的是 Vue 2 配 Node 14 或 16本地 Node 版本过高比如 20可能导致 node-sass 安装失败或者 sass-loader 报错。最稳妥的方式是使用 Node 16或者改用 sass 替代 node-sass。联调时可以用一个通用清单登录页面能否正常跳转学生登录后能否看到课程列表选课后剩余名额是否变化退课后能否重新选教师账号能否看到学生名单并录入成绩管理员能否打开/关闭选课开关。我测试时发现有时前端点击“选课”后接口报错 500但控制台只显示“服务器错误”。这时要看后端日志往往可以看到具体堆栈。最常见的报错是“Data truncation: Data too long”由于课程简介字段超长导致。解决方法是调整数据库 varchar 长度或前端把输入框加 maxlength 限制。5.4 接口文档如何辅助开发联调这个项目附带的接口文档建议不要直接当摆设。它通常以 markdown 或者 html 形式存在打开后能看到每个接口的路径、请求方式、参数类型、示例 JSON。对没接触过的同学最正确的用法是配合 Postman 或 Apifox 做一次全流程接口测试。先用文档里的管理员账号调登录接口获取 token之后每个需要鉴权的接口都在请求头里带上 Authorization: Bearer token。不要觉得这一步麻烦因为前端已经自动做了但你自己调接口时如果不带上 token会看到一堆 401。把这一步理解透你才能清晰知道前端 request.js 封装到底做了什么。接口文档中还可能会有错误码说明例如“1001 未到选课时间”“1002 课程已满”“1003 重复选课”。这些错误码在后端以自定义异常抛出前端 axios 拦截器里统一弹出 Message 提示。二次开发时我建议你照着这个模式继续扩展自定义异常而不是直接返回 Result 对象。6. 常见问题与排查技巧实录6.1 项目运行问题的对照速查表实际操作中收集到的高频问题基本固定整理成表照着排查能少走很多弯路。现象可能原因解决方案登录接口返回 404前端代理 target 端口不对修改 vue.config.js 后端地址接口返回 401token 未携带或过期检查 axios 拦截器正确传入 token数据库中文乱码建表字符集不是 utf8mb4修改数据库连接参数 characterEncodingutf8选课后剩余名额不变选课逻辑中未更新课程表人数检查 select 接口是否 update selected_count控制台找不到主类SpringBoot 主包扫描路径不对确认启动类在包结构最外层页面加载样式全乱Element UI 版本和 Vue 版本不匹配检查 package.json 中 element-ui 版本启动时端口被占后端 8081 或前端 8080 被占用改端口或杀掉占用进程导入 SQL 报错MySQL 版本兼容问题检查 SQL 中日期格式和保留字这个表是我从实际调试中总结的覆盖面足以应对大部分人跑通项目时的问题。如果你的问题不在表里还有一个通用定位思路先看浏览器 F12 Network 面板里请求是否发出再看状态码最后看后端日志。这三个节点能定位绝大部分前后端交互问题。6.2 选课并发冲突的模拟与验证很多同学答辩时被评委问“你的系统如何应对多人同时选课”如果只说“我用 synchronized”评委可能会继续追问“ synchronized 在集群部署下还有用吗”为了回答好这个问题建议自己先做一次并发模拟。用 Postman 的 Runner 功能或者写一个简单的 Java 多线程测试类模拟 20 个线程同时选同一个课程课程容量为 10。跑完后观察数据库里的选课记录数量是否超过 10如果没超过说明锁生效了如果超过了说明锁粒度有问题或者锁没加到正确位置。我在实际环境中看到项目原始的选课接口没有加锁而是靠数据库唯一索引拦截重复记录但唯一索引只能防“同一个人重复选”无法防“容量超卖”。所以我建议一定要把同步锁或乐观锁加到 service 方法中而不是加在 Controller 层。用乐观锁时可以在课程表加 version 字段update 时带 where version ?判断影响行数是否为 0如果为 0 就提示课程已满。这里再提供一个进阶技巧MySQL 隔离级别默认是 Repeatable Read如果不加锁两个事务并发时读取到的课程人数都是旧的然后都插入成功最终导致超选。如果你在课堂上能画出这个图说明理解深度完全不一样。6.3 二次开发时最容易忽略的三个扩展点这个项目完成度不低但距离“生产级”还有一段距离。作为一名看过不少同类项目的人我给你列出三个值得扩展的地方选其中一个做出来都能成为亮点。一是增加消息通知。学生选课成功后系统自动发一条站内消息提醒上课时间和地点。实现方式很简单加一个 notification 表在选课事务里插入记录前端轮询或 WebSocket 推送即可。二是增加选课时间冲突检测。现在的系统只校验了课程容量但没有校验“同一时间是否已经选了其他课”。这是选课系统里最核心的业务规则。扩展时需要在选课前查询该学生所有已选课程的时间段周几、第几节与目标课程的时间段比较如果有重叠就拒绝选课。这个功能做好了答辩时能把业务逻辑的严谨性充分体现出来。三是增加成绩统计图表。教师端录入成绩后管理员可以用 ECharts 展示各科成绩分布、学生平均绩点走势。前端引入 ECharts 并不复杂后端只要提供几个统计接口。用可视化结果撑起演示效果比单纯堆功能更令人印象深刻。6.4 写文档和答辩时的一些心得很多人拿到源码后总觉得“跑起来就万事大吉”但实际上评委会更关注你是否理解项目。建议按这个顺序准备答辩先讲项目背景和业务流程强调三种角色各自的操作闭环再讲数据库设计强调选课表如何通过字段设计避免重复选课接着讲后端核心接口重点讲选课校验步骤和并发解决方案最后讲前端交互打开页面现场演示登录、选课、退课、录入成绩四个动作。还有一个亲身经历想分享我帮某个同学排查问题时发现他在老师检查前的一晚上试图把端口从 8081 改成 80导致前端代理没改所有请求都失败。这种低级错误如果发生在演示现场会非常尴尬。最好的做法是演示前一天的晚上不要做任何大改动只做“重新导入数据库 启动后端 启动前端”三件事的完整排练确保各个环节一次通过。这份源码里还自带了接口文档很多人根本没打开过直到答辩时被要求展示接口设计才临时翻找。建议提前用 Apifox 把每个接口请求一遍截图放在答辩 PPT 里比临时敲命令有说服力得多。最后一个小技巧如果你准备拿这个项目作为毕业设计的基础我强烈建议你不要原封不动提交。花一个周末的时间至少把密码加密从 MD5 换成 BCrypt把选课接口加上同步锁再给课程表补一个“选课时间冲突校验”。这三个改动虽然代码量不大但能把项目的技术深度从“复刻教程”提升一个档位。还有一个细节接口文档里最好补上返回码的全局枚举表并且补一份“部署手册”说明从零到一如何启动环境。很多同学写文档只写接口忘了别人拿到项目后第一步要做什么这是体验感上最大的短板。既然选了这个方向就认真把它吃透。选课系统的核心不在增删改查而在一个明明看似简单的业务里把数据一致性、权限边界、系统扩展性都考虑到位。做完之后你会发现后面再做任何管理系统类项目都有一种轻车熟路的感觉。