
开学那阵子图书馆和自习室一座难求的场面相信大家都不陌生。我做过一次小调研发现很多高校和共享自习室的管理还停留在人工点名纸质登记的阶段管理员要对着Excel核对座位学生得提前一小时去排队占座。这套 SpringBoot Vue3 MyBatis 的 MVC 自习室管理和预约系统就是奔着这个问题去的——用一套前后端分离的完整源码把预约-签到-管理-统计这条链路彻底线上化。本文不聊浮夸的架构直接拆解这套系统怎么设计、表怎么建、并发预约怎么防超卖、前端权限怎么控以及部署上线时最容易踩的坑适合正在做毕设、想上手全栈项目或者自习室运营方想找一套现成方案的读者参考。1. 自习室预约系统的两类核心用户与需求拆解在动笔写代码之前我习惯先列清楚谁在用这个系统他们分别要解决什么痛苦。这套系统表面看是自习室预约两个词的组合实际上需求方分成两个完全不同的角色功能设计必须分开对待。1.1 学生端我要的不是花哨是能约上学生或者说普通用户的核心诉求非常朴素打开页面能看到哪些自习室有空位选一个时间段提交预约到点了扫码或点击签到离开时释放座位。但能约上三个字背后有几个容易被忽略的细节自习室和座位是两个概念。一间自习室里有几十个座位用户预约的粒度必须是座位级不然会出现预约了自习室但进去没位置的尴尬。时间段不能拍脑袋定。现实中自习室会划分时段比如上午 08:00-12:00下午 14:00-18:00系统要能配置时段预约时判断该座位的目标时段是否已被占用。签到要有容错。路上堵了、临时有事晚来十分钟不能直接算违约通常给一个签到宽限期比如预约开始后 30 分钟内可签到超时才释放座位并记录违约。个人中心必须能看到我的预约记录和违约记录否则用户不知道自己的信用状态后续限制预约也没法解释。1.2 管理员端我要的不是功能多是管得住管理员的痛点比学生更具体。首先管理员需要维护基础数据——自习室信息名称、位置、座位数、开放时段、座位信息编号、是否启用、时段规则。其次管理员要能查看所有预约记录手动处理异常情况比如用户爽约后的座位释放、纠纷申诉。最后统计报表是硬需求——哪个自习室使用率最高、哪个时段最拥挤、违约率有多高这些数据直接关系到自习室的运营策略。我做的版本里在管理员端额外加了一个座位状态总览页面用表格展示所有座位的当前状态空闲/已预约/使用中/已禁用。这个页面看起来不起眼但实际运营时非常好用管理员扫一眼就知道全场情况不用点进每个自习室去查。1.3 两类用户需求如何映射到功能模块需求梳理完功能模块基本就定了用户认证模块登录注册 JWT、自习室管理模块、座位管理模块、预约模块核心、签到/释放模块、违约记录模块、统计报表模块。注意预约模块一定要独立出来设计因为它既要读写座位状态又要生成预约记录还要和时间段校验逻辑打交道属于整个系统里最容易出并发问题的部分。到这里需求清单已经清楚接下来就是技术选型的问题——为什么这套源码用的是 SpringBoot Vue3 MyBatis MySQL而不是其他组合。2. 技术栈选型逻辑为什么这套组合适合此类项目很多初学者选技术栈是哪个火选哪个但做过几个完整项目后会明白选型看的不是流行度而是团队熟悉度 生态成熟度 部署成本三者的平衡。这套系统的组合恰好在这三点上都站得住。2.1 SpringBoot 3.x快速起服务生态不用愁后端选了 SpringBoot核心原因是它把能跑起来的 Web 服务这件事的成本降到了最低。内嵌 Tomcat、自动配置、起步依赖意味着我从零搭一个 REST API 服务只需要一个SpringBootApplication注解和几行配置。对于自习室这种业务逻辑不算极复杂的系统SpringBoot 的 MVC 分层Controller - Service - Mapper正好把代码组织得明明白白后期维护不用在一堆 Servlet 和配置文件里翻找。这里特别说下版本选择。SpringBoot 3.x 要求 JDK 17如果你的服务器或者本机环境还是 JDK 8建议老老实实选 SpringBoot 2.7.x 分支否则编译都过不了。我在这套源码里用的是 SpringBoot 3.x JDK 17 的组合后续部署章节会专门讲这个坑。2.2 Vue3 Vite前端工程化体验更好前端用 Vue3 是顺理成章的选择。相比 Vue2Vue3 的组合式 API 让逻辑复用变得简单比如预约表单的校验逻辑、token 的存取逻辑都可以写成独立的组合函数到处复用。搭配 Vite 做构建工具开发环境的冷启动速度快得感人HMR热更新几乎是秒级响应调试体验比 Webpack 时代舒服太多。后台管理界面我用了 Element Plus 组件库表格、表单、日期选择器、弹窗确认这些后台系统的高频组件开箱即用不需要自己手写样式。如果你只需要极度轻量的前台展示页也可以只用原生 Vue3 少量 CSS但涉及管理后台这种大量表格交互的场景组件库能省下至少一周的开发量。2.3 MyBatis 与 MySQL轻量且可控的数据访问层选 MyBatis 而不是 JPA/Hibernate是我一贯的偏好——SQL 需要手动控制。自习室预约系统里最关键的查座位在某时段是否被占这个操作SQL 的写法直接关系到性能用 MyBatis 的注解或 XML 可以清晰地看到每一条 SQL出了问题也好排查。JPA 虽然开发快但遇到复杂查询要么套方法名要么写 JPQL反而绕圈子。MySQL 则是这类业务系统的标准答案关系型数据模型天然匹配用户-座位-预约这种实体关系事务支持成熟InnoDB而且部署维护的资料一搜一大把遇到问题不容易卡住。整个技术栈组合下来这套系统的运行时链路是Vue3 前端通过 Axios 调用 SpringBoot 的 REST 接口SpringBoot 通过 MyBatis 访问 MySQL鉴权走 JWT 拦截器。链路简单清晰出现问题能快速定位是前端、后端还是数据库的问题。3. 数据库与表结构设计预约核心是座位时段的唯一性数据库设计是我做这类系统耗时最多的部分。表建得不好后面写代码会处处别扭。我强烈建议任何系统先花一天时间把 ER 图和表结构定下来再开始写代码。自习室系统的核心表大致是下面这 6 张。3.1 核心表清单与字段说明用户表sys_userid、username、passwordBCrypt 加密存储、real_name、phone、role0 学生/1 管理员、status、create_time。注意密码千万不要明文存这个没得商量。自习室表study_roomid、room_name、location、total_seats、open_time、close_time、status、description。open_time 和 close_time 用 time 类型或字符串08:00:00都行记住在代码里统一格式。座位表seatid、room_id、seat_number、status0 可用/1 禁用。seat_number 在同一个自习室内唯一用复合唯一索引(room_id, seat_number)约束。预约记录表reservationid、user_id、seat_id、room_id、reserve_date、time_slot_id、status0 已预约/1 已签到/2 已释放/3 已爽约/4 已取消、create_time、signin_time。这张表是系统的核心字段看起来简单但查询频率最高。时段表time_slotid、slot_name、start_time、end_time。比如上午 08:00-12:00晚间 18:00-22:00。违约记录表violationid、user_id、reservation_id、reason、create_time。用户爽约或者超时未签到这里就多一条记录。3.2 预约唯一性如何保障表设计的关键所在同一个座位在同一个日期 同一个时段只能被预约一次这个约束是整个系统的命根子。它不能只靠代码判断必须在数据库层面做到在预约记录表建立唯一索引UNIQUE KEY uk_seat_slot (seat_id, reserve_date, time_slot_id)。插入预约记录前先执行查询确认该组合不存在已预约/已签到状态的记录。唯一索引 代码双重校验。数据库的唯一索引是最后一道防线即使代码逻辑出现并发漏洞数据库也会拒绝重复插入避免数据脏掉。3.3 为什么需要单独设计时段表有人问时段不就上午下午晚上三个吗写死在代码里不就行了这属于典型的开发一时爽运营火葬场。自习室的开放时段会随着季节、节假日调整如果写死在代码里改一次时段就要重新发一次版。单独建一张时段表管理员在后台界面上就能增删改时段预约表单的时段选项动态从接口读取这才是可维护的设计。3.4 关于软删除与状态字段我在所有业务表里都保留了 status 字段做逻辑删除/禁用而不是直接物理 DELETE。比如座位被禁用不是删除记录而是把 status 置为 1。这样历史预约记录里的座位信息仍然是完整的统计报表也不会因为物理删除而数据断层。这个习惯在真实项目里非常重要——凡是涉及历史业务数据的表尽量别物理删。4. 后端核心实现预约防并发与状态流转是硬骨头后端你看代码的话会发现 Controller 层都很薄真正的工作量都在 Service 层尤其是预约相关的那几个方法。这里挑三个最容易出问题的点展开讲都是实战中踩过的坑。4.1 JWT 鉴权与拦截器前后端分离的登录态方案前后端分离的项目Session 方案天然不合适跨域、集群共享都是问题JWT 是事实标准。我在项目里的做法是用户登录成功后后端生成 JWT 字符串返回给前端expiration设置为 2 小时。前端把 JWT 存到 localStorage每次 Axios 请求在请求拦截器里带上Authorization: Bearer token。后端写一个JwtInterceptor实现HandlerInterceptor接口在preHandle里校验 token 是否有效、是否过期。校验通过就把用户 id 和角色塞进ThreadLocal或 request attribute后续 Controller 方法直接取。这里有个容易踩的坑拦截器拦截了/api/**的所有请求但登录接口/api/auth/login和注册接口/api/auth/register必须放行否则用户根本登不进去。我在代码里用excludePathPatterns配置了一个白名单数组凡是新增的公开接口比如获取自习室列表的预览接口都要记得加进去。4.2 预约座位与防超卖事务 锁怎么配合预约的核心流程是校验用户未违约超限 - 校验座位存在且启用 - 校验时段未占用 - 创建预约记录 - 更新座位状态。这个流程存在并发风险——两个用户同时预约同一个座位如果都通过了第 3 步校验就会产生两条预约记录。数据库唯一索引兜底但用户体验上会出现请求成功但实际插入失败的问题。所以我在 Service 层做了两层防护第一层悲观锁。查询座位是否被占用时使用SELECT ... FOR UPDATE锁住座位行记录事务提交后锁释放。这个方案实现简单正确性有保障缺点是并发量高时锁等待会影响吞吐。对于自习室这种 TPS 几十的规模悲观锁完全够用。第二层事务控制。整个校验 插入预约 更新座位放在一个Transactional方法里任何一个环节异常全部回滚避免出现预约记录建立但座位状态没更新的中间态。我实际验证过即使用 JMeter 模拟 50 个并发同时抢同一个座位最终能成功入库的也只有一条预约记录其他请求要么被唯一索引拒绝要么被行锁挡住。这就是事务和锁配合的效果。4.3 预约状态机每个状态怎么流转预约记录的状态字段有 0~4很多人写代码时直接在业务逻辑里if status 1 then status 2这样写一时爽后期加需求时状态判断越叠越乱。我用一个简单的状态机来规范流转预约0签到1用户到馆点击签到或扫描二维码。前置状态必须是 0且当前时间在预约时段内或宽限期内。释放2用户主动离开或已签到用户的时段结束。前置状态是 1。爽约3预约时段开始后 30 分钟未签到由定时任务自动置为 3并生成违约记录。取消4用户在预约时段开始前主动取消。前置状态是 0超过段起时间不允许取消。我把状态流转校验独立封装成一个ReservationStateMachine工具类每个状态转移方法里先校验前置状态再返回新的状态。这样做的好处是任何入口要改变预约状态都得走状态机不会出现某个野路子逻辑直接把状态改飞。4.4 定时任务处理爽约爽约的自动标记需要定时任务。我在项目里用的 Spring 自带Scheduled注解启动类加EnableScheduling然后写一个任务方法每 5 分钟扫描一次预约日期等于今天、时段已开始超过 30 分钟、状态仍为 0的记录批量更新为 3 并插入违约记录。这里有个性能细节扫描条件和更新条件要用索引字段reserve_date、time_slot_id否则表数据量大了以后定时任务会拖慢数据库。我实际观察过几千条预约记录的体量下这个任务每次执行都在 10 毫秒以内完全不是瓶颈。5. 前端 Vue3 落地要点路由守卫、状态管理与接口封装前端代码的结构我按 Vite Vue3 Pinia Element Plus 组织。表面上这就是标准后台前端模板但有几个细节值得单独说因为这些细节直接决定了开发效率和后期维护的舒适度。5.1 前端路由权限控制动态路由还是简单守卫自习室系统的角色只有两种学生/管理员没必要用动态路由那种重方案。我采用的是路由守卫 菜单控制的轻量做法路由表里给管理端页面添加meta: { roles: [admin] }标记。router.beforeEach守卫里先判 vue-router 的全局守卫中用户是否已登录localStorage 有无 token没有就去登录页有 token 再判断目标路由的 roles 是否包含当前用户的角色不匹配就重定向到 403 页面。这么做的原因是对于角色固定、权限粒度粗的系统动态路由后端根据角色返回路由表前端动态 addRoute带来的复杂度大于收益。写起来还容易出刷新页面后路由丢失的问题排查成本很高。5.2 Pinia 管理用户信息与座位状态Vue3 官方推荐 Pinia 做状态管理这次我确实没再用 Vuex。用 Pinia 存两个关键东西userStore存用户 id、昵称、角色、token。登录成功后调用userStore.setUserInfo()前端所有组件共享不用每个页面都从 localStorage 读。roomStore存自习室列表和选了自习室之后的座位列表避免在多个页面间反复请求相同数据。有一个经验值得分享Pinia 的 store 里只存需要跨组件共享的数据不要什么都往里面放。比如某个页面独有的表单草稿放在组件自己的ref里就行放进 store 反而会产生这个数据为什么被改了的调试难题。5.3 Axios 拦截器统一错误处理与 token 刷新问题我在src/utils/request.js里封装了 Axios 实例三个拦截器逻辑请求拦截器自动加 token。响应拦截器统一解包res.data如果 code 为 401说明 token 失效清空本地用户信息并跳转登录页如果 code 为其他业务错误码统一通过 Element Plus 的ElMessage弹出错误提示。错误处理网络错误、超时等统一提示。这里对401 后跳登录页有个坑——如果多个接口同时失败会触发多次跳转。我习惯加一个isRedirecting标志位跳转前判断是否已经在跳转过程中避免重复跳转导致路由栈混乱。5.4 预约流程的前端交互设计预约操作不是简单的点击提交按钮。为了减少无效提交前端交互上我做了三个细节预约前先选日期再选时段然后加载座位列表。座位的颜色标记状态——绿色空闲、灰色已占用、红色已禁用。选中灰色座位时直接禁用提交按钮同时给出提示。这一步能挡掉大量无意义的接口请求。提交预约前弹确认框显示座位号 日期 时段的摘要信息让用户二次确认。这个设计对减少错误预约有奇效我实测下来误触率降了一大截。预约成功后前端不急着跳转而是展示预约详情和签到提醒比如提示可以提前 30 分钟到馆签到。把后续动作直接摆到用户面前能显著降低爽约率。6. 联调与部署从本地跑通到上线我踩过的实物坑这部分是眼睛看到和血泪教训的分水岭。代码在本地跑通只算完成了 60%联调和部署阶段才是真正磨人的地方。挑四个最常见的坑给正准备把这套系统部署上线的朋友提个醒。6.1 跨域配置前端端口 5173 访问后端 8080 怎么放行Vite 默认端口 5173后端 8080前端直接fetch必然跨域。我提供两套方案二选一即可方案 A开发环境Vite 配置server.proxy把/api前缀的请求代理到http://localhost:8080前端代码里统一走相对路径/api/xxx。这个方案的好处是浏览器请求的是同源地址不存在跨域问题也不需要后端开 CORS。方案 B生产环境Nginx 统一入口location /api代理到后端服务。在我这套源码里采用的是方案 A 的配置Vite proxy后端同时加了WebMvcConfigurer的跨域配置方便直接部署到测试环境的人。注意跨域配置要写在 Spring 的addCorsMappings里允许来源*允许方法GET, POST, PUT, DELETE, OPTIONS。6.2 JWT 密钥与时长配置JWT 密钥是写在application.yml里的比如studyroom.jwt.secret。这个值在生产环境一定要改成复杂的随机字符串且通过环境变量注入不能提交到代码仓库。JWT 过期时间我默认 7200 秒2 小时但真实场景下用户可能坐在自习室里一待就是一下午2 小时过期后请求全部 401体验很糟糕。建议改成 7200 秒的 2 倍4 小时或者实现刷新 token的机制。前端收到 401 后直接踢出登录页这个逻辑对长时间停留的用户不太友好。我后来加了一个简单的无感刷新响应拦截器发现 401 时先尝试调用/api/auth/refresh接口刷新 token成功了就自动重发原请求。不过这个功能依赖后端提供一个 refresh 接口属于增强项基础版本里没包含。6.3 SpringBoot 3.x JDK 17 的部署注意事项很多人的生产服务器还是 CentOS 7 JDK 8直接把 SpringBoot 3.x 的 jar 丢上去会直接报UnsupportedClassVersionError。这一点在选型时必须提前确认。我在源码 README 里明确写了环境要求JDK 17、MySQL 5.7或 8.0、Node.js 16构建前端用。部署时如果服务器上没有 JDK 17两个思路装一个 Oracle JDK 17或者用 docker 镜像打包。我个人的建议是如果条件允许直接用 Docker 编排后端服务 MySQL 是最省心的方式避免环境差异带来的各种诡异问题。6.4 Nginx 部署前端并解决 Vue Router 刷新 404前端npm run build之后生成 dist 目录放到 Nginx 的html目录访问首页没有问理。但如果有人直接访问/seats这种深层路由刷新页面就会出现 404因为 Nginx 找不到对应的静态文件。标准解法是在 Nginx 配置里加一个try_fileslocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files的作用是如果请求的不是真实文件就统一返回index.html由 Vue 的路由接管页面渲染。这个配置不加上部署完测试时一定会遇到——所以我先写在这里免得你查到半夜。6.5 MySQL 时区导致的时间错乱最后一个高频坑MySQL 的时区设置和 Java 的时区不一致导致存入数据库的时间比实际时间差了 8 小时或 13 小时。我在连接串里显式指定了serverTimezoneAsia/Shanghai在 Spring 配置里设置了spring.jackson.time-zoneGMT8数据库连接 URL 保持统一。如果你发现预约记录的 create_time 和实际时间对不上优先检查这三处是否完全一致。这是最容易忽略、又最影响体验的问题。7. 系统管理与统计报表看似简单实则提升运营效率的功能预约把用户端搞定后管理者看着后台一堆数据最想的其实是到底哪个自习室热门、哪个时段闲置。所以统计报表不是可有可无是这套系统价值感的直接体现。7.1 使用率统计的 SQL 思路我实现了一个报表接口返回每个自习室在某个日期范围内的预约使用率。核心 SQL 思路是先查出该自习室的座位总数再统计某天有预约记录状态为已签到/已预约的座位时段数两者相除得到使用率。这类统计天然适合用 MyBatis 写原生 SQL用 JPA 反而绕。在性能上统计接口可能会被频繁刷新我做了一层简单的缓存——Redis 或者 Caffeine 都行。我基础版本里用的 Caffeine5 分钟过期报表数据本身就不要求绝对实时缓存 5 分钟完全能接受。7.2 违约机制如何影响预约权限学生的违约次数累计到一定值比如 3 次系统应该限制其下月不能再预约。这个逻辑放在用户登录和预约两个入口同时校验登录时查出违约次数超过阈值就在前端顶部展示警告预约时后端直接拒绝返回违约次数超限请联系管理员。这套机制能让使用者对规则产生敬畏感。如果没有这个约束座位预约服务的公信力很快就会崩掉——毕竟谁也不想约了位置被一堆人放鸽子。8. 扩展思路从自习室到通用资源预约最后说一下这个项目后续还能怎么演。这套系统的表结构和预约流程本质上是资源 时段 用户 状态机的通用模型。把自习室表换成会议室、把座位表换成设备、把时段表换成可配置规则就是一套会议室预约系统加上付款字段就能扩展成付费的共享工位租赁系统。我在源码里刻意没有把业务写死比如座位表和自习室表中间留了足够的扩展字段description、remark预约记录表也带了 room_id方便后期做跨房间查询。如果你打算在这个项目基础上做二次开发我的建议是不要轻易改动预约状态机那部分代码那是整个系统的核心骨架。数据库结构如果没想清楚宁可不加字段也不要乱加无索引的字段。前端组件如果要做功能扩充优先复用 Element Plus 提供的现成组件自己封装高级组件要慎重。自习室预约这个选题技术难度不高但麻雀虽小五脏俱全——认证、权限、事务、并发、定时任务、统计报表全栈开发的核心知识点都覆盖到了。我把这套系统源码的架构和实现思路拆到这里希望能给正在做类似项目的人一些参考。如果你动手实现过程中遇到并发预约或者 Nginx 部署上的问题按上面说的思路排查基本都能落地解决。