
学生选课系统这几乎是每个高校信息化建设的必做项目也是Java全栈入门绕不开的经典场景。SpringBoot Vue.js MyBatis MySQL这套组合在课程设计、毕业设计甚至中小型商用项目中反复出现不是没有理由的。它覆盖了一个业务系统该有的全部要素前端展示交互、后端接口开发、关系型数据库设计、业务事务控制知识点密度适中做一遍能沉淀出一整套可复用经验。我在帮人跑这种项目时发现网上流传的很多“源码”其实问题都不少——数据库表设计缺约束、选课接口不带事务、分页插件配置过时、前端组件彼此状态不同步。真正能直接跑起来、又讲得清原理的完整案例反而稀缺。这篇就按我对这类系统的最佳实践梳理一遍从总体设计思路到数据库建模、后端接口实现、前端Vue页面再到部署上线和踩坑记录按从头做一个项目的方式顺序写。想快速跑通一个能演示的完整选课系统照这篇做完就能交付。1. 项目整体设计与技术选型1.1 为什么还是选SpringBootVue.js这套组合先说个实在观点选课系统这类围绕增删改查展开的业务用SpringBootVue.js是性价比最高的方案之一不是因为它最“新潮”而是因为它非常适合这个业务形态的复杂度。从后端角度SpringBoot解决了传统SSH/SSM项目里繁琐的XML配置问题内嵌Tomcat让打jar包直接跑这对刚接触企业级开发的同学尤其友好。它提供的starter机制让数据库操作spring-boot-starter-jdbc MyBatis整合、Web接口开发spring-boot-starter-web、统一异常处理等能力都能快速组装不用重复造轮子。从前端角度Vue.js用单文件组件方式解决了页面逻辑和DOM操作耦合问题。选课系统虽然业务不复杂但页面交互不少课程列表要支持条件筛选、选课按钮要实时反馈状态、个人课表要按周次渲染。Vue的响应式数据绑定让这些交互的代码量直接减少一个量级。配Element UI做后台管理界面开发效率比原生JS写DOM高太多。为什么不用更重的前后端不分离方案比如JSP或Thymeleaf模板。说实话如果只是做个简单演示模板方案更快但选课系统是一个典型的带权限角色区分的业务系统——学生端、教师端、管理员端看到的页面和操作完全不同。前后端分离后角色控制和页面渲染逻辑由前端路由处理后端只需要提供按角色鉴权后的REST接口职责边界更清晰也更贴近真实企业项目结构。1.2 项目模块划分与功能清单动手写代码之前一定要先做功能梳理。这个环节直接决定你的数据库表设计和接口设计跳过去后面肯定要来回返工。按我对同类系统的经验一套能演示、能答辩、能上线使用的选课系统至少覆盖这几个角色和功能模块学生端。登录后能看到当前学期的待选课程列表按课程名称或教师姓名筛选选课后能在“我的课表”里看到已选课程支持退选操作选课结果要有状态反馈比如选课时间和退课截止时间的限制。教师端。教师登录后可以维护自己主讲的课程包括课程名称、学分、上课时间、教室、课程容量可以查看自己课程的被选学生名单必要时调整课程状态比如停开、结课。管理员端。管理员管理基础数据学生信息、教师信息、课程信息的新增、修改、删除、重置密码。同时能查看全系统的选课统计数据——哪门课选的人最多、哪些课程容量一直空缺。非功能需求。密码不能明文存储统一加盐加密接口要做登录鉴权未登录不能访问选课接口选课高峰期要考虑并发问题不能出现超选现象。这些功能点对应到技术上就是一套标准的前后端分离单体应用加一个关系型数据库。模块划分我建议做成后端按controller/service/mapper/entity分包前端按views页面级组件、components复用组件、router、api接口封装、store状态管理分层。1.3 前后端分离架构下的数据流向理解数据怎么在前后端之间流动整个项目串起来就不慌了。选课系统一次完整的“学生选课”操作数据流向大概是这样的学生在Vue页面点击“选课”按钮。前端通过axios发POST请求携带学生的token和课程id请求后端接口 /api/student/selectCourse。后端Controller接收请求先经拦截器校验token合法性确认当前登录用户是学生且有选课权限。Service层开始核心业务逻辑校验课程是否存在、是否仍可选、是否已选过该课然后执行数据库插入选课记录并增加课程已选人数。MyBatis通过Mapper接口和XML映射将SQL语句执行到MySQL数据库。事务提交成功后逐层返回结果给前端前端更新页面上的课程状态。这套链路里最容易出问题的是第4步——业务校验和防超选的并发控制。这也是后面我会专门花篇幅讲的重点。2. 数据库设计选课核心逻辑怎么落地2.1 核心表结构设计与字段解析数据库表设计是整个项目的根基。很多网上能下载的选课系统源码问题往往出在表设计上没有任何约束数据脏到看一眼就想删项目。我一直坚持一点数据库层面能做约束的绝对不要只靠程序来控制。先列基础表按多年实操习惯我通常设计五张表用户表、学生信息表、教师信息表、课程表、选课记录表。用户表sys_user负责登录鉴权。字段设计id主键、username用户名唯一、password密码加密存储、role角色区分学生、教师、管理员我用整数枚举1学生2教师3管理员扩展性比字符串好、status状态1启用0禁用、create_time创建时间。如果选的表上区分度足够也可以学生和教师都走用户表关联各自信息表这样登录逻辑能统一处理。学生表student关联用户表student_no学号唯一、name姓名、gender性别、class_name班级、major专业、user_id关联用户表id。班级和专业的取值在课程设计中一般是字符串直接存如果需要做筛选分析再拆成字典表也不迟。教师表teacherteacher_no工号、name姓名、title职称助教、讲师、副教授、教授、department所属院系、user_id关联用户id。课程表course这是选课系统的核心表字段设计直接影响业务是否能正确运行。我列一下字段清单和每个字段存在的必要原因字段名类型说明idbigint主键自增course_namevarchar(100)课程名用于列表展示和模糊搜索teacher_idbigint关联教师表一位教师可主讲多门课creditdecimal(3,1)学分选课后成绩单要用course_timevarchar(50)上课时间存成可读文本如“周一3-4节”course_locationvarchar(100)上课地点capacityint课程容量上限selected_countint当前已选人数防超选的关键字段statustinyint选课状态0未开始1选课中2已结束create_timedatetime创建时间注意课程表没有直接把学生信息存进来而是通过选课表建立多对多关系。这一点很重要一门课对应多个学生一个学生选多门课正确建模方式一定是独立一张选课关联表。选课记录表course_selectionid主键、course_id课程id、student_id学生id、select_time选课时间、status选课状态1有效0退选留痕。在表设计阶段就加一个 UNIQUE KEY uk_course_student(course_id, student_id)这是防止学生重复选课的最底层安全保障。就算程序逻辑哪里有漏洞数据库也会把这条重复数据拦下来。2.2 防超选设计乐观锁还是悲观锁选课系统最容易在“最后一门课只剩下一个名额”时出问题。如果两个学生同时点击选课程序先查剩余名额发现还剩1个两人都执行插入——最后可能导致选课人数超过容量上限这就是“超选”。处理防超选常见两种方案。悲观锁是直接对课程行数据加锁SELECT ... FOR UPDATE让同一时间的其他选课操作排队等待。这种方式安全性最高但并发高时会有锁等待开销适合选课抢课高峰期的强一致场景。我更推荐用乐观锁思路在应用层控制。给课程表加一个version字段或者直接用 selected_count capacity 作为更新条件更新课程人数时执行带条件的UPDATE语句。举例UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count capacity然后判断这条UPDATE语句影响的行数——如果影响行数为0说明课程已经满员这次选课就不能进行。同时插入选课记录也在同一个事务里执行事务提交后要么课程人数和选课记录一致要么全部回滚。这套方案没有锁等待在选课场景下实测够用。2.3 初始化脚本与测试数据准备表结构设计完一定要准备一套完整的初始化SQL脚本包含建库、建表、默认数据。别嫌麻烦这一步能帮你省一整天联调时间。脚本里除了表结构至少要有一个默认管理员账号admin/admin123密码存加密值、一个演示学生账号、一个演示教师账号以及10门左右的课程数据。课程数据要覆盖特殊情况有容量已满的课程、有选课状态不同的课程这样前端才能完整测试各种按钮状态。MySQL建表时我习惯用InnoDB引擎、utf8mb4字符集。utf8mb4不是可选项是必须项——现在学生姓名里有生僻字或者表情符号都很正常utf8会直接报错。排序规则用utf8mb4_general_ci即可不需要上utf8mb4_unicode_ci对选课系统这个量级来说性能差异可以忽略。建表语句里所有时间字段建议用datetime不要用timestamp。timestamp有2038年问题且受时区影响datetime存储更直观对系统管理后台维护更友好。3. 后端核心实现SpringBootMyBatis实战3.1 后端工程结构与接口设计后端工程我习惯按下面的包结构组织清晰且贴近主流项目习惯com.example.selectcourse ├── controller // REST接口层 ├── service // 业务逻辑层接口实现 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象接收前端参数 ├── vo // 视图对象返回前端数据 ├── config // 配置类拦截器、CORS、分页插件等 ├── common // 通用类统一返回结果、异常处理、工具类 └── interceptor // 登录鉴权拦截器接口设计遵循RESTful风格按资源命名。选课系统核心接口我列一下这部分直接照用即可接口方法说明/api/auth/loginPOST登录返回token和用户角色/api/course/listGET课程列表支持分页和条件筛选/api/course/{id}GET课程详情/api/student/selectPOST学生选课参数courseId/api/student/cancelPOST学生退课参数courseId/api/student/myCoursesGET我的已选课程/api/teacher/course/publishPOST教师发布课程/api/admin/student/listGET管理员查看学生列表统一返回结果类Result很关键。所有接口都返回同一结构code200成功其他失败、message提示信息、data业务数据。这样前端axios拦截器只需要处理一次响应状态不用每个接口单独判断。我们项目里就这么干省掉了巨大的重复代码量。3.2 用户登录与Token鉴权实现登录功能是系统的入口。密码存储用BCrypt加密Spring Security里的BCryptPasswordEncoder可以单独拿来用注册时加密存储登录时校验明文和密文。数据库里的密码不应该是明文——我见过太多课程设计代码直接明文存密码这放在现在属于不可接受的低级问题。登录成功后返回一个token。选课系统这个量级用JWTJSON Web Token是不错的选择。为什么不用传统Session前后端分离后前端可能部署在和后端不同域名下Session配合CORS会话保持比较麻烦而Token无状态前端每次请求带在请求头里就行实现简单。在pom.xml中引入jjwt依赖登录成功时生成token放入响应体。安全起见token要设置过期时间一般24小时——选课系统不是超高频操作场景太短会让用户频繁登录体验差太长又不安全24小时比较合适。登录接口的核心逻辑// 登录成功生成JWT Token String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24*3600*1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();前端拿到token后存入localStorage持久化刷新不丢失每次axios请求在请求拦截器里自动带上Authorization头。后端用一个HandlerInterceptor拦截所有需要登录的接口从请求头解析并验证token验证失败直接返回401。不需要登录的接口只有登录接口本身在配置里放行即可。3.3 核心选课接口事务与防重详解这是整个后端最重要的代码没有之一。选课接口表面上是一次insert操作实际上涉及课程存在校验、状态校验、重复选课校验、容量校验、插入选课记录、更新课程人数六步操作。其中任何一步失败都要保证数据库不变所以必须加事务注解。直接在Service实现类方法上加 Transactional(rollbackFor Exception.class) 就足够了。指定rollbackFor Exception.class是防止个坑Spring默认只对RuntimeException回滚如果选课过程抛的是受检异常比如自定义BusinessException不继承RuntimeException不加这个参数事务就不会正确回滚。很多源码里不写这个参数线上出问题就是在这种细节上。先给出整个Service层的选课实现代码骨架Override Transactional(rollbackFor Exception.class) public Result selectCourse(Long studentId, Long courseId) { // 1.查询课程信息 Course course courseMapper.selectById(courseId); if (course null) { return Result.error(课程不存在); } // 2. 校验课程状态是否可选 if (course.getStatus() ! COURSE_STATUS_SELECTING) { return Result.error(当前课程不在选课时间内); } // 3. 校验是否已选过该课程 int count courseSelectionMapper.countByCourseIdAndStudentId(courseId, studentId); if (count 0) { return Result.error(您已选过该课程不能重复选择); } // 4. 防超选条件更新课程已选人数 int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { return Result.error(该课程名额已满); } // 5. 插入选课记录 CourseSelection selection new CourseSelection(); selection.setCourseId(courseId); selection.setStudentId(studentId); selection.setStatus(1); selection.setSelectTime(new Date()); courseSelectionMapper.insert(selection); return Result.success(选课成功); }对应Mapper里的SQL!-- 防超选的核心SQL条件更新 -- update idincreaseSelectedCount UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count lt; capacity /update这段代码里步骤4用的是条件更新而不是先查后更这是避免并发超选的关键点。我在本地模拟并发选课开20个线程同时选同一门只剩1个名额的课实测过加了这样的条件更新后只有一个线程能成功其他全部返回“名额已满”。如果按一般的先SELECT判断再UPDATE写法必然出现超选因为两个线程可能同时读到剩余1个名额。3.4 MyBatis分页插件PageHelper的配置与用法课程列表必须做分页这是后台管理系统的刚需。MyBatis最常用的分页方案是PageHelper插件但配置不当会出各种怪问题。首先在pom.xml引入依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency新版SpringBoot2.x/3.x老版本PageHelper有兼容问题1.4.7是目前我用得比较稳的版本。配置上在application.yml里加pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truereasonable: true的意思是在页码超出范围时自动校正到首页或末页而不是直接报错。用户体验上合理一些不会出现“点下一页突然白屏”的问题。使用方式很简单查询前调用PageHelper.startPage(pageNum, pageSize)紧跟着的第一次MyBatis查询会自动拼接LIMITPageHelper.startPage(pageNum, pageSize); ListCourseVO list courseMapper.selectCourseList(); PageInfoCourseVO pageInfo new PageInfo(list);踩坑提示PageHelper官方文档强调“紧跟后第一个查询会被分页”但实际使用中如果startPage之后还有其他逻辑代码先执行了其他查询第一个被分页的可能不是你想分页的那个查询。稳妥做法是startPage紧接着写在Mapper调用前面一行中间不夹任何东西。还有统计count的SQL在某些老版本会额外执行一次导致接口变慢1.4.x版本已经优化了不需要额外处理。4. 前端Vue.jsElement UI实现要点4.1 前端工程结构与路由权限设计前端我用Vue CLI或Vite创建项目配合Vue Router和Vuex/Pinia做路由和状态管理。Element UI或Element Plus做组件库。工程结构按页面维度组织src ├── api // 封装axios请求接口 │ ├── auth.js // 登录相关接口 │ ├── course.js // 课程相关接口 │ └── student.js // 学生选课相关接口 ├── router // 路由配置 ├── store // 全局状态用户信息、token ├── views // 页面组件 │ ├── Login.vue │ ├── StudentCourseList.vue │ ├── StudentMyCourse.vue │ ├── TeacherCourseManage.vue │ └── AdminStudentManage.vue └── utils └── request.js // axios实例封装一个容易忽略的问题是全局错误处理。在axios拦截器里统一处理401token失效跳转登录和500后端异常弹出Message提示各页面组件里就不用反复写try-catch了。这是实战项目必须的成熟做法课程设计里很多人忽略了这一层答辩时一旦token过期体验就很拉胯。路由权限设计上用Vue Router的路由守卫实现角色控制。路由meta里标记需要的角色守卫里判断当前登录用户的角色是否匹配不匹配就重定向到登录页或首页router.beforeEach((to, from, next) { const user store.state.user if (to.meta to.meta.role) { if (!user || user.role ! to.meta.role) { next(/login) return } } next() })4.2 选课列表页表格、筛选与选课交互选课列表页是学生端最核心的页面。我用Element UI的el-table展示课程列表顶部放筛选条件包括课程名称输入框和教师姓名输入框配合“查询”和“重置”按钮。课程表格列设计课程名称、教师、学分、上课时间、地点、容量、已选人数、操作按钮。已选人数这列有讲究当已选人数等于容量时行内按钮要置灰显示“已满”不能让学生点击后才被后端拒绝前端交互要先把明显不可操作的状态挡掉。操作按钮的状态逻辑const getButtonInfo (row) { if (row.selectedCount row.capacity) { return { disabled: true, text: 已满 } } if (row.selected) { return { disabled: true, text: 已选 } } if (row.status 0) { return { disabled: true, text: 未开始 } } return { disabled: false, text: 选课 } }点击选课按钮后调用后端接口成功就弹Message.success提示同时更新当前行的已选人数并刷新列表确保状态同步。这里有一个小坑如果选课后只改本地数据不重新请求接口后续基于后端数据的其他操作比如我的课表页就可能不同步。稳妥做法是选课成功后调用一次课程列表刷新接口数据始终以服务端为准。4.3 axios封装与前后端联调要点前后端联调最大的坑是跨域。开发环境我用Vue CLI的devServer代理解决配置如下// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端请求 /api/xxx 时开发服务器会转发到后端的8080端口前端不会有任何跨域报错。上线部署时这个代理角色由Nginx承担后端不用额外开启CORS一致性更强也更安全。如果非要在后端开CORS用配置类注册CorsFilter也行但能不开就不开。axios实例封装核心是请求拦截器携带token和响应拦截器统一处理错误// utils/request.js const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) router.push(/login) } else { Message.error(服务器异常请稍后重试) } return Promise.reject(error) } )有一个很常见的联调问题后端返回的时间字段默认是一长串时间戳数字比如 1721433600000不是“2025-07-20 08:00:00”这样的格式。解决方式是后端实体类的日期字段加 JsonFormat(pattern yyyy-MM-dd HH:mm:ss)这样Jackson序列化时就会按指定格式输出。如果你不处理前端表格里显示的就是一串数字演示必翻车。5. 常见问题与排查实录5.1 MySQL安装与连接常见错误选课系统项目跑不起来有相当一部分卡在MySQL环境上。我常遇到的几个经典错误每个都是老朋友一样常见MySQL安装完成后连接报错ERROR 1045 (28000): Access denied for user rootlocalhost。选项很多初始密码策略不对、安装时没设密码、密码输入错误。排查路径是先试试用sudo mysql直接登录部分Linux发行版root用户默认用auth_socket认证登录后执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 新密码再回头用客户端连接。另一个高频错误是报错java.sql.SQLException: Unknown database说明连接字符串里的库名不对。建了库才连库先执行 CREATE DATABASE select_course DEFAULT CHARACTER SET utf8mb4;。配置文件里连接串的几个关键参数spring: datasource: url: jdbc:mysql://localhost:3306/select_course?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/Shanghai 必须加不加高版本MySQL驱动会报时区相关错误useSSLfalse避免连接时证书校验的告警和潜在问题characterEncodingutf8保证中文不乱码。这三项缺一不可很多人配置半天连不上就是少了时区参数。5.2 MyBatis常见问题缓存、XML不生效、参数绑定MyBatis的缓存机制简单区分一下一级缓存是SqlSession级别的默认开启同一个SqlSession里查询相同SQL会先走缓存二级缓存是mapper级别的默认关闭需要手动开启。选课系统不需要特意开二级缓存因为业务数据频繁变化缓存反而容易造成数据一致性问题。网上有些源码在二级缓存配置时踩了序列化失败的坑没必要引入那个复杂度。XML不生效经常是这几个原因XML文件没放在Mapper接口同包目录下Maven默认只编译src/main/resources下的文件放java包下需要pom配resources、XML里的namespace和接口全限定名不一致、mapper-locations配置路径写错。我的建议是XML统一放resources/mapper目录在application.yml配置mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.selectcourse.entityXML里的参数绑定问题也是重灾区。比如XML写 #{courseId}但Mapper方法参数没写Param注解小数据量可能没事多参数场景必报错。规范做法是多参数接口必须加Paramint countByCourseIdAndStudentId(Param(courseId) Long courseId, Param(studentId) Long studentId);5.3 前端开发调试的常见坑前端开发时遇到问题先按优先级排查。页面白屏优先看浏览器控制台报什么错——大概率是组件引用路径大小写错误或依赖没装。Element UI的表格渲染正常但自定义列不渲染检查作用域插槽变量名是否和prop对得上。很多人在Vue项目里碰到过“按钮点击无响应”的排查。先看网络面板确认请求是否发出再看控制台有没有JS报错两步定位速度远快于盲目翻代码。如果请求发出了但功能没生效用Vue DevTools插件检查组件状态数据有没有变化——在浏览器里打开Vue DevTools面板点开组件树实时看data里的字段值。这比console.log来回打点调试效率高得多。如果Vue DevTools突然打不开了大概率是浏览器插件版本和Vue版本不匹配升级插件或检查是否被浏览器自动停用即可。前端请求跨域报错如果是开发环境确认vue.config.js代理配置是否生效改完配置要重启npm run serve代理配置不会热更新——这个我忘过好几次每次都会迷惑一下为什么配置了还是报错。5.4 选课功能数据一致性排查实录我有一次在给别人排查系统时发现选课偶发超选。当时的现场是课程容量10人数据库里选课记录却有11条。一开始怀疑事务没生效检查后发现Service方法被同类内部调用导致Transactional失效New了一个Service对象绕过代理。这是事务失效的经典场景同类里方法A调用方法BB上面有TransactionalA不加事务注解B的事务会失效因为Spring事务基于代理实现内部调用不会经过代理。另一个一致性问题是选课记录和课程人数分两处存储某些极端情况下数据对不上。排查方向确认是否有别的地方直接改了课程表的selected_count确认退课方法是否同时删除选课记录和减少人数。最稳的兜底方案是在选课记录表加触发器同步人数但一般情况下事务和条件更新就够用触发器会降低可维护性能不加就不加。6. 部署上线与后续扩展方向6.1 开发环境打包与部署一个完整的选课系统部署方案我推荐后端jar包前端静态文件Nginx反向代理的组合。这也是目前小型Web项目的标准部署姿势简单、稳定、易维护。后端打包在项目根目录执行mvn clean package如果之前编译过且想跳过测试加速打包加 -DskipTests。产物在target目录下文件名一般是 select-course-0.0.1-SNAPSHOT.jar。用 java -jar select-course-0.0.1-SNAPSHOT.jar 即可运行。生产环境数据库连接配置不要写死在yml里通过启动参数覆盖比如java -jar select-course.jar --spring.datasource.password生产数据库密码前端打包在项目根目录执行npm run build产物在dist目录把dist里的所有文件上传到服务器比如/var/www/select-course/。Nginx配置这样写server { listen 80; server_name yourdomain.com; # 前端静态文件 location / { root /var/www/select-course; index index.html; try_files $uri $uri/ /index.html; # Vue Router history模式必须配这行 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files那一行是Vue Router history模式部署的命门少了它一刷新页面就404。如果不想搞懂history模式的机制最省事的办法是路由全用hash模式URL里带#号那样Nginx不用做任何额外配置。课程设计演示场景我建议直接用hash模式省心不出错。6.2 项目后续可扩展方向选课系统做完基础功能之后还能往几个方向扩展每个方向都不复杂但能显著提升项目完整度。批量导入。学生、教师、课程信息支持Excel批量导入用Apache POI或EasyExcel解析Excel文件。这个功能在真实使用场景几乎是刚需——谁也不会手动录入几千个学生的信息。EasyExcel是阿里开源的用起来比POI简单API设计得更人性化选课系统这个量级导入几百行数据完全够用。消息通知。选课结果通过系统消息或邮件通知学生。Spring Boot整合JavaMailSender选课成功就自动发送邮件这个功能在毕业设计里非常加印象分。不过要注意发邮件要放在事务提交之后用事务同步器的afterCommit或直接放在Controller层不然事务回滚了邮件却发出去了数据不一致。课表冲突检测。判断所选课程的上课时间是否已有其他课程占用。这个功能需要课程时间以结构化方式存储比如星期几第几节维护一张课程时间明细表。如果course_time还是“周一3-4节”这种文本就只能用字符串匹配做简单判断不够可靠。这个功能扩展需要连带改数据库设计所以建议在最初设计表时就预留结构化的时间字段后续改会牵连很多代码。统计分析。管理员端用ECharts展示图表每门课的选课人数柱状图、院系选课分布饼图、学分绩点分布散点图。前后端分离下前端拉数据后直接用ECharts渲染后端只需要提供聚合查询接口开发成本不高。7. 个人实践中的一些体会这个项目做完最有价值的收获不只是学会了SpringBoot和Vue怎么用而是理解了“一个能真正运行的系统”和“一个只是照着教程拼起来的项目”之间的差距。我在改过几轮选课系统的代码之后最大的感受是核心业务逻辑写得好不好在演示的时候差别不大但在数据量上来、并发上来之后差距是天壤之别。选课接口那一个条件更新加事务的设计是最值得反复琢磨的几行代码。类似的数据库建表时手一抖少建一个唯一索引后面程序逻辑写得再严谨也总会有一个你没想到的路径会把脏数据插进去。数据库层的约束就是最后一道不可逾越的防线。对需要拿这个项目做课程设计或毕业设计的同学我的建议是先把数据库脚本和核心接口跑通再扩展前端页面。顺序不能反大多数项目做不下去都是因为一上来就想着把页面调得好看结果后端接口全是mock数据。先让核心链路登录-查课-选课-查课表端到端跑通再装饰细节这个项目就已经成功了一大半。