做自习室预约系统这件事,我前后折腾了大半个月,踩了不少坑也积累了不少心得。这个基于SpringBoot + Vue的MVC自习室管理和预约系统,大概是很多计算机专业同学毕业设计或课程项目的热门选题,也是很多小型创业团队做共享空间管理时愿意参考的一套基础架子。今天我把整套系统的设计思路、核心代码、数据库方案和部署细节完整梳理一遍,特别是那些网上教程一般不会主动告诉你的坑,都一并写出来。
这套系统能做什么?简单说就是三件事:管理自习室和座位信息、让用户在线预约座位、提供后台的数据统计和基础管理能力。技术栈非常经典:后端SpringBoot + MyBatis + MySQL,前端Vue + Vue Router + Axios,整体遵循MVC分层思想。适合正在做课设、毕设的学生,也适合想快速搭一套轻量级预约系统的开发者。
1. 为什么做这个系统:需求拆解与技术选型
1.1 自习室预约到底解决了什么问题
去图书馆或者付费自习室抢座位的经历相信不少人都有过。传统方式要么是线下排队登记,要么是微信群接龙,再原始一点就是放一张纸自己签名。这些问题很典型:座位状态不透明,去了才发现满座;管理员无法实时掌握使用率,座位长期被占但没人来;用户和管理员之间信息断层,投诉和纠纷不少。
所以我做的这套系统核心就是解决三个问题:座位资源可视化、预约流程线上化、管理数据可统计。用户端看到的是一个座位图,哪些座位空闲、哪些被占一目了然;预约操作点几下就能完成,取消、续约也能自助处理。管理员端则能看每日预约量、座位利用率、用户活跃度这些关键指标,不用再靠Excel手工统计。
系统角色划分也很清晰:普通用户负责预约、取消、查看个人记录;管理员负责自习室和座位维护、预约审核或关闭、查看统计报表。这套RBAC(基于角色的访问控制)模型不复杂,但足够支撑起一个完整的业务闭环。
1.2 SpringBoot + Vue + MyBatis + MySQL这套组合的取舍
我在一开始也纠结过技术选型。Node.js + Express做后端?Python + Django?最终还是选了SpringBoot这套组合,理由非常实际。
SpringBoot是目前国内企业级开发占有率最高的框架之一。它最核心的价值就是简化配置,内嵌Tomcat,一个jar包就能跑起来,不需要额外部署WAR包。配合Maven或Gradle管理依赖,几行配置就能集成MyBatis、MySQL、Redis这些中间件。对于校内项目或者中小团队来说,SpringBoot的学习资料最丰富,遇到问题的解决方案也最多,这个优势在开发中非常实际。
Vue选择的是Vue 2还是Vue 3?我建议新项目直接上Vue 3 + Composition API。2025年这个时间点,Vue 2已经停止维护了,生态里新的UI库也基本都适配Vue 3。Vue全家桶里真正开发必须掌握的就三样:Vue Router负责页面路由,Pinia或Vuex负责状态管理,Axios负责和后端接口通信。
MyBatis和MySQL的组合更是经典中的经典。MySQL是开源关系型数据库里生态最成熟的,安装部署简单,性能足够中小规模应用使用,而且和SpringBoot整合非常顺滑。MyBatis则让你保持对SQL的完全控制权,复杂查询不会被ORM框架的限制卡住。有些人会问为什么不用MyBatis-Plus?我这里用原生的MyBatis是为了把XML映射和动态SQL这些底层机制讲清楚,理解了这一层,再上手MyBatis-Plus就是一天的事。
2. 数据库设计与核心表结构
2.1 核心数据表:用户、自习室、座位、预约单
数据库设计是这套系统的地基,表结构设计得好不好直接影响后续业务逻辑的复杂度。我设计的核心表一共有五张:用户表、自习室表、座位表、预约单表,还有一张管理员表。下面把每张表的重点字段讲清楚。
用户表比较常规,重点字段有id、用户名、密码(BCrypt加密存储)、手机号、角色标识(user/admin)、状态字段和创建时间。密码一定不能明文存储,这个是最基本的安全底线。状态字段用来表示账号是否被冻结,方便管理员做限制。
自习室表相对简单,一个自习室包含名称、位置、开放时间、座位总数、描述信息。这里有个小设计点:座位总数不要冗余存储,而是通过统计座位表中某个自习室id的数量实时计算。如果你想优化查询性能,可以在自习室表里加一个total_seats字段做冗余,但要注意维护数据一致性。
座位表是整套系统的核心,字段包括id、自习室id、座位编号(自习室内唯一)、座位类型,比如单人桌、双人桌、靠窗座位、安静区座位等,还有座位状态和座位坐标等。为什么加坐标字段?因为前端要渲染座位图,没有坐标你就无法准确把座位画在对应位置上。
预约单表字段最多,也是最容易设计出错的一张表。核心字段有:预约单号(业务编号,方便人工核验)、用户id、自习室id、座位id、预约日期、开始时间、结束时间、状态字段(待使用/已使用/已取消/已过期等)、创建时间。这里要特别强调一个设计经验:业务上需要向用户展示的编号和数据库主键id一定要分开。数据库主键autoincrement的id不要直接暴露给用户,因为会泄露系统数据量,也容易被人遍历接口。
2.2 座位状态与预约状态的流转设计
状态设计是最容易做乱的环节。我前后改了三版才理清。核心思路是把座位状态和预约状态拆开,不能混在一起。
座位只有三种状态:空闲、占用、禁用。占用表示这个座位在当前时间段被预约了;禁用是管理员手动关闭某些座位,比如空调坏了、灯管不亮临时维修。这里有一个非常容易掉坑的地方:座位本身没有“时间”的概念。同一个座位早上可能是空闲的,下午可能是占用的。所以我用的是预约记录来反推座位状态,而不是在座位表上存一个update_status_time字段。座位表上的status字段更准确说是“运营状态”:可用或禁用。真正的实时占用状态由一个视图或查询接口动态计算:当前时间范围内是否存在未取消的预约记录。
预约状态我设计了四个:待使用、已完成、已取消、爽约。待使用是预约成功但还没到时间;已完成是使用时间结束后,系统自动更新或管理员手动确认;已取消是用户主动取消;爽约则定义了这样一个规则:预约时间开始后30分钟内未签到,自动标记为爽约。这套状态机看似简单,但最终落库的时候要注意:状态字段不要用数字,尽量用字符串varchar,存英文枚举值,比如BOOKED、FINISHED、CANCELLED、NO_SHOW。项目组里新人接手时看到字符串状态一看就懂,看到0和1还得猜含义。
2.3 并发防超卖:唯一约束与乐观锁
并发问题是在做预约系统时最需要提前思考的地方。多个用户同时点击同一个座位的预约按钮,如果处理不当就可能出现“超卖”——一个座位同一时间被预约给多个人。
我用了三层机制解决这个问题。
第一层是数据库唯一约束。在预约单表上建一个联合唯一索引,字段组合是seat_id +预约日期+开始时间。这样数据库层面就保证了同一座位在同一时段只能有一条有效预约记录。只要用户取消预约,原记录变成取消状态,新预约才能再次插入。要注意唯一索引必须带上状态条件吗?MySQL不支持函数索引的写法比较复杂,所以我的做法是:取消的记录不物理删除,但预约唯一索引只约束status为生效状态的记录。怎么实现?这就要用到MyBatis动态SQL:插入前先执行一个“活性检查”查询,确认该座位该时间段没有状态为待使用/已使用的记录,再执行插入。数据库唯一索引用来做最后一道兜底,它能拦截掉极短时间内出现的并发冲突。
第二层是业务层面的互斥锁。我选择在Service层中使用synchronized关键字按座位维度加锁——更严谨的做法是使用数据库的悲观锁SELECT ... FOR UPDATE,锁住座位表的行,再执行插入。但由于自习室预约场景并发量不会特别高,synchronized锁绑定座位id字符串的intern方法即可满足需求。不过使用synchronized注意锁的粒度要细,不要锁整个方法,否则所有座位的预约都会互相阻塞,性能瞬间崩掉。
第三层是兜底的异常处理。即使前面两层都过去了,最后一步插入时如果唯一索引冲突抛了DuplicateKeyException,也要捕获这个异常并转成友好的业务提示:“该座位已被手速更快的小伙伴预约了”。用户看到这个提示体验还算可以,不会觉得自己点了个假按钮。
3. 后端MVC架构落地与关键代码解析
3.1 项目分层:Controller、Service、Mapper到底各管什么
MVC三层架构在SpringBoot项目里已经演化成更细的分层:Controller层、Service层、Mapper层,再外加一个entity/model层放实体类,dto层放参数对象,vo层放返回结果对象。很多人刚学的时候容易把Controller写成“万能类”,所有逻辑都塞里面,这是典型的错误姿势。
我的分包结构是标准做法,直接照着建就行:
com.studyroom ├── controller // 接口层,接收请求、校验参数、返回结果 ├── service // 业务层,处理核心逻辑、事务控制 │ └── impl ├── mapper // MyBatis的数据访问接口 ├── entity // 数据库表对应的实体类 ├── dto // 接收前端参数的传输对象 ├── vo // 返回给前端的结果封装 ├── config // 配置类,比如跨域配置、拦截器配置 ├── common // 通用类:统一返回结果、异常处理、常量 └── utils // 工具类Controller层只做三件事:接收参数、调用Service、返回统一结果对象。它不应该出现任何具体的业务判断逻辑。比如预约请求进入Controller后,它要做的就是把预约参数绑定成一个DTO类传给Service,然后返回Result.success(data)。至于校验座位是否存在、时间是否合法、用户是否重复预约,这些全在Service里完成。
Service层是业务逻辑的核心,所有判断、计算、状态流转都在这层完成。这里有件事需要注意:涉及多表更新的操作,比如取消预约需要同时修改预约状态、更新座位运营状态、可能要写一条操作日志,必须在Service方法上标注@Transactional。否则就算第一句SQL执行成功,第二句报错,数据库留下脏数据,排查起来极其痛苦。
Mapper层就纯粹是数据访问。接口方法的注解或XML里的SQL只负责和数据库打交道,不做运算不做判断,把查询结果原样返回。我在项目里统一使用XML文件写SQL,因为动态SQL标签如if、where、foreach在XML里可读性更高,复杂联表查询也好维护。简单的单表查询用注解@Select也可以,但为了风格统一,我全部走了XML。
3.2 MyBatis的XML映射与动态SQL
MyBatis的XML文件是这个项目里一眼看上去最繁琐、但实际最灵活的部分。它最大的优势就是动态SQL。比如管理员在后台筛选预约记录,会有多个筛选条件:按状态查、按日期查、按自习室查、按用户手机号查。这些条件组合起来可能几十种情况,如果全写死在Java代码里,那打补丁得累死。
我的做法是这样的:用一个Map或一个DTO接收所有查询参数,然后在XML里用where标签+if标签动态拼接。这样当某个参数为空时,对应的SQL条件片段就不会拼进去,查询语句会自适应变化。
下面是一个预约记录分页查询的XML片段:
<select id="selectReservationPage" resultType="com.studyroom.vo.ReservationVO"> SELECT r.id, r.reservation_no, r.room_id, r.seat_id, r.user_id, u.username, u.phone, r.reserve_date, r.start_time, r.end_time, r.status, r.create_time FROM reservation r LEFT JOIN user u ON r.user_id = u.id <where> <if test="status != null and status != ''"> AND r.status = #{status} </if> <if test="roomId != null"> AND r.room_id = #{roomId} </if> <if test="reserveDate != null"> AND r.reserve_date = #{reserveDate} </if> <if test="keyword != null and keyword != ''"> AND (u.username LIKE CONCAT('%', #{keyword}, '%') OR u.phone LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY r.create_time DESC LIMIT #{offset}, #{pageSize} </select>这里有个细节值得留意:LIKE查询的写法。我之前见过有人直接在Java代码里拼好%...%再传到SQL里,这样也work,但存在注入风险。用CONCAT函数在SQL层拼接更安全。LIMIT后面的offset和pageSize用#{}参数占位,MyBatis会自动做预编译,防SQL注入。这一点是面试高频考点,也是实际开发必须养成的好习惯。
另外resultType和resultMap的选择上,我大部分联表查询用resultType直接映射到VO对象,只要数据库字段名和VO属性名通过下划线转驼峰映射就能对上。需要在application.yml里开启map-underscore-to-camel-case: true这个配置。
3.3 预约与取消预约的接口实现
预约接口是整个系统最核心的接口,它的实现逻辑我拆成了五个步骤:参数校验、重复检查、冲突检查、座位状态检查、插入预约单。下面把关键代码的思维流程讲一遍。
参数校验阶段要检查必传字段:用户id、自习室id、座位id、预约日期、开始时间和结束时间。开始时间必须早于结束时间,预约日期不能是过去日期。这些校验放在Service里能保证即使绕过前端校验也拦得住。
重复检查就是检查同一个用户同一个时间段内是否已经有预约。规则可以做成:一个用户同一时间段只能有一个有效预约,避免“占着茅坑不拉屎”的恶意预约行为。冲突检查则查座位在目标时间段是否已有其他用户的预约。
座位状态检查要查座位的运营状态是否可用。如果这个座位被管理员标记为禁用,那预约请求要直接拒绝。最后一步才是生成预约单,状态设为待使用,同时返回给前端一个预约成功的提示和预约单号。
取消预约的逻辑相对简单,但要仔细考虑时间限制。我设定的规则是:预约开始时间前30分钟允许取消,已经超过时间就只能走“超时未到”流程。这里有一个特殊处理:取消操作要同步做两件事,改预约单状态为已取消,同时如果当前没有其他预约占用该座位,座位状态恢复为空闲。
为了让读者更好理解,我把核心的Service方法伪代码写出来:
@Override @Transactional(rollbackFor = Exception.class) public ReservationResponse reserve(ReserveRequest request) { // 1. 参数校验 if (request.getStartTime().compareTo(request.getEndTime()) >= 0) { throw new BizException("开始时间必须早于结束时间"); } // 2. 检查用户是否已有冲突预约 int count = reservationMapper.checkUserConflict( request.getUserId(), request.getReserveDate(), request.getStartTime(), request.getEndTime()); if (count > 0) { throw new BizException("你已有同时段的预约记录"); } // 3. 检查座位是否可用 Seat seat = seatMapper.selectById(request.getSeatId()); if (seat == null || SeatStatus.DISABLED.getCode().equals(seat.getStatus())) { throw new BizException("座位不可用"); } // 4. 检查座位同一时段冲突 int seatConflict = reservationMapper.checkSeatConflict( request.getSeatId(), request.getReserveDate(), request.getStartTime(), request.getEndTime()); if (seatConflict > 0) { throw new BizException("该座位当前时段已被预约"); } // 5. 生成预约单 Reservation reservation = new Reservation(); reservation.setReservationNo(generateReservationNo()); // ... set other fields reservationMapper.insert(reservation); return ReservationResponse.from(reservation); }generateReservationNo我是这样生成的:每天日期字符串加六位随机数,比如20250601 + 823471。加上唯一索引防止极端情况下两个单号撞车。这个单号的价值主要体现在线下场景:自习室管理员看到单号就能快速在系统里查到对应预约,比用数据库id好太多。
4. Vue前端实现与交互细节
4.1 项目初始化与路由设计
前端用Vue脚手架创建项目后,首先要装依赖:vue-router负责路由,pinia负责状态管理,axios负责发HTTP请求,element-plus或vant做UI组件库。我这套PC端管理类页面比较多,选的是Element Plus。
路由设计我采用了动态路由的思路:登录成功之后,根据后端返回的角色信息动态添加路由。普通用户能访问的路由是:首页、自习室列表、座位预约、我的预约、个人中心;管理员能额外访问:座位管理、自习室管理、预约管理、数据统计。这样避免把所有路由一次性注册,用户手动输入URL访问不该进的页面直接被404或重定向。
实现核心是在router.beforeEach的全局前置守卫中加逻辑:取到用户的token,再根据角色拼接需要动态添加的路由。这个方案要注意一个问题:刷新页面时路由会被重置,必须在刷新前把路由信息存到pinia或localStorage里,下次进入时再重新addRoute。
4.2 自习室列表与选座交互
用户进入预约流程的核心路径是:选择自习室 -> 查看座位图 -> 点击座位 -> 确认预约信息 -> 提交成功。
自习室列表页比较简单,就是一个卡片列表。每个卡片展示自习室名称、位置、开放时间、剩余座位数。剩余座位数不要单独写死,而是每次查询时动态计算。我这边是用一个接口获取所有自习室,再返回每个自习室的可用座位数。
座位图是前端开发中最需要花心思做的一块。我设计了一个Canvas绘制的座位平面图:后端返回每个座位在自习室中的相对坐标(x, y),前端根据坐标绘制座位方块,不同状态的座位用不同颜色区分:绿色空闲可点击、灰色已占用、黄色被选中。用户点击空闲座位后,弹出侧边栏显示预约时间选择器,选好时间段后提交。
这个交互看似简单,但雷区不少。最大的坑是点击事件绑定。如果你用div渲染多个座位,事件冒泡可能导致用户点一个座位被判定成点击另一个。解决办法是为每个座位设置唯一的data-index属性,事件处理时用event.target.dataset.index来锁定目标,而不是依赖事件对象的其它属性。另外还要给选中的座位做高亮,并且一定要处理用户点击A座位再点击B座位的情况,上一次选中的状态要取消掉。
具体到座位坐标返回的接口设计,我推荐一次返回该自习室当天所有座位信息的列表,包含id、座位编号、x坐标、y坐标、状态。前端拿到数据后渲染即可。这里不要在选座时才逐个请求座位详情,会造成大量HTTP请求拖慢页面响应。
4.3 状态管理:用户信息与预约状态
Pinia是Vue 3官方推荐的状态管理库,相当于是Vuex的进化版。它的代码比Vuex简洁很多,去掉了mutations,action里直接改state。我把用户登录信息、token、预约筛选条件这些全局共享数据都放进了Pinia。
实际开发中比较关键的是Token管理。用户登录成功后,后端返回一个token(我用的是JWT),前端把token存到localStorage里,同时设置axios的请求拦截器,在每次请求前自动把token放进请求头Authorization字段。响应拦截器里做统一错误处理:后端返回401时自动清除token并跳转登录页;返回业务错误码时直接弹出ElMessage提示用户。
预约状态的响应式更新也很重要。用户成功取消一个预约后,之前座位图上对应座位的状态还显示为灰色已占用,但数据库里已经变成空闲了。这时候必须触发数据刷新。我的做法是把当前自习室的座位数据存到Pinia的管理模块里,取消预约成功后调用seatStore的fetchSeats方法重新拉取座位数据,保证UI和数据库状态一致。千万别图省事只在前端改一个座位状态变量,一旦刷新页面就露馅,而且并发情况下用户看到的可能是过期数据。
5. 部署运行与常见问题排查
5.1 本地快速启动:初始化SQL与配置文件
整套系统跑起来的第一步是初始化数据库。提前把建库建表SQL脚本准备好,包括核心表数据和几个测试用户数据。MySQL 8.x版本要注意字符集:数据库和表都用utf8mb4而不是utf8,因为utf8在MySQL里不是标准的四字节UTF-8,emoji等特殊字符存不进去会报错。这个问题非常隐蔽,排查很久才发现的。
后端配置文件application.yml里我最常被问到的几个配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/study_room?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.studyroom.entity configuration: map-underscore-to-camel-case: trueserverTimezone=Asia/Shanghai是很多新人忽略的地方。MySQL 8.x驱动默认时区是UTC,如果不指定,查出来的时间会比本地时间早8小时,预约时间的显示全部错乱。allowPublicKeyRetrieval=true这个参数是在用caching_sha2_password认证插件时需要的,不加会报Public Key Retrieval is not allowed错误。
5.2 高频报错与解决方案
我把开发和部署中遇到的几个高频问题整理成表。这些问题都是新手期最容易触发、且真实项目里也极有可能再现的。
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| Failed to configure a DataSource | application.yml找不到配置或类扫描路径不对 | 检查注解@SpringBootApplication所在包是否能扫描到配置类 |
| Invalid bound statement (not found) | mapper接口和XML文件未绑定 | 检查XML文件位置是否在mapper-locations配置路径下,方法名是否一致 |
| Cannot load driver class: com.mysql.cj.jdbc.Driver | MySQL驱动未引入 | 在pom.xml中引入mysql-connector-java依赖,版本需与本地MySQL对应 |
| 日期错乱少8小时 | 时区配置未设置 | 增加serverTimezone=Asia/Shanghai参数 |
| 跨域请求被拦截 | 前后端端口不一致 | 在SpringBoot中配置CorsFilter或使用@CrossOrigin注解 |
| 前端请求404或500 | 后端接口路径或参数名不一致 | 用浏览器开发者工具的Network面板查看具体请求和响应 |
| 唯一索引冲突Duplicate entry | 同一座位同一时间段重复预约 | 在Service中执行冲突检查并捕获异常返回友好提示 |
跨域问题我要单独说。我开发时前端跑在5173端口(Vite默认),后端跑在8080端口,两者端口不同,浏览器拦截跨域请求很正常。解决方式有两种:后端统一配置一个CorsFilter,允许指定源和指定方法;另一个是后端使用Spring Cloud Gateway等网关代理,但单机开发时一个CorsFilter就够用了。生产环境建议使用Nginx做反向代理,把前端静态文件和后端接口放到同一个域名下,从根源上规避跨域。
5.3 性能与并发优化心得
自习室预约这类系统的并发量不会特别夸张,但是某些高峰时段,例如图书馆的期末复习周,还是会有一波瞬间高并发。我的优化经验分三档。
第一档是前端限流。座位图上的座位数量有限,用户需要先选择时间段再提交,避免所有人都同时在预约按钮上狂点。提交按钮设置60秒的防重复点击限制,用户操作逻辑上先拦住一部分无效请求。
第二档是后端缓存。热门自习室的座位状态数据可以缓存到Redis,减少数据库的查询压力。但缓存会让数据状态有一定的延迟,需要配合缓存过期时间或者消息通知主动失效。自习室预约系统的数据一致性要求并不那么高,因为用户查询座位状态时看到有一两秒的延迟完全可以接受,但预约提交瞬间必须读最新数据。
第三档才是数据库性能优化。给预约单表建立合适的联合索引:预约日期+状态+自习室id,座位表建立自习室id索引,用户表手机号建唯一索引。这几种索引组合能覆盖绝大部分业务查询场景。不要盲目给所有字段加索引,写多读少的字段加索引反而拖慢插入速度。
还有一个经验是尽量把复杂SQL拆分。比如统计报表需求:每日预约量、座位利用率、自习室排行。这类统计查询在数据量小的时候用一条SQL就能搞定,但数据量一旦上来,group by和子查询会让数据库压力剧增。我的做法是把统计任务拆成定时任务,每天凌晨用Spring Boot的@Scheduled注解生成当天的统计汇总数据存到统计表,前台展示时只需查统计表,查询性能提升非常明显。
6. 从课设到真实项目的提升点
从一个能跑起来的课设系统到一个稍微接近生产环境的应用,中间还差几个关键动作。整理项目时我用下面几条标准来衡量系统成熟度,也可以对照看看你的项目卡在哪一档。
第一是日志链路。开发阶段print()完事直接输出到控制台没问题,但部署到服务器后就必须依赖日志定位问题。我用的方案是Logback按天滚动切割,ERROR级别和INFO级别分开输出。预约成功、取消、失败这些核心操作至少记录一条INFO日志,包含用户id、座位id和操作结果,方便日后做问题排查。
第二是参数校验的规范化。不要在前端做了一堆校验就认为万事大吉。后端必须用JSR 303的@Valid注解做参数校验,在DTO字段上标注@NotNull、@NotBlank、@Length等注解,写起来零成本,但能拦截掉大量无效请求。
第三是接口文档。以前很多小团队不爱写接口文档,前后端联调靠口头沟通,效率极低。后来普遍的方案是集成SpringDoc或Swagger,启动项目后自动生成接口文档。另一个更好维护的方式是写Apifox或Postman的接口集合,把每个接口的请求参数和返回结果固化下来,新成员接手时直接看接口集合就能上手,不用读一遍源码。
第四是数据安全。密码加密使用BCrypt,登录接口加简单的验证码或图片验证码做防爆破。预约的接口要验证当前登录用户和操作对象是否一致,防止通过篡改请求参数操作他人的预约记录。这个越权问题在课设里常常被忽略,但在真实项目中属于高危漏洞。我用的是拦截器加ThreadLocal保存当前登录用户信息,Service层取用户id时统一从这个ThreadLocal取,前端传来的任何user_id都不作为权限判断依据。
7. 实操中的个人体会
整个项目做完回头复盘,我最大的感受是技术选型重要,但比技术选型更重要的是对业务边界和状态变化的清晰认知。这套系统的业务本质并不复杂,就是一个座位的占用时间片管理,但围绕这个本质展开的字段设计、状态流转、并发控制、交互体验,每一环都会决定系统的稳定性和易用性。
对我个人来说,动手做一个项目永远比看书看视频学得快。遇到问题靠搜索引擎、靠官方文档、靠上下文调试慢慢解决,踩过坑的记忆最牢固。也建议你把这个项目跑起来之后,尝试加一个功能或者换一种实现方式:比如把座位预约改成计数预约、加一个基于Redis的排队功能、把统计报表改成图表可视化。不要照着我写的代码抄一遍就完事,改造的过程中你才会真正理解哪一步为什么这么设计。