
简介本资源是一套完整的教室预约管理系统前后端源码面向计算机专业本科生及初学者适用于课程设计、毕业设计或全栈开发入门实践。系统采用Spring Boot Vue前后端分离架构后端基于SSM框架与RESTful API设计前端使用Vue 2/3含118个.vue文件配合Axios实现异步通信并内置管理员与普通用户双角色权限控制覆盖教室查询、预约、审核、冲突检测等核心业务场景。压缩包共19195个文件主体为12017个JS、2169个JSON、1509个MD文档、873个LICENSE、336个CSS及118个Vue组件文件辅以YML配置、Java源码、SQL脚本与Node.js依赖包总大小62.48MB结构清晰、注释完备附带详细运行说明与环境配置指南。目前已有1734人学习下载开箱即用可直接部署调试是理解企业级预约类系统设计逻辑与技术协同的优质实战范例。 一到考试周学校的空教室就成了稀缺资源。学生要找个能自习、能讨论的地方得挨个教室推门看管理员要协调临时调课、活动场地只能靠微信群接龙和Excel表格来回对更头疼的是教室被临时借用后没有记录设备坏了也不知道该找谁。我做过一个基于 Spring Boot 和 Vue 的教室预约管理系统核心就是把这些线下流程搬到线上统一管理教室信息、开放预约申请、自动做冲突检测、留好审核追溯记录。整个过程做下来踩了不少坑也积累了一些值得写出来的经验这篇就当是完整复盘。这个项目特别适合两类人参考一类是正在准备毕业设计、想做前后端分离项目的同学另一类是学校里负责信息化建设、想把教室资源管起来的老师或管理员。文章会按完整的项目线来写——从痛点分析、技术选型、数据库设计到后端预约状态机、前端页面实现再到部署联调的真实踩坑记录每个环节我都会讲清楚为什么这么做。1. 教室预约为什么总是一团乱系统要解决的真实痛点先别急着写代码。我见过太多人拿到这类题目直接开建表结果做完以后自己都理不清业务逻辑。所以第一步得先搞清楚教室预约到底在管什么。1.1 线下管理的三大常见问题第一是资源不透明。教室有没有被占用、被谁占用、用到几点这些信息分散在各个部门手里有的靠教室门口贴的课表有的靠管理员脑子记。学生想找一间空教室只能一层一层楼跑跑到了才发现里面在上课。第二是冲突无法提前发现。临时安排一场考试、一次班会、一个社团活动往往要同时协调好几个教室。线下用Excel排期的时候同一个教室同一时间段被重复登记的情况非常普遍而且经常是活动当天才暴露出来。第三是数据没法追溯。教室的空调坏了、投影仪不亮了要查是谁用的、用了多久、有没有报修记录全凭记忆。学期末要做教室使用率统计的时候手里只有一堆乱七八糟的纸质登记表统计工作量大到让人崩溃。1.2 预约系统的三个核心角色在设计系统前先把使用角色画清楚。这套系统我最终确定了三类用户权限划分得很干净管理员负责维护教室信息、审批预约申请、发布公告、查看统计报表拥有全部权限。教师可以提交预约申请用于上课补课、课后辅导、组织活动可以查看自己名下的预约记录。学生可以浏览教室空闲情况提交自习或班级活动的预约申请也可以取消自己发起的预约。值得提醒的是很多类似的毕设项目会把角色做成两级管理员和普通用户但实际用下来你会发现教师和学生对待教室预约的行为模式差异很大——教师预约通常和教学计划绑定需要打印课表作为凭证学生预约更多是短时自习需求灵活更容易取消。所以从设计之初就把三类角色分开后续做权限控制和状态流转会省很多事。1.3 系统的核心业务闭环这套系统的业务主线其实只有一条查询教室 → 提交预约 → 管理员审核 → 使用教室 → 状态归档。所有功能都是围绕这条主线展开的。从这条主线可以推导出最基础的功能清单教室管理教室信息的增删改查、启用/停用、按校区/楼栋/容量/设备条件筛选。预约管理发起预约、取消预约、管理员审核通过或驳回以及预约记录的列表查询。冲突检测同一教室同一时间段的预约冲突校验这是系统的技术核心后面专门讲。公告管理管理员发布和维护公告用户端展示最新通知。统计看板按日/周/月统计教室使用率按原因统计驳回情况。用户管理登录注册、信息维护、权限控制。这些功能听着常规但每一个细节都会牵扯出数据库设计和接口设计的问题。如果你是在做毕设功能清单做到这一步开题报告里的研究内容一节基本就能写满了。2. 技术方案怎么定Spring Boot Vue 的前后端分离选型思路项目题目定了用 Spring Boot 和 Vue但用什么和为什么用是两个问题。这章我把选型思路拆开讲清楚有些是技术上的必然有些是实际开发中的权衡。2.1 为什么后端选 Spring Boot教室预约系统的后端逻辑并不算复杂核心就是增删改查加一个冲突检测。这种项目用 Spring Boot 的优势非常明显内嵌 Tomcat打一个 jar 包就能跑不用单独装服务器部署简单。Spring Boot 的 Starter 机制让依赖管理变得很干净加一个 Web 模块就够起步。生态成熟接数据库用 MyBatis 或 JPA 都是很成熟的路子网上资料多遇到问题容易查。对于毕设或课程设计来说Spring Boot 是市面上使用率最高的框架之一老师认可度高答辩时技术点也容易讲清楚。版本这里单独提一句。如果你的开发机 JDK 环境是 8 或 11那就用 Spring Boot 2.7.x稳定、资料多、兼容性好如果你是新装了 JDK 17 或更高版本可以直接上 Spring Boot 3.x但要注意 3.x 是从 javax 包迁移到了 jakarta 包很多旧代码里的 import 要改。我当时用的组合是 JDK 8 Spring Boot 2.7.14 MyBatis-Plus 3.5.3跑下来没有什么坑。2.2 前端为什么选 VueVue 的优势是轻量、灵活、上手快对中小型后台管理系统来说特别顺手。教室预约系统的前端界面有好几个信息量大的页面教室列表、预约时间选择、审核列表用 Vue 的组件化开发可以把每一个业务模块拆成独立组件改动一个地方不影响全局。Vue 2 和 Vue 3 的选择上我的建议是如果是新学直接学 Vue 3 script setup语法这是当前主流方向如果是为了兼容学校已有的代码或参考前辈的毕设Vue 2.6 Element UI 依然是很成熟的组合。我当时做的是 Vue 2.6 Element UI因为那会儿 Vue 3 的生态还没有完全铺开Element UI 对 Vue 2 的支持最稳定。现在做的话我会优先 Vue 3 Element Plus原因后面第五章节会详细说。2.3 前后端分离的架构好处前后端分离这几年已经从加分项变成了默认项对这套系统来说分离架构带来最直接的两个好处开发职责清晰。前端同学只管页面和交互后端同学只管接口和数据两边约定好接口 JSON 结构就能并行开发互不阻塞。部署灵活。前端打包成静态文件扔 Nginx 里后端 jar 独立运行前端升级不用动后端后端重启也不会影响前端的静态资源服务。开发环境里解决跨域问题也比较简单Vue 的 devServer 配置一个 proxy 代理把/api开头的请求转发到localhost:8080就好完全不需要后端做 CORS 处理CORS 单独讲一下生产环境通常用 Nginx 反转替代开发环境用 proxy 最省事。2.4 其他技术栈的对比参考有人会问这套系统用 SSMSpring Spring MVC MyBatis行不行用 Python 的 Flask 或者 Django 行不行答案都是可行但要看你做这个项目的目标是什么。如果目标是把系统快速跑起来、逻辑清晰、工作量适中Spring Boot 比 SSM 省去了大量 XML 配置明显更高效如果目标是练 Python 后端那用 Flask 也能做但你要自己处理的东西比如数据库迁移、权限框架会多不少如果目标纯粹是为了通过答辩那选择你最有把握、能讲清楚每个模块原理的技术栈就是最好的。技术只是手段把业务讲透才是核心。3. 数据模型先行六张核心表和防冲突的唯一索引设计数据库设计是这类系统的地基。地基打不好后面写接口的时候就会发现各种别扭。第一步要梳理出实体之间的关系。3.1 实体关系梳理教室预约系统里的实体可以归纳为用户User、教室Classroom、教室类型ClassroomType、时间段TimeSlot、预约记录Reservation、公告Notice。需要注意的是教室和时间段这两个实体从业务上讲都是基础数据通常由管理员在系统初始化时维护好用户不会自己去创建。时间段如果直接做成字符串存到预约记录里查询和冲突判断都会变得非常痛苦所以一定要独立成表。实体间的关系也很清晰一个用户可以有多个预约1:N一个教室可以有多个预约1:N一个类型可以对应多个教室1:N一个时间段可以被多个预约使用1:N3.2 核心表结构设计直接上建表 SQL 太占篇幅了我用表格把关键字段列出来重点说明每个表为什么这么设计。user 表字段类型说明idBIGINT主键自增usernameVARCHAR(50)登录名唯一passwordVARCHAR(100)BCrypt 加密后的密码real_nameVARCHAR(50)真实姓名roleTINYINT0 管理员、1 教师、2 学生departmentVARCHAR(100)所属院系/部门phoneVARCHAR(20)联系电话created_atDATETIME创建时间角色字段一定要用数字枚举而不是字符串这样查权限的时候比较效率高也不会出现教师和老师这种语义不统一的脏数据。classroom 表字段类型说明idBIGINT主键room_noVARCHAR(50)教室编号如 A301唯一campusVARCHAR(50)校区buildingVARCHAR(50)楼栋floorINT楼层capacityINT可容纳人数type_idBIGINT教室类型外键has_projectorTINYINT是否有投影仪has_air_conditionerTINYINT是否有空调statusTINYINT0 启用、1 停用descriptionVARCHAR(255)备注描述教室编号建议格式固定比如校区前缀-楼栋-房间号这样前端展示的时候排序和筛选都会方便很多。classroom_type 表字段类型说明idBIGINT主键nameVARCHAR(50)类型名称普通教室/多媒体教室/机房/实验室descriptionVARCHAR(255)类型说明time_slot 表字段类型说明idBIGINT主键slot_nameVARCHAR(50)节次名称如第1-2节start_timeTIME开始时间如 08:00end_timeTIME结束时间如 09:40periodVARCHAR(20)上午/下午/晚上用于前端分组展示时间段表按学校的作息时间初始化。不同学校节次安排不一样有的上午四节有的上午五节所以这个表必须由管理员维护不能写死在代码里。reservation 表核心表字段类型说明idBIGINT主键user_idBIGINT预约人外键classroom_idBIGINT教室外键dateDATE预约日期time_slot_idBIGINT时间段外键purposeVARCHAR(255)预约用途statusTINYINT状态0 待审核、1 已通过、2 使用中、3 已完成、4 已驳回、5 已取消approve_user_idBIGINT审核人外键approve_remarkVARCHAR(255)审核备注cancel_reasonVARCHAR(255)取消原因created_atDATETIME创建时间updated_atDATETIME更新时间这张表的唯一索引是整个系统防冲突的第一道防线具体做法ALTER TABLE reservation ADD UNIQUE KEY uk_room_date_slot (classroom_id, date, time_slot_id);索引建好之后数据库层面就保证了一个教室在同一个日期、同一个时间段只能有一条预约记录不管状态如何。这比纯靠 Service 层代码判断要安全得多因为并发请求下程序逻辑可能同时通过检查但数据库唯一索引只会让其中一条插入成功。notice 表字段类型说明idBIGINT主键titleVARCHAR(100)标题contentTEXT内容publisher_idBIGINT发布人外键created_atDATETIME发布时间3.3 删除策略外键和逻辑删除预约记录一旦产生就带有可追溯的审计属性所以预约记录原则上不做物理删除。如需要把错误预约去掉走取消流程保留记录但变更状态。教室也是同样的道理。如果一个教室被预约过贸然物理删除会导致历史预约查询时的外键悬空。所以要加一个status字段做逻辑删除0 启用、1 停用停用的教室不再出现在可预约列表但历史记录仍然完整。这是很多新手容易忽略的细节。你以为删掉一条教室数据很干净结果跑统计报表的时候数据对不齐排查小半天才发现是外键问题。4. 后端实现的关键一战预约状态机和冲突检测后端接口的层次划分没什么悬念Controller 接收请求、Service 处理业务、Mapper 做数据交互。这一章重点讲两个最容易出问题、也最能体现项目深度的业务点预约状态机和冲突检测。4.1 预约状态机的完整流转我在前面列了预约记录的状态字段实际开发时最容易犯的错是把状态流转写散落在各个接口里今天改一个、明天改一个最后维护的人根本理不清哪些状态之间可以互相跳转。我的做法是先画一张状态流转表然后干脆把状态校验写进一个枚举类里后续所有接口共用这套校验逻辑当前状态可执行操作目标状态操作角色待审核0用户取消已取消5教师/学生待审核0管理员通过已通过1管理员待审核0管理员驳回已驳回4管理员已通过1用户取消已取消5教师/学生已通过1到开始时间使用中2系统定时任务使用中2到结束时间已完成3系统定时任务已取消5无终态-已驳回4无终态-已完成3无终态-这个状态机有两层妙处。第一业务上审核通过之后允许取消这个设计很关键——教室资源紧张时提前释放被占用的教室能减少资源浪费第二使用中和已完成状态由定时任务自动驱动避免管理员手动维护前提是后端用定时任务定期扫描预约记录把到点未开始的预约置为使用中把结束时间已过的置为已完成。定时任务我用的 Spring 自带的Scheduled每分钟跑一次SQL 大致是UPDATE reservation SET status 2 WHERE status 1 AND date CURDATE() AND start_time NOW() AND end_time NOW(); UPDATE reservation SET status 3 WHERE status 2 AND end_time NOW();这里有个小坑要注意time_slot 表里存的是 start_time 和 end_time但预约记录表没有冗余这两个字段所以做定时任务更新时需要 join 一张 time_slot 表。如果想省事也可以在创建预约时把开始时间和结束时间冗余到 reservation 表查询时少一次 join代价是数据冗余。我的建议是保留 join 的方式毕竟预约系统查询频率不高数据一致性更重要。4.2 预约冲突检测的双保险用户提交预约时后端要做的最核心校验是这个教室在这个时间段的预约是否冲突。我的实现方式是双重保险第一层Service 层业务校验。查询所有状态为待审核或已通过的冲突记录long count reservationMapper.countConflict(classroomId, date, timeSlotId, Arrays.asList(0, 1)); if (count 0) { throw new BizException(该教室在所选时间段已被预约); }第二层依赖数据库唯一索引兜底。即使业务校验因为并发或其他原因通过了数据库的唯一索引uk_room_date_slot也会保证同一教室同一天同一时间段只有一条记录。插入时捕获DuplicateKeyException异常返回一致的错误提示。这里必须说明一下业务校验为什么要查状态为待审核和已通过的记录而不查已取消和已驳回因为取消和驳回意味着该时间段已经释放可以再次被预约。所以冲突判断的逻辑天然绑定了状态机的流转规则。4.3 JWT 鉴权和接口权限控制登录模块我用的是 JWTJSON Web Token方案。流程很简单用户提交用户名密码后端用 BCrypt 校验密码。校验通过后生成 tokentoken 里包含用户 id、用户名、角色设置过期时间一般 24 小时。前端把 token 存到 localStorage每次请求在请求头带上Authorization: Bearer token。后端通过拦截器解析 token把用户信息放到 ThreadLocal 里供业务层获取。权限控制上通过自定义注解RequireRole配合拦截器实现RequireRole(Role.ADMIN) PutMapping(/classrooms/{id}) public Result? updateClassroom(PathVariable Long id, RequestBody ClassroomDTO dto) { // 只有管理员才能调用 }拦截器扫描 Controller 方法上的注解如果用户角色不匹配直接返回 403。这样比在每个方法里手动判断角色干净得多。4.4 MyBatis-Plus 的使用经验项目里我用了 MyBatis-Plus因为它把单表 CRUD 的代码量压缩到很低。教室信息管理、预约记录分页查询、公告列表这类接口基本只需要定义实体类和 Mapper 接口继承BaseMapper就能完成大部分操作。分页查询配合 MyBatis-Plus 的Page对象PageReservationVO page new Page(current, size); LambdaQueryWrapperReservation wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(username), Reservation::getUsername, username) .orderByDesc(Reservation::getCreatedAt);这里要提醒的是MyBatis-Plus 的分页插件需要单独配置PaginationInnerInterceptor不是引入依赖就自动生效的。很多新手在这个地方折腾半天最后发现 SQL 里没有 LIMIT 语句。5. 前端怎么搭从页面清单到组件化实现前端部分我按页面规划 → 工程搭建 → 核心页面 → 关键封装这条线展开。这个项目的前端不算复杂但页面数量和交互逻辑要有清晰的规划否则写着写着就乱了。5.1 页面清单和路由规划前端一共需要这些页面路由路径页面功能说明/login登录页用户名密码登录/dashboard数据看板教室总数、今日预约数、使用率统计/classrooms教室列表教室卡片展示、筛选、详情弹窗/classrooms/:id教室详情教室设备信息、可选时间格子/reservation/create发起预约选择日期、时间段、填写用途/my/reservations我的预约我的预约列表、取消操作/admin/reservations预约审核管理员审核列表、通过/驳回操作/admin/classrooms教室管理教室的增删改查、启停用/admin/users用户管理用户列表、重置密码/admin/notices公告管理公告的发布和编辑路由规划遵循了用户端和管理端分离的思路。用 Vue Router 的嵌套路由和路由守卫根据用户角色控制进入权限router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); } else if (token to.meta.requiresAdmin userRole ! 0) { next(/dashboard); } else { next(); } });5.2 Vue 2 还是 Vue 3Element UI 还是 Element Plus前面提到过我当时用的是 Vue 2.6 Element UI。现在如果你是从零开始我更建议直接用 Vue 3 Vite Element Plus原因有几个Vite 启动速度比 webpack 快一个数量级尤其在开发大一点的项目时热更新体验差距明显。Element Plus 对 TypeScript 的支持更好组件维护也更积极。script setup语法减少了很多样板代码写起来更舒服。但如果你是为了参考大量现成的 Vue 2 教程和源码那继续用 Vue 2 也没有问题。选型的标准只有一个你能否用最短的时间把系统做出来、把原理讲清楚。技术没有绝对的好坏只有合不合适。5.3 核心页面实现细节教室列表页是最能体现组件化思想的页面。我把它拆成了 5 个组件RoomCard.vue单个教室卡片展示教室编号、容量、设备图标。RoomFilterBar.vue筛选栏按校区、楼栋、容量范围、设备条件筛选。PaginationBar.vue分页组件。RoomDetailDialog.vue教室详情弹窗展示全部信息。ReserveForm.vue预约表单内嵌在详情弹窗里。发起预约页面的交互是整个前端最复杂的一块。用户需要先选日期然后根据日期查看该教室当天的时间段占用情况。我的实现方式是用户选择日期时发起一个请求查询该教室在所选日期的预约状态前端把可预约和已占用的时间段用不同颜色渲染。已占用的格子禁用点击可预约的格子选中后下方显示预约用途输入框填写完提交。这里有一个体验上的细节不要把所有时间段一次性渲染出来按上下午晚上分组渲染。一天 10 个节次全部平铺在一个列表里用户很难快速定位。分组后再加一个只显示可预约的开关体验会好很多。个人预约列表页用el-table展示状态列用el-tag渲染不同颜色。取消操作一定要加二次确认弹窗防止误点。5.4 Axios 封装和请求拦截前端所有请求都走 axios我会统一封装在src/utils/request.js里import axios from axios; import { ElMessage } 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 Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } ElMessage.error(error.message || 请求失败); return Promise.reject(error); } );统一的请求封装带来的好处是每个页面不需要重复写 token 处理和错误提示逻辑代码量大幅减少也不容易出现有时带 token 有时不带的问题。5.5 响应式与移动端适配这套系统主要是给 PC 端用的但偶尔会有管理员用手机审核预约的需求。所以我在做布局时用了 Element UI 的el-row/el-col栅格系统让大屏显示多列小屏自动堆叠成一列。教室列表卡片的最大宽度做了限制避免在大屏上被拉得特别宽。移动端适配没有做很深的优化能用是底线。真要做到 iPhone 和安卓都能舒服操作建议给预约表单和审核列表单独做响应式布局这个可以作为后续迭代方向。6. 联调与部署跨域、时区、打包以及我踩过的坑最后这章是实操经验集中区。系统开发和部署这个环节技术文档里往往写得很简单真正跑起来才会遇到各种怪问题。我把踩过的坑按出现频率排个序每一个都用一句话说明现象、一句说明原因、再给解决方案。6.1 开发环境的跨域问题前后端分离开发时前端跑在localhost:8080Vite 默认端口后端跑在localhost:8081直接发请求会报 CORS 错误。我当时用的是 Vue CLI 的 devServer proxy不用在后端做任何 CORS 配置。在vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };这样前端请求/api/user/login会被转发到http://localhost:8081/user/login浏览器视角是同源请求不会触发跨域。生产环境则完全交给 Nginx 处理后面 6.4 节会讲。6.2 数据库时区导致的时间偏差有一个问题非常隐蔽MySQL 连接串里没有设置 serverTimezone 时Spring Boot 2.x 默认会按服务器的时区解析时间。如果你用的云数据库默认是 UTC而本地是东八区就会出现新增预约记录时created_at比实际时间少了 8 小时。解决的方案是在 JDBC 连接串里显式指定时区spring.datasource.urljdbc:mysql://localhost:3306/classroom?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai配置好之后日期查询比如查询今天所有的预约就正常了。这个问题如果不说新手基本都要排查一晚上。6.3 事务失效的坑预约接口是个典型的写操作先校验冲突再插入预约记录。为了保证校验和插入的原子性我在方法上加上了Transactional。但有个限制大多数人不知道——同一个类中方法直接调用事务注解不生效。因为 Spring 的事务是基于 AOP 代理实现的内部直接调用this.method()不会经过代理类所以注解不会生效。我当时写了一个ReservationService里面有一个createReservation()方法调用了本类的checkAndInsert()方法check 方法上虽然有Transactional但实际没生效。排查后发现要么把两个方法放到不同的 Service 类里要么在调用时用编程式事务。正确的做法是把冲突校验和插入逻辑放到同一个事务方法里而且这个方法是 Service 类的公开方法不要在内部用this调用自己Transactional(rollbackFor Exception.class) public Long createReservation(ReservationCreateDTO dto) { validateConflict(dto); Reservation reservation new Reservation(); // 设置字段... reservationMapper.insert(reservation); return reservation.getId(); }6.4 前端打包后的 Nginx 配置前端打包完成后部署到生产环境的惯例是用 Nginx 做静态文件服务。这里有一个必须处理的坑Vue Router 使用 history 模式时直接访问某个子路由路径比如/admin/reservations会报 404因为 Nginx 默认只找物理文件找不到对应路径的文件就返回 404。解决办法是配置 try_files让 Nginx 把所有路径都重写到index.html由前端路由接管server { listen 80; server_name classroom.example.com; location / { root /opt/classroom-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; 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 /api/的 proxy_pass 会把请求反向代理到后端服务注意proxy_pass http://127.0.0.1:8081/;结尾的斜杠它会把/api前缀去掉和后端接口路径保持一致。面试时这个点经常被问到可以重点准备一下。6.5 数据初始化的提醒系统部署后第一件事是插入管理员账号。我的做法是在后端启动时写一个CommandLineRunner检查 user 表是否有管理员记录如果没有就初始化一个默认账号但在正式环境一定要修改默认密码。教室、时间段等基础数据建议通过管理后台手动录入不要写死在初始化代码里。因为每个学校的教学楼结构、作息时间差异很大代码写死只会给自己找麻烦。6.6 其他高频踩坑点再补充几个使用频率不高但遇到了会很头疼的问题MyBatis-Plus 分页插件不生效需要创建MybatisPlusInterceptor的 Bean并注册PaginationInnerInterceptor不是引入依赖就默认开启的。删除教室关联数据报错如果存在预约数据关联会因外键约束失败。处理方案是逻辑删除而不是物理删除前面第三章已经讲过。Spring Boot 无法通过 ajax 拿到参数大概率是后端接口用HttpServletRequest.getParameter()拿 JSON 请求体。JSON 格式要用RequestBody接收表单格式才用RequestParam。前端页面刷新后 404就是 6.4 节说的 Nginx try_files 配置问题。上传文件大小限制如果做教室图片上传默认最大只有 1MB要在配置文件里调大spring.servlet.multipart.max-file-size。这些坑不是一个项目里全都能遇到但记录的意义在于某天你排查了三个小时的问题可能就是别人早就踩过的同一个坑。写在最后如果再让我做一次我会调整哪些地方项目整体跑通后回头复盘有几点是如果再让我做一次一定会调整的。第一后端接口返回值会从一开始就设计成统一结构。我在项目前期接口返回值写得不统一有的返回Result对象有的直接返回实体类前端封装 axios 拦截器时很被动。现在会从一开始就确定ResultTcode、message、data 三字段所有接口一律返回这个结构前后端联调会顺畅很多。第二预约时间段的粒度会做得更细。当前版本按学校的节次划分时间段比如第1-2节但实际使用中常有只借半小时的需求。后来想想可以把时间段细化到小时级或者支持自定义开始时间和结束时间同时时间片校验逻辑也顺手改成区间重叠判断。这个改动不影响整体架构但用户体验会好一大截。第三统计报表会做得更丰富。目前只做了最简单的使用率统计如果要真正支撑学校管理决策应该加上按院系预约排行、按教室利用率排行、按时间段热度分析等报表。这类功能后端就是多几张聚合查询 SQL前端加几个图表组件收益却很直观。最后说点实在的。虽然是一款以 Spring Boot 和 Vue 为主体的经典全栈项目但这个系统从需求梳理、数据建模到冲突检测、联调部署每一个环节都有独立的思考点。你照着做一遍等于把前后端开发、数据库设计、Linux 部署这些核心能力完整串了一遍。做这类项目的正确姿势不是敲完代码就跑而是每写一个模块都问问自己这个表为什么这样建这个接口为什么这样设计这个状态为什么要这样流转把这些想明白了无论项目本身还是你个人的能力都会有实打实的沉淀。本文还有配套的精品资源点击获取