
简介大学生家教管理系统编号25341是一份基于SpringBoot3Vue.js3的前后端分离毕业设计资源面向计算机相关专业学生尤其适合作为毕业设计、课程设计或Java全栈项目的完整参考。系统采用Vue.js3构建管理后台与用户前台后端使用SpringBoot3开发数据库选用MySQL8覆盖家教资料管理、课程安排、时间管理、教学评价、学生预约沟通等核心模块同时支持教师个人信息维护、教学进度跟踪、学生反馈分析以及管理员的资料审核与用户管理。整套资源共6个文件压缩包总大小210.26MB主要包含源码zip、数据库SQL、需求文档docx和演示录屏mp4其中启动教程视频可帮助读者快速完成环境配置、数据库导入和系统部署。目前已有90人学习下载适合希望深入理解SpringBoot3与Vue.js3整合开发、掌握前后端分离项目结构并用于完善毕业设计或提升全栈实践能力的学生。1. SpringBoot3Vue3 家教系统2025 年毕业设计该用什么组合做家教管理系统这类选题最怕的不是功能多而是技术栈落到一半发现 Spring 官方已经把老 API 换掉了。SpringBoot3 强制 JDK17javax.servlet 全面迁移到 jakarta.servletSpring Security 的配置写法也变了——这意味着网上大量 2019 年的教程只能参考思路不能直接复制粘贴。Vue3 这边同样是另一个世界Composition API、Vite、Pinia 共同构成了新的工程事实。这篇文章从 IDEA 新建 SpringBoot3 项目开始把家教系统的用户、课程、订单、结算四大块串成一条可运行的主线。适合正在做毕业设计、或者想在 SpringBoot3 上快速落地一个前后端分离项目的开发者。不需要你有微服务经验但最好已经写过一次增删改查。2. 用 IDEA 创建 SpringBoot3 后端JDK17 与 knife4j 一个都不能少2.1 IDEA 里创建 SpringBoot3 项目的 4 个注意点在 IntelliJ IDEA 里新建项目时选择 Spring Initializr先别急着勾依赖这 4 个注意点决定了后面能不能顺利跑起来。第一JDK 必须选 17 及以上SpringBoot3 的 class 文件版本直接要求 JDK17IDEA 里如果 SDK 列表没有 17需要先到本地安装再刷新。第二Spring Boot 版本选择 3.2.x 或 3.3.x不要选带 SNAPSHOT 的版本答辩现场临时下载依赖容易卡住。第三Group 和 Artifact 尽量保持简单例如com.example和tutor-admin。第四初始依赖只勾 Spring Web 和 ValidationORM、接口文档、JWT 这些放进 pom.xml 里方便后续一条条看报错。配置项推荐值说明JDK17SpringBoot3 最低要求19/21 可用但毕设环境用 17 最稳Spring Boot 版本3.3.x3.2.x 也可以跑优先用 3.3 的稳定版打包方式Jar前后端分离项目不需要打 war初始依赖Web、Validation其余依赖写进 pom避免初始化界面选择错误IDEA 创建 springboot3 项目时还有最后一个隐藏坑如果你的 IDEA 版本比较老New Project 界面可能还没有把 Spring Boot 3.x 当作默认选项。这种情况下不要手动下载旧版框架包硬凑直接去 start.spring.io 生成压缩包然后用 IDEA 以 Maven 项目方式打开效果一样。2.2 依赖清单SpringBoot3 家教系统 pom.xml 一次配齐下面这份 pom.xml 是家教系统后端的主干依赖覆盖了 Web 接口、参数校验、MyBatis-Plus 数据访问、knife4j 接口文档和 JWT 需要的相关库。注意 mysql 驱动在 SpringBoot3 里的 artifactId 变成了mysql-connector-j很多老教程写的mysql-connector-java在这个版本下已经不会被自动管理版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.x/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- MyBatis-Plus 官方为 SpringBoot3 提供的独立 starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.x/version /dependency !-- knife4j 适配 jakarta 命名空间的版本 -- dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi3-jakarta-spring-boot-starter/artifactId version4.x/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies版本号里的3.3.x、4.x代表当前小版本安装时可以换成 Maven 仓库里的最新稳定版。同时说明一个容易踩的坑MyBatis-Plus 老版的mybatis-plus-boot-starter不是为 SpringBoot3 准备的启动会直接报ClassNotFoundException或循环依赖。务必使用mybatis-plus-spring-boot3-starter这个独立分支。knife4j 同理knife4j-openapi3-jakarta-spring-boot-starter才是支持 SpringBoot3 的版本。2.3 用 knife4j 直接看到微信端的每个接口knife4j 在这个项目里扮演的是在线接口调试工具它的价值在前后端分离开发里非常直接后端写完一个接口前端不用等文档打开浏览器就能看到请求参数、返回结构和响应示例。配置文件里什么都不用加只要引入依赖启动后访问http://localhost:8080/doc.html就能看到 OpenAPI3 风格的接口列表。先写一个统一返回类避免每个接口各自返回不同格式的 JSON前端拦截器写起来才轻松。public class ResultT { private Integer code; // 0 成功1 业务失败401 未登录 private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.message ok; r.data data; return r; } // 省略 getter/setter 和 error 方法 }然后写一个最小的老师控制器验证 knife4j 能识别到接口RestController RequestMapping(/api/teacher) Tag(name 家教老师, description 老师基础信息与授课列表) public class TeacherController { GetMapping(/list) public ResultListTeacherVO list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { // 实际项目里这里调用 service 层分页查询 return Result.success(teacherService.page(pageNum, pageSize)); } }这里有两个参数值得解释defaultValue 1和defaultValue 10是给前端兜底的分页参数防止调用方不传 pageNum 时数据库直接执行一次全表扫描。统一返回Result的好处是前端 axios 拦截器只需要判断code这一个字段就能知道业务是否成功不用每个接口各自处理异常的 JSON 结构。如果你发现启动时 doc.html 打不开优先检查是否有旧依赖引入了javax.servlet相关的传递依赖而不是先去排查 knife4j 配置。3. Vue.js3 前端工程从 Vite 创建到路由守卫拦住未登录用户3.1 Vite 创建 vue3 工程并解决跨域代理前端工程我习惯用 Vite 创建命令如下。注意这里的模板名称就是vue不是vue-ts如果打算整篇用 JavaScript 而不是 TypeScript后者选错会多出一堆类型报错。npm create vitelatest tutor-web -- --template vue cd tutor-web npm install npm install vue-router4 pinia axios element-plus安装完依赖后第一件事是配置 Vite 开发服务器的代理这一步直接决定前后端联调时会不会被跨域拦住。编辑项目根目录的vite.config.jsimport { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true // 后端接口统一带 /api 前缀不需要 rewrite } } } })changeOrigin: true的作用是把请求头里的 Host 字段改成目标地址后端打印日志时看到的是 localhost:8080 而不是 5173避免一些基于 Host 做校验的场景误判。开发环境用代理解决跨域生产环境部署时再让 Nginx 做同样的反向代理前后端代码里都不需要开启 CORS这是分离项目最省心的链路。3.2 axios 封装携带 token 且统一处理 401Vue3 项目里每个页面单独写 axios 请求会非常难维护尤其是登录态过期这种全局逻辑。我一般会创建一个src/utils/request.js把所有请求统一收敛在这里下面是一份可以直接抄的封装。import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(accessToken) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(accessToken) window.location.href /login return Promise.reject(new Error(未登录)) } if (res.code ! 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(accessToken) window.location.href /login } return Promise.reject(error) } )这段代码里最容易漏掉的是两个 401 处理分支后端可能在网关层直接返回 HTTP 401也可能返回 HTTP 200 但 body 里code字段是 401两种情况都必须清理本地 token 并跳转登录页否则用户会一直停留在当前页面却无法点击任何按钮。timeout: 10000表示 10 秒没响应就中断家教系统的查询接口通常不会超过 3 秒这个值可以防止移动端弱网环境下请求长时间悬挂。3.3 Pinia 存登录态与路由守卫未登录跳转不再靠手写判断用户登录后拿到的 token 和基础信息放在 Pinia 里而不是每个页面各自从 localStorage 读取。Pinia 相比 Vuex 的优势是无需写 mutation直接修改 state 即可对学生项目来说心智负担小很多。// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(accessToken) || , userInfo: {} }), actions: { setToken(token) { this.token token localStorage.setItem(accessToken, token) }, logout() { this.token this.userInfo {} localStorage.removeItem(accessToken) } } })路由守卫写在src/router/index.js里在每个路由跳转前检查目标页面是否需要登录router.beforeEach((to, from, next) { const store useUserStore() if (to.meta.requiresAuth !store.token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这里有个顺序问题经常导致报错路由守卫代码必须在app.use(pinia)之后注册否则组件外调用useUserStore()会拿不到当前的 active pinia 实例。具体表现为控制台提示getActivePinia was called with no active Pinia。所以入口文件 main.js 里的顺序应该是先createPinia()再app.use(pinia)最后app.use(router)。4. 家教系统核心业务从建表到订单状态机的一次完整联调4.1 数据库表设计家长、老师、订单、课时记录家教系统不同于通用电商它的核心数据是“谁在什么时候给谁上课”所以表结构围绕排课时间展开。我采用的方案是家长和老师共用一张用户表用role字段区分避免登录鉴权时反复 join 两张独立的用户表。老师简介和收费标准单独放一张扩展表课程订单和课时记录各一张表整体五张表足够覆盖一个完整闭环。表名核心字段说明t_userid, role, openid, nickname, phone, statusrole 1 家长2 老师t_teacherid, user_id, subject, intro, price_per_hour, stars老师核心扩展信息t_teacher_scheduleid, teacher_id, date, start_time, end_time, is_booked可授课时段一个时段只能被一个订单占用t_course_orderid, order_no, student_id, teacher_id, schedule_id, course_hours, total_amount, status订单主表t_course_recordid, order_id, student_id, teacher_id, start_time, end_time, content, status每节课的上课记录订单表直接引用schedule_id而不是存一个上课时间段字符串原因是时间冲突校验可以下推到 t_teacher_schedule 表去判断订单本身只关心“预约了哪个时段”。这种方式在并发场景下更好控制先把时段标记为已预订再创建订单后续的累计课时、老师结算都从这两个表派生。4.2 课程冲突校验与订单状态机实现创建订单是系统性操作必须放在一个事务里完成。完整逻辑分四步查家长账号是否存在、锁定老师排课记录、计算金额、插入订单。这里用selectByIdForUpdate是 MySQL InnoDB 的行锁机制保证同一个 schedule_id 不会被两个并发请求同时抢到。Override Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderDTO dto) { User student userMapper.selectById(dto.getStudentId()); if (student null || student.getRole() ! 1) { throw new BizException(家长账号不存在); } TeacherSchedule schedule scheduleMapper.selectByIdForUpdate(dto.getScheduleId()); if (schedule null || schedule.getIsBooked() 1) { throw new BizException(该时段已被预约); } BigDecimal total teacherMapper.selectById(schedule.getTeacherId()) .getPricePerHour() .multiply(BigDecimal.valueOf(dto.getCourseHours())); CourseOrder order new CourseOrder(); order.setOrderNo(IdGenerator.next()); order.setStatus(UNPAID); order.setTotalAmount(total); courseOrderMapper.insert(order); schedule.setIsBooked(1); scheduleMapper.updateById(schedule); return order.getId(); }订单状态机建议按“UNPAID → PAID → 待上课 → COMPLETED”主链路设计另外保留 UNPAID 可取消、PAID 可退款的分支。多一个“待上课”状态看似冗余其实是为了上课时间在未来的订单家长支付后老师还没授课订单不能直接标完成只能等课时记录回写后再流转。如果老师端希望被预约后立刻收到提醒SpringBoot3 下可以用 Eclipse Paho 客户端或 Spring Integration MQTT 实现消息推送。但毕设场景我建议先做一张消息通知表订单创建后写入一条未读消息前端每 30 秒轮询一次。相比引入 MQTT这种做法不需要额外维护 broker答辩时也更容易说清楚可靠性问题。4.3 用 knife4j 走通「家长下单 → 管理员查看」完整链路后端启动后在浏览器打开 doc.html按下面顺序操作十几分钟就能验收完整业务流。先调用登录接口拿到 JWT点击右上角 Authorize 填入Bearer token之后所有接口请求都会自动带上认证头。然后调用家长下单接口传studentId、teacherId、scheduleId、courseHours四个参数最后调管理员订单列表接口看数据是否落库。联调现象优先检查项建议处理方式前端请求返回 404后端接口是否带 /api 前缀检查 vite 代理是否 rewrite 路径返回 401请求头是否携 Authorization查看 request.js 拦截器是否生效返回 500 SQL 报错控制台打印的 SQL 语句核对表名和字段的下划线映射浏览器 Network 无请求跨域预检失败开发用代理避免直接开 CORS这四类错误里最隐蔽的是第一类。如果后端 Controller 路径是/api/teacher/listvite 代理里把/api前缀去掉请求就会变成/teacher/list后端肯定 404。所以代理配置里rewrite是否需要写取决于前后端是否约定统一前缀不要盲目复制网上的配置。5. 答辩前必调的三个隐蔽细节JWT 刷新、接口幂等与订单分页5.1 双 tokenJWT 过期不再把老师踢下线单 token 方案下accessToken 过期时间是 30 分钟老师正在上课时突然被要求重新登录这种体验在答辩演示时很减分。常见做法是登录时同时签发 accessToken 和 refreshToken前者 30 分钟有效后者 7 天有效前端只保存 accessToken 在内存里refreshToken 放 localStorage。当接口返回 401 时用 refreshToken 调刷新接口换取新的 accessToken静默完成续期。private String createToken(Long userId, long expireMinutes) { return Jwts.builder() .subject(String.valueOf(userId)) .expiration(new Date(System.currentTimeMillis() expireMinutes * 60_000)) .signWith(secretKey) .compact(); } String accessToken createToken(userId, 30); String refreshToken createToken(userId, 7 * 24 * 60);刷新接口/api/auth/refresh必须放到 Spring Security 的 permitAll 白名单里否则 refreshToken 还没用就被安全拦截器挡掉了。这个细节很多教程不会写但答辩现场极易被老师问到。5.2 重复点击下单的幂等处理家长网络卡顿导致重复点击下单按钮如果没有幂等保护数据库会出现多条相同时间段的订单。最轻量的方案是在 t_course_order 上建立联合唯一索引把提交按钮的点击做到底层拦截ALTER TABLE t_course_order ADD UNIQUE KEY uk_student_schedule (student_id, schedule_id);Service 层创建订单时捕获 DuplicateKeyException转成业务异常提示“请勿重复提交”。这个方案不需要引入 Redis 分布式锁代码改动小适合作为毕业设计的亮点写在论文里。5.3 订单量大了之后的分页优化订单列表默认的 LIMIT offset 翻页在数据量到十万条后会明显变慢因为数据库要扫描并丢弃前面的行。常用的替代方案是 keyset 分页前端滚动加载时把上一页最后一条记录的排序字段值传回来-- 传统写法越翻越慢 SELECT * FROM t_course_order ORDER BY created_at DESC LIMIT 20 OFFSET 100000; -- keyset 写法走覆盖索引 SELECT * FROM t_course_order WHERE created_at #{lastCreatedAt} ORDER BY created_at DESC, id DESC LIMIT 20;注意 keyset 分页要求排序条件必须加id DESC作为次级排序否则两条记录 created_at 相同时下一页会漏数据或重复数据。这里的 id 是主键还是雪花 ID 不影响效果只要保证排序字段加上 id 作为次级排序即可。本文还有配套的精品资源点击获取