坐在电脑前敲下这套系统之前,我在图书馆被占座问题折磨了整整一个学期。高峰期座位紧张、人走书在、管理员每天逐排清书的场景,应该是大多数高校图书馆的常态。后来我用Spring Boot从零实现了一套自习室座位预约管理系统,在内蒙古电子信息职业技术学院图书馆试运行,覆盖两个自习区、428个座位,日均处理预约请求800多次,总算把座位调度这件事理顺了。
这篇内容我打算从需求分析一路讲到部署踩坑,中间穿插核心代码和数据库设计思路。不管你是做毕业设计,还是想给学校图书馆真正落地一套预约系统,或者纯粹想看看Spring Boot项目里怎么处理并发预约、状态机、定时任务这些问题,应该都能找到有用的东西。
1. 先搞清楚要解决什么:图书馆座位管理的真实痛点
很多同学做类似系统时,上来就建表、写代码,结果做完才发现跟实际需求对不上。我在动手之前,专门在图书馆蹲了两天,观察管理员日常工作,也和值班老师聊了不少。真实场景里的痛点,比想象中具体得多。
1.1 占座现象背后的管理困境
占座本质上是需求大于供给时,公共资源被低效占用。教学楼里的自习室、图书馆自修区都面临同样的问题:有人早上放一本书占座,下午才来;有人临时离开两小时,座位空着却没人敢坐;管理员清理座位时,经常因为“书是谁的”起争执。
传统管理方式有两种:一种是纯人工——管理员定时巡场,发现空位就收书;另一种是纸面登记——根柢还是靠人。这两种方式在座位少、人少的时候勉强能用,一旦座位超过两三百个,人工根本管不过来。这就是预约系统存在的意义:把座位使用权通过规则前置分配,减少随机冲突。
1.2 用户角色与核心诉求
从需求分析的角度,这套系统的用户可以拆成三类:
- 学生用户:需要快速找到空闲座位、提前预约、到馆签到、临时离开不被“清座”、查看个人预约记录。
- 管理员:需要维护座位信息、设置开放时间、处理违规记录、查看实时占用率、生成统计报表。
- 系统后台:要能自动处理超时未签到、释放违约座位、保障同一座位不被重复预约。
这三类诉求落到系统功能上,就变成了预约管理、签到签退、信用分、座位管理、报表统计五个模块。我在这张功能清单上和不少同学聊过,大家最容易漏掉的是“信用分”和“自动释放”,后面我会重点讲。
1.3 从需求倒推模块边界
需求确定后,模块边界自然就清晰了:
- 用户模块:登录注册、个人信息、信用分查询。
- 座位模块:自习室分区、座位状态查询、座位禁用/启用。
- 预约模块:预约、取消、签到、签退、暂离。
- 管理模块:用户管理、座位管理、违约管理、数据统计。
如果你是按毕业设计的标准来做,建议再加一个公告管理模块,方便图书馆发节假日开放通知。这个模块虽然简单,但在答辩时能体现你对业务完整性的考虑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型复盘:Spring Boot为什么是这套系统的最优解
这套系统的技术栈是:Spring Boot + Spring MVC + MyBatis Plus + Redis + MySQL + Vue + Element UI。前端用Vue做后台管理界面,学生端兼顾PC浏览器,整体是前后端分离架构。
2.1 Spring Boot在项目中的核心地位
Spring Boot最大的价值,是把Spring生态里繁琐的XML配置全部变成了自动装配。你引入一个spring-boot-starter-web,内嵌Tomcat直接跑起来,不用再部署war包到外部容器,这对快速交付一个垂直业务系统来说效率提升非常明显。
具体到图书馆预约场景,Spring Boot提供的几个能力都派上了用场:
- Spring MVC处理RESTful接口,前端通过Axios调用后端API完成数据交互。
- Spring Validation做参数校验,比如预约时间格式、座位ID是否为空,都在进入业务逻辑前挡掉。
- Spring Task实现定时任务,用来扫描超时未签到的预约记录并自动释放座位。这是系统里非常关键的一环。
- Spring AOP做统一日志和异常处理,方便排查线上问题。
2.2 Redis不是加分项,是并发锁的刚需
在这个项目里,Redis承载了两个职责:缓存热点数据和实现分布式锁。
预约高峰时,比如早上7点开放抢座,几百个人同时刷新座位列表。如果每个请求都直接查数据库,MySQL压力会非常大。我的做法是:座位状态变化不频繁时,把座位列表缓存到Redis,预约成功或释放座位时同步更新缓存。这样查询接口的响应时间从几十毫秒降到个位数毫秒。
更关键的是预约防冲突。同一个座位,A和B同时提交预约,如果用纯数据库校验,可能会出现两个请求都读到了“空闲”状态,然后都插入成功。这里必须加锁。我用的方案是Redis分布式锁(SETNX),锁的key是seat:lock:{seatId},获取锁之后才校验座位状态、执行预约插入。后面我会专门展开来讲并发控制。
2.3 前端为什么选Vue而不是JSP
传统毕设项目喜欢用JSP + JQuery,页面混在Java代码里。但真实的图书馆管理场景,需要座位可视化布局、实时状态刷新、报表图表展示,这些用Vue + Element UI做起来顺手得多,组件化开发维护也更轻松。
学生端的核心页面是选座页,我用的是Canvas和CSS Grid画座位图,不同颜色表示空闲、预约中、已占用、已禁用。点击空闲座位后弹出预约确认框,确认后调用后端接口。这个交互看着简单,但对前后端联调的要求不低,接口返回的每个座位状态字段都要和前端约定清楚。
2.4 为什么没上微服务
有人可能会问:既然都用Spring Boot了,是不是要搭配Spring Cloud Alibaba、Nacos这些?我的答案非常明确:不要。一个图书馆预约系统的并发量撑死几百人同时操作,单体应用完全够用。引入微服务只会增加部署复杂度和问题排查难度,对于图书馆这类场景纯属过度设计。
选用MyBatis Plus而不是原生MyBatis,是因为它内置了分页插件、代码生成器、条件构造器,CRUD操作不用写一堆重复XML。像座位表、预约表这种典型的单表操作,MP能直接省掉一半工作量,把精力放到业务逻辑上。
3. 数据库设计:三张核心表和一个状态机
数据库设计直接决定了后面写业务代码是否顺畅。我经历过先写代码再改表的痛苦,所以这次花了一周时间反复推敲表结构,一共设计了9张表,其中最核心的是:座位表、预约表、违约记录表。
3.1 座位表:房间、区域、座位三层结构
图书馆自习室不是一个大平层,而是分成多个区域,比如一层东区、一层西区、二层静音区。座位必须挂在区域下面,所以建表时要体现层级。
sql复制CREATE TABLE `seat` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '座位ID',
`room_id` BIGINT NOT NULL COMMENT '所属自习室ID',
`seat_no` VARCHAR(20) NOT NULL COMMENT '座位编号,如A-001',
`row_no` INT DEFAULT NULL COMMENT '排号',
`col_no` INT DEFAULT NULL COMMENT '列号',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0空闲 1占用 2禁用 3维修中',
`is_enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_room_status` (`room_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='座位表';
这里有个细节:我把room_id和status建了联合索引,因为前端查询座位列表最频繁的条件就是“某自习室下状态空闲的座位”。联合索引能让这个查询走索引,避免全表扫描。
座位状态我用TINYINT而不是VARCHAR,一是节省存储,二是避免字符串拼写错误。状态枚举在Java里定义成常量类,注释写清楚,团队协作时不会产生歧义。
3.2 预约表:状态字段是灵魂
预约表是业务复杂度最高的表。一个预约从创建到结束,会经历待签到、进行中、已结束、已取消、已违约五种状态。怎么设计这张表,决定了定时任务、签退逻辑、信用分判定好不好写。
sql复制CREATE TABLE `reservation` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '预约用户ID',
`seat_id` BIGINT NOT NULL COMMENT '座位ID',
`reserve_date` DATE NOT NULL COMMENT '预约日期',
`start_time` DATETIME NOT NULL COMMENT '预约开始时间',
`end_time` DATETIME NOT NULL COMMENT '预约结束时间',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待签到 1进行中 2已结束 3已取消 4已违约',
`signin_time` DATETIME DEFAULT NULL COMMENT '签到时间',
`signout_time` DATETIME DEFAULT NULL COMMENT '签退时间',
`violation_type` TINYINT DEFAULT NULL COMMENT '违约类型:1超时未签到 2超时未签退',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`, `reserve_date`),
KEY `idx_seat_date` (`seat_id`, `reserve_date`),
KEY `idx_status_date` (`status`, `reserve_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约表';
为什么要建idx_status_date这个联合索引?因为定时任务每次要查“当前日期以前、状态还是待签到”的预约,这个索引能让扫描范围变得很小。
状态字段的设计思路是状态机:每个状态只允许特定的流转路径。比如只有“待签到”能变成“进行中”(签到成功)或“已违约”(超时未签到),“进行中”只能变成“已结束”或“已违约”。我把状态流转逻辑写在Service层,用switch-case判断,保证每个入口都走同一套校验规则。
3.3 违约记录表与信用分
违约记录和预约是多对一关系,一个预约最多产生一次违约。信用分我采用的是“总分100分,每次违约扣5分,扣到60分以下禁止预约3天”的策略。这个策略不是凭空想的,是和管理员讨论后确定的——太严没人敢用,太松约束不住人。
sql复制CREATE TABLE `violation_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`reservation_id` BIGINT NOT NULL,
`violation_type` TINYINT NOT NULL COMMENT '违约类型',
`deduct_points` INT NOT NULL DEFAULT 5 COMMENT '扣除信用分',
`description` VARCHAR(255) DEFAULT NULL COMMENT '违约描述',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='违约记录表';
信用分不需要单独建表,在用户表里加一个credit_score字段就行。违约时更新用户表和插入违约记录,放在同一个事务里,保证数据一致性。这个事务我用@Transactional注解搞定,非常简单。
3.4 时间字段的隐藏坑
预约时间有一个容易踩坑的地方:用户预约的是“今天的18:00-21:00”还是“某个固定日期段”?我在设计时选择了reserve_date存日期、start_time和end_time存完整时间,避免跨天时出现歧义。后端在生成预约记录时,用reserve_date拼接上开放时间段的起止时刻,再存入start_time和end_time。
如果预约支持跨天(比如22:00到次日6:00),end_time小于start_time的情况就要特殊处理。我当时没做跨天预约,因为图书馆闭馆时间固定,但从通用性角度,这块逻辑可以在状态判断时预留好。
4. 预约并发控制:同一个座位被两个人同时抢到怎么办
这是整个系统里技术含量最高的部分,也是我在答辩时重点讲的内容。预约系统的本质是一个秒杀场景:座位数量固定,多个用户同时抢有限的资源。如果不做并发控制,就会出现“超卖”——一个座位被两个人预约成功。
4.1 超卖问题是怎么产生的
假设座位S当前状态是空闲。A用户和B用户同时发起预约请求,两个请求都执行了以下SQL:
sql复制SELECT * FROM seat WHERE id = ? AND status = 0;
两个请求都查到了空闲状态,于是都往下执行,插入两条预约记录。但实际上只有一个座位,这就产生了数据不一致。
解决途径有三个层次:
- 数据库层面:加锁或唯一约束。
- 应用层面:使用同步锁或分布式锁。
- 缓存层面:Redis原子操作。
我最终用的是组合方案:数据库唯一约束做兜底,Redis分布式锁做主要防冲突手段。
4.2 数据库悲观锁:简单但需谨慎
最直接的办法是使用SELECT ... FOR UPDATE,将座位记录锁住,另一个事务必须等待:
sql复制SELECT * FROM seat WHERE id = #{seatId} AND status = 0 FOR UPDATE;
拿到锁之后,再检查状态、插入预约、更新座位状态,整个操作放在事务里。这种方案实现简单,但存在两个问题:一是数据库行锁的性能瓶颈比较明显,二是锁的粒度是整个座位行,如果同时有大量请求打到同一个座位上,会导致数据库连接池被占满。
因此我没有把悲观锁作为主要方案,只把它当作最后一道防线,在事务里对座位状态更新时加了乐观锁控制:
sql复制UPDATE seat SET status = 1 WHERE id = #{seatId} AND status = 0;
如果这个UPDATE影响的行数为0,说明座位状态已经被别人改过,直接抛异常提示“座位已被预约”。
4.3 Redis分布式锁实战
用户发起预约的接口,我在外层加了Redis分布式锁:
java复制public Result reserveSeat(Long userId, Long seatId, String date) {
String lockKey = "seat:lock:" + seatId + ":" + date;
String requestId = UUID.randomUUID().toString();
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("手慢了,座位被其他人锁定,请重试");
}
try {
// 核心业务:校验座位状态、插入预约、更新座位缓存
return doReserve(userId, seatId, date);
} finally {
// 释放锁:比较requestId防止误删别人的锁
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
}
这里有几个细节值得注意:
- 加锁key必须包含日期。同一个座位今天和明天的预约是互不干扰的,如果不带日期,今天抢了锁,明天也无法预约,业务就错了。
- 锁超时时间设10秒,避免宕机死锁。但10秒也意味着如果业务逻辑超过10秒,锁会自动释放,可能产生并发问题。因此核心业务必须快,不能在里面查太多东西。
- 释放锁时用requestId做校验,防止某个请求超时后锁被释放、另一个请求又拿到锁,前一个请求执行finally时把后一个请求的锁误删。
4.4 锁的粒度能细化到什么程度
有同学问:如果用synchronized或者ReentrantLock行不行?在单机部署时是可以的,但一旦后面系统部署了多台服务器,JVM锁就失效了。Redis分布式锁天然支持跨进程,是我推荐的原因。
如果你做的毕设只是单机部署,用ReentrantLock更简单,但答辩时能讲清楚“为什么选分布式锁”是一个亮点。我当时就是从单机锁升级到Redis锁,把升级前后的并发压测结果做了对比,评委对这一块很感兴趣。
5. 状态流转与自动释放机制:座位不会无缘无故空出来
预约系统能不能真正落地,关键看座位能否被及时释放。如果用户预约了不来,座位也不释放,那系统的价值就大打折扣。我设计了完整的“预约状态机 + 定时任务 + 信用分约束”三合一机制。
5.1 预约状态机的完整流转逻辑
完整的状态路径如下:
- 用户提交预约 →
待签到(0) - 用户到馆扫码/点击签到 →
进行中(1) - 预约结束时间到,用户正常签退 →
已结束(2) - 用户在开始时间前取消预约 →
已取消(3) - 超过签到宽限期仍未签到 →
已违约(4) - 超过结束时间仍未签退 →
已违约(4)
这些流转我全部在Service层实现,不允许Controller直接改状态。这样做的原因是:状态变更往往伴随其他操作,比如变为“进行中”要更新座位为占用、变为“已违约”要扣信用分和插入违约记录。这些操作必须放在同一个事务里,集中管理才能保证一致性。
5.2 定时任务:Spring Task扫描超时未签到
Spring Boot内置的@Scheduled注解就能实现定时任务,不需要额外引入Quartz。我配置了三个定时任务:
java复制@Component
public class ReservationTask {
@Scheduled(cron = "0 */5 * * * ?")
public void releaseTimeoutReservations() {
// 每隔5分钟扫描:当前时间超过预约开始时间30分钟,且状态为待签到的预约
List<Reservation> timeoutList = reservationMapper
.selectTimeoutReservations(new Date(System.currentTimeMillis() - 30 * 60 * 1000));
for (Reservation r : timeoutList) {
// 将预约状态改为违约
// 将座位状态改为空闲
// 为用户扣信用分并插入违约记录
}
}
}
这里的“30分钟”是签到宽限期,是我和图书馆管理员商定的值:学生从校门口走到图书馆需要十分钟,再加上排队操作的时间,30分钟比较合理。你可以通过系统配置项把这个值做成可调的,而不是硬编码。
@Scheduled的cron表达式需要特别注意时区问题。服务器默认时区如果不是北京时间,定时任务执行时间会偏差。我在项目启动类里显式设置了时区:
java复制@PostConstruct
void setDefaultTimeZone() {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
}
这个坑当时排查了整整半天,最后发现是服务器UTC时间导致定时任务按美国时间执行。
5.3 签退和临时离开的阈值判断
签到之后,用户可能中途去接水、上厕所,座位会短暂空置。如果一空置就释放,用户回来发现座位没了,体验非常差。所以“临时离开”和“彻底离开”必须区分开。
我的设计是:用户端有一个“暂离”按钮,点击后座位标记为“暂离中”,保留30分钟;30分钟内用户返回点击“返回”,座位恢复“占用”;超过30分钟未返回,系统自动释放座位并记一次违约。这个逻辑和校园卡的“暂离”类似,用Redis的过期key实现最方便:
java复制// 用户点击暂离
redisTemplate.opsForValue().set(
"seat:temp:" + seatId,
userId.toString(),
30, TimeUnit.MINUTES
);
如果用户没点暂离直接走,座位还是“占用”状态,只能等预约结束时间到达,或者管理员手动释放。后来在试运行中,这个问题暴露明显——很多同学不习惯点暂离,管理员还是得频繁处理。改进方案是在座位旁贴二维码,离座时扫码点击“暂离”,降低操作成本。
5.4 信用分机制:让规则产生约束力
没有处罚机制的预约系统,违约率一定居高不下。我在用户表增加信用分字段,违约一次扣5分,并按分数区间做提醒和限制:
| 信用分区间 | 状态 | 限制措施 |
|---|---|---|
| 90-100 | 正常 | 无限制 |
| 70-89 | 提醒 | 每次预约时弹窗提示信用分偏低 |
| 60-69 | 受限 | 同时只能预约一个座位 |
| 60以下 | 处罚 | 3天内禁止预约 |
信用分每保持一周无违约,恢复2分,上限100。这个恢复策略加上扣分机制,能在“约束”和“鼓励”之间取得平衡。
信用分核减和恢复都是异步操作,我用Spring的事件机制(ApplicationEventPublisher)发布违约事件,在监听器里异步更新用户信用分。这样做的好处是预约主流程不会被信用分更新拖慢,接口响应更稳定。
6. 管理员端设计:可视化选座与数据看板
管理员端是这套系统里我花了不少心思的地方。面试官问管理员最需要什么时,得到的答案出奇一致:一眼看清整个自习区的占用情况,而不是在一堆表格里翻数据。
6.1 座位可视化布局的实现
我用Vue + CSS Grid实现了座位图:每个座位对应一个方格,背景色代表实时状态——绿色空闲、黄色预约中、红色占用、灰色禁用。管理员点击任意座位,可以查看该座位的今日预约记录、当前使用者信息,或者直接禁用/启用座位。
后端提供座位布局数据时,返回的是房间内所有座位及状态的列表,前端根据row_no和col_no字段完成Grid定位。这里需要注意:教学楼和图书馆的座位排布不是规则的矩形,有些区域中间有空地或柱子,row_no和col_no设计时要预留有空隙的编号方案,比如跳过某些编号来表示物理空间的间隔。
6.2 统计报表模块
管理员端第二个核心模块是统计报表,我基于ECharts做了三个图表:
- 自习室实时占用率折线图:展示一天内不同时间的占用峰值,方便图书馆决定是否需要延长开放时间。
- 座位使用频次排行:找出最热门的座位区域和最冷门的座位区域,为座位调整提供依据。
- 违约趋势图:按月展示违约数量,验收时可以用这个数据证明预约系统的必要性。
后端报表接口用的是聚合查询,按照日期和状态分组统计:
java复制@Override
public List<Map<String, Object>> getDailyStats(LocalDate start, LocalDate end) {
QueryWrapper<Reservation> wrapper = new QueryWrapper<>();
wrapper.select("DATE(create_time) as date", "COUNT(*) as total")
.between("create_time", start.atStartOfDay(), end.plusDays(1).atStartOfDay())
.groupBy("DATE(create_time)");
return reservationMapper.selectMaps(wrapper);
}
如果数据量大了,这些统计SQL会变慢,可以在预约表上增加按日的汇总表,每天凌晨通过定时任务生成前一天的统计快照。我在试运行阶段数据量不大,暂时用实时聚合,后续如果要扩展到全校多校区,就需要改成汇总表方案。
6.3 用户管理与公告发布
用户管理除了常规的禁用/启用账号,我还增加了“管理员代预约”功能。这个功能很实用——有些同学在手机上操作不熟练,或者临时忘记预约,可以找图书馆值班老师帮忙代约。代预约走的是同一个预约Service,只是userId由管理员指定。
公告管理就简单很多:一张公告表,后台维护标题和内容,前台首页展示最新公告。毕设项目加上这个模块,功能完整性一下子就体现出来了。
7. 部署上线的真实踩坑记录
系统开发完成不代表结束,部署上线才是真正检验工程质量的时候。我在部署到图书馆服务器时,踩了好几个坑,这里挑三个印象最深的讲。
7.1 内存不足引发的OOM
图书馆服务器配置不高,只有2核4G内存。Spring Boot应用启动后默认占用比较大,再叠加MySQL、Redis,内存经常飙到90%以上,后来直接OOM。
解决方案是手动调优JVM参数:
bash复制java -jar -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxMetaspaceSize=256m reservation-system.jar
把堆内存固定到512M,减少动态扩缩容带来的开销。同时MySQL的innodb_buffer_pool_size调到512M,Redis的maxmemory设置256M。调整后整个系统内存稳定在2.5G左右,服务器总算不报警了。
7.2 定时任务重复执行
部署时我用了两台服务器做简单的负载均衡,结果发现违约释放任务在每台服务器上都会执行一次,导致同一个预约被处理两次,释放座位的逻辑执行了重复操作。
解决方案有两种:一是引入分布式任务调度框架,杀鸡用牛刀;二是用一个Redis分布式锁保证同一时刻只有一台服务器在执行定时任务:
java复制@Scheduled(cron = "0 */5 * * * ?")
public void releaseTimeoutReservations() {
String lockKey = "task:release:lock";
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 4, TimeUnit.MINUTES);
if (!locked) {
return; // 另一台服务器已在执行
}
try {
// 执行任务逻辑
} finally {
redisTemplate.delete(lockKey);
}
}
如果只是单机部署,这个坑不会出现。但提前加上这个锁,可以在未来横向扩展时少改一次代码。
7.3 Nginx反向代理与上传大小限制
前端静态资源用Nginx托管,API请求通过反向代理转发到Spring Boot应用。配置时最常遇到的问题是client_max_body_size默认1M,导致用户上传头像、公告图片时直接413。这个配置改一下就行:
nginx复制server {
listen 80;
server_name library.example.edu.cn;
client_max_body_size 20m;
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 / {
root /opt/reservation-front/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}
另外,前端打包后是SPA应用,需要配置try_files保证路由刷新时不会404。这个不配置的话,用户刷新页面就会看到Nginx的404页面,体验很差。
部署完成后,我做了简单的压测:用JMeter模拟100并发同时预约同一个座位,最终只有1个请求成功,其余都返回“座位已被预约”,且数据库中没有产生重复预约记录。这个结果说明Redis分布式锁方案是有效的。
8. 给做类似项目的同学说几句掏心窝的话
市面上Spring Boot的图书馆预约系统、自习室管理系统项目很多,网上一搜一大把。但大多数只是demo级别,CRUD跑通就算完成。如果你想在毕业设计或课程设计中真正脱颖而出,以下三点值得注意:
第一,把业务闭环讲完整。 数据库建表、接口开发只是基础,预约状态的完整流转、超时释放、信用分机制、并发控制,这些才是能体现你思考深度的内容。答辩时不要只演示功能,要讲清楚功能背后的设计逻辑和遇到的坑。
第二,必须有数据支撑。 性能压测、并发测试的数据比代码本身更有说服力。我当时把100并发预约同一个座位的压测结果整理成图表,评委当场就点头了。你可以用JMeter或Postman做类似测试,把结果截图放进论文或演示PPT里。
第三,界面要干净、操作要简单。 图书馆里使用这套系统的很多是学生志愿者,他们没有技术背景。界面交互如果太复杂,培训成本会很高。Element UI、Vue这类组件库能让前端开发事半功倍,但真正重要的是交互流程要贴近用户习惯——选座页直接点图选座,预约记录清晰可查,违约提醒明确易懂。
这套系统从设计到落地用了大约六周时间,其中最耗时的是状态机和并发控制的部分,也是我觉得真正有价值的部分。后来图书馆老师反馈,试运行期间座位利用率提升明显,管理员巡场的频次也大大降低了。如果你也在做类似的项目,希望这篇分享能帮你避开一些我已经踩过的坑。
