尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

学生选课系统全栈开发实战:SpringBoot+Vue+MyBatis+MySQL设计部署详解

学生选课系统全栈开发实战:SpringBoot+Vue+MyBatis+MySQL设计部署详解 做学生选课系统说到底是把一条条课程数据、一个个学生身份、一节节时间资源串起来把谁在什么时间选了哪门课这件事管明白。这篇博文围绕SpringBootVueVueMyBatisMySQL这套企业级前后端分离架构完整拆解一个学生选课管理系统的源码设计、数据库建模、后端接口实现、前端页面交互以及真实部署中会遇到的坑。无论你是正在做毕业设计、课程设计还是想快速上手SpringBoot后端Vue前端的全栈开发套路这篇内容都能帮你少走不少弯路。先说结论这套系统跑通不难但要把选课容量、防重复选、退课名额释放、角色权限控制这些细节做干净还是有不少门道。下面我按实际开发顺序从技术选型、数据库设计、后端核心逻辑、前端对接一直讲到部署排错把每个环节的关键代码和思路都梳理出来。1. 系统整体设计与思路拆解1.1 为什么选SpringBootVueMyBatisMySQL这套组合学生选课系统不算复杂但胜在功能典型用户登录、角色权限、数据列表、增删改查、业务状态流转这些几乎是所有管理系统的共性需求。选这套技术栈不是因为它最花哨而是因为它足够标准踩坑资料多、上手成本低、维护成本也低。SpringBoot解决了传统SSH/SSM项目配置繁琐的问题。不用再写一堆XML配置文件起步依赖自动管理jar包版本内嵌Tomcat让项目直接通过main方法就能启动。对初学者来说最大的感受就是原来启动一个Web项目可以这么快。Vue负责前端页面。菜鸟教程式的渐进式框架组件化开发让页面拆成独立的.vue文件路由控制页面跳转配合Element UI这样的组件库表格、表单、弹窗、分页这些后端管理系统最常见的交互半天就能搭出像模像样的界面。前后端分离的好处也很明显前后端各干各的接口约定好调试互不干扰。Vue 3也可以但考虑到生态成熟度和学校主流教材Vue 2 Element UI仍然是不少毕设项目的稳妥选择。MyBatis作为持久层框架最大的优势是SQL可以完全自己控制。选课系统里有大量涉及多表关联、条件统计、动态查询的场景比如查某个学生已选的课程列表、查某门课的已选人数、按学院/学分/上课时间筛选课程。这些SQL写起来灵活调试也直观比全自动ORM更容易排查问题。MySQL作为数据存储端免费开源性能足够和SpringBoot、MyBatis的兼容性也经过了大量项目验证。整套组合下来就是一个经典到不能再经典的中小规模管理系统标配。1.2 角色权限与功能模块的划分逻辑这个系统的核心角色有三种管理员、教师、学生。为什么分成三种而不是一张用户表通吃从业务上来讲三种角色的操作边界完全不同。学生注册/登录、浏览课程列表、按条件查询课程、选课、退课、查看已选课程、查看个人成绩教师登录、发布课程、修改课程信息、查看选课学生名单、录入学生成绩管理员学生管理、教师管理、课程管理、选课数据统计、系统公告如果用一张user表加一个role字段去区分倒也能跑但代码里到处都要加if (role 1)的判断逻辑会越来越乱。更合理的做法是独立出一个角色字段配合后端拦截器做权限校验——每个接口在进入Controller之前先判断登录用户角色不符合直接返回403。前端再根据角色动态渲染菜单和按钮做到后端兜底、前端体验的双重安全。在实际源码里用户表sys_user通常保留username和password再用role字段区分角色。也有项目把学生、教师、管理员拆成三张独立表但这样登录逻辑就要分别处理反而麻烦。基于角色的单表字段区分更符合中小型系统的简洁原则。1.3 版本选型与踩过的版本坑版本选型看着不起眼但在实际开发中坑不少。这里直接给出一套我验证过的稳定组合照着用基本不会出幺蛾子。组件推荐版本说明JDK1.8兼容性最好网上资料最多不推荐强上JDK 17Spring Boot2.7.x稳定版和JDK 8完美配合自带Tomcat 9MyBatis Starter2.1.x或者2.3.x由Spring Boot 2.7.x自动管理版本MySQL8.0.x 或 5.7建议8.0注意时区参数Maven3.6.3避免太老版本导致依赖解析失败Node.js14.x或16.x跑Vue 2项目的工程化环境Vue2.6.x Element UI 2.15.x前端框架组件库稳定Axios0.21.x前端HTTP请求库版本适配是我踩过最惨的坑最开始用Spring Boot 3.0 JDK 17结果MyBatis的starter版本不兼容、第三方工具类全是基于javax包的而Spring Boot 3把javax换成了jakarta一堆代码改到头大。后来全部回退到2.7.x JDK 8几分钟就全部跑通了。老实说做这种管理系统真的没必要追求最新版本——稳定跑通、资料好查才是第一位的。2. 数据库设计与持久层实现2.1 核心表结构与建表SQL数据库是整个系统的地基表设计好不好直接决定后面SQL写起来痛不痛苦。核心表归纳下来是五张用户表、课程表、选课记录表、成绩表、公告表可选。学生和教师统一放在sys_user表里通过role字段区分。核心字段要包括id、username、password、real_name、role、create_time。密码存储我不建议用明文用MD5加盐或者BCrypt加密安全性高出一大截。课程表是course字段包括id、course_name、teacher_id、credit、capacity、selected_count、class_time、location。其中selected_count表示已选人数这个字段在并发选课的场景下需要谨慎处理后面会专门讲。选课记录表用sc命名student_course主键设计为联合主键(user_id, course_id)防止同一个人选同一门课两次这是数据库层面最朴素也最有效的防重复手段。下面是精简版的建表SQLCREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 2 COMMENT 0-管理员 1-教师 2-学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_id BIGINT NOT NULL, credit INT DEFAULT 2, capacity INT DEFAULT 60, selected_count INT DEFAULT 0, class_time VARCHAR(100) COMMENT 上课时间如 周一3-4节, location VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sc ( user_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, course_id), CONSTRAINT fk_sc_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) );几个关键细节解释一下第一字符集必须用utf8mb4不然用户在备注里输入个emoji表情就会报Data too long或乱码。第二联合主键是防重复选课的底线就算后端代码有漏洞数据库也会拦住重复插入。第三成绩字段直接放进选课表里教师录入成绩其实就是更新sc表的score字段不需要单独建一张成绩表。2.2 MyBatis核心SQL写法与动态查询MyBatis的XML配置和注解方式我都有用过选课系统这种多表查询频繁的项目更推荐XML方式。注解方式适合简单SQL一旦SQL超过三行、里面还有动态条件可读性会直线下降。XML把SQL集中管理后期排查问题也方便。课程列表查询是最典型的场景学生进入选课页面需要看到课程列表、授课教师姓名、当前已选人数/容量。这里必然涉及course表和sys_user表的联查。select idselectCourseList resultTypecom.example.vo.CourseVO SELECT c.id, c.course_name, c.credit, c.capacity, c.selected_count, c.class_time, c.location, u.real_name AS teacher_name FROM course c LEFT JOIN sys_user u ON c.teacher_id u.id where if testcourseName ! null and courseName ! AND c.course_name LIKE CONCAT(%, #{courseName}, %) /if if testteacherName ! null and teacherName ! AND u.real_name LIKE CONCAT(%, #{teacherName}, %) /if /where ORDER BY c.id /select这段SQL用到了MyBatis动态SQL的ifwhere标签。where标签有个小细节值得注意它会自动去掉第一个多余的AND/OR避免代码里拼SQL时还要纠结前面要不要加WHERE的经典问题。这是MyBatis相比直接JDBC拼接SQL的巨大优势。除了动态条件查询已选课程列表也是高频SQL。核心逻辑就是通过sc表反查course表select idselectSelectedCourses resultTypecom.example.vo.CourseVO SELECT c.id, c.course_name, c.credit, c.capacity, c.selected_count, c.class_time, c.location, u.real_name AS teacher_name FROM sc JOIN course c ON sc.course_id c.id JOIN sys_user u ON c.teacher_id u.id WHERE sc.user_id #{userId} /select这里有一个很容易踩的映射问题数据库字段是course_name而Java实体属性是courseName直接查询返回的结果会是null。解决方案是在MyBatis全局配置中开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true这个配置我已经不知道救了多少人不开它联查出来的字段全是空值排查半天往往就是这个原因。2.3 选课容量与并发控制的常见设计选课系统最容易出问题的场景就是并发选课高峰时期多个学生同时对同一门课发起选课请求如果代码没有做好并发控制课程容量只有60人结果选了65人进去数据就脏了。最简单的方案是先查再插的事务控制选中课程之前查一下selected_count小于capacity就插入选课记录同时把selected_count加1。但在高并发下两个请求同时查到count59然后同时插入最后一个课程就多了一个人。解决办法有几个方向第一种是数据库层面的乐观锁。在course表加一个version字段更新时带着version条件影响行数为0说明version被改了就提示选课人数已满。update idincreaseSelectedCount UPDATE course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND version #{version} AND selected_count lt; capacity /update这里一定要在WHERE条件里加上selected_count capacity通过数据库行锁的机制保证只有在未满员的情况下才会加人数。Java代码中判断update返回的受影响行数为1就表示抢占成功继续执行插入选课记录的SQL为0就说明课程满了或version冲突直接返回选课失败。第二种是直接给课程行加锁SELECT ... FOR UPDATE。这种方式在编写上更直观但死锁风险需要仔细测试性能也有一定消耗。对毕设和中小型系统来说乐观锁条件更新的方案最稳妥。我的建议是在线程并发量不夸张的场景下直接把容量判断和更新人数合并为一条UPDATE语句配合insert选课记录再用一个事务包起来基本就够用了。如果选课系统真的要支撑上千人同时抢课那就要引入Redis分布式锁或消息队列了但这就超出了学生选课系统的范畴了。3. 后端核心逻辑与接口实现3.1 登录认证与权限拦截的实现思路登录认证几乎是所有管理系统的第一道门槛选课系统也不例外。常规做法有两种Session方式和JWT Token方式。Session方式是传统方案登录成功后将用户信息存入Session后续请求通过Cookie携带SessionID来识别身份。实现简单但前后端分离时Session共享和跨域Cookie处理比较麻烦。前后端分离的项目更推荐JWTJSON Web Token方案。用户登录成功后后端签发一个包含用户ID、用户名、角色、过期时间的Token字符串前端拿到后存在localStorage里之后每次请求在请求头加上Authorization: Bearer 。后端通过拦截器或Spring Security解析Token识别当前登录用户身份和角色。对于学生选课系统我认为直接从简单入手即可不引入Spring Security这种重型框架而是利用SpringBoot的HandlerInterceptor实现一个自定义拦截器。核心逻辑如下public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { // 解析token把userId和role放进request属性方便后续Controller使用 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }这里把Token解析出的userId和role塞进request属性后续Controller方法里直接用RequestAttribute就可以取出来非常方便。角色权限校验还可以再写一个RoleInterceptor按不同接口路径匹配不同角色实现思路类似都是继承HandlerInterceptor然后重写preHandle。为了减少代码侵入登录接口本身不需要拦截可以用WebMvcConfigurer的addInterceptors方法配置拦截路径白名单registry.addInterceptor(authInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register);这个白名单的设计要仔细。比如教师录入成绩的接口必须要求老师权限学生选课接口必须要求student角色。如果权限控制做得比较粗会出现学生直接调管理员的接口删课程虽然前端页面没这个按钮但接口是裸奔的——所以一定要在服务端校验角色不能只靠前端隐藏按钮。3.2 选课接口的完整处理流程选课是系统的核心接口逻辑上要比普通接口多几步。我把它拆成四个关键步骤第一步从Token或请求参数中获取当前学生ID。注意学生ID不能从前端传来的JSON里读取否则学号是A的同学可以伪造请求帮学号是B的同学选课。必须通过Token解析出来的身份来确定用户。第二步校验课程状态。查出课程信息确认课程存在、已选人数小于容量。同时校验选课时间是否在系统开放时间内如果有的话。第三步执行选课。插入sc记录。这一步要利用数据库的联合主键防重复。第四步更新课程已选人数。乐观锁UPDATE。如果用事务统一管理核心代码大致是这样Service public class CourseServiceImpl implements CourseService { Autowired private CourseMapper courseMapper; Autowired private ScMapper scMapper; Transactional(rollbackFor Exception.class) Override public Result selectCourse(Long userId, Long courseId) { // 1. 判断是否已经选过 int existCount scMapper.countByUserIdAndCourseId(userId, courseId); if (existCount 0) { return Result.error(你已选过该课程请勿重复选择); } // 2. 乐观锁占坑只有未满才更新成功 int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { return Result.error(该课程人数已满选课失败); } // 3. 插入选课记录 scMapper.insert(userId, courseId); return Result.success(选课成功); } }这里有个小细节步骤1和步骤2在并发情况下可能会有间隙但并不影响最终一致性。因为即使两个请求同时通过步骤1步骤2的乐观锁UPDATE只会有一个成功另一个rows0直接报满。所以在高并发下偶尔会有学生明明没选过却提示人数已满这并不会造成数据错误属于可接受的业务提示。退课接口的逻辑就相反了先删除sc记录再把course表的selected_count减1。删除时也要判断记录是否存在不要出现删除0行却返回成功的情况。3.3 Service层与Controller层的职责划分很多新手写代码喜欢把业务逻辑全堆在Controller里一个接口对应一个方法几十行逻辑塞进去虽然能跑但后期维护极其痛苦。我更推荐严格分层Controller层只做三件事接收参数、调用Service、返回统一结果封装。Controller里不写任何if-else业务判断。Service层负责核心业务逻辑事务管理、业务流程编排、业务规则校验。像选课、退课、成绩录入这些核心操作都放在Service里。Mapper层DAO层只负责数据库交互一个方法对应一条SQL不做业务处理。统一结果返回结构也很重要。所有接口统一返回Result对象一般包含code、message、data三个字段。前端根据code判断请求成功与否避免出现HTTP 200但业务失败还要解析data里的值来猜的情况。public class Result { private Integer code; // 200 成功500 业务失败401 未登录 private String message; private Object data; public static Result success(Object data) { Result r new Result(); r.setCode(200); r.setData(data); return r; } public static Result error(String message) { Result r new Result(); r.setCode(500); r.setMessage(message); return r; } }这个Result类看着简单实际使用中价值非常大。前端Axios拦截器可以直接根据code做统一错误提示不用每个接口单独写一遍你已选过该课程人数已满这类提示逻辑。4. 前端Vue实现与接口对接4.1 前端工程结构与路由设计Vue前端项目一般用Vue CLI或Vite创建。以Vue 2为例工程创建命令vue create course-system-frontend创建完成后在项目根目录安装Element UI和Axiosnpm install element-ui -S npm install axios -Smain.js中引入Element UIimport Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router Vue.use(ElementUI) new Vue({ router, render: h h(App) }).$mount(#app)路由设计上按角色划分模块。为了减少首屏加载时间用Vue Router的懒加载模式。路由懒加载按需加载对应的组件页面比较多时能显著提高首屏速度。在路由守卫中做登录校验是前端控制访问权限最直接的方式没有token就跳转到登录页有token但角色不匹配就跳转到对应角色的首页。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } next() })另外导航守卫里还可以根据当前用户的role动态生成菜单。比如管理员路由包含用户管理学生路由没有这个菜单。虽然真正的权限控制要靠后端拦截器但前端动态菜单能让界面更清爽。4.2 Axios请求封装与拦截器设计前端所有接口请求都建议封装在统一模块中而不是每个页面直接调用axios.get。这样在统一处理Token、统一处理错误码、统一加载状态时能省掉大量重复代码。先封装一个request.js模块import axios from axios import { Message } from element-ui import router from /router 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) { return res } else if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未登录或登录已过期)) } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { Message.error(网络连接异常) return Promise.reject(error) } ) export default request封装好之后页面里调用接口就非常干净了import request from /utils/request export function selectCourse(courseId) { return request.post(/course/select, { courseId }) } export function getCourseList(params) { return request.get(/course/list, { params }) }这种封装最大的价值在于新增一个接口时只需写一行函数定义接口异常统一弹Message提示代码可读性也高很多。实际开发中我还发现一个细节baseURL用/api这种相对路径开发时通过Vue CLI的proxy配置将/api转发到后端localhost:8080避免跨域。部署时再用Nginx反向代理到后端服务前端代码里不需要写死IP和端口非常灵活。4.3 选课页面的核心交互实现选课页面是最核心的前端页面用Element UI的Table组件展示课程列表加上搜索条件和分页。table的核心列设计一般包括课程名称、授课教师、学分、上课时间、上课地点、容量、已选人数、操作按钮。操作按钮需要根据状态显示不同的文字和样式如果已选显示已选并且置灰如果满员显示已满否则显示选课。具体实现时课程列表接口返回的数据里包含一个字段isSelected这是后端在当前用户登录状态下特别查询返回的方便前端直接判断按钮状态。el-table :datacourseList v-loadingloading el-table-column propcourseName label课程名称 width200/el-table-column el-table-column propteacherName label教师 width150/el-table-column el-table-column propcredit label学分 width80/el-table-column el-table-column propclassTime label上课时间 width180/el-table-column el-table-column propcapacity label容量 width80/el-table-column el-table-column propselectedCount label已选人数 width100/el-table-column el-table-column label操作 width150 template slot-scopescope el-button typeprimary sizesmall :disabledscope.row.selectedCount scope.row.capacity || scope.row.isSelected clickhandleSelect(scope.row) {{ scope.row.isSelected ? 已选 : 选课 }}/el-button /template /el-table-column /el-table选课按钮点击后调用后端接口成功会刷新列表并弹成功提示失败则弹错误消息。我在实际开发中遇到一个体验问题选课成功后如果不刷新列表isSelected还是旧的false需要重新拉一遍课程列表。这个小细节处理好之后选课成功和已选状态切换就很丝滑了。页面加载时调用分页接口getCourseList({ pageNum: this.pageNum, pageSize: this.pageSize, courseName: this.keyword }) .then(res { this.courseList res.data.list this.total res.data.total })这里分页参数和后端保持一致后端使用PageHelper插件可以非常方便地实现分页查询设置PageHelper.startPage(pageNum, pageSize)之后紧接着的SQL查询就会自动拼接LIMIT语句同时返回的总条数也会被拦截器自动处理。4.4 我的课程页面与成绩查看功能学生选完课后需要有一个我的课程页面来查看已选课程列表。这个页面的表格和选课页面类似但操作按钮变成退课和查看成绩。退课操作需要一个二次确认弹窗用Element UI的MessageBox做确认防止学生误点this.$confirm(确定要退掉这门课程吗, 提示, { confirmButtonText: 确定退课, cancelButtonText: 取消, type: warning }).then(() { cancelCourse(courseId).then(() { this.$message.success(退课成功) this.loadCourses() }) })教师端查看选课学生名单和录入成绩页面交互逻辑类似只是数据源不同。录入成绩用el-input-number或者el-input做成可编辑的单元格或者弹窗表单提交后调用录入成绩接口。5. 项目部署运行与常见问题排查5.1 本地从零启动完整流程假如你拿到一套完整的源码怎么把它跑起来我按实操顺序整理一下。第一步准备环境。安装JDK 8、Maven 3.6、MySQL 8.0、Node.js 14并配置好环境变量。第二步导入数据库。用Navicat或其他MySQL客户端执行项目提供的前置SQL脚本。如果没有现成脚本就按前面的建表SQL手动建库建表再插入几条测试数据。第三步修改配置文件。打开后端项目的application.yml检查这几项数据库地址、端口、账号密码。很多新手启动报错80%是这一步骤出了问题。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true注意url里一定要加上serverTimezoneAsia/Shanghai否则MySQL 8.0会报时区错误。第四步启动后端。在项目根目录执行mvn spring-boot:run等待控制台出现Started Application in x.xxx seconds字样就说明后端启动成功。第五步启动前端。前端项目目录下安装依赖并启动开发服务器npm install npm run serve控制台会出现访问地址通常是http://localhost:8081。默认端口如果冲突在vue.config.js里配置devServer.port同时配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }到这里浏览器打开前端地址注册一个学生账号就能体验完整的选课流程了。5.2 常见问题速查表我把反复遇到的几类问题整理成表格按照排查优先级排列问题现象根本原因解决办法启动后端报错ConnectException: Connection refusedMySQL没启动或端口不对检查MySQL服务状态确认3306端口可连接报错Unknown database course_system数据库没创建或名字不一致执行建库SQL核对application.yml里的库名报错Public Key Retrieval is not allowedMySQL 8.0连接参数缺少allowPublicKeyRetrieval在JDBC URL中追加allowPublicKeyRetrievaltrue前端访问后端接口报404代理路径或接口路径不匹配检查vue.config.js的proxy配置和请求的baseURL前端接口返回数据里外键字段为null没有开启驼峰映射在mybatis配置中设置map-underscore-to-camel-casetrue中文乱码数据库字符集不是utf8mb4修改库/表字符集为utf8mb4端口被占用8080被其他程序占用换端口或杀死占用进程Maven依赖下载失败网络问题或仓库源问题配置阿里云镜像仓库这些坑基本覆盖了我在实际部署中遇到过的情况。其中Public Key Retrieval is not allowed是MySQL 8.0特有的很多人在MySQL 5.7上能跑通的连接配置到8.0就报这个错解决方案就是URL添加上面说的参数或者修改MySQL用户的加密方式。5.3 容易忽略但很实用的几个细节第一个是IDE的Maven配置。很多学生遇到依赖下载失败其实不是代码问题而是Maven用了默认的中央仓库国内访问不稳定。在settings.xml中配置阿里云镜像基本能解决mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror第二个是Lombok的使用。Lombok能省掉大量getter/setter代码但需要IDEA插件支持而且IDEA版本和Lombok版本存在兼容性问题。如果实体类上加了Data但项目里调用getId()却编译报错多半是IDEA的Lombok插件没装或版本不对。第三个是前端npm install时报node-sass安装失败。老项目里常遇到node-sass它与Node版本绑定Node版本太高太低都会挂。2025年的视角下建议优先选sassdart-sass而不是node-sass或者在项目依赖里把node-sass替换成sass。这也是很多Vue 2旧项目的一个历史包袱。第四个无关技术但非常关键密码处理。我见过很多毕设项目把用户密码明文存在数据库里甚至登录验证直接SELECT * FROM user WHERE password xxx这非常危险。至少也要做MD5盐存储或者直接使用BCrypt。这个习惯不光是项目要求更是未来工作的职业底线。6. 二次扩展与项目进阶方向如果源码跑通之后想加点亮点让系统在答辩或实际使用中更有说服力这几个方向我建议优先考虑。第一个是引入Redis做课程缓存和选课计数器。课程列表是查询频率最高的接口用Redis缓存课程列表数据可以显著降低数据库压力。选课人数也可以直接在Redis里做原子自增INCR命令选课成功后异步同步到MySQL这样并发支持能力会上一个档次。实际项目里这个方案很常见能让面试官觉得你理解了缓存和数据库的一致性问题。第二个是使用Spring Boot的定时任务实现选课开放与关闭。比如在配置表中设置选课开始时间和结束时间然后写一个定时任务自动切换课程状态。这比手动在代码里判断当前时间要优雅得多也更接近真实教务系统的行为。第三个是增加数据导出功能。学生成绩单、选课名单用Hutool或Easy Excel直接导出成Excel文件。这个功能很多系统都没有但实际使用中需求量很大尤其是教师端导出选课学生名单属于加分项。第四个是引入Logback日志配置。把操作日志、异常日志按级别输出到文件方便线上问题排查。毕设答辩时面试官问系统出了异常你怎么排查拿出日志文件来谈会显得专业得多。我自己实际做完这套系统之后的体会是技术本身并不难难的是把边界情况想全、把每个按钮的状态判断清楚、把每条SQL在各种场景下都验证到位。选课系统麻雀虽小五脏俱全做完它你对SpringBoot、Vue、MyBatis、MySQL整套技术栈的理解会从学过变成会做这种转变才是最值钱的部分。最后再分享一个小技巧开发阶段用Postman把每个接口的请求参数和返回结果整理成文档前端对接时会给对方省下大量沟通成本。接口文档不规范导致的前后端扯皮是我见过最消耗项目进度的事情没有之一。把接口定义清楚、状态码约定好你会发现整个开发过程顺畅太多了。
返回列表