实验室预约与资源管理平台 - 需求分析与设计说明
1. 需求分析
1.1 用户角色与需求
| 角色 |
核心诉求 |
主要用例 |
| 管理员 ADMIN |
保证资源台账准确、预约秩序可控 |
实验室/设备/课程的增删改查;资源状态流转(报修、启用);用户账号管理;审批任何预约;查看全局日历与统计 |
| 教师 TEACHER |
排课与科研实验,不想被冲突打乱 |
创建/维护自己的实验课程;按课程一键生成整期预约;审批学生预约;发起多次预约(如每周教研) |
| 学生 STUDENT |
快速找到可用资源并预约 |
查询实验室/设备可用性;发起单次/多次预约;查看与取消自己的预约;签到与完成 |
1.2 预约场景定义
| 场景 |
输入 |
系统行为 |
对应类型 |
| 单次预约 |
实验室(可选设备)、一个开始/结束时间 |
校验冲突后生成 1 条预约;学生进入待审批,教师/管理员直接通过 |
SINGLE |
| 多次预约 |
日期区间 + 星期集合 + 每日时段 |
按周展开为多个场次,写入 1 条系列 + N 条预约;任一场次冲突则整批拒绝 |
RECURRING |
| 课程绑定预约 |
课程 ID(可指定设备) |
按课程排课规则展开整期场次,绑定课程 ID;仅授课教师或管理员可发起 |
COURSE |
关键业务规则
- 冲突判定必须覆盖:同实验室、同设备、同申请人三个维度。
- 学生预约需要审批;教师/管理员预约自动通过(仍要过冲突校验)。
- 被驳回/取消/完成的预约不再占用资源。
- 资源处于「维护中」不可预约;人数不得超过实验室容量。
- 单次预约 ≤ 8 小时、最多提前 60 天、不得预约过去时间,且需落在实验室开放时间内。
- 不允许跨天预约(结束时间为次日 00:00 除外)。
2. 领域模型
设计要点
- 「多次预约/课程预约」采用 系列(Series)+ 场次(Reservation) 两层结构:
每次实际占用都是一条独立
reservation 行,冲突检测、审批、状态流转
都只需处理统一的时间区间模型,避免规则分叉。
- 时间区间统一使用 左闭右开
[startTime, endTime)。
reservation.version 为乐观锁字段,防止并发审批互相覆盖。
2.1 数据表
| 表 |
关键字段 |
说明 |
sys_user |
username(UK), password(BCrypt), role, enabled |
三类角色 |
lab |
code(UK), capacity, status, open_time, close_time |
开放时间参与冲突校验 |
device |
code(UK), lab_id, status |
设备必须归属实验室 |
course |
code(UK), teacher_id, lab_id, student_count, start/end_date, weekdays, start/end_time |
自带排课规则 |
reservation_series |
user_id, lab_id, device_id, type, weekdays, 起止日期/时间 |
重复规则原始记录 |
reservation |
lab_id, device_id, course_id, user_id, start/end_time, status, version |
冲突检测主体,建立 (lab_id, start_time, end_time) 等索引 |
3. 状态机模型
3.1 资源状态机(实验室 / 设备共用规则)
| 起始 \ 目标 |
IDLE |
RESERVED |
IN_USE |
MAINTENANCE |
| IDLE |
— |
✅ |
❌(需经 RESERVED) |
✅ |
| RESERVED |
✅ |
— |
✅ |
❌(需先释放) |
| IN_USE |
✅ |
❌(需先释放) |
— |
❌ |
| MAINTENANCE |
✅ |
❌ |
❌ |
— |
- 实现:
EnumStateMachine 通用基类 + LabStateMachine / DeviceStateMachine 注册迁移表;
非法迁移抛出业务错误 3003。
- 资源状态由预约生命周期驱动:
ReservationService.syncLabStatus/syncDeviceStatus
根据「是否存在 IN_USE / 占用态预约」推导目标状态,并沿状态机的合法路径推进
(如 IDLE → RESERVED → IN_USE)。
- 维护中的资源不会被状态同步覆盖,保证报修期间状态稳定。
3.2 预约状态机
4. 预约冲突检测算法
4.1 规则清单
| 类型 |
触发条件 |
错误码 |
INVALID_TIME_RANGE |
时间段为空或 end <= start |
3002 |
PAST_TIME |
开始时间早于 now - 容差(30min) |
3002 |
ADVANCE_LIMIT_EXCEEDED |
开始时间晚于 now + 60 天 |
3002 |
DURATION_EXCEEDED |
单段时长 > 8 小时 |
3002 |
OUTSIDE_OPENING_HOURS |
不在实验室开放时间内或跨天 |
3002 |
RESOURCE_UNAVAILABLE |
实验室/设备处于维护中 |
3002 |
CAPACITY_EXCEEDED |
人数 > 实验室容量 |
3002 |
COURSE_REQUIRED / COURSE_LAB_MISMATCH / COURSE_NOT_ACTIVE |
课程绑定校验 |
3002 / 2203 |
INTERNAL_OVERLAP |
请求内部多个时间段互相重叠 |
3002 |
LAB_CONFLICT / DEVICE_CONFLICT / USER_CONFLICT |
与已有占用态预约重叠 |
3002 |
分类优先级:同实验室 > 同设备 > 同用户,一条已有预约最多产生一条冲突。
4.2 区间重叠算法
采用 排序 + 双指针扫描,而不是两两比较:
- 重叠判定:
a.start < b.end && b.start < a.end,因此 10:00-12:00 与
12:00-14:00 不算冲突(首尾相接合法)。
- 复杂度:排序
O((n+m) log(n+m)),扫描近似线性;单次预约时 n=1,实际为一次索引查询。
- 由于
existing 按 start 单调递增,pointer 只前进不回退。
4.3 数据库层粗筛与并发控制
resources/mapper/ReservationMapper.xml 中的 selectConflicting:
- 利用
(lab_id, start_time, end_time) 等索引把候选集缩小到极少量行,
精确判断交给无副作用的 ConflictDetector。
- 创建/审批在同一个
@Transactional 中先加锁查询、再检测、最后写入,
避免「先查后写」竞态;审批时还会排除自身做二次冲突复检,
防止多条待审批预约互相重叠。
4.4 边界情况(均有单元测试覆盖)
| 场景 |
期望 |
已有 [9:00,11:00),请求 [11:00,13:00) |
不冲突(相接) |
已有 [9:00,11:00),请求 [10:59,13:00) |
冲突 |
| 已有区间完全包含请求区间 |
冲突 |
| 请求时间段的 end == start |
INVALID_TIME_RANGE |
开始时间等于 now - 30min |
允许(容差) |
开始时间等于 now + 60 天 |
允许(边界含) |
| 时长恰好 8 小时 |
允许 |
| 跨天但结束于 00:00 |
允许 |
| 状态为 CANCELLED/REJECTED/COMPLETED 的预约 |
不参与冲突 |
| 重复规则命不中任何日期 / 超过 60 场 |
抛 INVALID_RECURRENCE_RULE |
| 请求内部两段互相重叠 |
INTERNAL_OVERLAP |
5. RBAC 权限矩阵
| 操作 |
管理员 |
教师 |
学生 |
| 查看实验室/设备/课程/日历 |
✅ |
✅ |
✅ |
| 实验室/设备增删改、状态流转 |
✅ |
❌ |
❌ |
| 课程增删改(仅本人课程) |
✅(任意) |
✅ |
❌ |
| 发起单次/多次预约 |
✅(自动通过) |
✅(自动通过) |
✅(待审批) |
| 发起课程绑定预约 |
✅ |
✅(仅本人课程) |
❌ |
| 审批预约 |
✅(除自己) |
✅(除自己) |
❌ |
| 取消/开始/完成预约 |
✅(任意) |
仅本人 |
仅本人 |
| 查看预约列表 |
全部 |
全部 |
仅本人(强制过滤) |
| 用户管理 |
✅ |
❌ |
❌ |
实现:@RequireRole 注解 + AuthInterceptor(JWT 解析、角色校验),
服务层再做数据级权限(所有者校验、授课教师校验)。
6. 接口设计
统一响应:{ code, message, data },code = 0 表示成功。
| 分组 |
接口 |
| 认证 |
POST /api/auth/login、GET /api/auth/me、POST /api/auth/logout |
| 实验室 |
GET /api/labs、GET /api/labs/all、GET /api/labs/{id}、GET /api/labs/available、POST/PUT/DELETE、PATCH /{id}/status |
| 设备 |
GET /api/devices、GET /api/devices/by-lab/{labId}、POST/PUT/DELETE、PATCH /{id}/status |
| 课程 |
GET /api/courses、GET /api/courses/all、POST/PUT/DELETE、PATCH /{id}/finish |
| 预约 |
POST /api/reservations、/recurring、/course、/check;GET /api/reservations、/{id}、/calendar;`POST /{id}/approve |
| 用户 |
GET/POST/PUT/DELETE /api/users/** |
7. 测试设计
7.1 测试分层
| 层次 |
代表用例 |
目标 |
| 算法单元测试 |
ConflictDetectorTest(40 例)、TimeSlotTest、RecurrenceExpanderTest |
全规则 + 边界值,纯函数无数据库 |
| 状态机测试 |
StateMachineTest |
全部合法/非法迁移与终态 |
| 服务层测试 |
ReservationServiceTest(44 例)、LabServiceTest 等 |
Mockito 隔离,覆盖权限、状态同步、异常分支 |
| Mapper 集成测试 |
ReservationMapperTest |
XML SQL 的区间语义、状态过滤、FOR UPDATE |
| 接口集成测试 |
ApiIntegrationTest(12 例) |
MockMvc + H2 全链路:登录、RBAC、CRUD、预约生命周期、日历 |
7.2 覆盖率(JaCoCo 门禁)
- 核心包(排除实体/DTO/VO/配置):行覆盖 98.1%,分支覆盖 92.8%
- 门禁:行 ≥ 90%、分支 ≥ 85%,不达标构建失败
- 报告位置:
target/site/jacoco/index.html
8. 扩展功能:日历视图
- 后端
GET /api/reservations/calendar?labId&from&to 返回区间内预约(一次查询整月)。
- 前端「预约日历」页使用 Element Plus
el-calendar 自定义日期单元格,
按状态着色(待审批/已通过/使用中/已完成),点击事件查看详情。
- 与冲突算法共用同一份数据模型,所见即校验依据。
9. 关键设计取舍
| 问题 |
方案 |
原因 |
| 重复预约如何落库 |
系列 + 场次拆行 |
冲突检测/审批/日历统一为区间模型 |
| 冲突检测放数据库还是应用层 |
数据库索引粗筛 + 应用层精判 |
兼顾性能、可测试性与跨库兼容(MySQL 无排他约束) |
| 并发双写 |
FOR UPDATE + 事务内复检 + 乐观锁 |
预约系统写冲突概率低但后果严重 |
| 权限控制 |
JWT 无状态 + 注解拦截 + 服务层数据权限 |
部署简单,规则集中可测 |
| 状态一致性 |
显式状态机 + 资源状态推导 |
避免「有预约却空闲」的脏状态 |