docs: bed & locker management design spec

This commit is contained in:
2026-07-09 11:44:24 +08:00
parent 115ee5206e
commit 543fddbc1e

View File

@@ -0,0 +1,256 @@
# 床位管理 & 柜子管理 — 设计规约
> 日期2026-07-09
> 状态:待评审
> 关联 PRD模块 7 宿舍管理
---
## 1. 概述
在现有宿舍管理Rooms基础上新增**床位管理**和**柜子管理**两个子资源。床位与入住流程强绑定(入住必选床位),柜子可选分配。两者均有独立生命周期(可单独标记维修),不随学生入住/退宿自动创建或删除。
### 1.1 核心设计决策
| 决策 | 选择 |
|------|------|
| 床位/柜子与房间的关系 | Room 1:N Bed / Room 1:N Locker子资源挂在房间下 |
| 床位与入住的关系 | 入住必选床位occupancies 新增 bed_id / locker_id |
| 前端交互方式 | 房间详情 Drawer 内嵌子 Tab床位/柜子),不改动侧边栏 |
| 入住流程改造 | 选房间 → 自动加载可用床位 → 必选床位 + 可选柜子 |
| 权限 | 沿用 room:view / room:edit不新增独立权限 |
---
## 2. 数据模型
### 2.1 新增表
```sql
CREATE TABLE beds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_id INTEGER NOT NULL REFERENCES rooms(id),
bed_number VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'available', -- available / occupied / maintenance
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(room_id, bed_number)
);
CREATE TABLE lockers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_id INTEGER NOT NULL REFERENCES rooms(id),
locker_number VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'available', -- available / occupied / maintenance
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(room_id, locker_number)
);
```
### 2.2 现有表变更
```sql
ALTER TABLE occupancies ADD COLUMN bed_id INTEGER REFERENCES beds(id);
ALTER TABLE occupancies ADD COLUMN locker_id INTEGER REFERENCES lockers(id);
```
- 两个字段均为 **nullable**,历史数据留空
- 新建入住时 bed_id 必填locker_id 可选
### 2.3 状态机
```
available ──入住──▶ occupied ──退宿──▶ available
│ │
└──手动维修──▶ maintenance ──手动恢复──▶ available
```
- `occupied``maintenance`**禁止**,必须先退宿
- `maintenance` 的床位不出现在入住选择列表中
---
## 3. 后端 API
### 3.1 床位
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/rooms/:roomId/beds` | 房间下所有床位列表 |
| GET | `/rooms/:roomId/beds/available` | 仅返回 available 状态床位(入住下拉用) |
| POST | `/rooms/:roomId/beds` | 新增床位 |
| PUT | `/rooms/:roomId/beds/:id` | 编辑床位(编号/状态/备注) |
| DELETE | `/rooms/:roomId/beds/:id` | 删除床位(仅当未被占用时) |
| POST | `/rooms/:roomId/beds/batch` | 批量创建床位(如"一键生成4张床" |
### 3.2 柜子
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/rooms/:roomId/lockers` | 房间下所有柜子列表 |
| GET | `/rooms/:roomId/lockers/available` | 仅返回 available 状态柜子 |
| POST | `/rooms/:roomId/lockers` | 新增柜子 |
| PUT | `/rooms/:roomId/lockers/:id` | 编辑柜子 |
| DELETE | `/rooms/:roomId/lockers/:id` | 删除柜子(仅当未被占用时) |
| POST | `/rooms/:roomId/lockers/batch` | 批量创建柜子 |
### 3.3 入住 API 变更
- `POST /occupancies` 请求体新增 `bedId`(必填)、`lockerId`(可选)
- 创建时校验:`bedId` 对应的床位 status 必须为 `available` 且属于指定 roomId
- 成功后后端事务:创建 occupancy + 更新 bed.status = 'occupied'+ locker 同理)
- `PUT /occupancies/:id`(换房/换床场景)同理校验
- 退宿时check-out恢复 bed/locker 状态为 `available`
### 3.4 房间删除/归档
- 归档房间时,不自动归档床位/柜子(保持数据完整性)
- 删除床位/柜子的 DELETE 为硬删除,仅在未被占用时允许
- 若房间已被归档,其床位/柜子的新增/编辑操作被拒绝
---
## 4. 前端 UI
### 4.1 房间详情 Drawer核心变更
**位置**`apps/admin/src/pages/Rooms/index.tsx`
- 将现有的简单 Modal 替换为 Ant Design `<Drawer>`
- Drawer 内含 `<Tabs>`,三个面板:
| Tab | 内容 |
|-----|------|
| 基本信息 | 现有房间字段展示 + 编辑表单(房号、楼栋、楼层、容量、类型、性别、租赁类别、月租金)|
| 床位管理 | `<Table>`:编号、状态 Tag、备注、操作编辑/删除);顶部工具栏:新增 + 批量生成 |
| 柜子管理 | `<Table>`:编号、状态 Tag、备注、操作编辑/删除);顶部工具栏:新增 + 批量生成 |
- 床位/柜子操作均为就地弹窗Modal不跳页
- 已归档房间:隐藏床位/柜子的新增按钮,操作列仅显示查看
### 4.2 入住登记改造
**位置**`apps/admin/src/pages/Occupancies/index.tsx`
现有"入住登记"弹窗改为**分步表单**或**分组表单**
```
┌─ 基本信息 ────────────────────────┐
│ 学生选择 [Select 搜索] │
│ 房间选择 [Select 搜索] │
│ 入住日期 [DatePicker] │
│ 计费起始 [DatePicker] │
│ 租赁类型 [长租/短租] │
│ 租赁方 [Select] │
└──────────────────────────────────┘
┌─ 床位/柜子分配 ───────────────────┐
│ 床位 * [Select] 空闲 3/4 │
│ 柜子 [Select] 空闲 3/4 (可选)│
└──────────────────────────────────┘
```
- 选择房间后,自动请求 `/rooms/:id/beds/available` 填充床位下拉
- 床位下拉旁显示"空闲 X / 总共 Y"提示
- 柜子下拉从 `/rooms/:id/lockers/available` 加载
- 入住列表表格新增"床位号""柜子号"两列
- 换房时也允许重新选择床位/柜子
### 4.3 宿舍总览RoomVisual
**位置**`apps/admin/src/pages/RoomVisual/index.tsx`
- 房间卡片底部增加床位占用信息:「🛏 2/4 床已占用」
- 统计栏:总床位数、可用床位数(替代或补充现有"可用床位"统计,当前是基于 capacity 计算的)
- 点击卡片进入房间详情时,直接定位到"床位管理" Tab
### 4.4 侧边栏 & 路由
**不变。** 不新增菜单项,不新增路由。所有操作在现有页面内完成。
---
## 5. 迁移策略
### 5.1 数据库迁移
1. 创建 `beds``lockers`
2. ALTER TABLE `occupancies` 添加 `bed_id``locker_id`nullable
3. 根据现有 `rooms.capacity`,为每个房间自动生成 capacity 张默认床位(编号 "1号床"~"N号床"status = 'available'
4. 不自动生成柜子(柜子数量无法从现有数据推断)
### 5.2 数据兼容
- 历史入住记录 bed_id/locker_id 为 NULL前端展示为 "-"
- 现有入住功能不受影响(入住弹窗中床位默认可选第一张可用床,或留空允许不选 — 取决于业务要求)
---
## 6. 边界情况 & 约束
| 场景 | 处理 |
|------|------|
| 床位被占用时标记为维修 | 禁止,提示"请先退宿" |
| 删除已被占用的床位 | 禁止,提示"该床位有人入住" |
| 房间已满,但还有空床位(数据不一致)| 允许入住以床位为准rooms.capacity 降级为展示用途 |
| 入住时选择房间后无可选床位 | 提示"该房间暂无可用床位",阻止提交 |
| 批量导入入住名单 | 模板新增床号列;导入时自动查找或创建床位 |
| 导出 | 床位/柜子数据包含在房间导出中,作为子 sheet |
---
## 7. 实现范围
### 包含
- beds + lockers 实体、迁移、CRUD API
- occupancies 表新增 bed_id/locker_id
- 入住/退宿/换房时床柜状态联动
- 房间详情 Drawer + 床/柜子 Tab
- 入住登记改造(床位必选)
- 宿舍总览卡片的床位统计
### 不包含
- 柜子钥匙管理(后续独立需求)
- 床位/柜子独立的全局列表页(预留 API不做前端
- 床位与学生的独立关联表(通过 occupancy 关联即可)
- 床位/柜子的操作日志(后续统一做时纳入)
---
## 8. 文件清单
### 后端NestJS
| 文件 | 操作 |
|------|------|
| `apps/server/src/entities/bed.entity.ts` | 新增 |
| `apps/server/src/entities/locker.entity.ts` | 新增 |
| `apps/server/src/entities/occupancy.entity.ts` | 修改(加字段)|
| `apps/server/src/rooms/rooms.module.ts` | 修改(注册 Bed/Locker|
| `apps/server/src/rooms/rooms.controller.ts` | 修改(加床位/柜子路由)|
| `apps/server/src/rooms/rooms.service.ts` | 修改(加床柜 CRUD|
| `apps/server/src/occupancies/occupancies.service.ts` | 修改(入住/退宿联动床柜状态)|
| `apps/server/src/occupancies/occupancies.controller.ts` | 修改(接收 bedId/lockerId|
| Migration 脚本 | 新增 |
### 前端React
| 文件 | 操作 |
|------|------|
| `apps/admin/src/pages/Rooms/index.tsx` | 重写 Detail Modal → Drawer + Tabs |
| `apps/admin/src/pages/Occupancies/index.tsx` | 改造入住登记 + 新增列 |
| `apps/admin/src/pages/RoomVisual/index.tsx` | 卡片底部加床位统计 |
---
## 9. 自我审查
- [x] 无 TBD / 占位符
- [x] 数据模型与 API 一致
- [x] 边界情况已覆盖(维修冲突、空床位、历史数据兼容)
- [x] 范围明确(包含/不包含)
- [x] 权限策略清晰(沿用 room:*