最近整理一套基于 Spring Boot Vue 的家教管理系统从前端选型、后端接口设计到部署上线整个链路完整跑通。这个项目以家教预约场景为核心覆盖多角色登录、科目筛选、讲师预约、订单管理等功能很适合作为全栈入门实践、课程设计参考或者直接改成本地生活类预约系统的基础骨架。今天就以这套家教管理系统为例聊聊它的整体设计思路、核心功能怎么拆、代码怎么组织以及实际开发中容易踩的坑。这个系统表面上是个“家教”业务但本质上是一个典型的“多角色 资源预约 订单管理”平台。看懂这套结构以后做咨询预约、维修上门、宠物寄养、健身私教这类系统都能直接套用。我尽量把项目里真正值得花时间的地方讲清楚不绕弯子。1. 家教管理系统的整体设计与技术选型1.1 系统定位与核心需求拆解家教管理系统解决的是一个很常见的线下撮合问题家长/学生想找家教不知道哪个老师合适、怎么约、怎么管理上课记录家教想接单缺乏统一展示和日程管理的渠道。放到线上核心链路就变成找老师看详情选时间提交预约老师接单课时完成双方互评。拆需求的时候可以先把角色分清楚。一个相对完整的小型家教平台最少要有三个角色管理员负责审核家教信息、管理科目分类、处理用户反馈、查看平台数据。学生/家长浏览老师列表、按科目或价格筛选、收藏老师、提交预约、确认上课、评价。家教/老师维护个人资料、设置可预约时间、接单/拒单、查看课时记录。角色定清楚以后功能列表自然就能列出来。学习类项目最忌讳一上来就想着“做得很全”什么功能都塞进去最后每个功能都半成品。我建议只做一条核心主线注册登录、老师展示、预约下单、订单状态流转、评价。扩展功能比如收藏、消息通知、数据统计可以在主线跑通以后再加。1.2 为什么选择 Spring Boot Vue这套组合在国内项目里特别常见选它不是因为“潮流”而是因为它把前后端的开发和部署成本都降到了比较低的位置。后端用 Spring Boot本质上是省掉了大量繁琐的配置。以前用 SSM 写一个接口要配一堆 XML现在一个注解加一个 starter 就搞定。它内置 Tomcat打包成 jar 直接跑适合快速开发。Java 本身的生态也成熟遇到问题容易搜到方案。前端用 Vue是因为它对初学者友好组件化开发思路清晰。Vue 的响应式数据绑定让页面状态维护变得非常直观加上 Element UI / Element Plus 这类组件库表单、表格、分页几乎不用自己手写。配合 Vue Router 做页面跳转和权限控制Vuex/Pinia 管理登录状态一套典型的中后台架构就出来了。比起直接用模板引擎做服务端渲染前后端分离的好处是接口和页面可以完全分开开发、分开测试、分开部署。改前端页面不用动后端代码反过来也一样。实际项目协作中这个优势非常明显。1.3 功能模块划分与信息架构具体到模块我把它分成了几块用户中心注册、登录、个人信息、密码修改、头像上传。老师模块老师列表、条件筛选科目、价格区间、评分、老师详情、老师资质信息维护。预约模块可预约时间段展示、提交预约、预约确认/拒绝、课程状态流转。评价模块预约完成后双方互评评分影响老师的综合评分。管理后台科目分类管理、老师入驻审核、预约记录查看、基础数据统计。收藏模块学生收藏意向老师收藏列表单独展示。每一块单独拿出来都不复杂但放在一起就需要考虑它们之间的关系。比如预约状态变了是否要影响老师可预约时间评价完成以后评分怎么计算并更新到老师列表排序里这些跨模块的数据一致性恰恰是项目的难点和价值所在。1.4 开发环境与版本选型建议实际项目里版本搭配问题经常被忽略。Spring Boot 2.x 和 3.x 的差异、Java 版本要求、Vue2 和 Vue3 的差异、Element UI 和 Element Plus 的差异这些在起步阶段就要定好。我推荐一组比较稳妥的组合组件推荐版本说明JDK1.8 或 11Spring Boot 2.x 兼容性好部署资源占用低Spring Boot2.7.x稳定、资料多、和 MyBatis-Plus 兼容好MyBatis-Plus3.5.x单表 CRUD 不用写 SQL分页好用MySQL5.7 或 8.0中小项目足够8.0 注意驱动和时区配置Vue2.6/2.7选 Vue2 Element UI网上现成方案最多Node.js14/16对应老版本 CLI 工具兼容性更好具体版本不用死磕最新因为学习型项目重要的是能稳定跑起来。选一套经过大量验证的版本组合可以节省非常多折腾环境的时间。2. 数据库设计与核心表结构拆解2.1 数据库设计的基本原则数据库设计是这个项目里非常关键的一步。做预约类系统表关系不复杂但容易“乱”。我的原则是先梳理实体再梳理实体间关系最后反推字段。实体包括用户、家教老师信息、科目分类、预约单、评价、收藏。老师和用户是“一对一”因为老师本身也是一个用户学生和老师是“多对多”通过预约单和收藏表关联。设计表的时候有两点值得注意。第一多角色用户尽量放在同一张 user 表里用 role 字段区分。分开建 student 表、teacher 表、admin 表会非常别扭因为这不利于统一登录认证。第二字段类型要提前想好比如价格用 decimal状态用 tinyint 或 int时间统一用 datetime。2.2 六张核心表的字段设计实际项目里我设计了六张核心表下面挑关键的说明一下。用户表t_user字段类型说明idbigint主键usernamevarchar(50)登录账号passwordvarchar(100)加密后的密码nicknamevarchar(50)昵称phonevarchar(20)手机号avatarvarchar(255)头像地址roletinyint1管理员 2学生 3老师statustinyint是否禁用create_timedatetime注册时间老师信息表t_teacher_info字段类型说明idbigint主键user_idbigint关联用户表subject_idbigint授课科目titlevarchar(100)老师头衔如“重点大学硕士”introtext个人简介pricedecimal(10,2)每小时价格ratingdecimal(3,2)综合评分teach_yearsint教龄audit_statustinyint0待审核 1通过 2拒绝科目表t_subject最简单就 id、name、sort、status 几个字段。预约订单表t_appointment是业务核心字段类型说明idbigint主键order_novarchar(64)订单编号student_idbigint学生用户IDteacher_idbigint老师用户IDsubject_idbigint科目IDappoint_datedate预约日期time_slotvarchar(20)时间段如“09:00-10:00”addressvarchar(255)上课地址pricedecimal(10,2)成交价格statustinyint0待确认 1已确认 2已拒绝 3已完成 4已取消create_timedatetime申请时间评价表t_comment包含 order_id、user_id、rated_user_id、content、score 等字段。收藏表t_favorite包含 user_id、teacher_id、create_time用来记录关注关系。2.3 状态设计的几个细节订单状态我用了 0、1、2、3、4 这种数字映射。有人喜欢直接用字符串“PENDING”、“ACCEPTED”可读性强但从数据库存储和索引效率角度数字更省空间。我的建议是数据库存数字后端代码里定义常量或枚举类。状态流转要规划清楚用户提交预约后状态为待确认老师确认后变为已确认老师拒绝后变为已拒绝已确认的订单在课程结束后变为已完成待确认和已确认状态用户都可以主动取消。这里容易出的问题是状态流转没做统一控制代码里到处都能直接改状态最后很难排查。最好把状态变更封装到一个 service 方法里统一校验当前状态是否允许流转到目标状态这样能避免很多逻辑漏洞。2.4 并发与唯一性约束注意点预约类系统要特别注意重复预约的问题。比如同一个老师的同一个时间段两个学生同时提交预约可能都成功。数据库层面至少要做一层保护最简单有效的方式是在预约表上建立唯一索引比如 (teacher_id, appoint_date, time_slot) 联合唯一。这样即便程序里有并发缝隙数据库也会拒绝第二条重复记录。另一个细节是时间字段不要用 varchar 存时间排序和查询都麻烦。日期用 date时间点用 time 或 datetime。前端传参时注意格式转换Java 端用 LocalDate/LocalDateTime 接收配合 Jackson 的日期格式化配置基本不会出问题。3. Spring Boot 后端核心实现与代码组织3.1 后端工程结构与接口分层后端模块划分我建议按功能分包而不是按技术分层。很多初学者喜欢建 controller、service、mapper 三个包然后所有东西都往里扔这样项目一大了很难维护。更合理的做法是com.example.tutor ├── controller │ ├── AuthController.java │ ├── TeacherController.java │ └── AppointmentController.java ├── service │ └── impl ├── mapper ├── entity ├── dto ├── config ├── common │ ├── Result.java │ ├── ResultCode.java │ └── BusinessException.java └── utils └── JwtUtils.java按功能模块命名 controller一眼就知道这个接口管什么。dto 目录放前端传输对象避免直接暴露数据库实体结构。common 放统一响应结果和异常处理这是整个后端设计里很基础也很重要的一环。3.2 统一响应结果与全局异常处理前后端分离项目里接口返回格式一定要统一。我用的格式是{ code: 200, message: 操作成功, data: {} }对应的 Result 类核心代码如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合 RestControllerAdvice 做全局异常处理业务异常抛 BusinessException其它未知异常记录日志后返回统一格式。这样 controller 里不用每个接口都写 try-catch代码干净很多。实际开发中我遇到过一个坑异常处理里容易把错误信息直接返回给前端比如数据库连接失败时把完整异常堆栈交给页面。这不仅不安全用户也看不懂。应该只给前端友好提示详细堆栈打到日志文件里。3.3 登录认证与 JWT 权限控制登录认证是每个系统都逃不掉的部分。我的方案是 Spring Security JWT但不用 Security 默认的复杂过滤器链而是自定义一个 JWT 认证过滤器逻辑足够简单也容易理解。JWT 签名生成和校验的工具类核心代码如下public class JwtUtils { private static final String SECRET your-secret-key; private static final long EXPIRE_TIME 1000 * 60 * 60 * 12; public static String createToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }前端登录成功后会拿到 token存到 localStorage后续请求在 Authorization 头里携带。后端自定义拦截器逐个解析请求头里的 token校验通过后把用户信息放入上下文。这里需要注意 token 过期处理。登录状态过期不能只靠后端返回 401前端也要相应处理否则用户操作到一半没有任何提示体验很差。后端的统一异常处理里要捕获 token 过期异常返回 401 状态码前端 axios 响应拦截器里判断 401 后自动跳转登录页并清除本地 token。3.4 预约下单与状态流转的实现预约下单是整个系统业务逻辑最集中的地方。创建预约单不能只做一次 insert至少要包含几个步骤校验当前用户是否是学生角色校验老师是否存在且审核通过校验选中的时间段与当前时间是否合理校验该时间段是否已被预约计算价格并生成订单号插入预约记录返回预约成功结果。这个流程必须放在一个事务方法里。如果中间任何一步校验失败整个操作回滚不然会出现订单数据不完整的问题。我用 Transactional 注解保证同一线程内的数据操作要么全部成功要么全部回滚。订单号生成我推荐“日期 随机数”的方式比如String orderNo AP DateTimeFormatter.ofPattern(yyyyMMddHHmmss).format(LocalDateTime.now()) String.format(%04d, new Random().nextInt(10000));这个方法虽然简单但够用不会出现明显的重复问题。如果追求更严谨可以在数据库里加唯一索引兜底。状态更新的代码示例Transactional public void confirmOrder(Long orderId, Long teacherId) { Appointment appointment appointmentMapper.selectById(orderId); if (appointment null) { throw new BusinessException(订单不存在); } if (!appointment.getTeacherId().equals(teacherId)) { throw new BusinessException(无权操作该订单); } if (appointment.getStatus() ! 0) { throw new BusinessException(当前状态不允许确认); } appointment.setStatus(1); appointmentMapper.updateById(appointment); }状态校验里把用户权限和状态流转逻辑放一起避免出现“老师把学生的订单改了”这类越权操作。3.5 分页查询与条件筛选老师列表页需要支持按科目、按价格范围、按评分排序这些场景用 MyBatis-Plus 的分页查询配合 LambdaQueryWrapper 写起来非常快。public PageTeacherVO queryTeachers(Integer page, Integer size, Long subjectId, String keyword) { PageTeacher pageParam new Page(page, size); LambdaQueryWrapperTeacher wrapper new LambdaQueryWrapper(); wrapper.eq(Teacher::getAuditStatus, 1); if (subjectId ! null) { wrapper.eq(Teacher::getSubjectId, subjectId); } if (StringUtils.hasText(keyword)) { wrapper.like(Teacher::getTitle, keyword).or().like(Teacher::getIntro, keyword); } wrapper.orderByDesc(Teacher::getRating); return teacherMapper.selectPage(pageParam, wrapper); }分页的参数 page 和 size 建议做上限限制防止有人传一个 size10000 把整个表拉出来。实际接口压测的时候这种问题很容易暴露。返回给前端的结构里把 total、pages、records 全部拿出去前端表格翻页就能直接用。3.6 文件上传与静态资源处理头像上传是个很常见的功能但文件存储位置很多人处理不到位。我建议开发阶段把文件存储到本地磁盘通过一个虚拟路径映射对外访问。比如在 application.yml 里配置spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB web: resources: static-locations: classpath:/static/,file:/upload/上传接口用 MultipartFile 接收文件保存时注意文件名不要用原始文件名避免路径穿越和安全问题。我会用 UUID 重新生成文件名保留原扩展名。还有一个容易踩的坑是 Nginx 部署后文件 404。本地能访问不代表服务器上能访问因为静态资源映射的绝对路径在服务器上可能不存在。部署时一定要确认上传目录存在并且有写入权限否则接口报错但前端看不出原因。4. Vue 前端页面组织与功能实现4.1 前端项目结构划分前端部分我用 Vue2 Vue Router Vuex Axios Element UI 这套组合。项目目录如下src ├── api │ ├── auth.js │ ├── teacher.js │ └── appointment.js ├── assets ├── components ├── router │ └── index.js ├── store │ └── modules │ └── user.js ├── utils │ └── request.js └── views ├── home ├── teacher ├── appoint ├── order └── adminapi 目录把所有请求接口独立出来页面组件不直接写 axios这样接口统一管理后期改地址方便。utils/request.js 负责处理 token 注入和响应拦截。4.2 Axios 封装与请求拦截器Axios 封装是前端非常基础的一层。我通常会做三件事请求头加 token响应时判断业务状态码401 时自动登出。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message || Error)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default servicebaseURL 配合开发代理使用部署时改成 Nginx 反代路径。很多同学问我为什么接口请求一直 404十有八九是 baseURL 和后端 context-path 没对齐。4.3 路由权限控制按角色控制页面访问是前端权限的常见需求。路由配置里通过 meta.roles 标明哪些角色可以访问该页面然后在全局前置守卫里做判断。router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() return } if (!token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(Number(role))) { next(/403) return } next() })这种前端路由控制搭配后端接口的权限校验前后端各防一层。必须强调前端控制只是体验优化真正安全的后端一定会在接口层做二次校验防止有人绕过前端直接调接口。4.4 核心页面交互逻辑首页是最花功夫的页面。家教列表要展示科目筛选、价格排序、评分排序、卡片式布局还要处理加载状态和空数据。我一般用 Element UI 的 el-card 配合 el-tag 展示科目标签底部放“查看详情”和“立即预约”按钮。预约流程页有一个很关键的点时间段选择。老师会维护自己的可预约时间学生选择日期后前端显示该老师当天可约的时段。如果老师没设置可约时间列表为空这时候要给出友好提示而不是让用户对着空白页面发呆。订单页面向学生展示自己提交的预约记录状态就那几个数字。展示时用状态映射函数转成文字和 tag 颜色比如 0 显示“待确认”用警告色1 显示“已确认”用成功色。代码里写个字典export const ORDER_STATUS { 0: { text: 待确认, type: warning }, 1: { text: 已确认, type: success }, 2: { text: 已拒绝, type: info }, 3: { text: 已完成, type: primary }, 4: { text: 已取消, type: danger } }管理后台页面可以做成左侧菜单、顶部面包屑、右侧内容区。内容区放科目管理表格、老师审核表格、订单统计面板。统计面板用卡片展示总数再配合 ECharts 画一个简单的订单折线图项目整体完成度会明显提升。4.5 表单校验与交互细节表单校验属于细节但决定用户体验。Element UI 表单校验建议直接用 rules 写死规则比如手机号格式、价格必须大于 0、时间范围不能为空。这样减少大量的手写 if-else。提交预约的时候我建议二次弹窗确认把预约时间和价格都展示清楚让用户最后确认一次。这个细节很多人不做结果误操作以后订单不能删很影响体验。做成确认弹窗成本很低但实际效果很好。5. 前后端联调与项目部署实战5.1 本地联调常见配置前后端分离项目联调的第一步是解决跨域问题。开发阶段最简单的方案是配置 Vue 的代理把请求转发到后端的 8080 端口。在 vue.config.js 里加这段module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这里有个经常搞混的点如果前端请求路径是 /api/teacher/list后端接口路径是 /teacher/list那么代理需要把 /api 去掉再转发所以 pathRewrite 配置很重要。如果后端接口本身就带 /api 前缀就不用 rewrite。后端也可以开启 CORS 支持用过滤器统一加响应头。我习惯把两种都配好本地联调走代理跨域请求由后端兜底放行。生产环境用 Nginx 反代基本不会遇到跨域问题。5.2 数据库脚本与初始化数据项目里一定要带上初始化 SQL 脚本。很多同学拿到的项目跑不起来就是因为缺初始化数据或者 SQL 文件版本和代码对不上。我习惯在项目的 sql 目录下放两个文件schema.sql 只建表data.sql 插入默认管理员、科目分类、几条演示老师数据。管理员账号密码必须写在 README 里。密码存数据库时用 BCrypt 加密后端提供注册接口加用户注册成功后默认是学生角色老师角色需要后台审核。这里要注意演示数据里如果直接插入明文密码代码的登录校验是匹配不了的。5.3 前端构建与打包前端构建命令很简单但实际执行会踩不少坑npm install npm run build打包产物在 dist 目录。有几点经验第一npm install 报错时优先删除 node_modules 和 package-lock.json 重新安装很多时候是依赖缓存问题。第二打包前要看 .env.production 里的接口地址如果是相对路径 /apiNginx 要配好代理如果是完整地址要确保后端地址可以被外网访问。第三dist 目录部署到 Nginx 之后静态资源路径如果显示 /js/xxx.js 找不到多半是打包配置里的 publicPath 没设成相对路径或根路径。5.4 Nginx 部署后端与前端以一整台服务器为例我的部署方案是这样的后端 Spring Boot 应用跑在 8080 端口前端静态文件放在 /usr/share/nginx/html 目录Nginx 监听 80 端口并把静态请求和后端 API 请求分开处理。Nginx 配置示例server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /upload/ { alias /data/upload/; } }这里最关键的是一行 try_files $uri $uri/ /index.html;。它保证前端路由跳转刷新后不会 404。很多人前端打包部署后点击页面跳转正常但一刷新就白屏就是因为没配这一行。后端启动命令建议写成脚本方便停止重启。简单的方式用 nohupnohup java -jar tutor-system.jar --server.port8080 app.log 21 日志输出重定向到 app.log方便排查问题。生产环境如果追求稳定可以用 systemd 管理进程但这个项目规模用 nohup 完全够。5.5 服务器环境准备清单部署前要确认服务器的 Java、MySQL、Nginx、Node 环境都正确。我列一个检查清单JDKjava -version确保是 1.8 或合适版本不是 JRE 也不是高版本不兼容的 JDK。MySQL数据库能连上字符集是 utf8mb4账号密码正确服务已启动。Nginx已安装且配置文件语法检查通过nginx -t。防火墙开放 80 和 8080 端口云服务器安全组也要放行。上传目录确保存在并且有写权限。部署的整个过程看起来繁琐其实就几条命令。真正容易出问题的地方在三处数据库连接串里的时区配置、防火墙端口没放行、Nginx 配置文件语法错误。这三处我都会在部署前仔细检查。6. 常见问题与排查技巧实录6.1 接口 404 或 502 的排查思路前端能打开但发请求就报 404 或 502这是最高频的问题。排查顺序很重要。先在浏览器 F12 的网络面板看请求链接比对前端 baseURL、代理路径、后端接口路径是否一致。再看后端控制台有没有报错日志如果后端没收到请求问题就在 Nginx 或代理配置如果后端收到但返回 404通常是请求路径没对上。502 则是后端进程挂了或者端口不对先看一下进程在不在ps aux | grep java netstat -tlnp | grep 8080这两条命令能解决大部分后端“假死”问题。6.2 数据库连接失败数据库连接失败基本是三类原因MySQL 服务没启动、账号密码错误、连接串参数有误。启动报错常见的是Access denied for user rootlocalhost (using password: YES)这是账号密码错误或账号权限问题。还有一种Communications link failure这通常是 MySQL 服务没启动或者连接串里的 host/port 写错。MySQL 8.0 还要注意驱动类名变成了 com.mysql.cj.jdbc.Driver连接串要加 serverTimezoneAsia/Shanghai否则时间可能偏差 8 小时。6.3 JWT 登录失效与状态丢失用户登录后一段时间再操作提示登录过期这算正常现象但很多人会把过期时间设得太短。开发调试时我习惯把过期时间设长一点比如 12 小时正式部署再调成 2 小时。如果用户操作过程中频繁被踢大概率是前端请求头没传 token或者 token 存储的 key 和后端解析的不一致。排查方法登录成功后用浏览器工具看 localStorage 里有没有 token刷新页面后 token 还在不在。如果刷新就丢那是 localStorage 写入时机不对需要检查登录回调函数是不是把 token 存得太早或太晚。6.4 文件上传相关的内存和大小限制上传头像时接口直接报错最常见的是 multipart 上传大小超限。Spring Boot 默认上传限制只有 1MB。我在配置里调整为 5MB 后上传基本没再报错。但如果上传文件非常大还会遇到临时目录空间不足的问题Linux 下容易这样需要检查 /tmp 目录空间。6.5 前端白屏与开发环境异常前端页面白屏在浏览器控制台通常会看到 JavaScript 报错。最常见的几种某个组件导入路径错误、接口数据字段名对不上导致模板渲染报错、浏览器缓存了旧的打包文件。排查时先清缓存、强刷再看 console 报错信息定位组件。Vue 项目里 data 中没定义的字段直接拿来渲染结果会是 undefined但控制台不一定报错需要自己留意。6.6 事务失效问题预约创建加事务但测试时发现数据没回滚这在 Spring Boot 里很经典。常见原因是 Transactional 方法被同类内部调用Spring 的 AOP 代理失效。比如 OrderService 里一个方法调用同类的另一个带 Transactional 的方法事务配置不会生效。解决方案是把需要事务的方法放到另一个 Service 里调用或者自己注入自己。这个问题排查起来很隐蔽但实际开发中经常遇到。6.7 并发测试下重复预约问题我试着用并发工具同时提交两份相同的预约请求结果数据库里出现了两条记录。第一道防线是程序逻辑校验但并发场景下程序校验有间隙必须靠数据库唯一索引兜底。建立联合唯一索引后重复插入会报 DuplicateEntry 异常。我在代码里捕获这个异常转成友好提示“该时间段已被预约”问题就解决了。7. 个人经验与后续扩展建议整个系统跑通下来我个人最深的体会是这类预约管理项目工程量不在代码多少而在状态管理和细节兜底。比如订单状态流转的权限校验、并发预约的唯一约束、前端刷新 404、上传路径映射这些出问题之前都想不到但每一个都能让人折腾一整天。做这套项目的时候我建议按阶段推进先把后台管理做出来能把老师和科目管理跑通再去做前台老师展示和筛选最后做预约流程和评价。每一步都跑通后再进入下一步不要一口气写到底。前两步做完你已经得到了一个信息展示系统预约流程才是整个项目的重头戏。如果后续想继续扩展可以往这些方向发力接入支付流程做课时费在线结算增加消息通知让老师和学生实时收到订单状态变更引入课程表功能让老师批量设置可预约时间段或者做一个简单的数据大屏展示平台运营数据。这些方向都不难但价值感很强。最后分享一个小细节项目里的 SQL 初始化文件和 README 一定要写清楚。一个能在 10 分钟内跑起来的项目比自己摸索着配环境要省太多时间。给别人使用或评审这个项目时印象分会高很多。