选课系统这种题目在毕设和课设里绝对是常青树。但大多数交上来的版本说白了就是个课程表的增删改查学生登录进去点个选课按钮教务后台加个课程管理答辩时演示五分钟老师问一句两个人同时选最后一个名额怎么办直接就卡壳了。我这篇要聊的就是基于JavaSpringBootVue这套组合拳做出来的选课系统重点不是把页面截图画一遍而是把选课这个业务里真正有技术含量的东西——冲突检测、容量控制、并发抢课、权限模型——一个个拆开讲明白。无论你是拿它当毕设交差还是想真正弄懂一个完整前后端分离项目该怎么落地这篇都能给你一些参考。1. 选课系统的核心业务考验冲突检测、容量限制与并发抢课很多人一上来就建表写接口把选课理解成往选课记录表里插一条数据这是最大的误区。选课这个动作表面上是一次INSERT实际上背后牵着一连串的业务约束少考虑一个系统上线就会被学生骂到重启。1.1 比增删改查复杂得多的隐性规则我梳理了一下一套完整的选课业务至少得扛住这几条规则学生不能选同一门课两次唯一性约束学生选课时不能和已选课程的上课时间冲突时间冲突检测课程有容量上限满了就不能再选容量控制学生有学分上限超过培养计划要求应被阻止学分约束退课要释放名额其他人能立刻抢到状态流转选课窗口有开放时间不在时间段内不能操作窗口期控制单独看每一条都不难难的是它们会叠加。例如学生A选了周一第一节的高数再选同一时段的线性代数时间冲突校验就要遍历他所有已选课程学生A退掉高数的那一瞬间学生B恰好提交了高数的选课请求这时系统既要保证B能选上又不能把容量扣成负数——这就是典型的并发问题。1.2 把业务约束翻译成技术设计我在设计时把这些规则分成了三层第一层是数据库约束能靠唯一索引解决的绝不放业务层。比如选课记录表对学生ID课程ID建联合唯一索引这是兜底方案就算代码里漏了判断数据库也会挡住重复选课。第二层是业务校验放在Service层的事务边界里。选课前先查课程容量、查时间冲突、查学分限制全部通过再执行插入。第三层是并发控制解决两个请求同时读到容量1的问题。这个后面单独开一节细说因为这才是整个系统里最值得在答辩时拿出来讲的亮点。2. 技术选型为什么落在这套组合上SpringBootVue的现实理由选课系统的技术栈网上搜出来一半是SSMJSP一半是SpringBootVue。SSM那套不能说不能做但在2024年这个时间点我建议新项目直接SpringBootVue理由很实在。2.1 前后端分离带来的开发效率提升SSMJSP的时代前端页面是后端Java程序员用JSTL标签在HTML里嵌代码写的。改个按钮样式要重启服务器前端调试靠浏览器里看渲染后的HTML偶尔还得和Tomcat的缓存较劲。而SpringBootVue是前后端分离后端只写RESTful接口返回JSON前端Vue负责渲染和交互两边可以并行开发。对学生团队来说这个模式还有个隐性的好处分工明确。一个人负责后端接口一个人负责Vue页面代码冲突少Git合并的时候不会三天两头为同一个JSP文件打架。就算是一个人做完整个项目思路也更清晰——你写接口的时候不用管页面长什么样写页面的时候不用管SQL怎么查。2.2 SpringBoot解决了SSM时代最烦人的配置用过SSM的人都知道spring-mvc.xml、mybatis-config.xml、web.xml三件套每个都要手写一堆bean配置环境稍微不一样就报各种类找不到。SpringBoot用自动配置把这些默认行为包起来了Maven引入依赖后application.yml里配个数据源一个SpringBootApplication注解就能启动整个Web服务。这里有个细节值得注意SpringBoot内置了Tomcat打包成jar后直接java -jar就能跑不像SSM还得单独装个Tomcat再丢war包。对于最后要部署到服务器上演示的项目来说jar包方式省掉了很多环境折腾。2.3 Vue在前后端分离里到底承担了什么Vue负责的部分是单页面应用SPA的交互层。选课系统的典型痛点在于课程列表、已选列表、可选余量这些数据变化频繁如果还用传统的整页刷新每次点选课按钮页面都要闪一下体验很糟糕。Vue的响应式机制让数据变化直接驱动DOM更新选课成功后课程卡片上的已选人数立刻1不需要刷新页面。而且Vue生态里的Vue Router和Pinia或Vuex在选课系统里都有实打实的用途——Vue Router做路由守卫根据用户角色拦页面Pinia存登录状态、用户信息、当前选课窗口状态避免每个组件都去重复请求接口。3. 数据库模型设计六张核心表如何支撑完整选课流程数据库是选课系统的地基表设计得好不好直接决定后面写SQL是享受还是受刑。我当时的设计围绕六个核心实体展开用户学生教师管理员、课程、教学班、选课记录、时间安排、公告。下面逐个拆解。3.1 用户表三类角色用一张表还是三张表很多初学者喜欢建三张表student、teacher、admin各存各的字段。我建议合并成一张user表用role字段区分角色类型。原因很简单登录认证只需要查一次用户表不用先判断你是什么角色再去对应的表里查三张表意味着登录时要查三张表或者加一个type字段告诉你去哪张表查纯属增加复杂度。user表的核心字段id、username、passwordBCrypt加密存储、real_name、role1学生/2教师/3管理员、email、phone、create_time。学生特有的字段比如学号、班级可以建一张student_profile表补充或者干脆冗余在user表里用student_no字段存看你对范式的执念有多深。我个人倾向于冗余因为查学生信息时少一张表join。3.2 课程表与教学班表一个经典的建模分歧点最简单的建模是一张course表包含课程名称、教师ID、上课时间、容量。但这样有个问题同一门高等数学两个老师各开一个班时间和教室都不一样怎么处理正确做法是拆成两层course表存放课程基础信息课程名称、课程编号、学分、课程性质必修/选修、课程简介teaching_class表存放教学班信息课程ID、教师ID、学期、上课时间、上课地点、容量、已选人数、选课状态开放/关闭这样的好处是全校开设了哪些课程查course表我这个学期能选哪些班查teaching_class表。学生选课实际选的是教学班而不是课程。这一层概念在答辩时跟老师讲清楚直接证明你不是只会建表的门外汉。3.3 选课记录表状态机设计与唯一索引选课记录表course_selection是整个系统的核心表字段包括id、student_id、class_id、select_time、status已选/已退/已修完。这里有两个关键设计第一student_id和class_id必须建联合唯一索引。前面我说过这是兜底约束有了它哪怕代码里漏了判断数据库也会拒绝重复选课。第二用status字段做状态机而不是直接物理删除记录。学生退课的时候执行UPDATE把status改成已退而不是DELETE。好处是保留完整历史以后统计某门课有多少人选过又退了非常方便。坏处是查询当前已选列表时要注意加条件status已选。3.4 时间安排表把周几第几节建模成可计算的数据上课时间冲突检测核心依赖时间安排表。我用的方案是teaching_class_time表字段包括id、class_id、day_of_week1-7、start_section开始节次、end_section结束节次、start_week、end_week起止周次。检测冲突的逻辑就变成查出学生所有已选教学班的时间记录再查出目标教学班的时间记录逐个比较是否同一天 节次区间是否有交集 周次区间是否有交集。这个判断逻辑用SQL写比较绕我建议取到Java内存里用对象遍历判断教学班数量有限性能完全不是问题。4. 后端设计的关键决策JWT权限、事务边界与超卖防护后端是整个系统的中枢前面说的所有业务规则最终都要落到代码里。这一节挑三个我踩过坑、也最有讲头的设计点来说。4.1 JWT登录与权限控制不依赖Session的前后端分离方案前后端分离项目里Session这套方案不太好用了因为前端和后端可能不在同一个域跨域请求带Cookie会有各种限制。我采用的是JWTJSON Web Token流程非常简单用户登录成功后后端生成一个JWT返回给前端前端存在localStorage里每次请求在Header里带上Authorization: Bearer 后端通过拦截器解析token取出用户ID和角色放行或拒绝。代码实现上我封装了一个JwtUtil工具类和JwtInterceptor拦截器。拦截器里从请求头取token解析失败返回401成功就把用户信息塞进ThreadLocalController直接getCurrentUser()就能拿到当前登录用户。权限控制上我用了三种方式组合拦截器统一校验是否登录然后自定义一个RequireRole注解挂在方法上拦截器里解析注解和角色匹配最后在前端Vue路由守卫里也做一层角色控制。前后端双重校验的好处是后端保证数据安全前端保证用户体验比如学生根本看不到教师端的管理按钮。4.2 事务边界为什么选课方法必须加Transactional选课的代码逻辑是查课程状态 → 查容量 → 查冲突 → 查学分 → 插入选课记录 → 已选人数1。这个流程里有多个SQL操作任何一步失败都不能让其他步骤生效否则会出现选课记录插入成功了但人数没加这种脏数据。所以Service层的selectCourse方法必须加Transactional利用Spring的声明式事务保证原子性。这里面有个容易被忽略的点事务一定要加在public方法上而且要避免同类内部调用导致事务失效。我当时就犯过这个错——在一个类内部直接调用另一个被Transactional标注的方法结果事务没生效排查了半天才发现是Spring代理机制的问题。4.3 超卖问题的本质与三种解决方案选课并发和电商秒杀本质上是同一个问题多个请求同时读到剩余容量1然后同时执行插入最终容量变成负数或者一个名额被多个人抢到。我当时尝试了三种方案各有优劣第一种是synchronized加锁。最简单但只对单机有效而且锁的是当前JVM实例部署多副本就失效了。而且后端的接口层加锁会影响所有用户的请求吞吐。第二种是乐观锁。在teaching_class表加一个version字段更新已选人数时执行 UPDATE teaching_class SET selected_count selected_count 1, version version 1 WHERE id ? AND version ?如果影响行数为0说明version被改过了说明并发冲突重新查数据再重试。这个方案不用lock性能好但更新失败后的重试逻辑要自己写如果冲突频繁会糟心。第三种是数据库悲观锁 SELECT * FROM teaching_class WHERE id ? FOR UPDATE先锁住课程行再查容量、判断、插入、更新人数最后提交事务释放锁。这个方案逻辑最清晰、正确性最高缺点是并发量大时这个行会成为热点请求排队。最终我选了悲观锁作为核心方案理由很直接选课系统的并发量没到需要为了性能牺牲正确性的程度而且FOR UPDATE的代码逻辑最好理解答辩时跟老师解释起来也顺畅。为了对冲性能问题我加了一个前置的Redis缓存校验选课前先查Redis里的剩余容量大于0才走后续逻辑等于用一个薄缓存挡住了大量无效请求。5. 前端与联调Vue组件划分、路由守卫和跨域踩坑前端这块很多后端同学觉得是苦力活不就是写页面吗。但选课系统这种交互密集型的项目前端设计得好不好直接影响你答辩时的演示效果。5.1 页面结构与组件拆分我按角色把页面分成了三个模块学生端登录注册页、可选课程列表页带筛选和分页、我的课表页按周一到周日展示课程卡片、选课结果页、个人中心。教师端我的课程管理、开课申请、选课学生名单、成绩录入。管理员端用户管理、课程管理、教学班管理、选课窗口设置、公告管理。组件层面我把课程卡片CourseCard、时间选择器WeekPicker、选课弹窗SelectDialog、数据表格DataTable这些复用度高的部分抽成了公共组件。特别是CourseCard一个组件同时用于学生选课列表和个人课表只是传入的props和emit的事件不同代码复用很干净。5.2 路由守卫与角色权限的前端控制Vue Router的beforeEach守卫是前端权限控制的核心。我在全局路由配置里给每个路由加了meta.roles数组守卫里判断当前用户角色是否在允许列表里不在就重定向到登录页或者403页面。这个设计的细节在于路由守卫只能控制页面跳转不能控制接口数据。比如学生手动输入教师端接口的URL后端拦截器会拦截他前端路由根本到不了那个页面但恶意用户可以通过控制台直接发请求。所以前端控制是体验层的,后端控制才是安全层的,两者缺一不可。这个观点我在答辩时特意强调了老师明显更认可。5.3 跨域问题与axios封装前后端分离项目联调时第一关就是跨域。后端的请求来自http://localhost:8080后端接口在http://localhost:9090浏览器默认会拦截这种跨端口请求。我的处理方案是后端配置CORS过滤器允许指定前端的地址跨域访问同时允许携带凭证。代码就是实现WebMvcConfigurer接口重写addCorsMappings方法。需要注意allowedOriginPatterns不能简单的用*否则和allowCredentials(true)冲突这个问题网上报错的帖子一抓一大把。axios封装方面我做了三件事统一baseURL配了环境变量开发环境指向本地后端生产环境指向服务器、请求拦截器自动携带token、响应拦截器统一处理401跳转和业务错误码弹提示。这层封装看起来简单但所有页面都受益至少帮你省掉每个接口都手动写Header的重复劳动。6. 部署上线与开源资料整理让毕设从能跑到能展示项目写完代码只是第一步能部署上线、能讲清楚、能让人复现才是完整的交付。我见过太多同学代码写得没毛病但最后演示用localhost跑老师想看部署文档又没有评分直接被拉低了一档。6.1 前后端分离部署的最简方案我的部署方案是后端用Maven打包成jar包在服务器上安装JDK17和项目配置保持一致直接java -jar跑起来前端用npm run build生成dist文件夹用Nginx托管静态文件同时Nginx反向代理把/api开头的请求透传给后端的9090端口。Nginx配置里最核心的一段是 server { listen 80; server_name your-domain.com; location / { root /opt/frontend/dist; index index.html; } location /api/ { proxy_pass http://localhost:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个小坑Vue Router如果用了history模式Nginx必须配置try_files重写到index.html不然刷新页面会404。如果不熟悉这个配置我建议直接改成hash模式地址栏会带个#号但对演示毫无影响。图省事的话这是最稳的选择。6.2 数据库脚本的整理规范很多人提交的SQL脚本就是一套建表语句我来回看过那么多项目觉得至少要准备三份东西第一份是schema.sql完整的建表语句包含索引、外键约束慎重用外键很多生产项目反对外键但毕设项目用外键反而直观、默认值、注释。注释一定要写字段含义一目了然。第二份是data.sql基础数据至少要包含一个管理员账号、一个教师账号、几个学生账号、几门课程的测试数据。账号密码建议统一比如密码都是123456但入库时要存BCrypt加密后的密文不然学生拿到源码直接拿明文密码做登录一行代码都不用改将来出现安全漏洞就是源头问题。第三份是README.md写清楚如何初始化数据库、如何修改配置文件里的数据库连接信息、如何启动后端和前端的完整步骤。6.3 文档和讲解视频的加分项毕设答辩通常看三样论文或设计文档、演示效果、你对技术的理解深度。文档方面一定要画清楚系统架构图、角色权限图、核心业务流程时序图重点把选课的并发控制逻辑讲明白复试老师对这个问得比较多。讲解视频的话我建议按这个顺序录系统介绍 → 前端演示管理员创建课程、教师开课、学生选课→ 亮点代码讲解事务控制、并发方案、权限模型→ 演示两个人同时选最后一个名额的场景可以用两个浏览器窗口操作这种做法比任何口头说明都有说服力。7. 关于这个项目的扩展思路从课设走向工程化的几条路选课系统做完、答辩完如果还想让它继续在你的简历上发挥作用有几个扩展方向我觉得挺有意思也都踩在前人的脚印上。第一个方向是缓存优化。把查询可选课程列表这个最频繁的接口用Redis做缓存课程选课人数变动时实时更新缓存。这个优化做得好可以扛住更高的并发也说明你懂缓存和数据库的一致性问题。第二个方向是消息队列入场。如果选课人数达到几千人同时抢一门课后台逻辑可能存在性能瓶颈可以把选课请求写入消息队列再异步处理前端轮询选课结果。这能展示你的架构思维简历上的含金量直接大不一样。第三个方向是横向扩展成其他管理系统。选课系统的核心模型是用户-角色-资源和名额竞争这套东西改一改就能变成会议室预约系统、实验室预约系统、图书馆座位管理系统参加比赛换皮很快。第四个方向是加微信小程序端。小程序端的登录可以直接用微信的code2Session换openid课程列表和选课操作通过wx.request调用后端接口只需要在小程序后台配置合法域名把请求地址改成服务器地址就行业务逻辑全部复用代码量增加不大。模板里其实已经预留了接口层的通用封装加上小程序端之后前后端分离的价值会体现得更明显——同一套后端APIWeb端、小程序端都能用。选课系统说到底是个麻雀虽小五脏俱全的经典题目。它没有像推荐系统那么玄乎的算法也没有像高并发秒杀那么夸张的性能压力但它把登录授权、角色权限、业务规则、事务控制、并发竞争、前后端分离这些日常开发中最常遇到的坑都浓缩了一遍。把每一个细节搞扎实了你会发现自己对这整个技术栈的理解会通透不少。
