简介:本资源是一套基于SpringBoot与Vue.js实现的前后端分离学生选课管理系统,专为计算机专业本科毕设(bishe)及Java全栈初学者设计,覆盖课程管理、选课退课、师生信息维护、成绩录入等高校教务核心业务场景,助力学习者快速掌握主流技术栈的工程化落地能力。压缩包共107个文件,含49个Vue组件(实现学生/教师/管理员三端交互界面)、24个Java后端服务类(含SCTService、CourseTeacherService等业务逻辑)、14个MyBatis映射XML、6个JS工具脚本,以及SQL建库脚本、YML配置、HTML入口页等,整体仅266KB,轻量易部署。已有1284人学习下载,资源经严格调试可直接运行,附完整源码、数据库脚本与清晰分层目录结构,特别适合毕设开题、课程设计与SpringBoot+Vue实战练手。
1. 这不是又一个“Hello World”项目:为什么选课系统是前后端分离的黄金练兵场
你点开这个标题,大概率正面临三种情况:刚学完SpringBoot和Vue基础,想找个不 trivial 又不至于太复杂的项目来串联知识;正在准备技术面试,需要一个能讲透、能延展、能体现工程思维的实战案例;或者手头真有个校内小需求,得快速搭个可用的选课后台。这三类人,我都带过——从大三学生到三年经验的开发,最后都卡在同一个地方:不是写不出功能,而是写出来的东西一碰就散,改个字段要前后端一起动,加个权限要重写半套逻辑,部署时发现前端静态资源和后端接口根本对不上号。而“学生选课管理系统”恰恰是那个最合适的切口:业务边界清晰(学生、课程、教师、选课记录四张核心表),流程可闭环(登录→查课→选课→退课→查成绩),但又足够真实——它天然包含角色权限(学生只能看自己课,教师能管自己课,管理员全览)、状态流转(选课中/已确认/已退选)、并发控制(热门课抢课)、数据一致性(选课成功必须扣库存+记日志)这些工业级问题。我去年帮某高校信息中心做轻量版选课工具时,第一版用传统JSP硬怼,上线三天就因并发选课失败被叫停;第二版直接上SpringBoot+Vue分离架构,用JWT做会话,Redis缓存课程余量,MyBatis-Plus自动生成CRUD,两周内交付稳定版本。关键不在技术多炫,而在整个链路里每个环节都经得起推敲:前端Vue路由守卫拦未登录请求,后端SpringBoot拦截器校验JWT并注入用户上下文,数据库事务保证选课与库存更新原子性。这不是教科书里的理想模型,而是每天都在发生的现实约束。
2. 架构设计不是画PPT:为什么必须拆成前后端,以及拆到什么程度
2.1 前后端分离不是“把HTML扔给Vue”,而是职责的物理隔离
很多人以为前后端分离就是“后端写API,前端调API”,结果代码仓库里还混着Thymeleaf模板,Controller里塞着ModelAndView跳转逻辑。真正的分离,是让两个团队能完全独立演进。举个具体例子:当教务处突然要求在选课页面增加“课程难度星级”和“往届学生评语摘要”,前端团队拿到接口文档后,30分钟就能在Vue组件里加两个字段渲染;而后端只需在Course实体类里加两个@ApiModelProperty注解,MyBatis-Plus自动映射,连SQL都不用改。但如果还是老式MVC,前端改HTML,后端改Controller返回Model,还要同步更新JSP模板,一次小需求就得拉通会议、协调测试、重新发版。我见过最惨的案例是某职校系统,因为一个“课程封面图”字段新增,前后端各改了5个文件,联调花了两天,最后发现是Nginx配置漏了图片路径代理。
2.2 SpringBoot后端只做三件事:数据管道、业务规则、安全闸门
后端的核心价值从来不是渲染页面,而是成为可靠的数据中枢。在本系统中,SpringBoot层严格限定为:
- 数据管道:通过MyBatis-Plus操作MySQL,所有DAO层方法命名遵循
selectByStudentIdAndStatus()这类语义化规范,避免queryList()这种模糊命名;使用@TableField(exist = false)明确标出非数据库字段,防止误插入。 - 业务规则引擎:选课逻辑绝不放在前端校验。比如“同一学期同一课程只能选一次”、“退课截止时间前才能操作”,这些规则全部在Service层用@Transactional包裹,配合
@Valid校验DTO,错误时抛出统一BusinessException,由全局异常处理器转成400响应体。 - 安全闸门:JWT鉴权不是简单加个
@PreAuthorize("hasRole('STUDENT')")。我们实际实现中,Token解析后将用户ID、角色、学院ID存入ThreadLocal,后续所有DAO查询自动拼接AND college_id = ?条件,彻底杜绝越权查看其他学院课程数据。
提示:别用Spring Security默认的内存用户管理。真实场景中,用户信息来自LDAP或学校统一认证平台,我们用
UserDetailsService定制加载逻辑,密码校验走BCrypt,Token过期时间设为2小时——既防暴力破解,又避免学生选课到一半突然掉线。
2.3 Vue前端不是“套壳浏览器”,而是独立应用生命周期管理
Vue项目必须脱离后端模板独立存在。我们采用vue-cli创建标准项目,public/index.html里只留一个<div id="app"></div>,所有路由、状态、API调用全部在src目录下完成。关键设计点:
- API分层:
src/api下按模块划分(course.js,selection.js),每个文件导出函数如getCourseList(params),内部用Axios封装baseURL、超时、错误拦截。绝不允许在组件里直接写axios.get('/api/course/list')。 - 状态管理克制:Vuex只存全局状态(用户信息、菜单权限),课程列表、选课表单等页面级状态直接用
ref()或reactive()管理。曾有团队过度使用Vuex导致调试困难,一个课程搜索条件变更要追踪7个mutation。 - 路由守卫实战:
router.beforeEach()不只是判断登录态。我们额外检查to.meta.requiresRole,若用户角色不匹配则跳转403页;对/course/detail/:id这种动态路由,守卫中预加载课程数据并存入Pinia,避免组件内重复请求。
3. 核心功能落地:从数据库建模到接口联调的完整链路
3.1 数据库设计:用MyBatis-Plus反向驱动开发节奏
我们放弃手动建表,全程用MyBatis-Plus的AutoGenerator生成。先定义实体类,再让框架建库——这倒逼你思考数据结构本质。以核心的Selection(选课记录)表为例:
@TableLogic // 逻辑删除,避免误删历史数据 public class Selection { @TableId(type = IdType.ASSIGN_ID) // 雪花算法ID,避免MySQL自增主键暴露业务量 private String id; @TableField(value = "student_id") private String studentId; // 外键,但不设数据库约束,由应用层保证一致性 @TableField(value = "course_id") private String courseId; @TableField(value = "status") private Integer status; // 0-待确认, 1-已选中, 2-已退选, 3-已取消(管理员操作) @TableField(value = "created_time", fill = FieldFill.INSERT) private LocalDateTime createdTime; }生成后,MySQL表结构自动带is_deleted字段和created_time时间戳。重点在于student_id和course_id不设外键——线上环境外键会拖慢高并发写入,我们用Service层的双重校验兜底:选课前查studentId是否存在,查courseId是否有效且未关闭。
3.2 关键接口实现:选课动作的原子性保障
选课是最易出错的环节。前端点击“立即选课”按钮后,后端必须保证三件事同时成功或同时失败:1)插入选课记录;2)课程余量减1;3)记录操作日志。SpringBoot用@Transactional声明式事务,但需注意两点:
- 传播行为:
@Transactional(propagation = Propagation.REQUIRED)是默认值,确保同一事务内; - 异常类型:默认只回滚
RuntimeException,所以Service方法里抛new RuntimeException("库存不足"),而非new Exception()。
实际代码中,我们用Redis做库存预占,避免数据库锁竞争:
@Service public class SelectionService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Transactional public Result selectCourse(String studentId, String courseId) { // 1. Redis预占库存(Lua脚本保证原子性) String key = "course:stock:" + courseId; Boolean result = redisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.eval("if redis.call('decr', KEYS[1]) >= 0 then return 1 else redis.call('incr', KEYS[1]); return 0 end".getBytes(), ReturnType.INTEGER, 1, key.getBytes()) ); if (!result) { throw new BusinessException("课程已满,请稍后再试"); } // 2. 插入选课记录 Selection selection = new Selection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setStatus(1); // 已选中 selectionMapper.insert(selection); // 3. 记录日志(异步,不影响主流程) logService.asyncLog("学生" + studentId + "选课成功", courseId); return Result.success(); } }注意:Redis库存和MySQL实际库存需定时任务对账。我们每晚2点执行
SELECT COUNT(*) FROM selection WHERE course_id = ? AND status = 1与Redis值比对,差异超阈值则告警。
3.3 Vue前端关键交互:如何让选课过程“看得见摸得着”
前端不是简单调API。以课程列表页为例,我们做了三层体验优化:
- 加载态精准控制:不用全局loading,而是为每个卡片设置
loading状态。点击“选课”按钮后,该卡片显示“选课中...”,其他课程仍可操作。 - 错误反馈具象化:API返回
{code: 400, msg: "库存不足"}时,不弹alert,而是在按钮下方显示红色提示“❌ 该课程剩余名额0”,3秒后自动消失。 - 本地状态即时同步:选课成功后,立即执行
this.courseList.find(c => c.id === courseId).selected = true,避免刷新页面——用户感知就是“点一下就变绿了”。
路由配置也暗藏技巧:/course/select/:id页面用beforeRouteEnter守卫预加载课程详情,但若用户直接输入URL访问,需在mounted钩子中补加载,防止SSR场景下数据为空。
4. 部署与运维:从本地开发到阿里云上线的避坑实录
4.1 开发环境:用Docker Compose一键拉起全栈依赖
本地开发最怕“在我机器上是好的”。我们用docker-compose.yml统一环境:
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: course_db ports: - "3306:3306" redis: image: redis:7-alpine ports: - "6379:6379" nginx: image: nginx:alpine volumes: - ./nginx.conf:/etc/nginx/nginx.conf ports: - "8080:80" depends_on: - frontend - backend frontend: build: ./frontend # 构建后生成dist,供nginx挂载 backend: build: ./backend environment: - SPRING_PROFILES_ACTIVE=dev ports: - "8081:8081"关键点:Nginx配置nginx.conf里,location /api/代理到后端,location /指向前端dist目录。这样前端访问/api/course/list,Nginx自动转发到http://backend:8081/api/course/list,彻底解决跨域问题,且与生产环境一致。
4.2 生产部署:阿里云ECS上的最小可行方案
上云不等于堆配置。我们用2核4G ECS(约¥80/月)跑通全流程:
- 后端:SpringBoot打成jar包,用
nohup java -jar app.jar --spring.profiles.active=prod &启动,配合systemd服务管理进程。 - 前端:
npm run build生成dist,用scp上传到ECS的/var/www/html,Nginx配置静态文件服务。 - 数据库:直接用阿里云RDS MySQL 5.7,白名单只放ECS内网IP。
- HTTPS:阿里云免费SSL证书,Nginx配置
ssl_certificate和ssl_certificate_key,强制return 301 https://$host$request_uri;。
最常踩的坑是Nginx代理超时。选课接口可能因Redis锁等待耗时较长,需在location /api/块中加:
proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 60s;4.3 监控与日志:没有监控的系统等于裸奔
上线后第一件事不是测功能,而是看日志。我们用ELK(Elasticsearch+Logstash+Kibana)收集:
- 后端日志:Logback配置
RollingFileAppender,按天滚动,保留30天; - Nginx日志:开启
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time';,$request_time是关键,能定位慢接口。
在Kibana里建看板,重点关注:
request_time > 3s的请求TOP10;status == 500的错误趋势;/api/selection/select接口的QPS和成功率。
曾发现某次选课高峰,request_time突增,排查发现是Redis连接池耗尽。解决方案不是加机器,而是调整Lettuce连接池参数:
spring: redis: lettuce: pool: max-active: 20 # 默认8,调高 max-wait: 1000 # 毫秒,避免线程阻塞5. 面试与毕设高频问题:把项目变成你的技术名片
5.1 面试官最爱问的三个深度问题
Q1:如果选课并发量达到1000TPS,当前架构怎么优化?
答:分三层应对。第一层Nginx加limit_req zone=api burst=100 nodelay限流,防雪崩;第二层Redis用集群模式,课程库存Key按courseId % 16分片,避免单节点热点;第三层数据库读写分离,SELECT走从库,INSERT/UPDATE走主库,并用ShardingSphere做水平分表(按studentId哈希)。
Q2:JWT Token泄露怎么办?如何实现Token主动失效?
答:Token本身无法主动销毁,但我们用“黑名单+短时效”组合拳。Redis存blacklist:{token},过期时间=Token剩余有效期;登录时生成Token的同时,将jti(唯一标识)存入Redis;每次请求校验Token后,检查jti是否在黑名单。同时Token有效期设为2小时,降低泄露风险。
Q3:Vue路由懒加载后,首屏白屏时间长,怎么优化?
答:不止是import()。我们在vue.config.js中配置configureWebpack: { optimization: { splitChunks: { chunks: 'all' } } },把Ant Design Vue组件单独打包;用prefetch预加载首页必需的课程列表模块;最关键的是服务端渲染(SSR)——用Nuxt.js重构,但成本高,建议毕设阶段用prerender-spa-plugin生成静态首页。
5.2 毕设答辩必答清单
为什么选MyBatis-Plus而不是JPA?
JPA的@OneToMany在选课这种多对多场景下,关联查询容易N+1,且学校MySQL版本较老(5.6),JPA某些特性支持不全。MyBatis-Plus的LambdaQueryWrapper写法更贴近SQL思维,调试时直接看到执行语句。如何证明系统安全性?
展示三点:1)所有接口用@PreAuthorize标注权限;2)SQL注入测试——在课程名输入' OR '1'='1,后端日志显示MyBatis-Plus自动转义;3)XSS防护——前端用v-html的地方全部经过DOMPurify.sanitize()过滤。项目的创新点在哪?
不是技术堆砌,而是解决真实痛点。比如“课程余量实时显示”,我们用WebSocket推送库存变更,学生列表页不用轮询;再如“选课冲突检测”,前端在选课前调用/api/selection/conflict?studentId=xxx&courseId=yyy接口,后端查该生本学期已选课程时间是否重叠,提前拦截。
最后分享个血泪教训:某次部署后发现选课总是失败,查日志全是Connection refused。折腾两小时才发现,ECS安全组没开6379端口,Redis连不上。所以现在我的上线checklist第一条就是:“telnet rds-host 3306 && telnet redis-host 6379”。技术再牛,也得先让网络通。
本文还有配套的精品资源,点击获取