SpringBoot3+Vue3学生选课管理系统从零到答辩完整教程

发布时间:2026/9/29 21:07:07
SpringBoot3+Vue3学生选课管理系统从零到答辩完整教程 刚帮一个学弟把SpringBoot3 Vue3的学生选课管理系统从零到一跑通他之前连Maven是什么都不知道最后居然顺利过了毕业答辩。写这篇文章的念头就是那时候冒出来的。市面上的教程要么只讲前端要么只贴后端代码数据库脚本和文档往往要加群才给出了问题也没人管。真正能让你从“不会”到“能答辩”的完整链路其实很缺。我自己在做这个项目的时候也踩了不少坑比如SpringBoot3的jakarta包名迁移、Vue3组合式API的响应式丢失问题、Maven依赖冲突每一个都能卡你好几天。这篇就把我实际开发中用到的、能够跑通的完整方案拆开来讲。你不需要有很强的编程基础只要照着思路一步步来把核心逻辑吃透毕业设计和答辩都不会是问题。1. 选题拆解与整体方案设计1.1 为什么选“学生选课管理系统”做毕业设计先说选题这件事。每年毕业设计老师手上那一堆题目里学生选课管理系统几乎是“常青树”。原因很简单业务场景清晰技术覆盖全面规模大小合适。从业务上看选课系统涉及三种角色学生、教师、管理员。学生要登录、选课、退课、查成绩教师要登录、开课、录入成绩管理员要管理学生信息、教师信息、课程信息。这已经涵盖了典型的RBAC权限模型。从技术上看它需要完整的CRUD、分页查询、多表关联、事务处理还要应对并发选课这种经典场景。对一个本科或专科的毕业设计来说这个复杂度恰到好处——太简单显得没工作量太难又担心做不完。最关键的是它好讲。答辩的时候老师一定会问“你这个系统的亮点是什么”“数据库为什么这么设计”“选课超卖问题怎么解决的”。选课系统的每一个模块都是一个可以展开讲五分钟的知识点你不会冷场。1.2 技术栈确定的背后逻辑这里直接用我最终确定并跑通的方案层次技术选型主要用途后端核心SpringBoot 3.2.x项目基础框架自动配置简化开发持久层MyBatis-Plus 3.5.x数据库操作提供通用Mapper大幅减少SQL编写数据库MySQL 8.x数据存储事务支持好用户群体大前端框架Vue 3.4.x Vite构建前端页面组合式API开发效率高UI组件Element Plus提供现成的表格、表单、弹窗组件不用手写CSS权限JWT Spring Security登录认证与接口权限控制构建工具Maven 3.9.x项目依赖管理和后端打包为什么在SpringBoot2和3之间选了3现在已经是2025年了SpringBoot3是绝对的主流。虽然网上SpringBoot2的教程更多但3在性能和安全性上有明显提升而且毕业设计用新版本答辩老师问起来的时候你可以说“我选用了目前主流的SpringBoot3版本”。当然代价是——你踩坑时搜到的很多旧教程不能用这个我后面会详细讲。为什么用MyBatis-Plus而不是直接用MyBatis或JPA对于零基础的学生MyBatis-Plus的BaseMapper直接帮你把单表CRUD写好了你不需要手写每一句SQL。选课系统大部分是单表操作即使有联表查询写几个XML映射文件也完全够用。JPA虽然也能做但默认的懒加载和级联操作在只学了半桶水的情况下容易搞出N1查询问题不太好调试。1.3 系统功能模块与整体架构整个系统按角色划分为三大模块我整理成一张功能清单你跟着这个清单开发就不会漏功能管理员端学生信息管理新增、修改、删除、按学号或姓名模糊搜索、重置密码教师信息管理新增、修改、删除、按教师编号或姓名搜索课程信息管理新增、修改、删除、课程状态控制开放选课/关闭选课选课记录查看查看所有学生的选课记录可以手动退课教师端登录与个人信息维护课程管理开设新课、修改自己的课程信息、查看自己课程的学生选课情况成绩管理为选了课的学生录入成绩、修改成绩学生端注册与登录学号密码登录首次需要注册选课浏览所有开放选课的课程点击选课同一门课不能重复选退课在选课时间内可以退课退课后学分自动释放我的课表查看自己已选课程和成绩前后端通过RESTful API通信后端返回统一的JSON格式前端用Vue3接收后渲染。整个项目的分层结构是标准的三层架构后期我会详细展开。2. 数据库设计与核心表结构拆解2.1 数据库设计的基本原则很多零基础的同学拿到项目先写代码再想数据库这是本末倒置。数据库是系统的地基地基打不好后面代码写再多都是空中楼阁。我的习惯是先画表结构再写实体类再写Mapper最后写Service和Controller。选课系统的数据库设计遵循几个基本原则第一宁可多拆表不要一表多存。比如学生有学号、姓名、班级、系别如果你把课程名和教师名都存在选课记录表里就会出现数据冗余。更新了教师名称所有关联的选课记录还要跟着改非常麻烦。第二所有关联字段都要建索引。选课记录表里的student_id和course_id是高频查询字段不建索引的话学生一多查询就会变慢答辩时候老师如果问起来也好解释。第三主键不要用自增用逻辑主键。为什么自增主键在分布式或数据迁移时容易出问题。我这里用的是雪花算法生成的Long类型IDMyBatis-Plus支持ASSIGN_ID策略你不需要手动生成框架自己会处理。2.2 四张核心表的结构说明整个系统一共需要四张核心表加一张角色表下面是我实际用的表结构和设计理由每张我都标了注意点管理员表 sys_admin字段名类型说明idbigint主键雪花算法usernamevarchar(50)登录用户名passwordvarchar(100)BCrypt加密后的密码namevarchar(50)姓名create_timedatetime创建时间学生表 sys_student字段名类型说明idbigint主键student_novarchar(20)学号唯一索引passwordvarchar(100)登录密码namevarchar(50)姓名gendervarchar(10)性别class_namevarchar(50)班级majorvarchar(50)专业create_timedatetime创建时间教师表 sys_teacher结构与学生表类似字段改成teacher_no、title职称、department所属院系。课程表 course字段名类型说明idbigint主键course_codevarchar(20)课程编号唯一course_namevarchar(100)课程名称teacher_idbigint授课教师ID外键关联教师表creditdecimal(3,1)学分比如2.0、3.5max_studentint选课人数上限selected_countint已选人数statustinyint0-关闭 1-开放course_timevarchar(50)上课时间如“周一 3-4节”选课记录表 course_selection字段名类型说明idbigint主键student_idbigint学生IDcourse_idbigint课程IDselected_timedatetime选课时间scoredecimal(4,1)成绩教师录入默认NULLstatustinyint0-已退课 1-在读这个表就是学生和课程之间多对多关系的中间表同时这个表还承担了成绩存储的功能。另外我特别强调student_id和course_id要建联合唯一索引这是防止同一学生重复选课的最后底线。2.3 数据库脚本与初始数据准备你拿到的源码包里面会有一个sql文件夹下面有初始化脚本。我自己实际跑通的版本里初始化脚本做了两件事创建库表和插入初始数据。初始数据包括一个管理员账号admin密码经过BCrypt加密、几个测试学生账号、几个教师账号和几门课程。我把所有的SQL都写在一个init.sql里执行顺序是建库、建表、插数据。注意MySQL8默认字符集是utf8mb4建表的时候一定要用DEFAULT CHARSETutf8mb4不然后续插入中文数据会乱码。我这里用的是utf8mb4 utf8mb4_general_ci排序规则足够用了。建完表之后我建议你自己验证一下用Navicat或DataGrip连上数据库手动插几个测试数据然后用SELECT * FROM course;查一下。这一步看着多余但能确认环境没问题省得以后写代码出了问题分不清是代码的锅还是数据库的锅。3. 后端开发SpringBoot3核心模块实现3.1 环境准备与项目初始化先把自己的环境检查一遍这是我带人做项目的第一步也是最容易翻车的一步JDK 17以上SpringBoot3强制要求别用JDK8编译都过不了Maven 3.6IDEA 2023或2024版本MySQL 8.xNode.js 18前端开发用确认JDK版本在命令行执行java -version如果是17以上就可以。IDEA打开后配置文件里确认Project Structure里的SDK确实指向了17同时设置里把Java Compiler的Target bytecode version改成17。项目初始化用IDEA自带的Spring Initializr。Group填com.exampleArtifact填course-selectionJava版本选17或21依赖选择Spring Web、Spring Security、MyBatis Framework、MySQL Driver、Lombok、Validation。这一步能省掉后面很多手动配pom的时间。生成完项目后在pom.xml里加上MyBatis-Plus的依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency这里有一个非常关键的点MyBatis-Plus的mybatis-plus-boot-starter是给SpringBoot2用的SpringBoot3必须用mybatis-plus-spring-boot3-starter。我第一次在这个上面栽了跟头启动直接报ClassNotFoundException折腾了一晚上才发现是兼容性问题。另外JWT用的是jjwt 0.11.5这个版本对Java17兼容很好。3.2 项目目录结构与代码分层后端项目的包结构按职责划分参考我实际用的这套结构com.example.course ├── config # 配置类 │ ├── CorsConfig.java # 跨域配置 │ ├── MybatisPlusConfig.java # 分页插件配置 │ └── SecurityConfig.java # Spring Security配置 ├── controller # 接口层 │ ├── AdminController.java │ ├── StudentController.java │ ├── TeacherController.java │ └── CourseController.java ├── entity # 实体类 │ ├── Student.java │ ├── Teacher.java │ ├── Course.java │ └── CourseSelection.java ├── mapper # 数据访问层 ├── service # 业务逻辑层 │ └── impl # 实现类 ├── common # 公共组件 │ ├── Result.java # 统一返回结果 │ └── JwtUtil.java # JWT工具类 └── CourseApplication.java为什么要分这么多层答辩的时候这就是你的加分项。Controller只管接收请求和返回数据不写业务逻辑Service层存真正的业务代码比如选课要判断人数、要防重复Mapper层只和数据库打交道。这样分层的好处是职责单一出了问题知道去哪里找以后扩展也方便。Controller层的代码尽量瘦记得加RestController和RequestMapping统一接口前缀。Service层要面向接口编程写一个接口和一个实现类这也是被很多教程忽略但对答辩很重要的习惯。3.3 统一返回结果与跨域配置前后端分离项目中前后端要约定好数据格式。我这里定义了一个ResultT类Data public class ResultT { private Integer code; // 200成功4xx业务失败5xx系统异常 private String msg; // 提示信息 private T data; // 响应数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }前端拿到code200就正常处理数据code500就弹一条提示。这套格式很简单但能避免很多“前端不知道有没有成功”的尴尬。跨域配置也很重要。你的前端跑在localhost:5173后端跑在localhost:8080浏览器会拦截跨域请求你需要在后端加一个CORS配置。最简单的办法是写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }同时在前端的vite.config.js里配置代理把/api开头的请求转发到后端export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这样前端请求/api/course/list后端实际收到的是/course/list不跨域省心。3.4 登录认证与接口权限控制登录认证这块一开始很多人觉得难其实拆开了就是把两个核心问题解决掉怎么生成凭证怎么校验凭证。这里用JWTJSON Web Token方案吐个通俗的比喻JWT就像一张进入景区的门票门票上写着你的身份信息游客只要拿着门票景区就不用每次重新查你的身份证了。我在JwtUtil工具类里封装了三个方法生成Token、解析Token、判断Token是否过期。生成Token时把userId和角色类型放进去设置过期时间默认2小时。前端每次请求在请求头带上Authorization: Bearer xxx后端通过拦截器统一校验。Spring Security的配置类大概是这个逻辑Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/student/login, /student/register, /teacher/login, /admin/login).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/teacher/**).hasRole(TEACHER) .requestMatchers(/student/**).hasRole(STUDENT) .anyRequest().authenticated() ) .exceptionHandling(ex - ex.authenticationEntryPoint((req, resp, e) - { resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); })); return http.build(); } }提醒一个新手很容易踩的坑Spring Security拦截了请求之后如果你直接用POST请求登录接口默认还会开启CSRF防护导致403。记得显式关闭CSRF代码里那一行csrf.disable()就是这个作用。另一个坑是如果你自定义鉴权失败处理器或放行了某个接口但配置顺序不对后面会全部被拦截掉排查的时候先看permitAll()有没有生效。3.5 选课业务与并发防超卖逻辑选课是整个系统的核心业务也是答辩时老师最喜欢抓的点。你先想清楚选课需要满足哪些规则同一学生不能重复选同一门课课程已选人数不能超过上限选课后课程人数要实时更新为了防止“多个学生同时选同一门课导致超卖”我在代码里用了乐观锁。具体实现是在course表加一个version字段更新已选人数的时候带上版本号判断用MyBatis-Plus的Version注解标记。TableField(fill FieldFill.INSERT) Version private Integer version;在配置类里注册乐观锁插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }选课Service的实现逻辑伪代码如下Transactional(rollbackFor Exception.class) public ResultString selectCourse(Long studentId, Long courseId) { // 1. 查课程信息 Course course courseMapper.selectById(courseId); if (course null || course.getStatus() 0) { return Result.error(课程不存在或未开放选课); } // 2. 判断是否已选过 Long count courseSelectionMapper.selectCount(new LambdaQueryWrapperCourseSelection() .eq(CourseSelection::getStudentId, studentId) .eq(CourseSelection::getCourseId, courseId) .eq(CourseSelection::getStatus, 1)); if (count 0) { return Result.error(你已经选过这门课了); } // 3. 判断人数是否已满 if (course.getSelectedCount() course.getMaxStudent()) { return Result.error(课程人数已满); } // 4. 插入选课记录 CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setSelectedTime(new Date()); selection.setStatus(1); courseSelectionMapper.insert(selection); // 5. 更新课程已选人数 course.setSelectedCount(course.getSelectedCount() 1); courseMapper.updateById(course); return Result.success(选课成功); }注意看这里的Transactional注解这个非常关键。如果没有事务选课记录插入成功、但课程人数更新失败时数据就不一致了。加了事务之后要么全部成功要么全部回滚。还有一个隐藏问题为什么不用同步锁因为我这里是单机部署用ReentrantLock就能解决并发问题。但如果部署在多台服务器上每个进程各自一把锁是锁不住全局的这时候需要用分布式锁。毕业设计的场景下写清楚“当前是单机部署用乐观锁解决超卖问题”再提一句“生产环境需要分布式锁”老老师会觉得你有思考。3.6 分页查询与条件搜索管理员查看学生列表、学生查看课程列表这些都是高频功能。直接全部查出来返回给前端的话数据量一大就卡死。用MyBatis-Plus的分页插件几行就能实现。配置分页插件在3.5.5版本是Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Service层的实现public PageCourse getCourses(int page, int size, String name) { PageCourse p new Page(page, size); LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(name)) { wrapper.like(Course::getCourseName, name); } wrapper.orderByDesc(Course::getId); return courseMapper.selectPage(p, wrapper); }Controller层接收参数并返回public ResultPageCourse list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String name) { return Result.success(courseService.getCourses(page, size, name)); }前端Element Plus的el-pagination组件正好对接这套参数当前页和每页条数传给后端就行。4. 前端开发Vue3核心页面与交互4.1 Vue3项目初始化与基础配置前端脚手架用Vite创建这是目前Vue3生态最主流的构建工具。打开命令行工具在项目根目录执行npm create vuelatest里面选上Router和Pinia状态管理其他默认就好。创建完成后进入项目目录安装依赖npm install npm install element-plus axios为什么用Element Plus它的表格、表单、分页、弹窗组件都是现成的零基础也能快速搞定一个看着很专业的后台管理系统。如果是自己手写这些组件没个三四周搞不定而且丑。在main.js里全局引入Element Plusimport { createApp } from vue import App from ./App.vue import router from ./router import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(router) app.use(createPinia()) app.use(ElementPlus) app.mount(#app)4.2 路由配置与页面骨架设计按照角色拆分路由整体结构如下/src ├── api/ # 接口请求封装 │ ├── student.js │ ├── teacher.js │ └── course.js ├── router/index.js # 路由配置 ├── store/ # Pinia状态管理 ├── views/ │ ├── Login.vue │ ├── admin/ │ │ ├── AdminLayout.vue │ │ ├── StudentManage.vue │ │ ├── TeacherManage.vue │ │ └── CourseManage.vue │ ├── student/ │ │ ├── StudentLayout.vue │ │ ├── SelectCourse.vue │ │ └── MyCourse.vue │ └── teacher/ │ ├── TeacherLayout.vue │ ├── TeacherCourse.vue │ └── ScoreManage.vue路由配置里需要加一个“守卫”。没有登录就访问内部页面直接跳转到登录页。实现方式是router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })同时每个Layout组件内部左侧菜单栏根据角色显示不同菜单。这里用Pinia存一下当前登录用户的信息和角色页面刷新后还能保持登录状态。4.3 登录页面实现登录页面是用户第一个见到的页面UI上不能太寒酸。Element Plus的el-form加基本的校验规则就够了。一个需要处理的细节是登录后根据用户角色跳转到不同的首页。我这里后端登录接口返回的数据里带了一个字段role前端登录成功后判断角色然后router.push(/admin)或router.push(/student)。前端调用登录接口的代码所有接口用axios统一封装同时加拦截器import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )4.4 选课页面的核心交互逻辑选课页面的核心是一个课程表格加一个“选课”按钮。写代码的过程有两个要点第一表格的el-table绑定课程列表数据el-pagination对接分页参数。每次点击“选课”先调用选课接口成功后再刷新当前页数据。第二根据课程状态动态控制按钮是否可用。如果selectedCount maxStudent按钮置灰并显示“已满”如果status 0显示“未开放”已选过的课程显示“已选”。这个逻辑不复杂但能体现出前端基本功。核心代码片段el-table :datacourseList stripe el-table-column propcourseCode label课程编号 width120 / el-table-column propcourseName label课程名称 / el-table-column propcredit label学分 width80 / el-table-column propselectedCount label已选人数 width100 template #defaultscope {{ scope.row.selectedCount }} / {{ scope.row.maxStudent }} /template /el-table-column el-table-column label操作 width120 template #defaultscope el-button typeprimary sizesmall :disabled!canSelect(scope.row) clickhandleSelect(scope.row) {{ getBtnText(scope.row) }}/el-button /template /el-table-column /el-table我踩过一个坑scope.row直接往后端传递时如果请求头没有带上token会被后端拦截返回401前端弹了一堆“未登录”的错误。后来才意识到是axios拦截器没生效排查发现localStorage.getItem(token)返回了null是登录成功后存token那一步写错了位置。排查思路就是Controller加日志、前端打印请求头很快就定位了。4.5 API接口文件封装规范强烈建议把每个模块的接口单独写一个文件比如src/api/course.jsimport request from /utils/request export function getCourseList(data) { return request({ url: /course/list, method: get, params: data }) } export function selectCourse(courseId) { return request({ url: /course/select, method: post, data: { courseId } }) } export function dropCourse(courseId) { return request({ url: /course/drop, method: post, data: { courseId } }) }这样做的最大好处是当后端接口改动时你只需要改一个文件。而且组件里的调用代码会非常干净只关注页面逻辑不用关心URL拼接和传参。5. 调试部署与答辩避坑指南5.1 本地联调的三板斧前后端分离项目的联调阶段最常见的三个问题是接口返回404、接口返回500、跨域报错。其实排查思路都很固定。404的排查思路先看前端请求的URL和后端Controller映射的是否一致。比如前端请求/api/course/list通过代理转成了/course/list后端CourseController的RequestMapping值是/course方法上是/list那就能对应上。确认没问题后在Controller方法第一行加一个System.out.println(进来了)看请求有没有到达后端。如果没到达看代理配置如果到达了还404看方法路径。500的排查思路看IDEA控制台的异常栈。空指针还是SQL错误一眼就能看出来。这里推荐先用Slf4j加日志关键方法打印入参和出参。比如查询学生列表接口打印入参page、size、name打印出参total和records。日志打出来问题自动就浮出水面了。跨域报错的排查思路看浏览器Network标签页如果请求是OPTIONS类型且响应里没有Access-Control-Allowed-Origin头就是后端跨域配置没生效。检查CorsConfig有没有被Spring扫描到。5.2 打包部署的完整流程虽然毕业设计可能只需要跑通本地但如果你能说出打包部署的流程答辩印象分会高很多。后端打包非常简单IDEA右侧Maven面板双击package然后到target目录下找到course-selection-0.0.1-SNAPSHOT.jar文件。打开命令行执行java -jar course-selection-0.0.1-SNAPSHOT.jar前提是MySQL已启动且application.yml里的数据库配置正确。如果你的项目里配置了数据库密码打包后部署时想改配置可以加启动参数覆盖java -jar course-selection-0.0.1-SNAPSHOT.jar --spring.datasource.password你的密码前端打包npm run build打包完成后dist文件夹里面就是静态文件可以用Nginx部署。Nginx配置的关键是把静态文件路径指向dist目录把/api请求代理到后端8080端口。server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }5.3 答辩现场的高频问题与应答策略答辩答辩答和辩各占一半。我见过太多人代码敲得麻溜一到台上被问几句话就卡壳。这里提前准备几个高频问题背下来不吃亏。“你的系统有哪些功能角色”答整个系统分为管理员、教师、学生三个角色。管理员负责基础数据的维护包括管理学生和教师信息、管理课程信息教师可以开设课程、查看选课情况、录入成绩学生可以浏览课程、选课退课、查看成绩。每个角色登录后看到的功能菜单完全不同权限控制是通过Spring Security拦截器配合JWT实现的。“选课并发冲突你怎么解决的”答我用了乐观锁。课程表设置version字段更新已选人数时框架自动比对版本号冲突就更新失败。这种方式在并发量不高的场景下够用从理论层面来说如果要支撑更大的并发量可以考虑用Redis分布式锁或者数据库悲观锁。这里要顺着老师的话往下讲显得从容。“为什么选SpringBoot3不选SpringBoot2”答SpringBoot3是一个主流的长期支持版本基于Spring Framework6底层使用Jakarta EE9规范性能和安全性都有大幅提升。同时SpringBoot3要求Java17起步这是一个长期支持的LTS版本新项目选新版本有利于以后维护和扩展。用词尽量往规范上靠。“你的数据库为什么要分四张表”答这是基于第三范式的设计。学生、教师、课程、选课记录各自独立成表避免数据冗余。选课记录表是学生表和课程表的关联表同时扩展了成绩字段和选课状态字段。独立表的好处是维护方便修改课程名称只需要改course表不用改动其他表的引用。还有个答辩细节带一份数据库的ER图。提前用PowerDesigner或者Draw.io画好答辩时展示出来老师一看就知道你系统设计是下了功夫的。我学弟答辩时就是靠这个加了不少分。5.4 常见运行报错速查表我整理了开发过程中最常遇到的报错和解决办法你在做的时候照着排查就行错误现象原因解决方案启动直接报ClassNotFoundException: jakarta.servletSpringBoot3依赖跟Servlet API不兼容检查pom中依赖版本改用spring-boot-starter-web自带依赖Maven下载依赖失败网络问题或镜像源不稳定配置阿里云镜像IDEA中Setting的Maven仓库换成https://maven.aliyun.com/repository/public前端页面请求401未携带token或token过期检查登录成功后是否存token到localStorage检查axios拦截器是否设置请求头Data too long for column数据库字段长度不够修改表结构如name字段从varchar(50)加大到varchar(100)中文乱码建表或连接字符串没指定utf8mb4application.yml中数据库连接URL加上?useUnicodetruecharacterEncodingUTF-8Could not autowire报错IDEA识别问题或没有配置Mapper扫描启动类加MapperScan(com.example.course.mapper)同时确认Mapper接口上有Mapper注解端口被占用8080端口被其他程序占用命令行执行netstat -ano | findstr 8080找到PID并杀掉或改配置文件端口6. 调试工具推荐与我的个人经验最后分享两个后端开发绕不开的调试经验。第一个是接口测试工具。很多人用Postman但如果你懒得单独开一个软件IDEA自带的HTTP Client非常好用。在项目里建一个http目录写好测试请求随时可以重放接口GET http://localhost:8080/api/student/login Content-Type: application/json { username: 20230101, password: 123456 }第二个是Lombok的使用。项目里大量使用了Data注解你要是配置不对会导致IDE报错说找不到getter/setter。检查一下IDEA的Settings里Plugins是否安装了Lombok插件同时确认注解处理Annotation Processing已经开启。再讲讲我个人的体会。带人做完这个项目后最深的感受是零基础的同学遇到困难时容易怀疑自己——HTTP状态码看不懂、报错信息全是英文、框架自动装配的机制云里雾里。但事实上这些难点不是因为个人能力差而是因为知识体系里缺少了“工程化”的那一环。你不需要一开始就理解SpringBoot自动配置的底层原理你只需要知道配置好依赖按规范写代码框架会自动帮你完成绝大部分工作。剩下的问题靠日志、靠搜索引擎都能解决。我当时给学弟定的计划表你可以参考一下阶段任务预计时间第1周环境准备跑通一个最简单的登录接口1天第2周数据库设计完成所有建表脚本2天第3-4周后端全部接口开发14天第5-6周前端全部页面开发与联调14天第7周打包部署整理文档准备答辩3天这套节奏坚持下来时间完全来得及。最后再说一个实用的小技巧。开发过程中一定要用Git管代码每完成一个模块就提交一次。不用把代码传到GitHub上本地建一个仓库就行。这样做的好处是改崩了随时可以回退到上一个能跑的版本不用从头再来。我自己吃过这个亏搞了半天结果把好的代码改坏了最后的解决方案是删掉重来。从那以后我每做一个功能点就commit一次这个习惯让我少走了很多弯路。学生选课管理系统这个题目说到底是毕业设计里的一张安全牌。技术点不偏门业务逻辑不复杂但要做到完整、规范、有亮点还是需要花心思的。上面这些内容从数据库建模到后端实现再到前端交互是我实际跑通过一遍的完整方案你照着走大概率能在两周内把代码写完然后拿下一份漂亮的毕业论文和答辩表现。