如果你正在准备Java方向的毕业设计,看到“基于Java+Spring Boot的家教管理系统”这个题目,我先跟你交个底:这不是一个普通的“学生管理”增删改查项目,而是一个包含了家长、教师、运营方三方角色的全流程业务系统。你的答辩成绩、项目含金量,取决于你对“业务闭环”的理解,而不是你写了多少行代码。
这个系统要解决的真正问题是:家教市场上的信息不对称和流程管理混乱。家长找不到合适的老师,老师找不到稳定的生源,机构没法管控教学质量和财务结算。所以这个毕设的价值在于,它把一个真实的服务交易场景搬到了线上,用Java技术栈实现了从需求发布、师资审核、智能匹配、预约试听、下单支付、课程排期、课后评价到财务结算的完整链路。
这篇文章不写给代码生成器用户,写给真正想把这个项目做扎实、想顺利通过答辩、甚至以后想以此为基础扩展成完整商用项目的同学。我会从业务设计、技术选型、数据库建模、核心功能实现到高频坑的排查,把我带项目时积累的经验全部拆开讲清楚。
1. 先从业务说起:家教管理系统的核心边界在哪里
1.1 三个使用角色,决定了系统的基本骨架
很多同学拿到题目后第一反应是建表、写登录、写CRUD,结果做到一半发现系统变成了“用户管理+课程管理”两个孤立模块。这是最典型的毕设误区。做业务系统,第一步永远是梳理角色和使用场景。
家教管理系统的角色非常清晰,就是三类人:
- 家长端:发布家教需求、浏览教师列表、查看教师详情和评价、预约试听、下单购买课时包、查看孩子课程安排、课后评价。
- 教师端:注册并提交资质材料、等待平台审核、查看可接的需求订单、抢单或接受平台匹配、管理自己的课时表和上课记录、申请结算提现。
- 管理员端:审核教师入驻资料、管理学科和年级分类、处理订单纠纷与退款、查看平台运营数据、管理教师结算。
你要注意一个细节:家长和教师本质上都是用户,所以用户表可以统一,但家长和教师的扩展信息完全不同。教师需要审核资质、学科标签、授课年级、可授课区域、时薪报价;家长需要关联孩子信息,比如孩子姓名、所在年级、薄弱科目。这两种角色如果硬塞在同一张表里,后期扩展一定会出问题。
我建议的三段式设计是:统一账户表存登录凭证,分离的档案表存角色扩展信息,再通过一个角色字段把它们关联起来。这样做的好处是以后想加“机构管理员”或者“课程顾问”这类角色时,不需要改动用户主表,只需要新增档案表。
1.2 一条需求从发布到结算,中间经历了什么
“全流程对接与管控”这几个字,是整个题目的核心得分点。你要让答辩老师看到,你设计的不是一个信息展示平台,而是一个有状态流转、有业务规则、有交易闭环的管理系统。
我拿一条完整的家教订单来举例:
- 家长注册登录,填写孩子的基本信息和辅导需求,比如“高一数学,每周六下午2点到4点,家在朝阳区,希望女老师,时薪150左右”。
- 系统生成一条需求单,状态为“待匹配”。
- 需求单进入匹配池,有两种处理方式:一种是教师主动浏览需求池并抢单,一种是系统按学科、区域、年级自动推荐给符合条件的教师。
- 教师申请接单后,状态变为“待家长确认”。
- 家长同意后,双方可以发起“预约试听”,试听课时不扣费或扣体验课时。
- 试听满意,家长下单购买正式课时包,订单状态变为“待支付”。
- 支付成功后生成“已支付订单”,家长和教师开始排课。
- 教师按课表上课,家长确认消课,系统扣减课时。
- 课程完成后,家长对教师进行评价,教师获得对应的课时费结算记录。
- 教师账户金额达到可提现标准,发起提现申请,管理员审核打款。
你发现没有,这条链路里踩了好几个关键状态:需求状态、接单状态、订单状态、课程状态、结算状态。这些状态之间的流转规则,才是你的系统区别于普通展示网站的核心。我建议画一张状态流转图放在论文里,答辩的时候直接用这张图讲业务,比任何文字都有说服力。
2. 技术选型的底层逻辑:为什么偏偏是Spring Boot
2.1 Spring Boot + MyBatis-Plus:毕设性价比最高的组合
如果你参加过招聘或者看过最新的Java岗位描述,会发现Spring Boot几乎是所有Web后端岗位的标配。但作为毕设技术选型,它真正的优势不是“流行”,而是它把工程化门槛降到了极致。
Spring Boot核心价值是自动配置。你不用再像SSM时代那样手写一大堆XML配置文件,引入一个spring-boot-starter-web依赖,内嵌Tomcat自动启动,写一个@RestController就能对外提供接口。这意味着你可以把精力集中在业务逻辑上,而不是跟配置环境搏斗。
数据访问层我强烈推荐用MyBatis-Plus,而不是原生MyBatis。原因很直接:这个项目至少有八九张核心表,每张表都要写增删改查,用原生MyBatis意味着你要为每个实体写Mapper接口和XML文件,工作量巨大。MyBatis-Plus提供内置的BaseMapper,单表CRUD零SQL,还支持分页插件、逻辑删除、自动填充、乐观锁插件。毕设场景下,这些功能能帮你省掉起码30%的代码量。
不要盲目追求微服务架构。有的同学为了看上去高级,把系统拆成用户服务、订单服务、消息服务三个Spring Cloud模块,结果一个Demo级别的项目被拆得漏洞百出,部署答辩时服务之间通信失败,当场翻车。家教管理系统就是一个典型的单体应用,用单体架构完全能撑住,把代码分层做好,比什么都重要。
2.2 技术栈组合推荐和核心配置思路
我在实际带项目时,给学员推荐的是这样一套组合,你可以直接参考:
- JDK 8 + Maven 3.6+,稳定,兼容性最好。如果你要用JDK 17或Spring Boot 3.0,不是不行,但要注意MyBatis-Plus和某些工具类的版本兼容问题,答辩环境里折腾版本不如求稳。
- Spring Boot 2.7.x,这是2.x系列的长期维护版本,资料多,遇到问题好查。
- MySQL 8.0,存业务数据。
- Redis,存登录Token、做分布式锁、缓存热点数据。
- MyBatis-Plus 3.5.x,数据访问增强。
- Hutool,工具类库,处理日期、随机数、ID生成都很方便。
- JWT + Spring Security或拦截器,做登录认证和权限控制。毕设项目用拦截器就够了,Spring Security的过滤器链对新手不友好,写不好反而是负担。
- Lombok,消除Getter/Setter样板代码。
- 文件存储:本地磁盘存储就够用,把上传目录配置到application.yml里。不要为了“云存储”硬上MinIO或阿里云OSS,除非你有现成的服务器和对象存储资源。
提醒一句:Spring Boot 2.7.x对应的是Java 8,Redis客户端用Lettuce,MySQL驱动用
com.mysql.cj.jdbc.Driver。这些细节看似不起眼,但配置错了项目启动直接报错,而且错误信息对新手很不友好。
2.3 包结构规划:别把Controller写成万能类
我评审过很多学生的代码,最常见的毛病是:Controller里写业务逻辑、Service里调Service、一个方法几百行。代码能跑,但答辩时老师一问“你这个项目怎么分层的”,你支支吾吾答不上来,分数就下来了。
规范的项目结构应该长这样:
com.example.tutor ├── common // 通用类:返回结果封装、全局异常、常量、枚举 ├── config // 配置类:MyBatis-Plus分页、CORS、Redis、拦截器注册 ├── controller // 控制层:只负责参数接收和结果返回 ├── service // 业务层:业务逻辑和事务控制 │ └── impl ├── mapper // 数据访问层:继承BaseMapper的接口 ├── entity // 数据库实体 ├── dto // 前端传入参数对象 ├── vo // 返回给前端的视图对象 └── utils // 工具类:JWT、日期处理、文件上传这里我特别想强调一个点:DTO和Entity必须分离。有的同学图省事,直接把数据库实体暴露给前端接口,结果就是前端能传什么字段你完全不可控,比如用户传一个role=admin进来就能越权。正确做法是写一个LoginDTO接收登录参数,再通过UserVO返回给前端需要的信息,密码、盐值、逻辑删除标记这些敏感字段统统不返回。这不仅是安全要求,也是答辩老师很在意的工程素养。
3. 数据库设计:这个项目的命根子
3.1 九张核心表,每一张都要有明确职责
数据表设计直接决定了你的业务逻辑能走多远。我见过有同学把订单、课程、评价全塞在一张表里,字段列表拉出来快30列,看着都头疼。这里我给你一套经过实战验证的表结构方案,可以直接参考:
用户主表(user)
- id:主键,用自增或雪花ID
- username:登录账号,唯一索引
- password:BCrypt加密后的密码
- phone:手机号,唯一索引
- role:角色,1-家长,2-教师,3-管理员
- avatar:头像URL
- status:账号状态,0-禁用,1-正常
- created_at、updated_at:时间字段
教师档案表(teacher_profile)
- id、user_id:关联用户表
- real_name:真实姓名
- qualification:学历或毕业院校
- teaching_years:教龄
- id_card_url:身份证照片(仅做审核材料)
- certificate_url:教师资格证等资质材料
- subject_tags:可教科目标签,比如“高中数学”“初中物理”
- area:可授课区域
- hourly_rate:期望时薪
- audit_status:审核状态,0-待审核,1-通过,2-驳回
- audit_remark:审核备注
- introduction:自我介绍
学生档案表(student)
- id、parent_user_id:关联家长账户
- name:孩子姓名
- grade:年级
- school:学校
- weak_subjects:薄弱科目
家长需求表(demand)
- id、parent_user_id、student_id
- subject:需要辅导的科目
- grade:孩子年级
- area:上课区域
- salary_expectation:心理预期时薪
- schedule_desc:时间安排描述
- status:0-待匹配,1-已接单,2-已完成,3-已取消
- remark:补充说明
订单表(order)
- id、order_no:订单编号(唯一索引,生成规则:时间戳+随机数)
- demand_id:关联需求单
- order_type:1-试听订单,2-正式课时订单
- teacher_user_id、parent_user_id
- total_hours:总课时
- unit_price:单课时单价(存数值,单位是元)
- total_amount:实付金额
- pay_status:0-未支付,1-已支付,2-已退款
- order_status:0-待支付,1-待上课,2-进行中,3-已完成,4-已取消,5-退款中
- created_at、paid_at、completed_at
课表/排课表(course_schedule)
- id、order_id
- student_id、teacher_user_id
- start_time、end_time:上课时间段
- location:上课地点或线上链接
- status:0-待上课,1-已确认,2-已完成,3-已取消,4-缺勤
- checkin_code:课消码或确认码
评价表(evaluation)
- id、order_id、course_schedule_id
- from_user_id、to_user_id
- score:评分1到5
- content:文字评价
- created_at
结算/提现表(withdraw_record)
- id、teacher_user_id
- amount:提现金额
- status:0-申请中,1-审核通过,2-已打款,3-驳回
- apply_time、audit_time、remark
3.2 金额、状态与约束:新手最容易踩的三个细节
先说明金额问题。家教课时费可能出现“150元每小时”这样的报价,数据库里用DECIMAL(10,2)存是可以的,但我更建议以“分”为单位存整数,比如150元存15000。原因很简单:浮点数运算有精度问题,整数运算永远不会出现0.1+0.2不等于0.3的情况。如果你觉得以分存储阅读不方便,可以加一个VO转换层,返回给前端时除以100,展示成元。
再说状态字段。我见过很多同学的数据库里状态字段是varchar类型,塞的是“待支付”“已完成”这种中文文案。这非常糟糕。状态应该用整数枚举,含义统一集中在枚举类里管理。Java侧定义一个枚举类:
public enum OrderStatus { WAIT_PAY(0, "待支付"), WAIT_CLASS(1, "待上课"), IN_PROGRESS(2, "进行中"), COMPLETED(3, "已完成"), CANCELED(4, "已取消"), REFUNDING(5, "退款中"); private final int value; private final String desc; }这样做的好处是,业务代码里不会出现魔法数字,状态流转时你调用枚举做校验,逻辑一目了然。答辩老师看到你用了枚举而不是散落的字符串常量,观感会好很多。
第三是唯一约束。表设计时要把“这笔订单不能重复创建”“同一时间段老师不能有两节课”这类业务规则,用数据库约束兜底。比如订单表的order_no设置唯一索引,课表里加一个UNIQUE KEY uk_teacher_time (teacher_user_id, start_time, end_time),这样即使代码有并发漏洞,数据库也能挡住重复数据。
多表关系设计不要用物理外键,但必须建普通索引。如今主流实践是业务层保证数据一致性,物理外键在高并发插入时会影响性能且调用链复杂。你只需要在user_id、order_id、teacher_user_id这类关联字段上建索引,查询时就能命中索引,不会全表扫描。
4. 从空目录到一个能演示的完整系统:核心环节实操
4.1 工程初始化与基础配置
打开Spring Initializr,Group填com.example,Artifact填tutor-system,语言选Java,包选Jar,Java版本选8,依赖选Spring Web、MySQL Driver、Lombok。生成后拉进IDEA,再手动补上MyBatis-Plus和Redis相关依赖。
pom.xml里你要额外加的核心依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.18</version> </dependency>application.yml核心配置如下:
server: port: 8080 servlet: multipart: max-file-size: 20MB max-request-size: 50MB spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tutor_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有个很容易忽略的配置:MyBatis-Plus分页插件必须单独注册,不注册的话selectPage查出来的数据全是第一页,不会真正分页。在config包下新建MybatisPlusConfig:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }4.2 登录认证与权限控制:JWT + 拦截器方案
登录接口的逻辑并不复杂,但也是整个系统安全的地基。流程是这样的:
- 前端提交
username和password。 - 后端根据用户名查用户,用
BCrypt校验密码。 - 校验通过后生成JWT Token,把用户ID、角色、过期时间装进Token里。
- 把Token存入Redis,键为
login:token:{userId},设置过期时间。 - 返回给前端,前端每次请求在请求头(Header)里带
Authorization: Bearer {token}。 - 后端定义拦截器,在进入Controller之前校验Token是否合法、是否过期、Redis中是否存在。
拦截器实现的关键代码:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/auth/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } String realToken = token.substring(7); Claims claims = JwtUtil.parseToken(realToken); Long userId = Long.valueOf(claims.get("userId").toString()); // 校验Redis中Token是否存在,实现单点登录或主动退出 String redisToken = redisTemplate.opsForValue().get("login:token:" + userId); if (!realToken.equals(redisToken)) { throw new BusinessException(401, "账号已在其他设备登录"); } // 把用户信息放入ThreadLocal,后续Service可直接获取 UserContext.set(userId); return true; } @Override public void afterCompletion(...) { UserContext.clear(); } }这里有个加分设计:把Token存入Redis并做校验,可以实现“退出登录即失效”和“同一账号互踢”。如果不存Redis,JWT天然无状态,用户点了退出后Token在有效期内依然是可用的,这是很多毕设被追问时答不上来的安全漏洞。
权限控制方面,我建议不用Spring Security的重型过滤器链,而是写一个简单的角色校验注解。比如定义一个@RequireRole("teacher")注解,在教师接单、排课这些接口上加注解,拦截器里解析注解并校验当前登录用户角色。既能体现你对RBAC模型的理解,又不会把自己绕晕在Spring Security的过滤器链里。
4.3 教师入驻审核与需求匹配:从0到1打通第一个闭环
教师入驻审核是管理员端最重要的功能,也是体现“全流程管控”的招牌场景。
教师提交入驻申请后,teacher_profile表的audit_status变为0(待审核)。管理员端页面上展示待审核列表,管理员点击“通过”或“驳回”,后端更新审核状态,并给教师发送系统消息(或站内信)。如果驳回,必须填写audit_remark,教师端可以查看驳回原因并修改资料后重新提交。
这个模块里你不需要写复杂的代码,但要注意一点:审核操作必须添加事务和状态校验。比如教师提交申请后,管理员通过审核,此时如果教师状态被并发重复提交,可能导致audit_status从“已通过”改回“待审核”。解决办法就是在执行审核前先查询当前状态并做判断:
@Transactional(rollbackFor = Exception.class) public void auditTeacher(Long teacherId, Integer auditResult, String remark) { TeacherProfile profile = teacherProfileMapper.selectById(teacherId); // 只有待审核状态才能审核 if (profile.getAuditStatus() != 0) { throw new BusinessException("该申请已审核,请勿重复操作"); } profile.setAuditStatus(auditResult); profile.setAuditRemark(remark); teacherProfileMapper.updateById(profile); }需求匹配这个环节,是很多同学觉得“没东西可做”的地方。其实最简单的方案,就是给需求单打上“科目 + 年级 + 区域”三个标签,然后写一个教师端的需求列表接口,按这些标签做过滤和排序:
public PageResult<DemandVO> queryCanAcceptDemand(Long teacherUserId, int page, int size) { // 1. 查询教师自己的资质,拿到它可授学科和区域 TeacherProfile teacher = teacherProfileMapper.selectByUserId(teacherUserId); // 2. 按学科和区域过滤需求单,排除自己已接单或已取消的 LambdaQueryWrapper<Demand> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Demand::getSubject, teacher.getSubjectTags()) .like(Demand::getArea, teacher.getArea()) .eq(Demand::getStatus, 0) .orderByDesc(Demand::getCreateTime); // 3. 分页返回 }这样虽然简单,但足以演示“系统给你推荐了合适的单子”这个业务概念。如果你想做得更漂亮一点,可以给需求单和教师分别定义标签,用标签重叠度计算匹配分,按匹配分倒序输出。代码量不大,但是答辩时你能讲出“基于标签的推荐策略”,比一句“我直接查数据库”高出一个段位。
4.4 家长下单、支付与课程消课:把核心交易链路跑通
家长发起订单时,后端要做的事很多:先根据需求单找到确认接单的教师、计算订单总金额、生成唯一订单号、把订单状态置为“待支付”。我这里给你一个订单号生成规则:
String orderNo = "T" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000));生成订单号后不要直接返回链接,而是调一个“模拟支付”接口。因为真实支付需要商户号、证书等资源,毕设环境里不可能真接微信或支付宝。模拟支付流程是:前端弹出一个支付确认框,输入支付密码后调用后端/pay/mock接口,后端校验订单为待支付状态,直接把pay_status置为已支付,order_status置为待上课。
这看似简单,但有一个必须处理的问题:支付回调与订单状态的一致性。如果用户重复点击“支付成功”,后端会重复修改状态吗?所以支付接口要做幂等处理:
@Transactional public void mockPay(String orderNo) { Orders order = orderMapper.selectByOrderNo(orderNo); if (order.getPayStatus() == 1) { // 已支付过了,直接返回,不重复处理 return; } if (order.getOrderStatus() != 0) { throw new BusinessException("订单状态异常,无法支付"); } order.setPayStatus(1); order.setOrderStatus(1); order.setPaidAt(LocalDateTime.now()); orderMapper.updateById(order); }课程消课的思路类似。教师上完课后发起“确认上课”,家长端收到待确认提醒,家长点击确认后,系统扣减订单里的剩余课时。如果家长在规定时间内没有确认,系统可以做成自动确认。这块业务逻辑不难,关键是把字段设计好:订单表里有total_hours和used_hours两个字段,每次消课就是对used_hours累加,当used_hours等于total_hours时,订单状态变为已完成。
4.5 定时任务与消息提醒:让系统自己跑起来
一个让答辩老师觉得“这个系统是活的”的加分项,是定时任务。我建议你至少做两个场景:
第一个是超时未支付订单自动关闭。用户下单后20分钟未支付,订单自动作废,释放教师时间排期。用Spring自带的@Scheduled就能实现:
@Component public class OrderTimeoutTask { @Scheduled(cron = "0 */1 * * * ?") public void closeExpiredOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(20); List<Orders> expiredOrders = orderMapper.selectList( new LambdaQueryWrapper<Orders>() .eq(Orders::getOrderStatus, 0) .lt(Orders::getCreateTime, deadline) .eq(Orders::getPayStatus, 0) ); for (Orders order : expiredOrders) { order.setOrderStatus(4); orderMapper.updateById(order); } } }第二个是上课前提醒。任务每30分钟扫描一次课表,找到距上课还有1小时且状态为“已确认”的课程,调用一个发消息的服务。这个“消息服务”在毕设里可以简化为站内信或邮件。如果不想引入消息队列,就是最朴素的定时轮询+状态标记。
定时任务有一个坑必须提醒:任务执行要保证幂等,不能重复执行产生重复提醒。最简单的处理方式是在执行逻辑里加状态判断,比如查询只查
status = 1的未提醒课表,执行后把状态改为2(已提醒)。不要试图用分布式锁解决所有问题,毕设场景下做到业务幂等就够了。
5. 实战中的高频坑,和我的排查手册
5.1 抢单并发导致的数据错乱
家教系统里教师抢单是一个真实的并发场景。多个教师同时点击“接单”,后端接口如果这么写:
Demand demand = demandMapper.selectById(demandId); if (demand.getStatus() == 0) { demand.setStatus(1); demand.setTeacherUserId(currentUserId); demandMapper.updateById(demand); }两个请求同时读到status = 0,同时通过校验,先后执行更新,最后一个覆盖前一个,你以为只有一个老师抢到了,实际有两个人都显示抢单成功。解决思路有两个层面:
第一是数据库层面兜底。在需求表里加一个设计上“乐观锁”的版本号字段version,用MyBatis-Plus的@Version标注:
public class Demand { @Version private Integer version; }更新时MyBatis-Plus会自动带上WHERE version = ?条件,更新失败影响行数为0,业务层判断到影响行数为0就提示“手慢了,需求已被抢”。
第二是Redis分布式锁。抢单接口入口处用setIfAbsent加锁,锁的key可以是lock:demand:{demandId},拿到锁才能进入后续逻辑,执行完释放锁。这样能在高并发下保证同一时刻只有一个线程在处理某个需求单。
5.2 支付回调失败和重复回调
真实支付系统会有网络抖动导致异步通知失败的情况,模拟支付同样会面临前端把请求重试多次的问题。我的排查经验总结为三个要点:
- 支付接口必须幂等,订单号作为唯一业务键,已支付状态直接放行。
- 订单状态和支付状态是两回事,写一个定时任务每天核对一次“已支付订单”的
order_status是否应该推进。这是我前面说的“对账补偿”思路。 - 如果出现重复支付(理论上不应该),要支持退款流程,订单增加一个“退款中”状态,管理员审批后把
pay_status置为已退款。
5.3 跨域、Token过期与文件上传的连环坑
前后端分离项目必然遇到跨域。后端不做任何配置,浏览器直接拦截响应。在config目录加一个跨域配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }Token过期是另一个常见的用户体感问题。方案是前端在Axios统一响应拦截器里判断HTTP状态码401,跳到登录页并清除本地存储。后端还需要处理一个细节:拦截器里放行的接口不要一股脑全放行,比如/auth/login和/auth/register放行,但/admin/**必须严格校验管理员角色。
文件上传最玄学的坑是“本地能传,部署到服务器就不行”。排查思路按顺序来:先看Spring Boot的multipart配置,再看服务器的/tmp目录权限,最后记得在配置文件里指定一个持久化上传目录,不要用系统临时目录。如果上传的文件是图片,还要做图片格式后缀校验,防止有人传可执行文件。
5.4 MyBatis-Plus逻辑删除与唯一索引的冲突
MyBatis-Plus的逻辑删除功能很方便,但它和一个细节有冲突:如果user表的手机号是唯一索引,用户删除后(逻辑删除,deleted=1),你再注册一个同号码的新用户时,唯一索引仍然存在,导致插入失败。
解决方案有三种:一是唯一索引改成联合索引,把deleted字段加入索引,比如UNIQUE KEY uk_phone_deleted (phone, deleted);二是删除时把手机号改成旧号码_时间戳;三是干脆不做用户删除功能,只做禁用。我建议用第二种,简单直观,也不会破坏索引结构。
还有一个常见问题:MyBatis-Plus更新时null字段不更新。默认策略下,updateById传入的实体字段为null时,该字段不会被更新到数据库。有些同学想用updateById把某个字段置空,发现没生效,一脸懵。解决办法是字段上加@TableField(updateStrategy = FieldStrategy.IGNORED),或者改用UpdateWrapper里的set方法。
6. 演示、答辩与项目扩展的实战思路
6.1 巧用数据看板让系统看起来更完整
家教管理系统天然适合做数据运营看板。管理员端你可以放三个图表:
- 近30天订单量和营收趋势折线图。
- 热门学科分布饼图,按订单里的
subject字段统计。 - 教师服务评分排行列表。
图表用ECharts就行,后端提供一个聚合统计接口,用MyBatis-Plus的selectMaps加group by查询,返回List<Map<String, Object>>。这部分代码量不大,但效果极其抢眼。答辩现场把看板一展示,老师立刻会觉得这个项目不是简单的CRUD——它具备了数据分析和决策辅助的雏形。
6.2 答辩演示脚本:按用户故事讲,不要按接口讲
我见过太多学生答辩时带着电脑一个一个点菜单,从用户管理点到课程管理,毫无逻辑。正确演示方式是按“用户故事”串起来讲:
- 先用管理员账号登录,展示教师待审核列表,通过一位数学老师的入驻申请。
- 切换教师账号,登录后看到自己审核通过,进入需求池,抢到一单高中数学需求。
- 切换家长账号,发布一条孩子数学辅导需求(注意:发布前系统里最好已经有这条预置数据,现场输入浪费时间)。
- 演示系统自动匹配到了刚才那位数学老师,家长确认试听。
- 教师上传课表,家长确认,课程排期生成。
- 切换到教师端,点击“确认上课”,切回家长端,确认消课并评价。
- 切回管理员端,查看订单统计和营收图表,顺带演示财务结算列表。
这七个步骤展示下来,所有核心功能都覆盖到了,而且逻辑是一条完整的业务线。这就是“全流程对接与管控系统”最好的诠释。
6.3 时间不够时,什么功能可以砍
很多同学做毕设时觉得功能越多越好,结果把自己拖死在半路上。我给你的建议是抓大放小。核心中的核心是:用户登录、教师入驻审核、需求发布、订单支付、课程排课、评价。这六个功能构成完整闭环,必须做扎实。以下功能如果时间紧张,可以先砍掉或用最简单方案代替:
- 站内信通知:用数据库消息表+页面轮询实现,不做WebSocket。
- 优惠券/打折活动:完全不必要,家教平台不靠这个盈利。
- 多级分销:别碰,业务复杂度高且论文不好写。
- 在线网课直播:这是另一个项目了,你的题目是“对接与管控”,不是“在线教学”。
我在实际项目里见过太多学生因为纠结“要不要做个在线课堂”而拖了进度,最后连核心流程都没跑通。务必记住:毕设系统要的是完整和流畅,不是功能堆砌。
最后再分享一个我反复强调的小技巧:所有核心表都要预置演示数据,并且演示数据要贴近真实。老师名字可以叫“王明老师”,孩子叫“李小雨”,科目是“高一数学”,地址写“北京市朝阳区”,金额别出现负数或超长小数。真实感强的数据,能让答辩老师快速理解你的业务场景,也会让整个演示过程顺畅很多。这个项目做下来,你对Spring Boot的理解、对数据库设计的体会、对业务流程的敏感度,都会远超一个只会写增删改查的人。把这套东西吃透,毕业设计答辩就是一次普通的功能演示而已。