# 实验室预约与资源管理平台 面向学校实验室的场景化预约系统:统一管理**实验室**、**设备**与**实验课程**,支持 **单次预约 / 多次预约 / 课程绑定预约**三种业务模式,内置可测试的**时间冲突检测算法**、 **资源状态机**与**基于角色的访问控制(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` 通用错误。