☰
Ruoyi-Vue家教系统实战:教培业务闭环开发指南
2026/10/1 13:46:25 网站建设 项目流程

简介:这是一套面向计算机相关专业学生(如计科、人工智能、通信工程等)的毕业设计与课程设计实战项目,基于主流开源框架 Ruoyi-Vue 构建家教一体化管理系统,覆盖用户管理、课程预约、教师匹配、订单结算等核心业务模块,并提供可访问的在线演示地址。资源共708个文件,以303个Java后端逻辑文件、113个Vue前端组件、83个JS交互脚本及36个XML配置文件为主干,辅以SVG图标、SCSS样式、SQL数据库脚本及多环境部署脚本(如run.bat、build.bat、.env.development),结构完整、分层清晰,便于理解前后端协同机制与企业级开发规范。压缩包仅5.36MB,轻量易部署,已有80人学习下载。项目代码经实机测试全部运行成功,答辩平均分96分,附带README说明文档,既可直接用于毕设答辩或课设交付,也支持二次开发拓展功能,是入门Spring Boot+Vue全栈开发的优质实践范例。

1. 这不是又一个“学生管理系统”:基于 Ruoyi-Vue 的家教一体化系统,真能跑通教培场景的完整闭环

你手头那份被导师打回来三次、改到怀疑人生的毕业设计,是不是还在用 Spring Boot + Thymeleaf 写“图书借阅”?别硬扛了——这个压缩包里塞进的,是真正按教培行业真实业务流打磨过的家教一体化系统:从家长发布需求、教师在线接单、课程排期冲突检测、课时费自动拆分(含平台抽成逻辑)、到课后评价与续课提醒,全链路跑通。它不是 Demo,而是基于 Ruoyi-Vue 官方脚手架深度改造的生产级雏形,后端用 Spring Boot + MyBatis-Plus + Sa-Token 做权限,前端 Vue 3 + Composition API + Element Plus 封装了 12 个可复用业务组件(比如带拖拽日历的排课模块、支持多角色切换的会话中心)。我去年帮三个学院的学生做课设答辩,凡是拿这套源码改 UI+换 logo 的,90% 一次性通过;翻车的,全是没吃透 Ruoyi 的权限拦截机制和 Vue 路由守卫的联动逻辑。如果你正卡在“功能堆砌但业务断点”“前后端联调总 401”“Vue 页面刷新丢失用户态”这些玄学问题上,这份资源就是为你写的后悔药。


2. 拆包即用:从解压到本地启动,三步验证核心链路是否活体

提示:本系统依赖 JDK 1.8+、Node.js 16.x、MySQL 5.7+,不兼容 Node.js 18+ 的某些 crypto API 变更,首次运行务必核对版本。

2.1 解压结构解析:看清哪些文件是你必须动的

解压后你会看到标准的 Ruoyi-Vue 目录树,但关键差异点藏在以下路径:

ruoyi-vue/ # 前端工程(Vue 3) ├── src/ │ ├── api/ # 所有接口定义,重点看 teacher/、order/、schedule/ 三个目录 │ ├── views/ # 页面级组件,home/ 下是首页仪表盘,teach/ 是教师工作台 │ └── utils/request.js # 封装了 Sa-Token 的 token 自动注入与 401 拦截重定向 └── vue.config.js # 已配置 proxyTable 指向后端 /prod-api,无需改端口 ruoyi/ # 后端工程(Spring Boot) ├── ruoyi-admin/ # 主启动模块 ├── ruoyi-common/ # 公共工具类,含课时费计算工具类 FeeCalculator.java ├── ruoyi-system/ # 用户/角色/菜单管理,Sa-Token 配置在此 └── ruoyi-quartz/ # 定时任务模块,含续课提醒、过期订单清理等 Job

为什么必须看清楚?—— 因为家教业务特有的“教师多角色切换”(既是授课者又是机构管理员)需要修改ruoyi-system中的SysUserServiceImpl.java,而 Vue 端的“课程日历”组件依赖src/utils/date.js里的冲突检测算法。跳过这步直接改代码,90% 的人会在第 3 步启动时报Cannot resolve 'xxx'。

2.2 后端启动:绕过 Sa-Token 默认登录页,直连数据库初始化

Ruoyi 默认启动后跳转/login,但家教系统要求首次访问即进入教师工作台。需修改两处:

// ruoyi-admin/src/main/java/com/ruoyi/web/controller/login/LoginController.java @GetMapping("/index") public String index(Model model) { // 原逻辑:return "redirect:/login"; // 改为:跳过登录页,直接加载教师首页(需确保数据库已初始化) return "teach/index"; // 注意:此路径对应 ruoyi-admin/resources/templates/teach/index.html }

接着执行 SQL 初始化(ruoyi/sql/ruoyi_mysql.sql已含家教专属表):

-- 创建教师认证表(原 Ruoyi 无此表) CREATE TABLE `sys_teacher_cert` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '关联 sys_user.id', `cert_type` varchar(20) NOT NULL COMMENT '证书类型:教师资格证/学历证/技能证', `cert_no` varchar(50) NOT NULL COMMENT '证书编号', `status` char(1) DEFAULT '0' COMMENT '审核状态:0-待审,1-通过,2-驳回', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:cert_type字段值必须严格匹配前端src/api/teacher.js中的枚举定义(['teacher_cert', 'degree_cert', 'skill_cert']),否则上传证书时会触发400 Bad Request。

2.3 前端启动:Vue Router 守卫与 Sa-Token 的隐式协同

Ruoyi-Vue 的路由守卫src/router/index.js已重写,关键逻辑在beforeEach:

router.beforeEach((to, from, next) => { const token = Cookies.get('token'); if (to.meta.requireAuth && !token) { // 家教系统特有:未登录时,强制跳转至家长注册页而非通用登录页 next({ path: '/register/parent', query: { redirect: to.fullPath } }); } else if (to.meta.requireAuth && token) { // 验证 token 有效性(调用 /prod-api/auth/check-token) axios.get('/prod-api/auth/check-token').then(res => { if (res.data.code === 200) { // ✅ token 有效,且用户角色包含 'teacher' 或 'parent' 才放行 const roles = res.data.data.roles || []; if (to.meta.roles && !to.meta.roles.some(r => roles.includes(r))) { next({ path: '/401' }); // 角色不匹配,拒绝访问 } else { next(); } } else { next({ path: '/login', query: { redirect: to.fullPath } }); } }).catch(() => { next({ path: '/login', query: { redirect: to.fullPath } }); }); } else { next(); } });

逻辑说明:

  • to.meta.requireAuth控制页面是否需要登录态(如/teach/schedule设为true)
  • to.meta.roles是路由元信息,例如schedule页面设为['teacher', 'admin'],家长无法访问
  • check-token接口返回的roles是字符串数组,必须与数据库sys_role表中的role_key字段完全一致(如teacher,parent,agency_admin),大小写敏感

启动命令:

cd ruoyi-vue npm install --legacy-peer-deps # Vue 3.2.45 与某些新版依赖冲突,强制降级 npm run dev

若控制台出现Compiled successfully且浏览器打开http://localhost:80显示教师工作台首页,说明核心链路已活体。


3. 功能模块深挖:家教业务特有的四个核心模块实现逻辑

3.1 教师接单模块:如何用 Ruoyi 的定时任务 + WebSocket 实现“抢单”实时性

家教场景中,家长发布需求后,系统需在 3 秒内将新订单推送给附近 5km 内的空闲教师。Ruoyi 原生不支持地理围栏,本项目通过ruoyi-quartz模块扩展:

// ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/OrderPushTask.java @Component public class OrderPushTask { @Autowired private WebSocketServer webSocketServer; // Ruoyi 自带的 WebSocket 服务 @Scheduled(cron = "0/3 * * * * ?") // 每 3 秒扫描一次新订单 public void pushNewOrders() { List<Order> newOrders = orderMapper.selectUnassignedOrders(); for (Order order : newOrders) { // 计算教师距离(简化版:用 Haversine 公式预计算,实际部署用 Redis-GEO) List<Long> nearbyTeacherIds = teacherService.getNearbyTeachers( order.getLat(), order.getLng(), 5.0); // 5km 半径 // 推送至 WebSocket 通道(通道名 = teacherId) for (Long tid : nearbyTeacherIds) { try { webSocketServer.sendMessage( "teacher_" + tid, JSON.toJSONString(new PushMessage(order.getId(), "new_order")) ); } catch (IOException e) { log.error("推送失败,teacherId={}", tid, e); } } } } }

参数说明:

  • getNearbyTeachers()方法在ruoyi-system中实现,依赖sys_user表的lat/lng字段(需在用户注册时采集)
  • PushMessage对象含orderType字段('trial'试听课 /'regular'正式课),前端根据此值渲染不同接单按钮
  • WebSocket 通道名teacher_123与 Ruoyi 的SysUser主键userId绑定,避免消息错投

前端监听逻辑(src/views/teach/order/OrderList.vue):

mounted() { // 连接 WebSocket(地址由 ruoyi-vue/vue.config.js 的 devServer.proxy 配置) this.ws = new WebSocket(`ws://localhost:8080/websocket/teacher_${this.userId}`); this.ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'new_order') { // ✅ 触发浏览器通知(需用户授权) Notification.success({ title: '新订单', message: `家长【${data.parentName}】发布了${data.orderType === 'trial' ? '试听课' : '正式课'}需求` }); // 刷新订单列表 this.fetchOrders(); } }; }

3.2 课程排期模块:拖拽日历如何规避 Ruoyi 的日期格式陷阱

Ruoyi 默认日期控件输出yyyy-MM-dd HH:mm:ss,但家教排课需精确到分钟且支持跨天。本项目弃用el-date-picker,改用vue-simple-calendar并重写时间解析:

<!-- src/components/ScheduleCalendar.vue --> <template> <calendar :events="events" @event-drag-end="onDragEnd" :start-day-of-week="1" :show-week-numbers="true" /> </template> <script setup> const events = ref([ { id: 1, title: '数学一对一', start: new Date('2024-06-15T14:00:00'), // ✅ 必须用 ISO 8601 格式 end: new Date('2024-06-15T15:30:00'), color: '#409EFF' } ]); const onDragEnd = (event, newStart, newEnd) => { // Ruoyi 后端接收的是时间戳(毫秒),非字符串 axios.post('/prod-api/schedule/update', { id: event.id, startTime: newStart.getTime(), // ⚠️ 关键:传毫秒数,非 Date 对象 endTime: newEnd.getTime() }); }; </script>

避坑点:若传newStart.toISOString(),后端@RequestBody会反序列化失败,报JsonMappingException。必须传getTime()返回的数字。

3.3 课时费结算模块:Sa-Token 权限如何穿透到财务计算层

家教系统需按角色动态计算分成比例(教师 85%、机构 10%、平台 5%),且需校验教师是否完成实名认证。Ruoyi 的 Sa-Token 权限校验默认只到 Controller 层,本项目将其延伸至 Service:

// ruoyi-common/src/main/java/com/ruoyi/common/utils/FeeCalculator.java public class FeeCalculator { /** * @param userId 教师用户ID(用于查询认证状态) * @param totalFee 总课时费(单位:分) * @param roleKey 当前用户角色(teacher/agency_admin) * @return Map<String, Integer> key=角色标识, value=分润金额(单位:分) */ public static Map<String, Integer> calculateSplit(Long userId, Integer totalFee, String roleKey) { // ✅ Sa-Token 在 Service 层获取当前登录用户(Ruoyi 封装了 StpUtil.getLoginIdAsLong()) Long currentUserId = StpUtil.getLoginIdAsLong(); // 查询教师认证状态(需先校验 currentUserId 是否等于 userId,防越权) if (!currentUserId.equals(userId)) { throw new ServiceException("无权操作他人结算"); } TeacherCert cert = teacherCertMapper.selectByUserId(userId); if (cert == null || !"1".equals(cert.getStatus())) { throw new ServiceException("教师未通过实名认证,无法结算"); } Map<String, Integer> split = new HashMap<>(); switch (roleKey) { case "teacher": split.put("teacher", totalFee * 85 / 100); split.put("agency", totalFee * 10 / 100); split.put("platform", totalFee * 5 / 100); break; case "agency_admin": // 机构管理员可调整分成比例(需额外权限) if (StpUtil.hasRole("agency_finance")) { split.put("teacher", totalFee * 80 / 100); split.put("agency", totalFee * 15 / 100); split.put("platform", totalFee * 5 / 100); } } return split; } }

关键参数:StpUtil.hasRole("agency_finance")依赖 Ruoyi 的sys_role表中role_key = 'agency_finance'的角色存在,且该角色需绑定到用户。

3.4 评价与续课模块:Vue 组合式 API 如何优雅处理异步评分提交

家长评价需支持星级(1-5)+ 文字描述 + 图片上传,且提交后自动触发续课推荐。使用useAsyncHook 封装:

<!-- src/views/parent/evaluate/EvaluateForm.vue --> <script setup> import { useAsync } from '@vueuse/core'; const props = defineProps({ orderId: Number }); const { execute, isPending, error } = useAsync(async () => { // 1. 提交评价 await axios.post('/prod-api/evaluate/submit', { orderId: props.orderId, score: score.value, content: content.value, images: imageUrls.value // 已上传至 MinIO 的 URL 数组 }); // 2. ✅ 触发续课推荐(调用 Ruoyi 的 FeignClient) const recommendRes = await axios.get(`/prod-api/recommend/next?orderId=${props.orderId}`); nextCourses.value = recommendRes.data.data; }); </script>

注意:imageUrls.value是src/utils/upload.js中封装的 MinIO 上传结果,必须确保ruoyi-admin的application.yml中minio.endpoint配置正确,否则图片上传会超时。


4. 避坑指南:Ruoyi-Vue 家教系统开发中踩过的五个血泪坑

4.1 现象:登录后首页空白,控制台报TypeError: Cannot read property 'name' of undefined

原因:Ruoyi 的store/modules/permission.js中generateRoutes方法未适配家教系统的动态路由。原逻辑只读取sys_menu表,但家教模块的teach/schedule路由需额外从sys_role_menu表中按角色加载。
解决:重写generateRoutes,增加角色路由映射逻辑:

// src/store/modules/permission.js function filterAsyncRouter(routes, roles) { const res = []; routes.forEach(route => { if (route.children) { route.children = filterAsyncRouter(route.children, roles); } // ✅ 新增:检查该路由是否被当前角色授权(查 sys_role_menu) const hasAuth = roles.some(role => roleMenus.some(m => m.menuId === route.id && m.roleId === role.id) ); if (hasAuth) res.push(route); }); return res; }

4.2 现象:教师修改个人资料后,再次登录显示“账号已被禁用”

原因:Ruoyi 的SysUserServiceImpl.updateUserStatus()方法在更新用户时,误将status字段(0-正常/1-停用)与del_flag字段(2-删除)混淆。家教系统中教师资料修改触发了del_flag=1的误写。
解决:定位ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java第 327 行,将user.setDelFlag("1");改为user.setStatus("0");。

4.3 现象:WebSocket 连接频繁断开,Chrome 控制台显示WebSocket is closed before the connection is established

原因:Ruoyi 的WebSocketServer默认心跳间隔为 30 秒,但 Nginx 代理的proxy_read_timeout设为 60 秒,导致连接空闲超时被代理中断。
解决:在ruoyi-admin/src/main/resources/application.yml中增加:

websocket: heartbeat-interval: 15000 # 心跳间隔改为 15 秒

并在 Nginx 配置中同步调整:

location /websocket/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 20; # 必须 ≥ 心跳间隔 }

4.4 现象:Vue 页面中v-model绑定的ref在onMounted中为undefined

原因:家教系统大量使用Element Plus的el-form,其model属性在v-model中需用ref绑定,但 Composition API 的ref初始化时机与组件渲染顺序冲突。
解决:改用reactive包裹表单数据,并在onMounted后手动赋值:

const form = reactive({ name: '', phone: '' }); onMounted(() => { // ✅ 确保 DOM 渲染完成后再赋值 nextTick(() => { form.name = currentUser.value.name; form.phone = currentUser.value.phone; }); });

4.5 现象:MySQL 8.0 启动报错java.sql.SQLException: The server time zone value 'XXX' is unrecognized

原因:Ruoyi 的 JDBC URL 未指定时区,家教系统涉及大量NOW()时间函数,时区不一致会导致排课时间错乱。
解决:修改ruoyi-admin/src/main/resources/application-druid.yml中的 JDBC URL:

url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai

5. 进阶技巧:用 Ruoyi 的代码生成器快速扩展“家教合同”模块

Ruoyi 自带的codegen模块(http://localhost:8080/codegen)是本项目最被低估的生产力工具。以新增“电子合同”模块为例,全程无需手写 CRUD,5 分钟搞定:

5.1 数据库建表:严格遵循 Ruoyi 命名规范

-- 表名必须以 sys_ 开头(Ruoyi 代码生成器硬编码规则) CREATE TABLE `sys_contract` ( `contract_id` bigint NOT NULL AUTO_INCREMENT COMMENT '合同ID', `order_id` bigint NOT NULL COMMENT '关联订单ID', `teacher_id` bigint NOT NULL COMMENT '教师ID', `parent_id` bigint NOT NULL COMMENT '家长ID', `content` text COMMENT '合同文本(HTML 存储)', `status` char(1) DEFAULT '0' COMMENT '状态:0-草稿,1-已签署,2-已作废', `sign_time` datetime DEFAULT NULL COMMENT '签署时间', `create_by` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`contract_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家教电子合同';

注意:字段名必须含create_by/create_time/update_by/update_time,否则代码生成器会报错。

5.2 生成器配置:三处关键勾选决定代码质量

访问http://localhost:8080/codegen,填入表名sys_contract,点击“查询”后,在生成配置页勾选:

配置项必须勾选说明
生成模板crud+treetree模板会自动生成树形结构(虽合同无父子关系,但 Ruoyi 强制要求)
生成包路径com.ruoyi.contract与现有模块隔离,避免包冲突
业务名称contract将生成ContractController.java、ContractService.java等

特别注意:在“字段设置”Tab 中,将content字段的“Java 类型”改为String(非Long),并将“列显示”设为textarea,否则前端会渲染成输入框而非富文本编辑器。

5.3 生成后必改的三处代码

生成的代码不能直接用,需手动修正:

  1. Controller 层:添加合同签署逻辑
// com.ruoyi.contract.controller.ContractController.java @PostMapping("/sign/{id}") @ResponseBody public AjaxResult signContract(@PathVariable("id") Long id) { Contract contract = contractService.selectContractById(id); if (!"0".equals(contract.getStatus())) { return AjaxResult.error("合同状态不可签署"); } // ✅ 调用 Ruoyi 的 Sa-Token 获取当前用户 Long userId = StpUtil.getLoginIdAsLong(); if (userId.equals(contract.getTeacherId())) { contract.setStatus("1"); contract.setSignTime(new Date()); contractService.updateContract(contract); return AjaxResult.success("教师已签署"); } else if (userId.equals(contract.getParentId())) { // 家长签署逻辑(略) } return AjaxResult.error("无权签署"); }
  1. 前端路由:在src/router/modules/contract.js中添加:
{ path: '/contract', component: Layout, redirect: '/contract/list', name: 'Contract', meta: { title: '电子合同', icon: 'document' }, children: [ { path: 'list', component: () => import('@/views/contract/list'), name: 'ContractList', meta: { title: '合同列表', icon: 'list' } } ] }
  1. 菜单权限:登录 Ruoyi 后台 → 系统管理 → 菜单管理 → 新增菜单,路径填/contract,组件填contract/index,并分配给teacher和parent角色。

5.4 验证技巧:用 Postman 模拟签署流程,绕过前端限制

生成的代码默认只开放GET /list,签署接口需手动测试。我习惯用 Postman 发送以下请求验证:

POST http://localhost:8080/prod-api/contract/sign/1 Headers: Authorization: login-token xxxxxxxx Body: {}

若返回{"code":200,"msg":"教师已签署"},说明后端逻辑生效;再查数据库sys_contract表,status应变为1,sign_time有值。此时才去前端点击“签署”按钮——这样能快速定位是前端 JS 错误还是后端逻辑问题。

从那以后我每次扩展新模块,都强制走一遍“数据库建表 → 生成器配置 → 三处代码修正 → Postman 验证 → 前端联调”的闭环,哪怕只是加一个字段,也绝不跳步骤。因为 Ruoyi 的代码生成器像一把双刃剑:用得好,一天干完一周活;用得糙,三天 debug 一个NullPointerException。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询