简介基于Java的学生选课系统完整源码包采用前后端分离架构后端Spring Boot、前端Vue.js、数据库MySQL适合高校学生、Java开发入门者及需要快速搭建选课管理系统的个人或机构。系统覆盖用户管理、课程信息维护、选课/退课、权限控制与数据可视化等核心模块能帮助用户理解业务系统从数据表设计到接口联调的全流程。压缩包大小约21.84MB内部文件为Java后端源码、Vue前端源码及SQL脚本等工程文件具体文件总数暂未统计。资源已有164人学习浏览。包含完整前后端代码与数据库脚本可学习Spring Boot持久层写法、Vue路由与状态管理、JWT权限校验及ECharts图表展示等知识点同时支持二次开发便于扩展选课统计、成绩管理等功能适合课程设计、毕业设计或项目实战训练。1. 学生选课系统怎么选型一个能跑通前后端的完整 Java 课设样本很多人找 Java 课程设计或毕业设计源码时最怕的是拿到一套概念讲得天花乱坠、代码却跑不起来的半成品。这套基于 Java 的学生选课系统不是空壳项目它的后端用 Spring Boot前端用 Vue.js数据库落在 MySQL 上前后端分离的架构把用户管理、权限控制、选课退课、数据可视化这几条主线完整串了一遍。尤其适合两类人一类是正在准备 Java 课程设计、需要真实可运行代码做参考的学生另一类是接外包做二次开发、想找一个结构清晰、方便改造的业务骨架的开发者。这套系统能帮你解决的最核心问题很具体选课业务里的权限体系、并发冲突和数据统计到底该怎么落地。2. 系统骨架与数据建模Spring Boot Vue MySQL 怎么串起来拿到源码包先别急着启动。前后端分离项目的第一步是搞清楚目录结构和数据模型否则后端起来了前端连不上报错你都不知道该往哪边查。2.1 压缩包里的目录结构先分清前后端和数据库脚本Spring Boot Vue 的项目压缩包解压后一般能看到 backend、frontend、sql 三个顶层目录。以我拆过的同类课设源码为例目录通常长这样select-course-system/ ├── backend/ │ ├── src/main/java/com/example/selectcourse/ │ │ ├── controller/ # 接口层接收请求并返回 JSON │ │ ├── service/ # 业务层事务控制集中在这层 │ │ ├── mapper/ # MyBatis 数据访问接口 │ │ └── entity/ # 对应数据库表的实体类 │ └── src/main/resources/ │ ├── mapper/ # MyBatis XML 文件SQL 都在这 │ └── application.yml # 数据源、端口、JWT 密钥配置 ├── frontend/ │ ├── src/ │ │ ├── api/ # axios 请求封装统一管理接口地址 │ │ ├── router/ # 前端路由和登录守卫 │ │ ├── views/ # 页面组件一个页面一个目录 │ │ └── store/ # 用户登录状态管理 │ └── package.json # 前端依赖和 npm 启动脚本 └── sql/ └── init.sql # 建库建表脚本自带初始账号这套分层逻辑非常清晰后端只处理业务、返回 JSON前端只负责渲染和交互数据落在 MySQL。你在动手改代码之前我建议先打开 sql/init.sql把每张表的字段过一遍再打开 backend 下的 entity 一一对照最后去读 controller 里的接口。按这个顺序走比直接一头扎进 Vue 页面里查问题要快得多。从选型角度看为什么这套系统选 Spring Boot Vue 而不是传统的 JSP Servlet两个原因。第一Spring Boot 把 Tomcat 内嵌了本地启动不用单独装服务器Maven 配置好 JDK 就能直接跑这对新手非常友好。第二Vue 的组件化开发配合 axios 发请求前端代码的修改成本比 JSP 混合页面低很多。选课系统这种典型的管理型应用页面多、接口多、数据关联密前后端分离的调试体验明显更舒服。需要提醒的是这套东西对 Java 基础是有一点要求的至少你要熟悉 Maven 依赖、注解和 MyBatis 的基本写法不然排查问题会很吃力。2.2 数据库表设计选课的核心约束其实都写在表结构里这套系统的表不多但每张表都对应一个明确业务规则。选课领域最常用的三张表是用户表、课程表和选课关联表。-- 用户表学生和教师的账号信息 CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, -- 用户名唯一 password VARCHAR(100) NOT NULL, -- 加密后的密文 role VARCHAR(20) NOT NULL DEFAULT STUDENT, -- 角色区分权限 real_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 课程表课程基本信息与容量控制 CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher VARCHAR(50), credits INT DEFAULT 2, -- 学分 max_capacity INT DEFAULT 60, -- 最大选课人数 selected_count INT DEFAULT 0, -- 已选人数 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 选课关联表学生和课程的关系 CREATE TABLE student_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, -- 关联 sys_user.id course_id INT NOT NULL, -- 关联 course.id select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) );三张表把业务模型讲完了sys_user 管账号角色course 管课程容量student_course 管学生和课程的关联关系。注意两个关键约束。第一sys_user 的 username 上有唯一索引从数据库层面防止重复账号。第二student_course 上加了联合唯一索引 uk_student_course同一学生不能重复选同一门课这个约束是兜底设计即使后端校验逻辑漏了数据库也会拒绝重复插入不会产生脏数据。课程表的 selected_count 和 max_capacity 是容量控制的关键。常见的做法是把这两列设计成普通数字字段在业务层先查后写但更稳妥的做法是在一条 update 语句里完成判断和扣减把并发问题挡在数据库层面。这个后文第 3 章会给出具体代码。2.3 一条选课请求的完整链路前端到后端的每一步把请求链路走一遍就能理解这套系统为什么这么搭。学生在登录页输入用户名密码前端 axios POST 到 /api/auth/login后端校验通过后签发 JWT 返回前端。前端把 token 存进 localStorage之后每次请求都在 headers 里带 Authorization: Bearer token。后端有一个拦截器统一解析 token校验通过才放行到 controller。学生点选课按钮前端把 courseId 传给 /api/course/select后端先判断当前用户角色是 STUDENT再查课程容量和是否重复选课最后插入 student_course 记录。这条链路每一环都对应系统里真实的代码。建议你调试的时候不要跳着查按登录 → 查询课程 → 选课 → 查看选课记录的顺序每一环看接口返回的 JSON 是否符合预期。前端报错先开浏览器 F12 看 Network 面板请求没发出去是前端的问题请求发出去了但状态码 4xx/5xx去后端日志看异常栈。这个排查顺序能帮你省掉大量无效沟通。3. 核心业务实现从登录认证到选课退课的完整代码选课系统的核心业务集中在三块身份认证与权限控制、选课退课的并发处理、数据统计接口。这三块也是你在简历或答辩里最值得展开讲的部分。3.1 登录认证与接口权限控制JWT 怎么签发、怎么拦截先看登录接口。用户在登录页提交用户名和密码后端校验通过后签发一个 JWT 字符串返回给前端。这里用 JJWT 库来生成 token。// LoginService.java 登录成功后签发 JWT String token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这里有几个参数要解释一下。setSubject 存的是用户名claim 是自定义负载把 userId 和 role 放进去后端拦截器不需要再查一次数据库就能拿到当前用户信息。setExpiration 设置 24 小时过期时间这是单机开发最简单的做法如果做成生产系统一般会改成 access token 加 refresh token 的双 token 机制课设阶段不必强求。signWith 用 HS256 算法和 secretKey 签名secretKey 必须放在 application.yml 里不要硬编码在 Java 代码中。token 签发之后得有一个拦截器在每次请求时校验它。用 Spring 的 HandlerInterceptor 实现一个 JwtInterceptor注册到 WebMvcConfigurer 里// JwtInterceptor.java 请求拦截校验 token 并注入用户信息 public class JwtInterceptor implements HandlerInterceptor { private String secretKey your-secret-key; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求否则前端跨域会被卡住 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.substring(7)) .getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\token无效或已过期\}); return false; } } response.setStatus(401); return false; } }拦截器这块有两个值得注意的细节。第一是 OPTIONS 预检请求必须放行前后端分离部署在不同端口时浏览器会自动发一个 OPTIONS 请求探测后端是否允许跨域如果这里拦截了前端所有请求都会失败。第二是 role 通过 request 属性向下传递controller 里通过 RequestAttribute(role) 就能拿到当前用户的角色从而实现接口级权限判断。例如管理员添加课程接口在方法开头判断 role 不等于 ADMIN 就抛出 403。3.2 选课与退课并发控制和事务回滚的完整写法选课是学生选课系统里最容易出问题的接口。两个学生同时选同一门只剩一个名额的课如果处理不好就会出现超选。正确做法是把容量判断和人数扣减放到一条 SQL 里利用数据库行锁保证原子性。下面这段代码是一个完整实现// CourseService.java 选课逻辑 Transactional(rollbackFor Exception.class) public boolean selectCourse(Integer userId, Integer courseId) { Course course courseMapper.selectById(courseId); if (course null) { throw new BusinessException(课程不存在); } // 重复选课校验联合唯一索引在底层兜底 if (studentCourseMapper.existsByStudentAndCourse(userId, courseId) 0) { throw new BusinessException(您已选过这门课请勿重复选课); } // 容量判断和人数扣减合并成一条 update用受影响行数判断是否选满 int rows courseMapper.decreaseStock(courseId); if (rows 0) { throw new BusinessException(手慢了该课程刚刚被选满); } // 插入关联记录 studentCourseMapper.insert(userId, courseId); return true; }对应的 SQL 写成这样!-- CourseMapper.xml 条件更新只有还有名额时才减少库存 -- update iddecreaseStock UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count max_capacity /updatedecreaseStock 是关键。这条 update 自带判断条件MySQL 在更新这一行时会加行锁两个并发请求同时进来只有一个能更新成功另一个受影响行数为 0直接抛出业务异常。这就是把并发控制从业务层下沉到数据库层的思路也是面试里常考的乐观锁/条件更新场景。提醒一下如果你用的是 MySQL 默认的 REPEATABLE READ 隔离级别这种通过条件 UPDATE 加锁的方式是有效的因为更新的行会被锁定直到事务结束。退课逻辑正好相反先删关联记录再把 selected_count 减回去。这里有一个值得写进代码里的细节删除关联记录时要返回受影响行数如果行数是 0说明这条选课记录不存在直接抛出业务异常避免后面又对一门没选的课做库存恢复。退课方法同样要加 Transactional保证删除和恢复库存要么一起成功要么一起回滚。// CourseService.java 退课逻辑片段 int rows studentCourseMapper.deleteByStudentAndCourse(userId, courseId); if (rows 0) { throw new BusinessException(您未选择该课程无法退课); } courseMapper.increaseStock(courseId);3.3 数据可视化接口怎么把选课数据聚合成图表数据可视化部分的思路是后端提供聚合后的数据前端用图表组件渲染。最常见的图表是各课程选课人数统计后端一般写一个分组查询// StatsController.java 课程选课人数统计 GetMapping(/api/stats/course-enroll) public ListMapString, Object courseEnrollStats() { return courseMapper.selectEnrollStats(); }-- CourseMapper.xml 左连接统计每门课程已选人数 SELECT c.course_name AS name, COUNT(sc.id) AS value FROM course c LEFT JOIN student_course sc ON c.id sc.course_id GROUP BY c.id, c.course_name这段 SQL 用左连接把课程和选课记录关联起来COUNT(sc.id) 统计每门课的选课人数没有学生选的课也会显示为 0GROUP BY 按课程分组。前端拿到这份 JSON 数组灌进 ECharts 的柱状图 data 属性里就能渲染。接口设计上有两点一是返回字段名要和前端图表组件对应起来name 对应类目轴value 对应数值轴二是这类统计接口数据结构简单直接返回 ListMap 可以但如果有多个统计图表共用一套数据建议封装一个 StatsVO保持接口契约稳定。4. 本地部署与前后端联调把项目跑起来的完整步骤这部分我会把从拿到源码到浏览器看到页面的完整流程走一遍。这套系统用到 JDK 8 或 11、Maven、Node.js 和 MySQL环境变量没配好的话第一步就会卡住。你先在命令行里敲 java -version 和 mvn -v能正常输出版本信息再继续否则先去把 java 环境变量配置补上JAVA_HOME 指向 JDK 安装目录PATH 里加上 %JAVA_HOME%\bin配完重启命令行窗口才生效。4.1 后端启动导入数据库、配置数据源、Maven 运行第一步是导入数据库脚本。用命令行或者 Navicat 都行执行 sql 目录下的 init.sql系统会自动建库建表并写入初始管理员账号。# 在项目根目录执行把 init.sql 导入本地 MySQL mysql -uroot -p sql/init.sql导入成功后修改后端配置文件 application.yml。这里是最容易踩坑的地方MySQL 的地址、端口、用户名密码必须和你的本地环境完全一致。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/select_course?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.selectcourse.entity jwt: secret: abc123def456ghi789url 里的 useUnicodetruecharacterEncodingutf8 是中文显示的关键少了它存进去的中文可能变成问号serverTimezoneAsia/Shanghai 是 MySQL 8 连接必须带的时区参数不带会报时区错误。jwt.secret 是签名的密钥建议改成一段足够长的随机字符串。配置改好后启动后端cd backend mvn spring-boot:run看到 Started Application in x.xxx seconds 的日志就说明启动成功。先用浏览器直接访问 http://localhost:8080/api/course/list能返回 JSON 就说明后端和数据源没问题可以进行前端联调了。如果你需要把后端打成一个 jar 包丢到服务器上部署用以下命令cd backend mvn clean package -DskipTests java -jar target/select-course-0.0.1-SNAPSHOT.jar注意打包产物在 target 目录下jar 包名字取决于 pom.xml 里的 artifactId 和 version。用 -DskipTests 是避免测试类里如果有数据库连接相关的用例在打包阶段因为连不上测试库而直接失败。本地开发用 mvn spring-boot:run 就行生产部署再用 jar 包方式。4.2 前端启动安装依赖、配置代理、运行 Vue 开发服务前端这边先装依赖然后配置 API 代理。为什么要配代理因为开发时前端跑在 3000 端口后端跑在 8080 端口浏览器直接跨域访问会被拦截。Vue CLI 的 devServer.proxy 可以把 /api 开头的请求转发到后端从根上规避跨域问题。// vue.config.js 开发环境代理配置 module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } };cd frontend npm install npm run servenpm install 下载依赖时如果卡住大概率是网络问题可以把 registry 切换为国内镜像源再装。npm run serve 启动后打开 http://localhost:3000登录页能正常渲染就说明前后端已经连通。验证方法很简单打开浏览器 F12在 Network 面板里看登录请求状态码 200 且返回了 token就说明代理生效了。这里有一个容易让新手懵的点前端所有的 api 请求地址都以 /api 开头但后端 controller 的路由也是 /api 开头代理配置里 pathRewrite 没有改路径所以请求原样转发。如果你的后端接口路径不带 /api 前缀就需要用 pathRewrite 把前缀去掉再转发具体规则以源码里的接口路径为准。4.3 功能验证清单登录、建课、选课、看统计系统跑通之后建议按下面这份清单走一遍确认每个核心功能都正常。我一般习惯先验证预期路径再验证异常路径。用管理员账号登录确认能进入管理端页面用管理员身份新建一门课程设置课程名、教师、学分、容量用学生账号登录在课程列表里找到刚建的课程执行选课操作再次点击同一门课的选课按钮确认系统提示重复选课把课程容量修改为 1切两个学生账号同时选这门课确认只有一个成功进入数据可视化页面确认柱状图能展示各课程的选课人数这份清单看起来简单但它是后面所有改造的回归基准。改一次代码跑一遍清单能保证你没把原有功能改坏。尤其是并发选课那个场景建议用两个浏览器窗口、两个不同角色的账号去验证比单纯在 Swagger 里点一次覆盖得全面。5. 避坑指南选课系统跑起来之后最常见的五个问题代码跑通只是开始真正耗时间的是各种环境问题。以下是我拆这套系统时实际踩过、也看别人反复踩的问题按现象、原因、解决的顺序摆出来。5.1 现象npm run serve 启动失败报错 vue-cli-service 不是内部或外部命令现象很直接命令行提示 vue-cli-service 找不到。不注意看的人会以为是 Node.js 没装好其实原因多半是 npm install 没执行成功node_modules 不完整。项目压缩包从别的机器拷贝过来残留的依赖目录最容易引发这个问题。解决方法是先删掉 node_modules 和 package-lock.json重新执行 npm install。注意 npm install 报错时要看清是哪个依赖出了问题最常见的翻车点是 node-sass 这类需要原生编译的包它要下载二进制文件在部分网络环境下容易失败换成 sass 实现或者把 Node 版本降到项目要求的版本就能过。从那以后我每次拿到新项目第一件事就是清空依赖目录重装省得被残留依赖坑一晚上。5.2 现象登录请求返回 401但接口直接在浏览器里访问是正常的后端接口用浏览器访问能返回数据说明后端逻辑没问题问题出在前端请求没带 token或者 token 被拦截器判定无效。常见原因是前端登录成功后token 没有正确注入到 axios 的统一请求头。解决方法是打开浏览器的 Network 面板点任意一个需要认证的请求看请求头里有没有 Authorization 字段。如果没有去前端代码里找 axios 拦截器确认是否在拿到 token 后执行了类似 config.headers.Authorization Bearer token 的赋值。注意后端拦截器要求的是 Bearer 开头大小写和空格都不能错写成了 Bearer 少个空格后端解析不到 token 也会返回 401。5.3 现象选课接口抛异常但数据库里还是多了一条选课记录这是事务没生效的经典症状。代码里确实写了 Transactional但事务没生效的常见原因有两个。其一是 Transactional 加在同类内部方法互相调用的场景Spring AOP 代理不生效事务被静默跳过其二是业务方法内部 catch 了异常但没重新抛出事务感知不到错误自然不触发回滚。解决办法是让选课方法放在独立的 service 类里不要在同类中互相调用异常统一在最外层处理内层方法只负责 throw。还有一个细节容易被忽略Transactional 必须写成 Transactional(rollbackFor Exception.class)否则自定义业务异常作为受检异常发生时不一定会触发回滚数据就在你眼皮子底下留下来了。5.4 现象后端启动报端口被占用8080 已经在使用中开发机上有多个项目同时跑的时候最容易撞这个。Spring Boot 内嵌的 Tomcat 默认监听 8080另一个服务或某个中间件占了同一个端口启动就直接报错。解决方法是改后端端口把 application.yml 里的 server.port 改成 8081 之类的不常用端口同时同步修改前端 vue.config.js 里 proxy target 的地址。注意改端口不是只改一个地方前端代理、后端配置、如果有 CORS 白名单的话还要把新端口加进去三处一致才不会出问题。启动前先跑一下 netstat -ano | findstr 8080能看到占用端口的进程 PID再决定是关进程还是换端口。5.5 现象接口查询返回的数据中文乱码乱码问题在本地开发环境最常见原因通常是连接 MySQL 时没有指定 UTF-8 编码。第 4.1 节强调的 jdbc:mysql://localhost:3306/select_course?useUnicodetruecharacterEncodingutf8 这一段如果漏了 characterEncodingutf8或者建库时没指定 utf8mb4中文读写就会乱。解决方法是先确认建库语句用的是 utf8mb4再把 JDBC URL 补上编码参数重启后端。注意如果数据库里已经存入了乱码数据改配置只对新数据生效旧数据需要手动清理后重新插入。提示MySQL 的 utf8 和 utf8mb4 是两回事emoji 这类四字节字符必须用 utf8mb4 才能正常存取课设阶段直接统一用 utf8mb4 最省事。6. 把课设源码变成你的毕设四条可见的进阶改造路径这套系统的价值在于骨架完整、边界清晰但它离一个能写进简历的生产级系统还有几步距离。以下四个方向是我认为投入产出比最高的改造路径。6.1 权限模型从两角色扩展到 RBAC系统现在的权限只分管理员和学生两种角色改造成本不高。引入角色表和权限表sys_user 只保留 role_idpermission 表存菜单和操作权限前端路由守卫根据权限动态生成菜单。这样加一个「教师」角色不用改代码只加数据和权限配置就行。6.2 课程表补充学期与选课时间窗口真实选课系统里有学期、开课周次、选课时间段。可以在 course 表增加字段ALTER TABLE course ADD COLUMN semester VARCHAR(20), -- 例如 2025-2026-1 ADD COLUMN start_week INT, ADD COLUMN end_week INT;选课校验里加上当前日期必须在选课窗口内的判断。这块改动难度不大但会让事务逻辑更贴近真实业务答辩时也更有话可说。6.3 可视化从裸图表升级为仪表盘把统计接口做成聚合视图一次返回课程容量利用率、各学院选课率、退课趋势等多个指标前端用 Vue 组件把这些指标组织成仪表盘页面。改造重点在后端聚合查询的 SQL 效率和前端组件的解耦这两点正好是简历里能展开讲的内容。6.4 给选课加一个唯一索引之外的并发兜底条件更新已经能挡住大部分超选场景更稳妥的方案是引入 Redis 分布式锁或者用悲观锁 SELECT ... FOR UPDATE。如果没条件上 Redis直接在 selectCourse 方法里先锁住课程行再执行插入也是可接受的课设方案。// 悲观锁写法先锁课程行再执行校验和插入 Course course courseMapper.selectByIdForUpdate(courseId); if (course null || course.getSelectedCount() course.getMaxCapacity()) { throw new BusinessException(课程不存在或已选满); }每次改造完我都会强制自己走一遍第 4 章的验证清单尤其是并发选课那个场景必须亲眼看到第二个学生被挡下来才放心。从完整跑通到逐步改造这套源码你花两个晚上就能吃透之后无论是交课设还是准备简历心里都有底。希望这篇拆解帮到你。本文还有配套的精品资源点击获取