Files
lab/README.md

243 lines
12 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.
# 实验室预约与资源管理平台
面向学校实验室的场景化预约系统:统一管理**实验室**、**设备**与**实验课程**,支持
**单次预约 / 多次预约 / 课程绑定预约**三种业务模式,内置可测试的**时间冲突检测算法**、
**资源状态机**与**基于角色的访问控制(RBAC)**,并提供 Vue3 前端与日历视图。
## 技术栈
| 层次 | 技术 |
| --- | --- |
| 后端 | Spring Boot 4.1.1、MyBatis-Plus 3.5.17(XML 自定义 SQL + QueryWrapper)、JWT(jjwt)、BCrypt |
| 数据库 | H2(默认,免安装演示)/ MariaDB(`mariadb` Profile,schema 脚本已提供) |
| 测试 | JUnit 5、Mockito、MockMvc、AssertJ、JaCoCo(覆盖率门禁) |
| 前端 | Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router + Axios,包管理 pnpm |
## 快速开始
### 1. 启动后端(默认 H2 文件库,无需安装数据库)
```bash
./mvnw spring-boot:run
# 或者:mvn spring-boot:run
```
- 服务地址:http://localhost:8080
- H2 控制台:http://localhost:8080/h2-console (JDBC URL:`jdbc:h2:file:./data/labdb`,用户 `sa`,密码留空)
- 首次启动会自动建表并写入演示数据(`src/main/resources/db/schema.sql`、`data.sql`)
使用 MariaDB/MySQL 时:
```bash
# 先创建数据库:CREATE DATABASE lab DEFAULT CHARSET utf8mb4;
DB_HOST=localhost DB_USER=root DB_PASSWORD=yourpass \
mvn spring-boot:run -Dspring-boot.run.profiles=mariadb
```
### 2. 启动前端
```bash
cd frontend
pnpm install
pnpm dev # http://localhost:5173,已配置 /api 代理到 8080
pnpm build # 生产构建(vue-tsc 类型检查 + vite build)
```
### 3. Docker 一键部署(推荐用于演示/验收)
```bash
cp .env.example .env # 可选:按需修改端口/密码/JWT 密钥
docker compose up -d --build # 构建并启动 mariadb + backend + frontend
# 前端:http://localhost:8088 (Nginx 托管静态资源并反代 /api)
# 后端:http://localhost:8080
docker compose ps # 查看健康状态
docker compose down # 停止容器,保留数据库数据卷
docker compose down -v # 停止容器并删除数据目录(下次启动重建演示数据)
```
- 编排:`mariadb:11.4`(数据持久化到命名卷)+ 后端多阶段构建镜像(JRE 21,非 root 运行)+ Nginx 前端镜像
- 三个服务均配置健康检查,后端与前端会等待上游就绪后再启动
- 国内加速:后端构建自动使用**阿里云 Maven 镜像**(`docker/maven-settings.xml`),
前端使用 **npmmirror**(淘宝 npm 镜像);如需走官方源:
`docker build --build-arg MAVEN_MIRROR=false .`、`docker build --build-arg NPM_REGISTRY=https://registry.npmjs.org ./frontend`
- 可用环境变量(见 `.env.example`):`WEB_PORT`(默认 80)、`BACKEND_PORT`(默认 8080)、
`DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DB_ROOT_PASSWORD`、`LAB_JWT_SECRET`、`JAVA_OPTS`
### 4. Podman 部署
Podman 的 compose 实现与 Docker Compose 在「镜像名/容器名解析」上存在差异,
直接 `podman compose up -d` 可能出现以下报错:
```
Error: short-name "lab-reservation_backend" did not resolve to an alias ...
Error: "lab-backend" is not a valid container, cannot be used as a dependency ...
```
原因是 compose 未先构建本地镜像,Podman 便把 `lab-reservation_backend` 当作待拉取的短名镜像,
而系统未配置 `unqualified-search-registries`。**先显式构建镜像即可解决**:
```bash
./scripts/podman-deploy.sh up # 一键:构建镜像 -> 按序启动 -> 健康检查 -> 打印访问地址
./scripts/podman-deploy.sh status # 查看容器状态
./scripts/podman-deploy.sh down # 停止(保留数据目录)
./scripts/podman-deploy.sh down-v # 停止并删除数据目录(重建演示数据)
./scripts/podman-deploy.sh logs # 跟踪后端日志
./scripts/podman-deploy.sh diagnose # 部署失败时输出诊断信息(镜像/容器/日志)
```
脚本做了四件关键的事,规避 podman-compose 的兼容性问题:
1. `podman build` 显式构建 `lab-backend:latest` / `lab-frontend:latest`,避免短名拉取;
2. 启动前自动生成 `.podman-compose.generated.yml`(从 `docker-compose.yml` 中剔除 `depends_on`),
避免旧版 podman-compose 在 `up` 时等待依赖健康检查而卡住;
3. 按「数据库 → 后端 → 前端」逐个启动并等待就绪(MariaDB 用 `healthcheck.sh`,后端/前端用 HTTP 探测);
4. `down` 直接按容器名清理,避免对不存在容器的报错噪音。
**防卡死**:所有外部命令都带超时,任何一步超时会打印原因并退出,不会无限等待。
可用环境变量调节:`CMD_TIMEOUT`(单条命令,默认 60s)、`BUILD_TIMEOUT`(构建,默认 1800s)、
`READY_TRIES`(就绪轮询次数,默认 60 次 × 2s);用 `DEBUG=1 ./scripts/podman-deploy.sh up`
可以看到每一步实际执行的命令,定位卡在哪一步。
镜像已构建过时可跳过构建:`./scripts/podman-deploy.sh up --no-build`。
若已有历史失败状态,先清理再重来:
```bash
./scripts/podman-deploy.sh down-v && ./scripts/podman-deploy.sh up
```
手动等价步骤:
```bash
podman build -t lab-backend:latest .
podman build -t lab-frontend:latest ./frontend
podman compose up -d # 或 podman-compose up -d
```
Podman 注意事项:
- **端口**:前端默认映射到 `8088`(rootless Podman 无法绑定 80 等特权端口),
如需 80 可 `WEB_PORT=80 ./scripts/podman-deploy.sh up`,或将
`net.ipv4.ip_unprivileged_port_start=0` 写入 `/etc/sysctl.d/` 后 `sysctl --system`
- **短名镜像**:本项目所有基础镜像已写成全限定名(`docker.io/library/...`)。
若自行添加镜像,建议同样全限定,或在
`~/.config/containers/registries.conf` 配置 `unqualified-search-registries = ["docker.io"]`
- **旧版 podman-compose**:若提示不支持 `depends_on.condition`,请升级到 1.0.6+,
或先 `podman-compose up -d mariadb`,等数据库健康后再 `podman-compose up -d backend frontend`
- **数据目录**:数据库数据以 bind mount 方式保存在 `DATA_DIR`(默认 `./mariadb-data`,已在
`.gitignore` 中忽略),`down-v` 会删除该目录;目录中含历史 binlog 等文件时属正常现象
- **SELinux(Fedora/RHEL)**:挂载已带 `:Z` 自动打标;若自定义其他 bind mount 请同样加 `:Z`
### 5. 演示账号
| 账号 | 密码 | 角色 | 典型能力 |
| --- | --- | --- | --- |
| `admin` | `admin123` | 管理员 | 全部资源与用户管理、审批、状态流转 |
| `teacher1` | `123456` | 教师 | 课程管理、发起课程预约、审批 |
| `student1` | `123456` | 学生 | 发起预约、查看/取消自己的预约 |
## 功能地图
- **认证与权限**:JWT 登录、`@RequireRole` 角色注解、拦截器统一鉴权,前端路由/菜单按角色过滤
- **实验室管理**:增删改查、状态机流转(空闲 / 已预约 / 使用中 / 维护中)、可用实验室推荐
- **设备管理**:增删改查、归属实验室校验、状态机流转
- **实验课程**:教师排课(起止日期 + 星期 + 时间段)、一键生成整期课程绑定预约
- **预约中心**:
- 单次预约(datetimerange)
- 多次预约(按周重复,自动拆分为系列场次)
- 课程绑定预约(按课程排课规则批量生成)
- 冲突预检、审批(通过/驳回)、取消(可整系列)、开始/完成使用
- **日历视图**:按实验室 + 月份展示预约分布,点击查看详情
- **自动任务**:每 10 分钟自动完结已过期的预约并同步资源状态
## 核心设计
### 冲突检测
分层设计:数据库层用索引粗筛(XML 自定义 SQL,可选 `FOR UPDATE` 行锁),
应用层 `ConflictDetector` 做精确判断。
- 区间语义:左闭右开 `[start, end)`,**首尾相接不算冲突**
- 规则校验:时间区间合法性、过去时间、提前预约上限、单次时长、开放时间/跨天、
资源维护状态、容量、课程绑定关系、请求内部重叠
- 冲突分类:`LAB_CONFLICT` / `DEVICE_CONFLICT` / `USER_CONFLICT` 等,返回多冲突明细
- 并发控制:同事务内 `SELECT ... FOR UPDATE` + 冲突复检,避免并发双写
详细说明见 [docs/design.md](docs/design.md)。
### 状态机
- 实验室/设备:`IDLE → RESERVED → IN_USE → IDLE`,`IDLE ↔ MAINTENANCE`,非法迁移抛 `3003`
- 预约:`PENDING → APPROVED → IN_USE → COMPLETED`,`PENDING → REJECTED`,
`PENDING/APPROVED → CANCELLED`,终态不可再流转
## 测试与覆盖率
```bash
mvn verify # 运行全部测试并执行 JaCoCo 覆盖率门禁
```
- 测试规模:**219** 个用例(19 个测试类)(冲突算法边界、状态机、服务层 Mockito、Mapper/接口集成)
- 覆盖率(核心包,排除实体/DTO/VO/配置):**行 98.1%,分支 92.8%**
- 门禁:行 ≥ 90%,分支 ≥ 85%,不达标 `mvn verify` 直接失败
- 报告:`target/site/jacoco/index.html`
## 目录结构
```
├── src/main/java/com/example/lab
│ ├── common/ # 统一响应、异常、注解、上下文、JWT
│ ├── config/ # MyBatis-Plus、Web、安全、业务参数
│ ├── controller/ # REST 接口
│ ├── domain/
│ │ ├── conflict/ # 时间复杂度可控的冲突检测与重复规则展开
│ │ ├── entity/ # 实体
│ │ ├── enums/ # 角色/状态/类型枚举
│ │ └── statemachine/# 通用枚举状态机
│ ├── dto/ vo/ # 请求与视图对象
│ ├── mapper/ # MyBatis-Plus Mapper
│ ├── security/ # 认证授权拦截器
│ └── service/ # 业务服务
├── src/main/resources
│ ├── db/ # H2 / MariaDB 建表与演示数据
│ └── mapper/ # XML 自定义 SQL
├── src/test/java/... # 单元测试 + 集成测试
├── frontend/ # Vue3 + Element Plus 前端
│ ├── Dockerfile # pnpm 构建 + Nginx 托管(含 /api 反向代理)
│ └── nginx.conf
├── Dockerfile # 后端多阶段构建镜像
├── docker-compose.yml # mariadb + backend + frontend 一键编排
├── .env.example # 部署环境变量示例(复制为 .env 使用)
├── scripts/podman-deploy.sh # Podman 一键部署脚本
└── docs/design.md # 需求分析与详细设计
```
## 主要接口
| 方法 | 路径 | 说明 | 权限 |
| --- | --- | --- | --- |
| POST | `/api/auth/login` | 登录 | 公开 |
| GET | `/api/auth/me` | 当前用户 | 登录 |
| GET | `/api/labs` / `/api/labs/available` | 实验室列表 / 可用推荐 | 登录 |
| POST/PUT/DELETE | `/api/labs/**` | 实验室管理 | 管理员 |
| GET | `/api/devices` / `/api/devices/by-lab/{id}` | 设备列表 | 登录 |
| POST/PUT/DELETE | `/api/devices/**` | 设备管理 | 管理员 |
| GET | `/api/courses` | 课程列表 | 登录 |
| POST/PUT/DELETE | `/api/courses/**` | 课程管理 | 管理员/教师 |
| POST | `/api/reservations` | 单次预约 | 登录 |
| POST | `/api/reservations/recurring` | 多次预约 | 登录 |
| POST | `/api/reservations/course` | 课程绑定预约 | 管理员/授课教师 |
| POST | `/api/reservations/check` | 冲突预检 | 登录 |
| GET | `/api/reservations` / `/{id}` / `/calendar` | 列表 / 详情 / 日历 | 登录(学生仅本人) |
| POST | `/api/reservations/{id}/approve\|reject` | 审批 | 管理员/教师 |
| POST | `/api/reservations/{id}/cancel\|start\|complete` | 取消/开始/完成 | 本人或管理员 |
| GET/POST/PUT/DELETE | `/api/users/**` | 用户管理 | 管理员 |
统一响应格式:
```json
{ "code": 0, "message": "success", "data": {} }
```
业务错误码:`0` 成功、`1xxx` 用户与认证、`2xxx` 资源管理、`3xxx` 预约与冲突、`4xx/5xx` 通用错误。