# 床位管理 & 柜子管理 — 设计规约

> 日期：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:*）
