# 床位管理 & 柜子管理 — 设计规约 > 日期: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 内含 ``,三个面板: | Tab | 内容 | |-----|------| | 基本信息 | 现有房间字段展示 + 编辑表单(房号、楼栋、楼层、容量、类型、性别、租赁类别、月租金)| | 床位管理 | ``:编号、状态 Tag、备注、操作(编辑/删除);顶部工具栏:新增 + 批量生成 | | 柜子管理 | `
`:编号、状态 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:*)