First commit
This commit is contained in:
257
docs/design.md
Normal file
257
docs/design.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 实验室预约与资源管理平台 - 需求分析与设计说明
|
||||
|
||||
## 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 无状态 + 注解拦截 + 服务层数据权限 | 部署简单,规则集中可测 |
|
||||
| 状态一致性 | 显式状态机 + 资源状态推导 | 避免「有预约却空闲」的脏状态 |
|
||||
Reference in New Issue
Block a user