Files
gongxue-base/docs/superpowers/specs/2026-07-09-bed-locker-management-design.md

257 lines
9.5 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.

# 床位管理 & 柜子管理 — 设计规约
> 日期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:*