First commit

This commit is contained in:
2026-09-21 12:51:00 +08:00
commit 4fad31e438
145 changed files with 17216 additions and 0 deletions

257
docs/design.md Normal file
View 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 无状态 + 注解拦截 + 服务层数据权限 | 部署简单,规则集中可测 |
| 状态一致性 | 显式状态机 + 资源状态推导 | 避免「有预约却空闲」的脏状态 |