☰
Spring Boot家教管理系统全栈实战:数据库设计、JWT鉴权与部署
2026/10/5 11:29:34 网站建设 项目流程

春招那会儿我接过不少私活,其中很大一类就是帮本科生把毕业设计从"能跑"打磨到"能答辩"。做得最多的就是Spring Boot管理系统,什么图书借阅、宿舍管理、校园考勤,换一层业务壳子,骨架都大同小异。但说实话,"基于Spring Boot家教管理系统的设计与实现"这类题目,是我觉得最有业务味道、也最能体现设计能力的一个选题。它不像图书管理那样只对着CRUD猛怼,里面既有用户角色划分、又有订单和课时结算这种完整流程,还牵扯到权限控制和时间冲突校验。今天我就以这个题目为例子,把我做这类项目的完整思路、表结构设计、后端落地、前端联调、以及那些文档里绝对不会写的坑,一次性讲清楚。

这篇文章适合谁?适合正在做家教管理系统毕设的同学,适合想系统地走一遍Spring Boot全栈项目的初学者,也适合接单帮别人做毕设的开发同行。我会按照真实项目的推进顺序来写,从选题理由到数据库设计,从JWT鉴权到定时任务,每一段都是实操后的总结,不是那种只给截图不给代码的"教程"。

1. 为什么我推荐用Spring Boot做家教管理系统:选题与技术选型的真实考量

1.1 家教业务的核心痛点:信息不对称与流程管理

先聊一个最基础的问题:家教管理系统到底要解决什么?如果你只是把它当成一个"增删改查"的练习,那答辩的时候很容易被问住。家教行业本质上是一个撮合平台,牵涉到的角色至少有四类:家长、学生、家教老师、平台管理员。传统模式下大家靠微信群和电话沟通,信息极度分散,上课时间靠口头约定,课时费算不清楚,老师放鸽子了家长也没处投诉。

所以一个合格的家教管理系统,至少要管住三件事:谁来教、谁来学、怎么结算。老师和学生的匹配关系、课程的时间安排、课后的评价与课时确认,这三条线是最核心的。你去看很多高分毕设论文,功能列表拉得很长,但业务主线也就是这三条。想明白这一点,你在做需求分析的时候就不会跑偏。

这也是为什么我不建议把题目改成"家教信息发布系统"或者"家教预约平台"——"管理"这两个字意味着你要做的不仅仅是一个信息墙,而是一套有状态流转、有角色权限、有业务流程的后台系统,这恰恰是Spring Boot最擅长的领域。

1.2 技术选型对比:Spring Boot、SSM、SSH该怎么选

很多同学在开题的时候会纠结:学校老师让用SSM(Spring + SpringMVC + MyBatis),我看网上全是Spring Boot的教程,到底选哪个?

我的建议很直接:优先Spring Boot,除非导师硬性规定SSM。理由有三个:

第一,Spring Boot的自动配置机制帮你省掉了大量XML配置,尤其是数据源、事务管理器、拦截器这些东西,以前在SSM里要手写一堆配置文件,Spring Boot通过starter依赖和application.yml几行配置全搞定了。这不是偷懒,而是把精力腾出来放在业务逻辑上。

第二,招聘市场和开源社区早就倒向Spring Boot了,你写简历的时候"精通Spring Boot"比"熟悉SSM"有说服力得多。哪怕你只是做毕设,用Spring Boot也是给自己攒经验。

第三,Spring Boot 2.x和3.x选谁的问题。我的建议是选2.7.x。为什么?因为3.x要求JDK 17起步,而且很多第三方starter的兼容性还停留在2.x时代。你如果是为了稳定把毕设做完,JDK 8 + Spring Boot 2.7.x + MyBatis-Plus这套组合最稳,踩坑最少。当然如果你的导师要求新,那Spring Boot 3.2 + JDK 17也可以,但你要做好某些老教程代码不能直接抄的心理准备。

2. 需求边界画清楚,后面写代码才不拧巴

2.1 四类角色的权限划分

管理系统的核心思想就是"不同人看到不同的东西"。家教系统里我通常设置四种角色,权限从上到下是:

角色核心操作范围特殊能力
管理员全站用户管理、科目管理、订单总览强制下架违规老师、处理投诉
家长发布需求、搜索老师、发起预约对已完成的课时进行确认/评价
家教老师发布可授课程、查看预约、提交课时上传资质证明、维护可授课时间
学生查看课程、提交试听申请绑定在家长账号下

这里有个细节很多人不注意:学生和家长在业务上经常是分离的。小学生上课是家长付费、家长选老师,但真正上课的是孩子。所以在数据模型里,学生信息应该独立建表,家长账号通过student_id关联到孩子。如果你偷懒只做一个"用户表+角色字段",后面做"家长查看孩子的上课记录"这个功能时就会非常别扭。

2.2 核心业务流:从预约到授课到结算

我建议你把业务流程先画出来(不是画给答辩看,是画给自己写代码时理清头绪)。家教系统的核心链路通常长这样:

  1. 家长搜索老师 → 按科目、区域、价格筛选
  2. 家长发起预约 → 提交期望的上课时间
  3. 老师确认/拒绝 → 确认后生成课程订单
  4. 实际上课完成 → 老师提交课时记录
  5. 家长确认课时 → 系统自动累计已确认课时
  6. 结算阶段 → 家长充值/支付,老师申请提现

这条链路最大的特点是存在状态机。预约订单的状态至少要有:待确认、已确认、已拒绝、进行中、已完成、已取消。数据库里最好用一个status字段专门存这个状态,而不是通过删记录来表示取消。别小看这个设计,答辩老师最喜欢问的就是"订单取消后数据怎么处理",你在表里保留状态字段,直接就能答上来。

2.3 容易被忽视的非功能需求

很多同学做毕设只盯着功能清单,结果答辩的时候被一句"你这个系统并发怎么样"问懵。我不主张你为了毕设去搞微服务、消息队列这种重武器,但有几个非功能需求你要心里有数:

  • 参数校验:手机号格式、价格区间、时间格式,后端的@Validated注解不能省,别把校验全丢给前端。
  • 日志记录:操作日志表要建,至少记录谁在什么时间做了什么操作,关键业务操作(如取消订单)必须留痕。
  • 异常处理:全局异常处理器要写,不然前端动不动就拿到一个带堆栈的500页面,体验太差。

这些不是加分项,而是基本素养。答辩老师可能不会逐行看代码,但如果你能主动说一句"我用拦截器做了JWT登录校验,用全局异常处理器统一了返回格式",印象分直接上一个档次。

3. 数据库设计:家教系统的"地基"怎么打

3.1 核心表结构:用户、课程、订单、课时记录

数据库设计这一个章节在论文里能写十几页,但落到具体表结构,我建议你抓住五张主表和几张辅助表:

user表(用户表):

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `role` tinyint(4) NOT NULL COMMENT '1-管理员 2-家长 3-老师 4-学生', `status` tinyint(4) DEFAULT '1' COMMENT '1-正常 0-禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

teacher_profile表(老师信息扩展表):因为老师有科目、教学年限、认证状态、课时费等维度,单独拆一张表更清爽,用user_id关联。

course_order表(预约/课程订单表):

CREATE TABLE `course_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `teacher_id` bigint(20) NOT NULL, `student_id` bigint(20) NOT NULL, `parent_id` bigint(20) DEFAULT NULL, `subject_id` bigint(20) NOT NULL COMMENT '科目', `start_time` datetime NOT NULL COMMENT '上课开始时间', `end_time` datetime NOT NULL COMMENT '上课结束时间', `duration` int(11) DEFAULT '2' COMMENT '课时长(小时)', `unit_price` decimal(10,2) NOT NULL COMMENT '每小时单价', `total_amount` decimal(10,2) NOT NULL COMMENT '总价', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待确认 1-已确认 2-已拒绝 3-进行中 4-已完成 5-已取消', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

lesson_record表(课时记录表):老师上完课之后提交,家长确认后这个状态才变成已完成,这个表是为结算服务的。

这几张表之间的外键关系我不建议真的在数据库里加物理外键,逻辑外键就够了。理由是MyBatis-Plus做联表查询的时候,物理外键反而容易造成麻烦,而且如果老师被删除、但订单还要保留历史记录,物理外键会挡住这种"逻辑删除"的需求。

3.2 字段设计上的几个细节决策

这里分享几个容易踩坑的点:

第一,价格永远用DECIMAL,不要用FLOAT或DOUBLE。浮点数在计算机里是近似存储,0.1 + 0.2可能等于0.30000000000000004。金额字段一旦出现精度问题,结算那一步就全乱了。DECIMAL(10,2)足够应付单价和总价。

第二,时间字段建议直接存datetime,不要存字符串。有人图方便,把上课时间存成"2025-06-01 14:00"这种字符串,后面如果要查"某个时间段有哪些老师在教",或者做时间冲突校验,字符串比较会把你折腾疯。MySQL的datetime字段配合BETWEEN语句,比字符串好用一百倍。

第三,逻辑删除比物理删除安全。用户误操作删除了一条课时记录,如果物理删了,数据就没了。我一般在每张业务表加一个deleted字段(0/1),查询的时候默认过滤掉deleted=1的记录。MyBatis-Plus里用@TableLogic注解就能自动实现,所以这个成本极低。

第四,订单号要单独生成,不要用自增ID当订单号。自增ID暴露了业务量,而且用户看到订单号是"58"这种会有种不正规感。你可以用时间戳+随机数生成订单号,比如yyyyMMddHHmmss + 两位随机数,或者直接用IdWorker.getIdStr()(MyBatis-Plus自带雪花算法实现),一行代码搞定。

4. Spring Boot后端落地:骨架搭建与关键接口实现

4.1 目录分层:controller-service-mapper的职责边界

后端工程的包结构我建议按模块化来分,而不是按层级分。什么概念?按层级分是controller包下放所有Controller,service包下放所有Service;按模块分是order包下同时有OrderController、OrderService、OrderMapper。模块化的好处是改动一个功能时,你只需要在一个包结构内跳转,代码阅读成本低很多。

推荐包结构:

com.example.tutor ├── common // 通用类:返回结果、异常处理、常量 │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── BusinessException.java ├── config // 配置类:拦截器、跨域、密码加密器 ├── security // JWT相关:TokenUtil、JwtInterceptor ├── user // 用户模块 │ ├── UserController.java │ ├── UserService.java │ ├── UserMapper.java │ └── entity/User.java ├── order // 订单模块 └── lesson // 课时模块

这里一定要记住:Controller只做参数接收和结果返回,不写业务逻辑。我见过太多人把数据库更新语句直接写在Controller里,当时写起来爽,后面调试的时候想死。Service层是核心,业务逻辑全部放Service里,Controller保持薄薄一层。

4.2 登录认证:JWT方案在单体应用中的实现

家教系统里没有任何一个接口可以裸奔放行,除了登录和注册。我的做法是用JWT做无状态认证,全套流程是:

  1. 用户登录,后端校验用户名密码,密码用BCryptPasswordEncoder校验。
  2. 校验通过后,生成Token,把用户ID、角色封装进Token的claims里。
  3. 前端把Token存到localStorage,每次请求在Authorization头带上。
  4. 后端写一个拦截器,拦截非白名单路径,解析Token并放行。

核心代码大致是这样:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/api/subject/list"); } }
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException("未登录或登录已过期"); } // 解析Token,如果签名不对或过期会抛异常 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); request.setAttribute("userRole", claims.get("role")); return true; } }

这里有个容易踩的坑:拦截器抛异常,全局异常处理器接得住吗?需要注意,拦截器preHandle里抛出的异常,如果你是注册在WebMvcConfigurer里,全局异常处理器是能接到的;但如果你用HandlerInterceptor实现类里的逻辑抛了异常,而你的异常处理器没有配置对,前端会收到乱码。解决办法是确保@ControllerAdvice生效,并且自定义异常类里别吞掉堆栈信息。

Token如果要实现"用户被禁用后立刻失效",光靠JWT是做不到的,因为JWT是无状态的。我提供两个方案:一是把Token有效期设短一些(比如2小时),配合前端重新登录机制;二是在user表加一个token_version字段,每次登录+1,JWT里存这个版本号,服务端校验时比对版本号。方案二在毕设里属于亮点功能,答辩可以主动讲。

4.3 业务接口设计的几个原则

有几个接口设计的原则我写代码的时候一直在用:

原则一:接口返回结构要统一。所有接口都返回Result<T>,里面包含code、message、data三个字段。成功时code=200,失败时code=500或者业务码。前端拿到这个结构统一处理,不需要每个接口单独写异常逻辑。

原则二:列表查询千万别只做一个findAll。分页查询是标配,前端传pageNum和pageSize,后端用MyBatis-Plus的Page对象,一行代码就能搞定分页:

public Page<CourseOrder> getOrderPage(Long userId, int pageNum, int pageSize) { LambdaQueryWrapper<CourseOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(userId != null, CourseOrder::getParentId, userId) .orderByDesc(CourseOrder::getCreateTime); return orderMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }

LambdaQueryWrapper这个类强烈推荐使用,它用lambda表达式引用列名,编译期就能查出字段名错误,比写字符串"parent_id"安全得多。

原则三:修改和删除要加"是本人操作"校验。比如老师想取消一个订单,你得先查出订单里的teacher_id是不是当前登录用户,不是就拒绝。这个逻辑虽然简单,但很多初学者会忘,这是数据安全的基本底线。

5. 前端联调与打包:Vue + Spring Boot的协同细节

5.1 接口约定与联调环境

家教管理系统的前端我一般用Vue 3 + Element Plus,理由不用多说,组件成熟、文档丰富、招聘要求里也常出现。前后端分离开发时最大的问题是跨域。

Spring Boot后端解决跨域很简单,写一个配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8081") // 前端地址 .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }

如果你用了Shiro或者Spring Security,跨域配置要放行预检请求(OPTIONS),不然浏览器先发个预检请求被拦截,你后面真正的请求压根发不出去。

前后端接口字段的命名也要提前约定好。我习惯后端返回驼峰命名(比如createTime),前端直接用,不做转换。MySQL里的create_time通过MyBatis-Plus的驼峰映射自动转成createTime,所以Java实体类里也是驼峰,三层命名一致,省去很多烦恼。

5.2 Vue打包放进Spring Boot的坑与处理

很多同学做完前后端分离,老师却说"我要一个能直接跑的东西",这时候就需要把Vue打包后放到Spring Boot里,打成单jar运行。步骤很简单:

  1. 在Vue项目根目录执行npm run build,生成dist目录。
  2. 把dist里的文件复制到Spring Boot的src/main/resources/static目录下。
  3. 重新打包Spring Boot,java -jar启动后直接访问http://localhost:8080/就能看到前端页面。

但这里有几个坑几乎必踩:

坑一:Vue路由的history模式会导致刷新404。如果你用history模式路由,访问/order/list刷新页面时,Spring Boot会把请求交给DispatcherServlet,找不到对应的/order/list接口就会返回404。解决办法是在后端加一个路由转发规则:

@Controller public class RouteController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }

这个正则[^\\.]*的意思是"路径里不含点号"就转发到index.html,这样既能放行静态资源(js/css带点号),又能解决前端路由404。这个配置在毕设答辩里是一个很能加分的点,因为大部分同学根本不会遇到这个问题。

坑二:前端请求的BaseURL要跟部署环境适配。开发时前端代理指向http://localhost:8080,打包之后前端页面和后端同源了,如果代码里还硬编码了http://localhost:8080,部署到服务器上就废了。我的做法是在Vue里用环境变量区分:

// .env.development VITE_API_BASE_URL = '/api' // .env.production VITE_API_BASE_URL = '/api'

开发时配置Vite代理转发,生产时因为同源所以不需要代理。这样代码里就统一写axios.post(${baseUrl}/order/create),不会出现环境切换的麻烦。

坑三:文件上传路径别用相对路径。家教老师上传资质证明(教师资格证照片),很多同学会把文件保存到static/upload/目录下,这在本地跑好好的,部署到服务器上就出问题。更好的做法是配置一个绝对路径的上传目录,通过application.yml里的file.upload-path配置,然后用一个虚拟路径映射到它:

@Configuration public class FileConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }

这样前端访问/upload/xxx.jpg就能拿到图片,而文件本体存在服务器某个固定目录,不会因为重新打包jar而丢失。

6. 实测中踩过的坑与优化建议

6.1 Spring Boot版本和依赖兼容性问题

这几个月我帮人搭建环境时遇到最多的报错,九成出在版本上。Spring Boot 2.7.x对应MyBatis-Plus的mybatis-plus-boot-starter要选3.5.x,但如果你用Spring Boot 3.x,要求MyBatis-Plus 3.5.3以上并且包名会变(从com.baomidou变到com.github.pagehelper这种边边角角的兼容问题则更隐蔽),网上老教程的代码直接复制过来多半跑不起来。

我的建议是:做毕设就锁死一套经过验证的组合,别追求最新。推荐组合如下:

组件推荐版本
JDK1.8或11
Spring Boot2.7.18
MyBatis-Plus3.5.3.1
MySQL驱动8.0.33
Hutool5.8.x
JJWT0.11.5

Hutool这个工具库我强烈推荐,里面封装了日期处理、加密、文件上传、验证码生成等等常用方法,能少做很多轮子。JJWT是标准的Java JWT库,0.11.5版本对JDK 8友好,别去用io.jsonwebtoken:jjwt的旧坐标,那是另一个坑。

6.2 定时任务的实现与注意事项

家教系统里天然需要定时任务,比如:课时开始前2小时给老师发提醒,订单超过24小时未确认自动取消,月底生成账单。Spring Boot的定时任务用@Scheduled注解就够:

@Component public class OrderAutoCloseTask { @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次 public void closeExpiredOrders() { // 查询状态为待确认、创建时间超过24小时的订单,批量改为已取消 } }

但凡是定时任务,就避不开几个细节:

一是cron表达式别写错。Spring Boot的cron是6段(有时7段),秒 分 时 日 月 周,网上抄的表达式多半是Quartz的7段,跑不起来别慌,把首位去掉试试。

二是定时任务默认是单线程串行执行的。如果你有多个定时任务,默认情况下它们是排队完成的,如果某个任务执行时间过长,会卡住后面的任务。要并发执行,就配置一个TaskScheduler并设置线程池大小。这个点回答"系统如何优化性能"时可以作为一个小亮点。

三是线上环境里定时任务要关注幂等性。比如"自动取消超时订单"这个任务,如果执行到一半服务器重启了,重启后任务会重新执行,你怎么保证不会重复取消?我的办法是任务执行前判断订单当前状态,只有在待确认状态才执行取消,已经取消的跳过,天然幂等。

6.3 上线前要做的安全加固

毕设虽然很多只需要在本地演示,但如果你想把项目放到服务器上给老师远程看,以下安全措施一个都别省:

密码加密:禁止明文存密码,BCryptPasswordEncoder起步,这个Spring Security自带,不需要引入全套Security框架,单独引入spring-security-crypto依赖即可。

SQL注入防堵:MyBatis-Plus的LambdaQueryWrapper参数绑定天然防注入,但如果是手写XML里的${}传参就要小心,能用#{}就别用${}。

越权访问防护:JWT里存了role,但后端不能只看前端传的role,每个角色接口的鉴权必须依赖拦截器里从Token解析出的角色,不能信任前端请求体里的字段。

限制敏感接口的请求频率:比如发送短信验证码、登录接口这种,加个简单的Redis计数器限制频率。如果项目还没引入Redis,也可以先用一个ConcurrentHashMap手动计数兜底,差距区别只在分布式环境上。

7. 写在最后的项目落地个人体会

这个家教管理系统从建表到全部功能跑通,我前后花了大概四天。如果只是为自己做,三天就够,剩下一天全部消耗在环境和联调上了。给我的最大感受是:写业务系统的核心能力不是把代码敲出来,而是把业务关系理清楚——学生要不要单独建表?订单状态怎么流转?课时确认之后钱怎么算?这些问题想清楚了,Spring Boot只是一个顺手的工具,代码写起来非常快。但如果你一上来就闷头建工程写CRUD,后面改表的代价会教你做人。

另一个实操建议是:从中间表入手写代码,不要从头表写起。我每次都是先做订单表和课时记录表,因为这两张表关联了其他所有表,把它们跑通了,用户表和科目表怎么接都顺理成章。

最后留一个可以当答辩加分项/扩展方向的小提示:家教系统的推荐逻辑其实特别适合做简单化处理,比如"同科目优先、同区域优先、价格从低到高排序"就可以用一句SQL完成;如果你想再深入一步,还可以引入用户评价分作为排序权重,这就是一个轻量级协同过滤的雏形。这句话你写在论文结论部分,比空谈"未来可以接入大数据分析"要具体得多,也更能体现你真的想过系统演进的方向。

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

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

立即咨询