
从朋友家里给孩子找家教这件事说起吧。加了三个家长群翻了几十条聊天记录问了一圈熟人最终约了一位老师试听结果发现老师的时间跟孩子学校的日程冲突排课全靠本子记课时包还剩几节完全没人说得清。那段时间我正好在规划一个练手项目干脆把这个场景做成一套系统家教服务管理系统。技术栈就锁定SpringBoot、Vue和Node.js这也是当前前后端分离项目里最常见的一组搭配。这套系统我实际做下来功能覆盖了教师入驻、课程发布、家长选课、预约试听、下单支付、排课签到、消课记录、评价结算这几个核心闭环。它不只是一个管理后台更多解决的是家教行业里最头疼的“信息不同步”和“履约过程失控”的问题。如果你是在校学生想拿来做毕业设计或求职项目或者初创团队想快速搭一套家教平台的MVP这篇内容都值得你从头到尾看完。我会从业务梳理、技术选型、数据库设计、具体接口实现、前端踩坑、Node.js环境折腾一直到部署维护按实际开发顺序完整讲一遍。1. 把家教系统做成什么样先理清业务再谈技术1.1 从一条真实的找家教经历说起很多做这类项目的人一上来就急着建SpringBoot工程、写CRUD结果三个月后做出来一个“用户管理课程管理”的玩具。原因是没把业务想清楚。家教服务管理系统的核心不是“管理”而是撮合和履约——它要解决的是家长怎么高效找到靠谱老师老师怎么把自己的时间卖出去并且能按时结算平台怎么保证这个过程可信、可追溯。我自己那条真实的找家教经历暴露出的问题很有代表性老师时间碎片化孩子加课之后原来的时间安排全乱课时包用Excel记录上了几节剩几节对不上账试听之后老师到底讲得怎么样家长只能凭感觉反馈给班主任。这些问题归纳起来就是四个字信息断层。所以这套系统我设定了三个角度的目标去覆盖这些断层家长要能看到老师的真实评价和可预约时间老师要能在日历上管理自己的所有排课和课时包消耗平台管理员要能看到订单状态和交易流水。三个角色看到的数据来自同一套底层业务链路任何一端的信息更新其他两端能实时感知到。1.2 三条用户线和五个核心业务环节系统里我设计了三条用户线管理员平台运营方、教师端、家长端。这里不要一上来就做复杂的功能先把五个核心业务环节定下来所有代码都围绕这五个环节展开注册与认证教师提交资质材料管理员审核通过后才可发布课程。课程发布与浏览老师创建课程包带总课时、单价、适合年级和科目家长按条件筛选。预约与下单家长预约试听课试听满意后购买正课课时包生成订单。排课与履约购买后家长和老师共同约定上课时间系统生成课次上课后签到消课。评价与结算每节课后家长可评价平台根据课次记录生成教师结算单。这五个环节是顺序闭环的缺任何一个系统都会“漏”。比如如果少了认证环节教师资质审核就成摆设少了消课记录课包剩余课时就永远对不上。我建议你画一张流程图固定下来再开始建表写代码。我自己的经验是这个阶段花两周时间一点都不亏后面改业务模型的成本是前面的好几倍。2. 技术栈分工SpringBoot、Vue、Node.js各管哪一段2.1 后端为什么选SpringBootSpringBoot在Java生态里做中小型业务系统是最稳的选择没有之一。首先它对依赖做了统一管理starter机制让人不用再为Spring和SpringMVC之间的一大堆jar包配置头疼其次内嵌Tomcat打一个jar包就能跑部署成本很低再就是生态成熟Spring Security、MyBatis-Plus、Redis、EasyExcel这些配套方案一搜一大把开发效率直接起飞。家教系统里的订单、排课、课时统计都是事务性很强的操作尤其是支付回调、课程包剩余课时扣减这类场景一旦并发或异常没处理干净账就对不上。Java的SpringBoot相比Node.js或PHP在工程化、强类型约束和事务控制上更有优势团队协作时代码可维护性也会高不少。2.2 前端为什么用VueVue在这个项目里是最适合的前端框架。首先它上手门槛低模板语法直观比React的JSX心智负担小其次Vue 3的组合式API配合Vite开发体验很好热更新速度快。对于这种管理后台移动端H5的系统界面Vue的组件化开发方式也非常契合。我会把前端分成两个子工程家长/教师使用的用户端和管理员使用的管理端。两个端复用同一套组件库和请求封装但路由和页面权限完全隔离。UI组件库我用的Ant Design Vue表格、表单、弹窗这些高频组件都很成熟能省很多样式时间。2.3 Node.js在这套系统里到底扮演什么角色这是很多人理解最乱的地方。项目标题里写着springboot-vuenodejs于是有人以为要用Node.js做另一个后端这其实是误解。在我这个项目里Node.js的职责主要有两个。第一个是前端工具链。Vue项目创建、npm依赖安装、Vite启动开发服务器、最终打包上线全程都依赖Node.js环境。你看到的“前端跑不起来”的问题绝大多数其实是Node.js版本或npm配置的问题这个我后面会专门讲。第二个是给系统做辅助的轻量服务。比如我用Node.js写了一个WebSocket通知服务专门处理老师端新的预约提醒、上课前半小时的日程提醒。这个服务很小只做消息推送把数据存入Redis或MySQL后核心业务逻辑仍然由SpringBoot承担。之所以单独拆出来是因为WebSocket长连接和SpringBoot业务进程混在一起后续扩容和部署都会互相干扰。所以不要纠结于“Node.js和后端是什么关系”。在这个项目里它的定位是“前端构建环境轻量中间服务”核心后端永远是SpringBoot。搞明白这一点架构就不会乱。3. 数据库设计课时表才是系统的命根子3.1 核心表结构与角色建模数据库设计是整个系统最值得多花时间的地方。我最终拆出了九张核心表去掉一些扩展字段后给你看最关键的部分。表名核心字段说明sys_userid, phone, password, role, status, create_time统一登录表role区分ADMIN/TEACHER/PARENTteacher_profileid, user_id, subject, intro, qualification_url, audit_status教师资质扩展表一对一连sys_userparent_profileid, user_id, student_grade, address家长/学生信息扩展表course_packageid, teacher_id, name, total_hours, price, hour_unit, valid_days课程包/套餐表course_scheduleid, course_id, teacher_id, parent_id, start_time, end_time, status课次表单次上课安排order_infoid, order_no, package_id, parent_id, amount, status, pay_time订单表evaluationid, schedule_id, parent_id, teacher_id, score, content评价表settlementid, teacher_id, month, amount, status教师月度结算表sys_user把三种角色统一收口登录认证只查这一张表。角色差异化信息放到teacher_profile和parent_profile里避免一张用户表被无限加字段。家长端的“孩子年级”“上课地址”这种信息和账号密码混在一起会非常乱拆开之后逻辑才清晰。3.2 订单、课时、评价的状态流转状态流转是这种系统最容易写乱的地方。我总结了一个经验所有状态字段不要用字符串描述用int枚举在代码里建状态常量类统一管理。订单的状态机是这样的0待支付→1已支付→2已完成→3已退款。只有已支付的订单才能生成可排课的课次退款时若已有课次上完要从退款金额里扣减对应部分这一步逻辑最容易被漏掉。课次表course_schedule的状态更重要0待上课→1已完成→2已取消→3已请假。当课次变为“已完成”系统自动扣减课程包剩余课时。扣减操作必须放在一个事务里不能先改课次状态再单独写一个减扣时的方法否则中途报错就出现“课已上但课时没扣”的问题。评价表我建议关联schedule_id而不是order_id这样能做到每节课都有评价避免家长只在下单后评价一次、后续上课质量完全不可见的问题。3.3 几个反复修改的细节第一金额一律用decimal(10,2)不要用float/double。家教课时包可能出现0.5小时这样的计费单位浮点误差一旦出现结算对账时非常痛苦。第二课程包要存“总课时数”和“剩余课时数”两个字段不能只存总课时数然后每次去统计课次。虽然这破坏了“不冗余”的范式但业务查询时需要高频展示剩余课时实时count很耗性能冗余一个字段用事务维护是更实用的选择。第三时间字段全部用datetime不要用timestamp避免2038年问题和时区换算。排课的时间粒度按半小时一格前端日期时间组件直接限制到分钟。4. SpringBoot后端落地中的关键实现4.1 项目结构与统一响应后端工程我按controller/service/mapper/entity分四层这不是死板而是家教系统这种业务逻辑里夹杂大量状态判断的项目分层清晰能救命。Controller只做参数接收和响应封装Service写业务规则Mapper只做SQL交互。统一返回体是必须从一开始就做好的事。我定义了一个Result 类包含code、msg、data三个字段code为0表示成功。所有接口不管成功失败都返回这个结构前端拦截器只需要处理一次。另外全局异常处理用RestControllerAdvice把参数校验异常、业务异常和兜底异常分别映射到不同的code不然前端拿到的永远是500排查问题全靠日志。4.2 基于JWT的登录态与角色权限登录我用JWT而不是Session主要是考虑到系统可能会有多个部署节点SpringBoot和Node服务分开部署Session同步很麻烦。用户在sys_user表登录成功后生成一个tokenpayload里放userId、role和过期时间。前端每次请求在Authorization头里带这个token后端用一个拦截器解析并把用户信息放入ThreadLocal。角色权限没有引入Spring Security那一套完整框架只用了自定义拦截器。拦截器里从token取出role判断当前路径属于哪类角色接口/api/admin/**只有ADMIN能访问/api/teacher/**只有TEACHER能访问/api/parent/**只有PARENT能访问。这是最轻量、也最容易讲清楚权限逻辑的写法。提醒生产环境务必开启HTTPS否则token明文在网络传输里跟裸奔没区别。另外token过期时间别设太长我设的是7天配合前端在401时跳转登录页。4.3 排课冲突检测的两种写法排课是家教系统的特色功能也是技术上最有挑战的点。老师端在创建课次时必须校验该时间段是否已有其他课次否则时间就撞了。最简单可靠的方式是在插入前查重核心SQL长这样SELECT COUNT(*) FROM course_schedule WHERE teacher_id #{teacherId} AND status IN (0, 1) AND #{newStartTime} end_time AND #{newEndTime} start_time这个重叠区间判断用的是“新开始时间小于已存在结束时间且新结束时间大于已存在开始时间”的数学原理能覆盖所有部分重叠和完全包含的情况。只要count大于0就提示老师该时间段不可用。第二种写法是数据库锁。在teacher_id和start_time上建联合唯一索引的变体不现实因为时间是任意值没法靠唯一索引实现。所以我最终选择了第一种“先查后插”并配合一个分布式锁基于Redis的setnx防止两个请求同时插入同一老师的时间段。对家教系统这种并发量这个方案完全够用。4.4 图片与讲义的上传处理教师资质证书、课程封面、讲义PDF这些都要支持上传。启动类里配置好文件保存路径的映射然后通过ResourceHandler把磁盘路径和URL前缀对应起来。需要注意几点第一生产环境绝对不能把文件存在应用部署目录里因为应用一更新文件就没了。我习惯存在一个独立目录比如/data/homework/files再用配置项传入。第二上传接口要限制文件大小。SpringBoot默认单文件1MB我改成图片5MB、视频讲义100MB具体在spring.servlet.multipart下面配置。第三上传后数据库存相对路径不存完整URL这样换域名或换存储服务时不用改数据库。4.5 版本选择JDK 1.8还是JDK 17这是被热搜词都挂上号的坑“springboot版本太高”。我在项目一开始用过SpringBoot 3.1.x搭配JDK 17但后来发现很多云服务器上的JDK还停留在1.8而且有些第三方依赖没有升级到支持JDK 17。如果你的目标环境是JDK 1.8老老实实选SpringBoot 2.7.x不要追新。SpringBoot 2.7.x仍然支持JDK 8同时保留了SpringBoot 2.x的许多稳定依赖版本。如果非要上3.x代价是你几乎要把MyBatis-Plus、一些低版本的中间件客户端全部升级一遍精力消耗远超收益。这一点我吃过亏直接告诉你结论。4.6 单元测试别把接口跑通就完事很多个人项目没有测试接口能跑就提交了。但家教系统里像课时扣减、订单退款这种逻辑出问题的代价很大。我给核心Service层补了几个单元测试覆盖场景包括购买后生成订单、订单支付成功后给家长/老师发送通知、课次完成时扣减课时、课次取消时恢复课时。测试框架直接用SpringBoot Test加MockMvc重点测Service层的状态流转。写这个的好处不只是减少bug更重要的是你能放心地重构代码。我第一次重构课时扣减逻辑时跑了一遍测试才发现原来还有“试听课次不需要扣课时”这个分支这种bug靠手工点页面根本发现不了。5. Vue前端从零搭起的实操记录5.1 项目初始化与依赖安装前端两个工程我用Vite创建的Vue 3项目。创建一个项目很简单核心命令是npm create vitelatest parent-web -- --template vue npm install真正的问题出现在依赖安装阶段。因为npm默认源在国外国内网络环境下经常出现卡住、超时、安装一半失败的情况。建议第一时间把镜像源切到国内源执行npm config set registry https://registry.npmmirror.com再安装axios、vue-router、pinia、ant-design-vue整个流程大概几分钟就能跑起来。前提是Node.js已经正确安装并配好环境变量这一步的具体坑在第6章单独说因为实在是太多人在这卡住了。5.2 路由设计家长端和管理端分隔Vue Router的配置我不建议把所有路由都平铺在一个文件里。两个前端工程各自维护独立路由表比如家长端有课程列表、课程详情、订单中心、我的课表几个一级页面管理端有数据看板、教师审核、课程管理、订单管理、结算管理几个页面。实际开发中比较容易被忽略的是路由的懒加载和权限控制。懒加载用动态import就能实现const routes [ { path: /course/:id, component: () import(../views/course/CourseDetail.vue), meta: { requiresAuth: true } } ]路由守卫里加一道登录判断再根据角色字段区分可访问页面。我建议用“meta.requiresAuth”标记需要登录的页面在beforeEach全局守卫里处理逻辑集中可维护性好。5.3 axios封装与跨域联调axios封装是每次前后端联调的重头戏。我在src/utils/request.js里做了统一的实例核心是baseURL、请求头、响应拦截器和错误处理。响应拦截器里统一判断codecode为0直接返回datacode为401时清空本地token并跳转登录页其他code用antd的message组件弹出错误提示。前后端分离开发时有一个必然遇到的跨域问题。解决办法很简单在Vite的vite.config.js里配置代理把/api前缀转发到SpringBoot服务的8080端口server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/xxx时就自动转发过去浏览器层面不存在跨域。打包部署后由Nginx再转发一次这套方案在整个生命周期都通用。5.4 视频和地图两个容易被拖住的功能家教系统里老师可能会上传试听课的录播视频这里牵涉到一个高频热搜问题——vue播放m3u8。m3u8是HLS流媒体协议下的索引文件浏览器原生video标签不支持直接播放。我的解决方案是vue-video-player加videojs-contrib-hls在Vue 3里使用时要包一层组件。核心逻辑是在mounted时初始化播放器播放地址直接指向后端返回的m3u8 URL后端做好跨域允许即可。这个功能看着小实际坑不少最典型的是依赖版本冲突。video.js的7.x版本和vue-video-player的旧版本经常配合失败。我的建议是保留vue-video-player但不要安装最新的video.js锁定在^7.6.0这个版本区间实测稳定。另一个功能是地图。家教老师上门授课的场景需要家长填地址、老师查看位置。我用的是腾讯地图JavaScript API在Vue项目里没有官方vue组件需要手动引入。在index.html里加script标签引入地图SDK然后在组件里通过window.QQMapWX初始化。核心是腾讯的API Key在控制台申请时域名叫什么就填什么本地调试时填localhost。这一步经常有人配置错了导致地图加载白屏调试时先看Network里脚本是否加载成功。5.5 组件状态与computed的使用两个前端端里最常处理的状态有课程包剩余课时的展示、订单金额的计算、老师综合评分。这类由已有数据推导出来的展示数据直接用Vue的computed是最合理的选择。比如课程详情页需要同时展示价格和剩余课时用computed把格式化的价格算好模板里直接引用代码干净很多const formattedPrice computed(() { return ¥${course.value.price.toFixed(2)} 每课时 })千万不要把这些计算逻辑写在模板里模板会变得又长又难维护。也尽量别把每次计算都写成watch再存一个新变量Vue官方推荐的思路是能派生的状态就从现有状态派生computed就是为它准备的。这个习惯养成之后组件代码的阅读体验会好一个档次。6. Node.js环境问题排错实录从npm.ps1到版本管理6.1 装完Node.js后npm不能用问题在PowerShell这是全网搜烂了的问题在Windows上装完Node.js打开PowerShell执行npm -v结果报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这句话的意思不是你Node.js装坏了而是PowerShell默认的执行策略禁止运行.ps1脚本。npm这个命令在Windows上是通过npm.ps1这个脚本去调用的所以被拦了。解决办法有三种。第一种以管理员身份打开PowerShell执行Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned的意思是本地创建的脚本可以运行从网上下载的脚本必须有数字签名才运行这个策略在日常开发里最合适。第二种不想改策略的话就改用cmd来执行npm命令不经过PowerShell就不会触发这个限制。第三种执行单个命令绕过powershell -ExecutionPolicy Bypass -Command npm -v这个只对单次有效适合临时应急。从项目长期维护的角度我推荐第一种一劳永逸。这不是什么“绕过”操作就是显式调整Windows的安全策略方便本地开发。6.2 Node.js环境变量与镜像源配置很多前端工程跑不起来的另一个原因是Node.js虽然装好了但环境变量没配对。安装时如果漏了“Add to PATH”这一项cmd里敲node永远提示“不是内部或外部命令”。解决办法是手动把Node.js的安装目录加到系统环境变量Path里。在“此电脑→属性→高级系统设置→环境变量→系统变量”里找到Path新增一行C:\Program Files\nodejs然后新建一个NODE_HOME变量值也指向这个目录规范一点。配置完成后重启终端执行node -v和npm -v能输出版本号就是成功了。npm还有一个高频问题是安装依赖时权限报错在Windows上表现为EPERM或EACCES错误。这通常是因为某些模块在编译原生代码时需要C工具链而本机没装。简单的依赖直接重装可能解决复杂的包比如node-sass建议直接升级到支持当前Node版本的替代品比如sass版本一换问题就没了。6.3 依赖装不上、版本不兼容的正确排查思路前后端项目协作久了最怕的一件事就是别人代码能跑你clone下来跑不起来。这种“环境不一致”问题九成是Node版本或依赖版本差异造成的。我的建议是给前端工程固定Node版本管理在项目里加一个.nvmrc文件内容写当前开发用的Node大版本比如20.11.0。团队里所有人用nvm或nvm-windows来切换版本这能消灭一大批莫名其妙的报错。依赖层面有一句经验之谈package-lock.json一定要提交到代码仓库。它的作用就是把每一条依赖的精确版本锁死当你执行npm install时npm会按照lock文件里记录的版本去安装而不是按package.json里的范围去解析。没有这个文件A同事装的是1.2.3B同事可能装到1.2.8行为就可能不一致。如果某次安装完发现依赖有冲突第一步不要瞎删node_modules重装先看报错信息里提到的包名再执行npm ls 包名 看依赖树。大多数冲突都出在一个包的版本被间接依赖锁定手动指定一个兼容版本即可。7. 打包部署与运行维护7.1 后端打jar包与Docker部署SpringBoot后端部署最省心的方式就是打jar包然后交给Docker跑。在项目根目录执行mvn clean package -DskipTeststarget目录下会生成一个可执行jar。用java -jar可以直接跑但更规范的是构建Docker镜像。这里有个很常见的坑——JDK版本。如果你的项目是JDK 1.8编译的但服务器上的Docker镜像默认用了较新的JDK运行时会报unrecognized class file version这类错误。Dockerfile里必须明确指定基础镜像FROM openjdk:8-jre-alpine COPY target/demo-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [java, -jar, /app.jar]在配置了Docker Desktop的Windows机器上构建命令依次是docker build -t tutor-system-server:1.0 . docker run -d -p 8080:8080 --name tutor-server tutor-system-server:1.0文件上传路径和数据库连接配置通过环境变量传进去不要在镜像里写死。7.2 前端构建与Nginx托管前端打包前要检查Vite的环境变量。我建了.env.development和.env.production两个文件生产环境的文件里把VITE_API_BASE_URL配上实际的API域名或服务器地址。构建命令npm run build生成dist目录把dist目录里的文件上传到服务器然后用Nginx托管静态文件。Nginx配置有两个点要注意第一个是前端路由用了history模式需要把所有路径都重写到index.html不然刷新页面就404配置大概是location / { try_files $uri $uri/ /index.html; }第二个是/api接口反向代理到SpringBoot服务location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }7.3 数据备份与接口安全家教系统一旦真实运行订单数据和课时记录就是核心资产。数据库自动备份不能省。我用的是MySQL写了一个简单的cron脚本每天凌晨导出所有数据保留最近七天同时把备份文件同步到另一个磁盘。命令很简单但安全感提升非常多mysqldump -u root -p --all-databases /backup/tutor_$(date %Y%m%d).sql接口安全这块除了登录态的JWT校验有几个容易被忽略的细节。第一个所有管理端接口除了JWT校验还要在后端做角色判断不能只靠前端隐藏按钮第二个文件上传接口要校验Content-Type和文件头防止有人传jsp或html木马第三个如果要从第三方平台拉取数据或做接口对接可以用API Key方案在请求头里加授权标识后端用一个拦截器统一校验签名我之前在一篇文章里写过java springboot apikey安全对接的方法核心就是时间戳随机数签名防重放。还有一个实用细节SpringBoot默认的404/500错误页太简陋也容易暴露框架版本信息。我统一处理了全局异常让接口永远返回标准Result结构避免泄露内部堆栈。7.4 监控和日志系统上线后最痛苦的是用户说“下单失败”你却不知道发生了什么。所以日志必须从第一天就规范起来。我在每个Service方法的入口和出口打了info日志记录关键参数和耗时核心状态流转比如订单支付成功、课次完成单独打了业务日志方便日后排查对账问题。服务器上的日志按天滚动用logback配置里经典的size和time策略。我的习惯是保留30天日志同时每天的日志里包含请求者用户ID这样任何一笔订单异常都能快速定位到具体用户和操作时间。8. 几点个人体会这套家教服务管理系统从零到上线我折腾了大概两个月。回头看在所有功能里最不显眼但最重要的其实是“课次表”的设计。订单、课时、评价、结算都可以围绕它展开。如果你也要做类似的预约类系统一定要把核心资源的时间状态流转设计透其他都是浮云。另外一个很深刻的体会是环境问题比代码问题更磨人。前后端分离项目里Node.js版本、npm镜像、SpringBoot版本、JDK版本任何一个不匹配都会浪费你半天时间。我开始时习惯把所有版本固定在项目文档里后来干脆建了新项目的第一件事就是写一个README把环境版本、启动命令、常见报错全记下来新同事或者未来的自己上手都会快很多。最后一句话做一个完整项目最大的价值不在于你把CRUD写得多漂亮而在于你完整经历了从业务调研到设计、开发、部署的全过程踩过的每一个坑都会变成你判断新问题的直觉。这篇内容里的技术选型和踩坑经验希望对正在做同类系统的你有一些帮助。