
先说实话搜索框里敲下“SpringBootVue 线上辅导班系统源码”的那一刻你大概率不是真的缺一份代码——你是缺一个能讲清楚、能答上问、能跑得起来的毕设/课设项目。这行标题背后藏着的需求我太熟悉了要么是时间紧任务重想找个能直接改的底子要么是想自己搭但又怕踩坑想找一份真实可落地的参考再要么就是已经在写了写到一半发现前后端联调、权限控制、订单状态这些地方卡住了。这篇文章就按后者来写。我会把这个“SpringBoot Vue MySQL”的线上辅导班系统管理平台从技术选型、模块拆解、数据库设计、核心代码实现到部署Debug完完整整拆给你看。不吹不黑这套组合放在当下做毕设、课设来说依然非常能打面试谈资也够用。下面全是实操过的东西照着做你能省掉大量翻文档和踩坑的时间。1. 项目整体设计与技术栈选型拆解1.1 为什么是 SpringBoot Vue MySQL 这套组合先说结论这套技术栈不是“最潮”的但一定是现阶段做学习类项目最稳的组合。原因有三点我一个个讲。第一生态足够成熟。SpringBoot 帮你把 Spring MVC 的配置地狱全部解决掉了哪怕你之前只写过 Servlet也能快速上手。Maven 把依赖一拉一个SpringBootApplication主类就能启动整个后端。Vue 就更不用说了渐进式框架意味着你不需要一上来就啃 TypeScript、状态管理、路由守卫这些东西先会用{{ }}插值表达式、v-for、v-model就能写出能用的页面学习曲线相当友好。MySQL 则是应届生和在校生最熟悉的数据库Navicat 连上就能操作教程一搜一大把遇到问题找答案也容易。第二岗位需求量依然很大。你去看招聘网站上后端实习岗的要求SpringBoot 几乎是标配Vue 在前端岗也是主流技能。用这套组合做出来的项目无论是写进简历还是面试聊项目面试官都有共同语言聊起来顺畅通过率自然更高。第三前后端分离的开发模式本身就是行业标准。这个项目的意义不仅在于“能跑”更在于你能借此理解前端的axios请求、后端的Controller - Service - Mapper三层架构、RESTful API设计、JWT登录鉴权——这些全是企业开发中每天都在用的东西。会有多段代码需要你调试、需要你思考“前端传过来的数据后端怎么接”这个过程本身就是最好的学习。1.2 功能模块如何拆解三层角色的权限划分线上辅导班系统业务核心是“排课-选课-上课-管理-反馈”的闭环。我做成三个端每个端对应一套权限管理员端负责系统全局配置。管理讲师账号、审核课程上下架、查看全平台订单和营收、处理退款申请、发布系统公告、管理学员账号。讲师端负责具体的教学业务。申请开课、上传课程介绍和章节课时、查看自己课程的报名人数和学员评价、课后发布作业或资料。学员端浏览课程、收藏课程、下单购买、在线观看课程视频、提交课程评价、查看自己的学习进度。权限层面我直接用最朴素的方案后端拦截器校验 JWT再根据role字段决定能否访问接口。比如讲师删除课程、学员修改订单这类敏感操作必须在后端校验角色而不是前端把按钮隐藏就算完事。这一点在答辩时经常被问到是可以主动展示的亮点。1.3 这套方案在毕设/课设场景下的适配逻辑毕设评审最看重什么不是功能多么花哨而是逻辑自洽 能演示 能讲清楚。这个项目正好三样都占业务流清晰从注册登录到选课下单再到讲师排课每个环节都有据可查演示效果好Vue 管理界面做大屏展示非常加分而且技术点覆盖面广从 MyBatis 的 SQL 编写到 Spring Security/JWT 鉴权。这个覆盖面意味着答辩时不管老师从哪儿切入提问你都有内容可以回答。课设的话还可以按工作量“裁剪”只做学员端和管理员端砍掉讲师端独立账号体系或者砍掉在线播放改成课程资料下载。保留核心 CRUD 登录 权限课时安排在两三周内是可落地的。2. 核心功能模块的代码级拆解2.1 登录鉴权用 JWT 还是 Session我为什么这么选登录鉴权是这个项目的门面也是最容易被答辩老师追问的模块。先说结论我用的 JWTJSON Web Token。选它的原因很简单——前后端分离架构下后端不需要在 Session 里存登录状态服务端天然无状态将来你想加 Redis 做分布式会话也好、拆微服务也好都不需要改登录逻辑。JWT 的结构分三段Header加密算法和类型、Payload存放用户信息、角色、过期时间、Signature用服务端密钥签名防篡改。// JWT 工具类核心代码 public class JwtUtils { // 签名密钥生产环境务必配置到配置文件中并定期更换 private static final String SECRET your-secret-key-change-in-production; // 过期时间7天单位毫秒 private static final long EXPIRE_TIME 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { Date now new Date(); Date expireDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }我这里用角色role直接作为 Claim 写进 Token 里是为了让拦截器在解析 Token 时不必再查一次数据库就能拿到角色信息。要是 Token 被篡改签名校验那一步就会直接抛异常——这就是 JWT 天然防篡改的威力。你要记住一个原则Token 里不要放敏感信息比如手机号、身份证号一放就出事JWT 的 Payload 只是 Base64 编码不是加密谁都能解开看。2.2 权限控制的落地方式拦截器 注解简单但实用既然选择了 JWT那权限控制的落地方式就顺理成章了——写一个拦截器在请求进入 Controller 之前统一校验 Token 和角色。// 登录拦截器 Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行 OPTIONS 预检请求跨域时需要 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 从请求头中获取 Token String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { // 返回 401前端收到后跳转登录页 response.setStatus(401); return false; } try { Claims claims JwtUtils.parseToken(token.substring(7)); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role, String.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }但是这里有个坑实际项目中并非所有接口都需要登录。比如首页课程列表、课程详情这些数据是公开的学员注册接口也是公开的。所以我在拦截器里加了白名单机制registry.addInterceptor(authInterceptor) // 拦截所有接口 .addPathPatterns(/**) // 白名单不需要登录的接口路径 .excludePathPatterns( /api/auth/login, /api/auth/register, /api/course/list, /api/course/detail/**, /api/upload/** );白名单要精确到具体路径不要嫌麻烦。之前我见过有人图省事只拦截/api/admin/**结果学员自己的订单信息接口也没做校验被其他人遍历 ID 把数据全看光了。安全这块宁可多敲几行代码不要抱侥幸心理。2.3 课程管理讲师上传课程状态机控制上下架课程是系统的核心资源它的生命周期一定要用状态机来管理否则数据会乱成一锅粥。我的设计是课程表加一个status字段0表示草稿讲师正在编辑、1表示待审核讲师提交申请、2表示已上架管理员审核通过、3表示已下架管理员手动下架或课程过期。每次状态变更我都记录操作日志比如“讲师提交审核”“管理员审核通过”“管理员强制下架”。这样将来出问题能追溯答辩时展示这个模块也是加分项。上传课程这个环节最容易出问题的就是图片和视频上传。图片我用 Element UI 的el-upload组件配合后端接口搞定视频则因为文件大、占用带宽高直接走的第三方云存储而不是存在本地——毕设阶段教程视频可以直接用外部链接合法授权的公开课链接即可必要的时候自己录几段短视频传上去即可。上传接口的后端实现大概是这样的PostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { // 文件大小限制这个配置要记得在 application.yml 里设置默认只有 1MB if (file.getSize() 5 * 1024 * 1024) { return Result.error(文件大小不能超过5MB); } // 生成唯一文件名防止重名覆盖 String filename UUID.randomUUID().toString().replace(-, ) StrUtil.subAfter(file.getOriginalFilename(), ., true); // 存储到本地磁盘指定目录生产环境请替换为云存储 String filePath D:/uploads/ DateUtil.format(new Date(), yyyyMMdd) / filename; File dest new File(filePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 返回可访问的 URL这里需要你配置一个静态资源映射把 /upload/** 映射到这个磁盘目录 return Result.success(/upload/ DateUtil.format(new Date(), yyyyMMdd) / filename); }注意这里有个隐藏配置SpringBoot 默认不会把本地磁盘目录映射成静态资源你要写一个WebMvcConfigurer的配置类Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/uploads/); }这段配置我一开始没写然后上传文件一直 404排查了半小时才想起来是静态资源映射没做。真心希望你别踩这个坑。2.4 订单模块数据库事务 唯一约束双保险防超卖线上辅导班的“下单”和电商不太一样不需要走复杂的库存扣减流程但仍然要解决一个核心问题同一门课同一个学员只能购买一次多人同时抢最后一门课时不能让订单数量超过课时容量。解决第一件事最简单也最可靠的方法不是代码判断而是直接在数据库层加唯一约束。在订单表上加一个联合唯一索引(course_id, student_id)。这样哪怕代码出现了并发问题数据库也会拒绝重复插入属于兜底方案。解决第二件事用数据库事务 乐观锁。课程表加一个version字段下单时带着当前版本号去执行带条件更新Transactional(rollbackFor Exception.class) public Order createOrder(Long courseId, Long studentId) { // 查询课程信息 Course course courseMapper.selectById(courseId); if (course null || course.getStatus() ! 2) { throw new BusinessException(课程已下架或不存在); } // 检查课程容量 Integer count orderMapper.countByCourseId(courseId); if (count course.getCapacity()) { throw new BusinessException(课程已满员); } // 乐观锁更新课程容量防止同一个课程被多个学员同时抢最后一席 int rows courseMapper.capacityReduceWithVersion(courseId, course.getVersion()); if (rows 0) { throw new BusinessException(手慢了课程刚被抢完); } // 创建订单 Order order new Order(); order.setCourseId(courseId); order.setStudentId(studentId); order.setAmount(course.getPrice()); order.setStatus(0); // 0-待支付1-已支付2-已取消3-已退款 orderMapper.insert(order); return order; }Transactional这个注解保证了以上操作要么全部成功要么全部回滚。面试时老师问“怎么保证数据一致性”你就可以把这段代码逻辑讲给他听——先查状态、再扣减容量、失败就回滚。这套思路迁移到电商项目的库存扣减上也完全适用。再补充一个坑capacityReduceWithVersion这条 SQL 是核心。UPDATE course SET capacity capacity - 1, version version 1 WHERE id #{courseId} AND version #{version} AND capacity 0;这里AND capacity 0这个条件千万不能省。哪怕你的 Java 代码检查了count capacity也仍然有并发间隙而 SQL 层的条件判断才是最终的防线。写 SQL 的时候多想一层并发场景能避免丢失很多线上数据。2.5 排课与课时点击上下课前端实时反馈时间冲突线上辅导班还要支持讲师排课。所谓排课就是讲师为课程添加“上课场次”上课时间、下课时间、上课方式、课堂链接或教室编号。这块业务如果前后的日历功能都做复杂了就会消耗大量时间。我简化成表格新增编辑即可。真正的核心校验在后端——同一讲师在同一个时间段不能上两门课// 查询当前讲师在当前时间段是否有其他课程 int count scheduleMapper.checkConflict(teacherId, startTime, endTime); if (count 0) { throw new BusinessException(该时间段你已有其他课程请调整时间); }SQL 里的冲突判断逻辑长这样这里是核心逻辑SELECT COUNT(*) FROM schedule WHERE teacher_id #{teacherId} AND status 1 AND (start_time #{endTime} AND end_time #{startTime})两个时间段只要存在交集即A.start B.end AND A.end B.start就说明冲突了。这个逻辑是排课系统里最常考的知识点哪怕你后面做会议室预订、考场安排也还是这套思路。讲排课这块时把这个模型讲明白跟在座的课程设计评审老师聊半小时都够用。3. 数据库设计从 ER 图到建表 SQL 的实践笔记3.1 核心表结构与字段设计思路先列一下我设计的核心表它们之间有两两的关联关系user用户表存学生、讲师、管理员的账号密码密码存储用 BCrypt 加密角色用数字区分role字段 1 为学生2 为讲师3 为管理员。course课程表存课程名称、封面图、课程简介、价格、讲师 ID外键关联 user、状态、容量、已选人数、平均评分。course_chapter章节课时表存某个课程下有哪些章节课时包括章序、节序、标题、视频链接、时长。schedule排课表存讲师开课的场次信息关联 course记录上课时间、教室或直播链接、状态。order订单表存学员购买课程的记录关联 course 和 user学员。review评价表存学员对课程的评论和打分关联 course 和 user。category课程分类表存课程的分类标签比如“编程”“英语”“数学”用一列parent_id支持二级分类。每个表都要有create_time、update_time这两个通用字段这是基本素养别偷懒不加。另外经常被查询的字段务必加索引比如course.status、course.teacher_id、order.student_id、order.course_id。不加索引的后果是数据不到一万条的时候无所谓一旦数据量上来一条查询慢到几秒你写代码时没感觉演示时就会被老师当场抓住。3.2 为什么用逻辑删除而不是物理删除在这个系统里学员可以取消订单。那么取消订单是直接 DELETE 一行还是把 status 改成已取消强烈建议用逻辑删除改状态。学员下单的数据属于交易数据物理删除了讲师的销售额统计、管理端看板统计就全都算错了而且将来要做用户画像分析、数据分析时底层数据都丢了。这符合业界的普遍做法——交易类数据一律不改删只作状态流转。类全的“撤销”“作废”“取消”功能本质上全是状态更新。对于课程表如果讲师误删了课程导致学员端已经购买过的课程突然打不开了这会是很严重的体验事故。所以课程表也不要物理删除我加了一个is_deleted字段默认 0删除时改成 1查询时默认带上is_deleted 0条件。这里有个大坑要注意如果你用了 MyBatis-Plus它的逻辑删除是全局配置的你建表时一定要把is_deleted字段命名和配置对应好否则查询时莫名其妙的过滤不掉或者更新时把逻辑删除标记弄丢了。3.3 数据库连接池与事务配置application.yml里数据库这块的配置我列一份最常用的spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/online_course?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 # HikariCP 连接池配置默认就挺好一般不需要改 hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000useSSLfalse这个配置必须写否则连 MySQL 8.0 时会因为 SSL 证书问题报错日志里经常出现SSL connection error。serverTimezoneAsia/Shanghai也不能省不然时间字段查出来和本地差 8 小时排查起来相当痛苦。这两个属于高频踩雷点我已经习惯每次都写上。4. 前后端联调与工程化实践4.1 Vue 项目结构规划和路由设计Vue 端我用 Vue 3 Element Plus Axios Vue Router Pinia 这套组合。目录结构基本按功能划分src/ ├── api/ # 接口请求模块按业务域拆分 │ ├── user.js │ ├── course.js │ └── order.js ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia 状态管理 ├── views/ # 页面组件 │ ├── admin/ # 管理员端页面 │ ├── teacher/ # 讲师端页面 │ └── student/ # 学员端页面 └── utils/ # 工具函数 └── request.js # axios 封装路由设计上用一级路由做角色区分二级路由做具体页面。路由守卫的代码很简单// 路由守卫未登录跳转登录页无权限跳转401页 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.role to.meta.role ! store.user.role) { next(/401) } else { next() } })这里要注意前端路由守卫只是体验优化不能作为安全防线真正校验永远在后端。4.2 Axios 封装请求拦截器和响应拦截器所有请求都走封装好的request.js这样一次改动全局生效import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, // 通过 Vite 代理解决跨域 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 response.data, error { if (error.response?.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } )这里必须要说的是前后端联调最常见的问题——跨域。我直接在 Vite 的配置里设置代理开发阶段完全不用后端配合处理跨域// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, // 后端服务地址 changeOrigin: true } } }生产环境部署时Nginx 也按这个思路配置反向代理把/api开头的请求转发到后端服务。一套模式打天下。4.3 后端接收前端参数的最佳实践前后端联调时最常出现的错误就是“前端传的参数后端接收不到”。原因通常就两个参数名不一致或 Content-Type 不对。我总结一套自己的方案前端 POST 提交 JSON 数据通过axios默认就是这样后端直接用RequestBody接收实体类或 Map。实体类上务必加NotNull、NotBlank等校验注解避免空值直接进入业务逻辑。PostMapping(/api/course/create) public ResultString createCourse(RequestBody Valid CourseCreateDTO dto) { }前端上传文件时用FormData格式后端就用RequestParam(file) MultipartFile file接收。GET 请求的参数用?page1size10拼接后端用RequestParam接收带默认值防报错。GetMapping(/api/course/list) public ResultPageResultCourseVO listCourses( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId ) {}4.4 Element Plus 表格和表单的快速落地管理端页面Element Plus 的el-tableel-formel-dialog三件套能解决几乎所有的后台管理页需求。我给你一个表格分页的标准写法el-table :datatableData v-loadingloading el-table-column propcourseName label课程名称 / el-table-column propteacherName label讲师 / el-table-column propprice label价格 template #defaultscope ¥{{ scope.row.price }} /template /el-table-column el-table-column propstatus label状态 template #defaultscope el-tag :typestatusMap[scope.row.status].type {{ statusMap[scope.row.status].text }} /el-tag /template /el-table-column el-table-column label操作 width200 template #defaultscope el-button sizesmall clickhandleEdit(scope.row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table一个关键经验前端展示的状态文本和颜色一定要由一个统一的状态映射表去维护不要在模板里写死。这样你调整状态文案、改动颜色时只需要改一处。这是代码洁癖但能让你在演示时少出不少尴尬。4.5 图片上传和回显的完整流程头像和课程封面图的处理流程是前端选择文件 → 调后端上传接口 → 后端存储文件并返回 URL → 前端把 URL 回填给表单 → 提交表单时 URL 一并提交到后端。这样设计的好处是上传动作和表单提交动作解耦用户没点“保存”之前图片已经传到服务器了保存时不需要再传一次文件只传字符串 URL。el-upload的配置里action指向后端的/api/upload接口on-success回调里把返回的 URL 存起来。注意name属性要和后端的RequestParam(file)参数名保持一致否则文件传不上去。5. 部署上线从本地跑到服务器全过程5.1 后端打包与部署后端用 Maven 打包成一个可执行 jarmvn clean package -DskipTests这样会在target目录下生成带-SNAPSHOT.jar后缀的 SpringBoot 可执行 jar。服务器上只需要装 JDK然后java -jar online-course.jar --spring.profiles.activeprod我把配置文件拆成了application-dev.yml和application-prod.yml用spring.profiles.active切换环境。这是好习惯本地数据库密码和线上数据库密码不该是同一个日志级别、文件存储路径也应该分开配置。5.2 前端打包与 Nginx 配置Vue 打包npm run build生成dist目录里面是纯静态文件。我把dist里的文件直接放到 Nginx 的 html 目录下然后配上反向代理server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/dist; index index.html; # 解决 Vue Router 在 history 模式下刷新页面 404 的问题 location / { 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; } # 静态资源映射 location /upload/ { alias /var/www/uploads/; } }try_files $uri $uri/ /index.html;这行必须写。Vue Router 如果用 history 模式刷新子路由页面会直接 404就是缺了这行。如果你不想折腾也可以直接用 hash 模式URL 里会多一个#但对演示来说无伤大雅。5.3 MySQL 部署后的初始化和数据迁移服务器上的 MySQL 装好后用 Navicat 连上直接导入本地的 SQL 文件即可。生产环境数据库账号不要用 root我新建一个online_course用户只授予他操作这个库的权限CREATE DATABASE online_course DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER course_userlocalhost IDENTIFIED BY 这里写一个强密码; GRANT ALL PRIVILEGES ON online_course.* TO course_userlocalhost; FLUSH PRIVILEGES;数据库编码必须用utf8mb4不要用utf8。utf8在 MySQL 里其实是残缺版存不了生僻字和 Emoji例如用户昵称里带个小表情直接插入报错。这是很多新手栽过的坑我列在这里希望你直接跳过。6. 常见问题与排查技巧实录6.1 前后端联调高频问题速查表我把联调阶段最常遇见的几个问题做成速查表对着排查就行现象可能原因排查方法前端请求报 404后端没有这个接口或路径拼接错误打开浏览器 Network对比实际请求 URL 和后端RequestMapping路径前端请求报 405请求方式不对GET POST 混用检查前端method是否和后端GetMapping/PostMapping一致请求返回 401Token 缺失、过期或格式不对看请求头里 Authorization 是否正确携带了Bearer前缀请求返回 403角色权限不足确认当前登录用户的角色是否能访问该接口参数收不到但请求正常Content-Type 不匹配或参数名不一致后端实体类字段名要和前端 JSON 的 key 完全一致时间字段差了 8 小时数据库时区设置问题检查数据库连接串是否配置serverTimezoneAsia/ShanghaiJava 启动时报端口被占用上次没停掉进程命令行执行 netstat -ano6.2 事务和数据一致性相关 Bug 复盘我自己的项目里曾经出过一个很有意思的 Bug学员下单成功后课程列表的“已选人数”没有立即增加。原因是我只用Transactional管理订单插入但忘记在同一个事务里更新课程表的已选人数。一开始看着“好像没生效”其实是事务提交前一定不要只写业务里某一环必须保证多表更新的原子性。在事务里保证多表操作的一致性可以这样做Transactional(rollbackFor Exception.class) public void completeOrder(Long orderId) { // 先锁订单行防止重复处理 Order order orderMapper.selectByIdForUpdate(orderId); if (order null || order.getStatus() ! 0) { throw new BusinessException(订单不存在或状态已变更); } // 更新订单状态 order.setStatus(1); orderMapper.updateById(order); // 课程已选人数 1 courseMapper.increaseEnrolledCount(order.getCourseId()); // 生成学习记录学员就能看到这门课了 studyRecordMapper.insert(...); }selectByIdForUpdate就是行级锁SELECT ... FOR UPDATE。这种“先锁定再操作”的方式可以保证同一时间只有一个线程能处理这笔订单彻底避免重复处理问题。不过它属于悲观锁并发高的时候会有锁等待但这个系统量级完全够用。6.3 前端常见报错跨域、脚手架、路由一次说清Vue 开发时的报错新手看得最多的就是Access to XMLHttpRequest has been blocked by CORS policy。看到这个错首先别慌你做的第一件事不是改后端 CORS 配置而是先确认 Vite 代理配了没有。如果代理配了还有这问题大概率是某个请求直接写死了完整域名http://localhost:8080而不是走代理前缀/api。建议全局搜http://看看有没有写死的请求地址。另一个高频报错是在使用vue-router时报Cannot read properties of undefined (reading push)原因基本都是router实例没注入到当前组件。解决的办法是import { useRouter } from vue-router不要在非组件环境里直接this.$router。再说说vue devtools插件它是调试 Vue 页面的神器安装了以后能直观看到组件的 props、data、state排查数据流向非常方便通过浏览器扩展商店装好即可就不用单独去下载什么离线包了。6.4 性能优化这四件事性价比最高这个项目性能优化的顺序我的经验如下第一优先级给高频查询字段加索引这个前面说过一次 SQL 优化解决 80% 的慢查询。第二优先级列表接口改用分页查询。用 MyBatis-Plus 的Page对象几行代码就能搞定别一次性把所有数据丢给前端。第三优先级前端表格懒加载。数据量大时后端直接返回 10 条先展示滚动到底再加载下一页。第四优先级首页课程列表做缓存。用 Spring 自带的Cacheable加 Redis 支持或者最朴素的本地 Caffeine 缓存都能明显提升响应速度。毕设阶段用ConcurrentHashMap做一个简单的本地缓存也能应付。面试如果有人问“项目性能怎么优化的”你就按这个顺序讲足够证明你思考过。7. 学习路径、答辩要点和扩展方向7.1 拿到这套源码后正确的学习顺序真心建议别急着把代码全跑起来就算完。按照下面这个顺序走收获会大得多第一步先跑通。把数据库 SQL 导入 MySQL改好数据库连接串启动后端再启动前端把主流程走一遍。第二步浏览代码结构。后端看 Controller 层理解每个接口的路径和参数前端看 api 目录理解每个接口是怎么被调用的。第三步挑一个模块完整读一遍。比如“学员下单”这个流程前端点下“立即购买”按钮 → 后端 Controller 接收参数 → Service 校验业务规则 → Mapper 操作数据库 → 返回结果给前端。读完你就会发现整个项目其实就是无数个这样的流程组成的。第四步动手改需求。给自己布置一个小任务比如“给课程增加一个‘是否推荐’的字段推荐位课程展示在首页”。你需要改动数据库表、后端实体类、后端 Mapper、前端表单、前端列表卡片这一套流程走完你对项目的掌握程度会达到一个新的量级。7.2 答辩时怎么说才能拿高分答辩的核心不是复述功能而是讲清楚三个问题为什么要这样做、这样做解决了什么问题、有没有更好的方案。举个例子讲登录鉴权时你可以这样说 “这个项目我用的是 JWT 方案没有用 Session。因为前后端分离后后端如果做 Session 存储就要求客户端一直持有同一个会话这在多端登录或者将来做微服务时会比较麻烦。JWT 是无状态的后端只需要验证签名不需要存储会话状态而且 token 里可以携带角色信息实现无状态鉴权。缺点是 token 一旦签发在过期前无法主动注销所以我又设计了前端拦截 401 自动跳转登录的机制来改善体验。”这段话表达了三个不同层面的思考比只背概念强多了。如果老师追问“JWT 被窃取了怎么办”你不要慌如实说“通常我们会把 token 过期时间设短一些配合刷新 token 机制来缓解风险但更底层的还需要 HTTPS 保证传输安全”。这种如实回答比硬编一个方案更能赢得认可。7.3 还能往哪些方向做二次开发这套系统扩展空间很大列举几个极具性价比的方向接入 Redis把课程列表缓存到 Redis把 JWT 的 token 存一份用于主动注销学到的知识点可以延伸到企业开发。接入支付宝沙箱支付把订单状态机从“待支付/已支付”升级成真实的支付回调这是很有含金量的订单系统进化。消息通知学员购买成功后讲师端收到站内信或邮件通知课程即将开始时提醒学员。用 RabbitMQ 做异步削峰学习价值更高。直播课堂集成排课时生成一个直播间用 WebSocket 实现聊天室多人互动。这个功能演示效果拉满。数据统计分析管理端做一个数据看板展示课程报名趋势、营收统计、热门课程 TOP10。用 ECharts 画图视觉冲击力很强工作量适中。8. 写在最后希望你知道的事这篇文章写到这里核心的东西其实已经讲完了。但最后我还是想多说几句因为我在做类似项目的过程中发现很多人卡住的根本不是技术而是心态。第一次跑通这样一个前后端分离项目你可能会遇到各种各样的问题环境变量配置不对、依赖下载失败、端口冲突、数据库版本过低、前端依赖装不上……这些我都经历过。你只要记住一个原则报错信息是人类最好的老师。拿到报错先复制到搜索引擎搜看看别人怎么解决的然后动手改改完再跑跑了再报错循环几次你就打通了。如果你正在为毕设或课设做这个选题我的建议是不要急着“为了演示而演示”先把核心链路彻底吃透。面试官和答辩老师都是老江湖你是真懂还是背稿子几句话就能试探出来。真正掌握了这套 SpringBoot Vue 开发的完整闭环你收获的不仅仅是一个可以提交的项目更是一种解决问题的框架思维。这种能力比你简历上多一行“熟练掌握”有价值得多。最后分享一个我自己的习惯每次做完一个模块我都会把当时的场景用 Markdown 记录一遍当时想做什么、用了什么方案、遇到了什么坑、怎么解决的。写这篇文章的动力也来源于此。以后你再碰到类似的项目翻一翻自己的笔记相当于和一个有经验的自己对话那感觉相当不错。