
1. 项目全貌与技术选型为什么是SpringBootVue这套组合先聊点实在的。在线课程管理系统这种题目在毕业设计和初级开发者练手项目里面出现频率极高但你千万别觉得它烂大街就没含金量。恰恰相反正因为做的人多面试官和评审老师对它的功能边界、代码质量、设计思路都会有比较明确的预期。一个把选课并发、权限控制、课程状态流转这些细节都处理干净的项目远比一个堆砌了十几个华而不实功能点的项目更能体现你的工程能力。这个项目本质上做的是这么一件事用SpringBoot提供后端API服务用Vue搭建前端管理界面用MySQL存数据用MyBatis做数据持久化访问实现一个支持多角色管理员、教师、学生的课程管理平台。学生能浏览课程、选课退课、查看自己的课表教师能发布课程、管理课程内容与学生名单管理员负责用户管理、课程审核、数据统计。整套系统是前后端分离架构前端通过HTTP请求调用后端RESTful接口数据以JSON格式交互。我强烈建议初学者用这个项目作为前后端分离的入门实战。理由很简单它的业务模型足够清晰不涉及复杂的算法和分布式问题但又完整覆盖了用户认证、权限控制、CRUD操作、关联查询、文件上传、数据统计这些企业级开发的核心场景。换句话说你把这个项目吃透了市面上大部分管理类系统的开发套路你基本就摸清了。技术选型为什么是这套组合而不是别的先看后端SpringBoot目前依然是Java后端开发的绝对主流它解决了传统Spring配置繁琐的问题内嵌Tomcat、自动配置、起步依赖这些特性让你能快速搭建一个可运行的服务。MyBatis则是一个轻量级的持久层框架SQL由开发者自己控制对于课程管理这种涉及多表关联查询、动态条件筛选的业务场景写SQL反而比ORM框架更直观、可控。再看前端Vue有渐进式的学习曲线无论是用Vue2还是Vue3其响应式数据绑定和组件化思维能让你比较快地上手页面开发。Element Plus这套UI组件库更是把后台管理页面的表单、表格、弹窗都给你封装好了不需要从零写样式。这套方案对应届生来说还有一层现实意义Java后端岗位的招聘需求量大SpringBoot几乎是必问技术栈Vue则是前端框架里最好上手、文档最友好的之一。用一套技术栈同时覆盖前后端你简历上能写的东西立刻充实起来而且每一块都是面试高频考点。2. 数据库设计课程系统的地基别在这里省时间很多同学做这类项目喜欢上来就写代码结果做到后面发现表结构不合理又回头改数据库连带后端Mapper和前端页面一起返工非常痛苦。我踩过这个坑之后总结出一个原则数据库设计宁可多花两天也不要动工写业务代码表结构定了后面就是填肉的过程。2.1 核心表结构规划一个在线课程管理系统核心表的数量控制在6到8张比较合理。太少说明功能有缺失太多说明你没有做好抽象把不该拆的拆了。第一张是user用户表。这张表要能同时承载管理员、教师、学生三种角色所以除了基本的id、username、password、real_name、email、phone、avatar之外必须有一个role字段做角色区分。我建议role用整数类型0表示管理员、1表示教师、2表示学生不要用字符串因为整数在做权限判断时写起来更简洁而且没有历史包袱。第二张是course课程表。核心字段包括id、course_name、course_code课程编号、teacher_id关联用户表表示授课教师、category_id关联课程分类、credit学分、class_hour学时、capacity选课容量、selected_count已选人数、course_time上课时间比如周一 3-4节这样的描述、course_location上课地点、course_desc课程简介、cover_url课程封面图、status课程状态0未发布、1已发布、2已结课、create_time、update_time。第三张是course_category课程分类表字段很简单就是id、category_name、sort_order。之所以单独建表而不是在课程表里直接存个分类字符串是为了后续做分类筛选和统计时SQL写起来更优雅而且避免了分类名称重复、不一致的问题。第四张是student_course选课记录表这是整个系统的核心关联表。字段为id、student_id、course_id、score最终成绩、status选课状态0已选、1已退选、2已结课、create_time选课时间。这里要注意成绩字段放在选课记录表而不是课程表因为一个学生对应一门课只有一条选课记录成绩是这条记录的属性。第五张是course_resource课程资料表用于存放教师上传的课件、教案等文件。字段包括id、course_id、resource_name、resource_url、resource_type1课件、2教案、3其他、upload_time。这张表要不要做取决于你想不想把文件上传功能纳入系统我个人建议加因为这是简历上可以体现的亮点而且实现起来并不复杂。第六张是notice公告通知表用于管理员发布系统公告或者教师发布课程通知。字段id、title、content、publisher_id、publish_time、target_role通知对象0全部、1教师、2学生。2.2 建表SQL关键片段与设计说明用户表的核心SQL大概是这样的CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码BCrypt加密存储, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role tinyint NOT NULL DEFAULT 2 COMMENT 角色0管理员 1教师 2学生, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0禁用 1正常, create_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段这里要特别说明一下。很多教程直接明文存密码我只能说毕设答辩的时候遇到懂行的老师这就是送命题。实际开发中密码一定不能明文存储我们这里用的是BCrypt加密Spring Security里自带这个能力即使你不打算引入完整的Spring Security框架也可以用spring-security-crypto这个单独的库来做密码加密和校验。BCrypt的特点是每次加密同一个明文得到的密文都不一样但校验时能正确匹配这增加了暴力破解的成本。选课记录表的设计需要特别注意联合唯一索引CREATE TABLE student_course ( id bigint NOT NULL AUTO_INCREMENT, student_id bigint NOT NULL COMMENT 学生ID, course_id bigint NOT NULL COMMENT 课程ID, score decimal(5,2) DEFAULT NULL COMMENT 成绩, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0已选 1已退选 2已结课, create_time datetime NOT NULL COMMENT 选课时间, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;我把status放进联合唯一索引这里有些讲究。假设一个学生选了课又退选再重新选同一门课如果唯一索引只包含student_id和course_id第二次插入就会主键冲突。把status放进去同一学生同一课程可以分别存在一条已选记录和一条已退选记录这个设计既保证了不会重复选课又保留了学生的选课历史轨迹。当然如果你想更严格地只保留一条记录退选时用UPDATE而不是DELETE那也是另一种思路但那样就看不到退课历史了不利于后面做统计分析。2.3 表关系与冗余字段的取舍表之间的关系比较直观课程表通过teacher_id关联用户表通过category_id关联分类表选课记录表通过student_id和course_id分别关联用户表与课程表。这是一个典型的多对多关系通过中间表解耦的案例一个学生可以选多门课一门课可以被多个学生选。冗余字段方面课程表里的selected_count已选人数就是一个刻意为之的冗余设计。按理说想统计已选人数SELECT COUNT(*) FROM student_course WHERE course_id ? AND status 0就能查到但在选课列表页每次查询都要做一次COUNT汇总对数据库压力不小而且查询性能会随着数据量增长明显下降。所以我在课程表里直接维护一个selected_count字段学生选课成功后对它加1退选成功后减1。这里一定要在同一个数据库事务里完成两条SQL的更新否则会出现数据不一致。很多初学者忽略了这个点就会出现课程列表显示没选满点选课却提示已满的诡异Bug。另外还有一个容易忽略的点数据库的字符集排序规则要统一使用utf8mb4而不是utf8。为什么因为utf8在MySQL里最多只支持3个字节的字符存储不了emoji和部分生僻字。人名这种东西你永远不知道会出现什么字直接用utf8mb4最稳妥。这属于建表初期省事后面省心的小细节。3. 后端核心模块实现接口设计、权限控制与业务逻辑后端是整个系统的中枢神经。SprinBoot的作用是快速搭建服务骨架真正体现水平的是分层设计、权限控制、事务处理和并发场景下的业务逻辑。这一节我把几个核心模块的实路径和设计思路拆开讲。3.1 项目目录分层与启动配置项目后端目录我习惯这样分src/main/java/com/example/course/ ├── config # 配置类CORS跨域、拦截器、全局异常 ├── controller # 控制层接收请求、参数校验、返回结果 ├── service # 业务层核心业务逻辑 │ └── impl # Service接口实现 ├── mapper # MyBatis数据访问层接口 XML ├── entity # 实体类与数据库表对应 ├── dto # 数据传输对象入参/出参封装 ├── common # 通用类统一返回结果、状态码、异常处理 └── interceptor # 拦截器JWT鉴权、角色校验为什么一定要分层最直接的好处是出了问题能精准定位。前端传参不对看Controller业务逻辑有漏洞查ServiceSQL写错了看Mapper。如果你把所有逻辑全塞在Controller里一个类几百行代码排查问题时想找业务规则就像在杂物间找一根针。另外分层的代码在简历上写起来也更好看面试官一看就知道你有工程化意识。application.yml中比较关键的配置是数据库连接和数据源spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.course.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置强烈建议打开。它能把数据库字段的snake_case下划线风格如create_time自动映射到实体类的camelCase驼峰风格如createTime少写大量无聊的映射配置。3.2 登录认证与JWT权限控制登录认证是一个管理系统最基本的门槛。我用的方案是JWT配合拦截器做无状态鉴权这也是目前前后端分离项目的主流做法。流程是这样的用户提交用户名密码后端验证通过后生成一个JWT令牌返回给前端前端把令牌存起来后续每次请求都在HTTP头部的Authorization字段带上这个令牌后端拦截器解析令牌判断请求是否合法、当前用户是谁、有没有权限操作这个接口。JWT本身是一个三段式的字符串分别是Header头部、Payload载荷、Signature签名。Payload里我一般存放userId、username、role这三个核心信息过期时间设置为2小时。不存放密码等敏感信息因为JWT的Payload只是Base64编码不是加密任何人解出来都能看到内容存密码等于裸奔。拦截器的实现关键在于自定义注解配合HandlerInterceptor。开发一个RequireRole注解标注哪些接口需要什么角色才能访问Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); }然后在拦截器的preHandle方法里先解析JWT拿到角色再判断当前请求的HandlerMethod上有没有标注RequireRole有的话就校验角色是否匹配。管理员接口标RequireRole(0)教师接口标RequireRole({0,1})学生选课接口标RequireRole({0,2})。这样做的好处是权限逻辑集中在一处而不是散落在每个Controller方法里新增一个接口时在方法上加一行注解就能完成权限声明。CORS跨域也是一个必踩的坑。前后端分离开发时前端跑在8080端口后端跑在8081或者9090端口浏览器会拦截跨域请求。解决办法是在后端写一个CORS配置类允许指定的前端源访问同时允许携带Authorization请求头。注意这里不能直接allowedOrigins(*)再加allowedHeaders(*)因为在携带凭证allowCredentials的场景下通配符是不被允许的。这是一个细节性的坑很多人配置完CORS发现明明后端接口用Postman测没问题前端一调就报CORS错误就是这个地方没处理干净。3.3 课程管理与选课业务事务与并发控制选课功能是系统核心中的核心这里有两个典型的业务难题超卖问题和重复选课问题。超卖问题说的是课程容量只有50人但第51个学生也能选课成功。如果代码逻辑只是先查一下selected_count是不是小于capacity小于的话就插入选课记录并更新selected_count加1那么在高并发场景下两个学生同时查到selected_count49都判断可以选课结果都走了后续流程课程实际选入人数变成51。这就是经典的并发竞态条件。解决方案不外乎两种乐观锁和悲观锁。悲观锁思路是使用数据库的SELECT ... FOR UPDATE在事务里先把课程行锁住其他请求只能等待当前事务提交后才能继续select idselectCourseForUpdate resultTypeCourse SELECT * FROM course WHERE id #{courseId} FOR UPDATE /select在执行插入选课记录之前先锁住课程记录再判断容量如果没满则插入并更新数量然后提交事务释放锁。这样并发请求会排队执行从根源上杜绝了超卖。乐观锁思路是在课程表加一个version字段执行更新时检查版本号UPDATE course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND version #{version}如果更新影响的行数是0说明版本号已经被其他事务改过就抛出异常提示选课人数已满或课程状态已变化。对于课程管理系统这种并发量级两种锁都够用。我个人偏好乐观锁因为它在读多写少的场景下性能更好也不需要长事务占用数据库连接。但要注意乐观锁需要处理更新失败后的重试逻辑代码上会稍微复杂一些。重复选课问题靠的是我们在数据库设计阶段加的联合唯一索引兜底。即使代码层面有Bug两个请求同时发起数据库的student_id course_id status唯一索引也能保证只有一条插入成功另一条抛DuplicateKeyException。我在Service里catch住这个异常统一转换成业务提示你已经选过该课程了用户看到的是一个友好的业务提示而不是一串错误堆栈体验完全不一样。选课Service的完整流程应该是这样从JWT中获取当前用户id和角色校验角色是学生。查询课程信息校验课程状态为已发布。校验该学生是否已经选过该课程查询student_course表选过则直接返回提示。开启事务用乐观锁/悲观锁控制容量更新。插入选课记录更新课程已选人数。提交事务返回选课成功。这6步里第3步是提前拦截第4步是并发兜底两层防护合在一起选课逻辑才算安稳。3.4 文件上传课件资源的存取方案课程资料上传这个功能实现方案用的是本地磁盘存储加数据库记录URL的方式。前端通过Element Plus的上传组件选中文件把文件POST到后端的/api/resource/upload接口后端用MultipartFile接收校验文件大小和后缀名后写到服务器指定目录文件名用UUID 原始扩展名重命名以避免冲突然后把文件访问路径存到数据库。public String uploadResource(MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID() ext; String filePath uploadDir / newFileName; file.transferTo(new File(filePath)); return /files/ newFileName; }这里要配置一个静态资源映射让外部可以通过URL直接访问到上传目录里的文件。SpringBoot里通过WebMvcConfigurer来配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(file: uploadDir /); }上传目录我建议放在项目外面不要放在resources里。因为项目重启或者重新部署时resources目录会被覆盖放在外面才能保证文件持久存在。这个属于典型的实操经验文档里不会写Demo项目里也往往不会涉及但真实项目百分之百要考虑。4. 前端页面搭建Vue3 Element Plus实战记录前端部分我用的是Vue3加Element Plus加Vite构建工具。这套组合是目前前端开发的主流配置Vite的冷启动速度和热更新体验比旧版本的Webpack快得多能显著提升开发效率。前端架构采用的是标准SPA单页面应用模式通过Vue Router管理页面路由通过Pinia管理用户登录状态和全局信息。4.1 前端项目结构与路由权限前端目录结构这样规划src/ ├── api/ # 接口请求封装按模块拆文件 ├── assets/ # 静态资源 ├── components/ # 公共组件头部、侧边栏、分页 ├── layout/ # 主布局侧边栏顶栏主内容区 ├── router/ # 路由配置 ├── store/ # 状态管理Pinia ├── utils/ # 工具函数request封装、token存取 └── views/ # 页面组件 ├── admin/ # 管理员页面 ├── teacher/ # 教师页面 ├── student/ # 学生页面 ├── login.vue # 登录页 └── dashboard.vue # 首页仪表盘路由权限这里我用的是简单实用的方案路由分为公共路由和需要登录的路由登录后根据角色动态生成具体业务路由。具体做法是在router的beforeEach守卫里检查本地存储中的token和用户信息没有token就跳转到登录页有token但访问的路由不在当前角色的允许列表里就跳转到首页。菜单栏的渲染数据不是写死的而是根据登录用户的角色从后端拉取。比如管理员看到的是用户管理、课程审核、数据统计、公告管理教师看到的是我的课程、课程审核、选课名单、资料上传学生看到的是课程浏览、我的选课、我的成绩、课程公告。这种动态菜单的好处是前端不用写一大堆v-if判断显示逻辑改权限只需要后端返回不一样的菜单配置。4.2 Axios请求封装与接口对接Axios的二次封装是前端工程里必不可少的环节。我从项目实践的角度讲一下要解决的问题统一处理请求头、统一错误提示、统一处理后端返回格式、携带token。import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.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 { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default service这个封装的思路是token统一从本地存储取出放到请求头里后端返回的code不等于200就弹出错误提示遇到401未认证或token过期就清除登录状态并跳回登录页。一套代码管全局每个页面调接口时只需要关心业务数据本身不用每次重复处理鉴权和错误逻辑。开发调试阶段有个在Vite里配置代理的技巧。前端开发服务器跑在5173端口后端跑在9090端口在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } }这样前端请求/api/course/list会自动转发到http://localhost:9090/api/course/list在开发阶段完美避开跨域问题。等项目上线部署时再用Nginx做反向代理把/api前缀的请求转发到后端服务。4.3 关键页面实现细节课程列表页是学生端最核心的页面承担着课程浏览和选课操作两大功能。我用Element Plus的表格组件展示课程信息包括课程名、授课教师、学分、上课时间、已选人数/容量、课程状态等列。课程封面图用图片预览组件展示课程简介超长时用tooltip显示全文。页面顶部放搜索栏支持按课程名称模糊搜索和按分类下拉筛选这两个条件搭配后端的分页参数能实现一个比较丝滑的查询体验。需要着重说明的是选课按钮的状态控制逻辑。按钮的disabled状态绑定一个计算属性当且仅当以下条件全部满足时才允许点击用户已登录、用户角色是学生、课程状态是已发布、已选人数小于容量、并且当前用户尚未选过这门课。这个判断最好不要在前端只做一层后端Service里还要再做一次完整性校验前端控制体验后端控制安全双保险都上了才算合格。我的选课页面用卡片方式展示已选课程每张卡片包含课程名、教师、上课时间和退课按钮。退课操作需要弹确认框避免学生误触导致退课成功。成绩展示则是一个表格列出课程信息和成绩分数成绩为空时显示暂无成绩。教师端的课程管理页面定位略有不同教师需要的是对课程全生命周期的操作。列表页能看到自己创建的课程每行有编辑、发布/下架、查看选课名单、上传资料等按钮。这里要处理一个权限细节教师只能操作自己创建的课程所以查询时Service里要额外加一个teacher_id 当前登录用户id的过滤条件防止教师在知道接口地址后直接篡改其他教师的课程信息。这种越权访问问题在安全测试中非常常见属于典型的IDOR漏洞初写接口时就要注意规避。管理员端的用户管理页面和课程审核页面相对标准化用户管理就是对用户表做CRUD课程审核则是查看教师提交的课程决定通过还是驳回。5. 前后端联调、项目部署与常见报错排查前后端各自开发完最重要也最容易出问题的是联调阶段。前后端联调的过程中最大的障碍往往不是功能逻辑本身而是定位问题的效率。我一般会按固定排查顺序来先在浏览器F12开发者工具看Network面板里请求的状态码再看后端控制台日志异常堆栈最后检查是不是数据库层面出了问题。这个顺序能帮你快速缩小问题范围。5.1 联调阶段的经典问题实录第一个高频问题是后端接口返回的JSON日期格式和前端组件要求的格式不一致。比如选了课后端返回的createTime: 2025-01-15T10:30:00这种带T的ISO格式在前端表格里显示就非常难看。解决办法一个是后端在application.yml里统一配置JSON序列化格式另一个是前端利用第三方库格式化我更推荐后端配置的方式全局生效不用每个页面处理。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二个高频问题是前端传递的JSON字段名和后端实体类的属性对不上。比如前端传createTime而后端实体字段是create_time结果死活绑定不上。这个问题的根源是命名规范不统一解决办法是前端统一用驼峰命名后端配置map-underscore-to-camel-case: true这样两边就能自动对齐。第三个高频问题是文件上传接口请求头没设置对。前端用FormData方式上传时不要手动设置Content-Type为application/json要让它保持multipart/form-data边界自动生成否则后端接收不到文件。很多初学者在这里卡很久其实是因为对HTTP请求头理解不透。5.2 项目部署从开发环境到服务器部署这块最简化的方案是把前端构建后的静态文件放到Nginx里托管后端打成JAR包用Java命令在服务器上运行然后通过Nginx反向代理把/api开头的请求转发到后端端口。这套部署方式虽然是简化的但已经覆盖了生产环境部署的核心思路。前端构建命令是npm run build构建完成后会在dist目录生成静态文件把这些文件整个上传到服务器的/usr/share/nginx/html目录。Nginx的配置如下server { listen 80; server_name your_domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }需要特别注意的是try_files $uri $uri/ /index.html;这一行它的作用是所有前端路由都回退到index.html由Vue Router接管避免用户刷新页面时出现404。没有这行配置你部署上线后点菜单进到子页面一刷新浏览器就报404非常影响体验。后端打包部署mvn clean package -DskipTests java -jar target/course-system-0.0.1-SNAPSHOT.jar生产环境下我一般会用nohup命令让JAR包后台运行并配合日志输出nohup java -jar target/course-system-0.0.1-SNAPSHOT.jar app.log 21 启动后先看日志确认端口和数据库连接正常然后用curl测试一下接口通不通最后再把域名解析指向Nginx。5.3 常见问题排查速查表我整理了这五类项目里最常遇到的报错及排查方法做成一个速查表卡住时对着看效率很高。报错现象可能原因排查步骤与解决前端请求接口报404Nginx配置错误或后端接口路径不匹配先看Network里请求的完整URL有没有经过/api前缀再看后端控制台有没有对应的请求日志前端请求接口报401token缺失、过期或被拦截器拒绝查看浏览器Application里有没有tokenF12看请求头Authorization是否携带用Postman直接测接口确认是否是前端问题报400 Bad Request参数类型不匹配或缺少必需参数检查前端传递参数名与后端RequestParam或DTO字段是否一致日期格式是否被正确解析报500 Internal Server Error后端运行时异常通常是SQL或空指针查看后端日志异常堆栈第一行定位到具体代码行数据库中是否有对应数据SQL语句是否有语法问题数据库中文乱码连接字符串或数据库字符集不对确认连接URL含characterEncodingutf8MySQL表字符集为utf8mb4这些都是建库时就要定好的对于报错排查最核心的建议是养成看日志的习惯。不要一报错就复制错误信息去搜索引擎先自己花两分钟看异常堆栈最顶部的几个关键行往往问题就发生在你能看懂的那一行附近。6. 防坑经验总结做完这个项目我才悟到的几件事这是我做完整套系统之后最大的几点体会专门写在这里。第一点体会是不要把JWT当黑盒用。很多教程教你依赖库生成解析JWT就行但你必须清楚Signature部分的签名机制是怎么一回事。后端每次收到请求解析出token里的userId和role之后要对这两份关键信息保持轻信态度更稳妥的做法是在内存里维护一张在线用户表每次请求都从在线表里获取最新角色信息而不是完全依赖token里的角色判断。否则用户被管理员改了角色或禁用后原本签发的token在过期前依然有旧权限这在真实系统里是严重的安全隐患。第二点是关于MyBatis的XML与注解选择。我个人强烈推荐把复杂SQL写在XML里注解适合写简单查询多表关联和动态SQL写在注解里可读性非常差。接口与XML通过namespace绑定需要注意确保XML文件里的mapper namespace...与Mapper接口的全限定类名完全一致否则启动时就报错这个错误信息虽然看得懂但第一次遇到时还是挺容易犯迷糊的。第三点是所有前端传过来的数据都要做后端校验。有同学觉得前端已经用了Element Plus的表单校验后端就不管了这是把安全底线完全交给了客户端。前端校验是用户体验后端校验才是真正的安全屏障。非法用户在控制台构造一个JSON请求就能绕过前端校验直接攻击后端接口所以后端每个写入操作都要校验参数非空、长度限制、类型正确简单场景用Spring的Validation注解就够。第四点也是最重要的一点这个课程系统的后续扩展空间非常大。如果你还有余力可以往这些方向演进引入Redis缓存课程热门列表、用Spring Security替换手写拦截器、引入消息队列处理选课高峰的削峰、用WebSocket实现课程公告的实时推送、接入阿里云OSS存储课件文件。每多走一步你简历上的项目含金量就上升一个台阶。但步子也不要迈太大先把主线功能做扎实把权限控制和并发问题处理干净这个系统就已经超出大部分同类型毕设项目的水平了。我个人在实际操作中的体会是做这种全栈项目从一开始就要有全局视角数据库设计时就想到后面的业务代码怎么写后端写接口时就要考虑到前端使用方会怎么调用。把这条思路贯穿始终你会发现开发过程意外地顺滑很多问题根本不会在联调阶段才暴露出来。最后再分享一个小技巧开发完项目后一定要写一份README文档把你做的功能点、用的技术栈、部署步骤、包含的配置都整理清楚。这份文档不仅是答辩时的底气和背稿更是毕业设计评分和面试官考察的加分项。