Files
lab/docs/design.md
2026-09-21 12:51:08 +08:00

258 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实验室预约与资源管理平台 - 需求分析与设计说明
## 1. 需求分析
### 1.1 用户角色与需求
| 角色 | 核心诉求 | 主要用例 |
| --- | --- | --- |
| 管理员 ADMIN | 保证资源台账准确、预约秩序可控 | 实验室/设备/课程的增删改查;资源状态流转(报修、启用);用户账号管理;审批任何预约;查看全局日历与统计 |
| 教师 TEACHER | 排课与科研实验,不想被冲突打乱 | 创建/维护自己的实验课程;按课程一键生成整期预约;审批学生预约;发起多次预约(如每周教研) |
| 学生 STUDENT | 快速找到可用资源并预约 | 查询实验室/设备可用性;发起单次/多次预约;查看与取消自己的预约;签到与完成 |
### 1.2 预约场景定义
| 场景 | 输入 | 系统行为 | 对应类型 |
| --- | --- | --- | --- |
| 单次预约 | 实验室(可选设备)、一个开始/结束时间 | 校验冲突后生成 1 条预约;学生进入待审批,教师/管理员直接通过 | `SINGLE` |
| 多次预约 | 日期区间 + 星期集合 + 每日时段 | 按周展开为多个场次,写入 1 条系列 + N 条预约;任一场次冲突则整批拒绝 | `RECURRING` |
| 课程绑定预约 | 课程 ID(可指定设备) | 按课程排课规则展开整期场次,绑定课程 ID;仅授课教师或管理员可发起 | `COURSE` |
**关键业务规则**
1. 冲突判定必须覆盖:同实验室、同设备、同申请人三个维度。
2. 学生预约需要审批;教师/管理员预约自动通过(仍要过冲突校验)。
3. 被驳回/取消/完成的预约不再占用资源。
4. 资源处于「维护中」不可预约;人数不得超过实验室容量。
5. 单次预约 ≤ 8 小时、最多提前 60 天、不得预约过去时间,且需落在实验室开放时间内。
6. 不允许跨天预约(结束时间为次日 00:00 除外)。
## 2. 领域模型
```
┌────────┐ 1 n ┌────────┐ 1 n ┌────────┐
│ Lab │─────────│ Device │ │ Course │
└───┬────┘ └────┬───┘ └───┬────┘
│ 1 │ n │ 1
│ │ │
│ n ┌───────┴─────────────────┘
┌───┴───────────┴───┐
│ Reservation │ n ────── 1 ┌──────────────────┐
│ (场次/时间区间) │ │ ReservationSeries │
└───┬───────────────┘ └──────────────────┘
│ n
│ 1
┌───┴────┐
│ User │
└────────┘
```
**设计要点**
- 「多次预约/课程预约」采用 **系列(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 资源状态机(实验室 / 设备共用规则)
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> RESERVED: 预约通过
IDLE --> MAINTENANCE: 报修/维护
RESERVED --> IN_USE: 开始使用
RESERVED --> IDLE: 取消/驳回/过期
IN_USE --> IDLE: 完成使用
MAINTENANCE --> IDLE: 维护完成
```
| 起始 \ 目标 | 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 预约状态机
```mermaid
stateDiagram-v2
[*] --> PENDING: 学生提交
[*] --> APPROVED: 教师/管理员提交
PENDING --> APPROVED: 审批通过
PENDING --> REJECTED: 驳回
PENDING --> CANCELLED: 本人/管理员取消
APPROVED --> IN_USE: 签到开始
APPROVED --> CANCELLED: 取消
APPROVED --> COMPLETED: 到期自动完结
IN_USE --> COMPLETED: 完成使用
REJECTED --> [*]
CANCELLED --> [*]
COMPLETED --> [*]
```
## 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 区间重叠算法
采用 **排序 + 双指针扫描**,而不是两两比较:
```
输入:requested[0..n)(请求时间段,乱序),existing[0..m)(占用态已有预约)
1. 过滤非法/被忽略/非占用状态的 existing,两侧各按 start 升序排序
2. pointer = 0
3. for each existing r:
4. while pointer < n and requested[pointer].end <= r.start: pointer++ // 完全在 r 之前的请求段
5. for j = pointer; j < n and requested[j].start < r.end; j++:
6. if r.overlaps(requested[j]): 记录冲突(j, r)
输出:冲突列表
```
- 重叠判定:`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`:
```sql
SELECT ... FROM reservation
WHERE start_time < #{end} AND end_time > #{start} -- 区间重叠
AND status IN (...) -- 只取占用态
AND (lab_id = ? [OR device_id = ?] [OR user_id = ?]) -- 三个维度
[AND id <> #{ignoreId}] -- 更新/审批时排除自身
[FOR UPDATE] -- 并发写场景行锁
```
- 利用 `(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|reject|cancel|start|complete` |
| 用户 | `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 门禁)
```
mvn verify
```
- 核心包(排除实体/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 无状态 + 注解拦截 + 服务层数据权限 | 部署简单,规则集中可测 |
| 状态一致性 | 显式状态机 + 资源状态推导 | 避免「有预约却空闲」的脏状态 |