Java养老系统源码详解:楼宇床位 care_space 与房态图 Spring Boot 实践
在线演示 :管理端后台
标签:#Java #Spring Boot #MyBatis #养老系统 #建筑管理 #房态图 #care_bed
1. 前言:床号在 care_bed(床位表),不在 care_older(老人档案)
做机构版时,前台同事总问:档案里为啥不直接显示床号?我当时的做法是------老人主档 (care_older)只放人;住没住、住哪 交给入住单;这张床现在什么颜色 看 care_bed.status。楼宇、楼层、房间在 care_space 里级联,选完房型自动生成几张床。
这篇文章把 hub(中台模块) 里和楼宇、床位、房态图相关的代码串一遍:CareSpaceServiceImpl 快速盖楼、CareBedServiceImpl 判断能不能删床、房态 JSON 由 institution(机构模块) 的 CareInstitutionBedRoomStateBridge 来拼。你排查「快速添加失败、房态不刷新、在住床删不掉」时,按这个顺序看就行。
对照 演示站:建筑管理 · 楼宇设置 (四列、快速添加)→ 房型管理 (bedCount 决定每间几张床)→ 接待 · 房态图 (按楼层请求 GET /care/bed/room-state)。本地接口前缀 /admin-api/care/space/*、/admin-api/care/bed/*,请求头带上 tenant-id。

图1 楼宇设置 · 四列级联对应 care_space.type 0/1/2

图2 档案列表 · 床号不在 care_older,避免换床时改老人主档
2. 表结构:care_space(楼宇/楼层/房间)与 care_bed.status(床位状态 0~5)
care_space.type:0 楼宇、1 楼层、2 房间;房间可挂 layoutId(房型)。care_bed 冗余 buildingId/floorId/roomId,便于列表与报表,不必每次 join 树。
bash
@TableName("care_bed")
public class CareBedDO extends CareOrgBaseDO {
@TableId
private Long id;
private String name;
private Long roomId;
private Long floorId;
private Long buildingId;
private Long layoutId;
private BigDecimal bedPrice;
/** 1养老 2护理 */
private Integer bedAttr;
/** 0空闲 1预订 2在住 3请假占用 4维修 5试住 */
private Integer status;
private Long olderId;
private Integer sort;
}
| status | 含义 | 房态图典型色 | institution 谁写入 |
|---|---|---|---|
| 0 | 空闲 | 灰/空床 | 退订、腾床、释床 |
| 1 | 预订 | 绿订 | CareReserveServiceImpl |
| 2 | 在住 | 蓝在床 | CareCheckinServiceImpl.assign |
| 3 | 请假占用 | 橙 | CareLeaveServiceImpl |
| 5 | 试住 | 专用 | CareTryliveServiceImpl |
bash
@TableName("care_space")
public class CareSpaceDO extends CareOrgBaseDO {
@TableId
private Long id;
private String name;
private Long parentId;
private String ancestors;
/** 0楼宇 1楼层 2房间 */
private Integer type;
private Long layoutId;
private Long areaId;
}
- ancestors :
0,buildingId,floorId链,建床时解析出buildingId/floorId写入 bed 冗余列。 - layoutId :仅房间有效;非房间类型在 Service 里
clearLayoutIfNotRoom置空。
房态按楼层拉床时走 Mapper 默认方法,避免 XML 里再写一遍排序:
bash
@Mapper
public interface CareBedMapper extends BaseMapperX<CareBedDO> {
default List<CareBedDO> selectListByFloorId(Long floorId) {
return selectList(new LambdaQueryWrapperX<CareBedDO>()
.eq(CareBedDO::getFloorId, floorId)
.orderByAsc(CareBedDO::getRoomId)
.orderByAsc(CareBedDO::getSort)
.orderByAsc(CareBedDO::getId));
}
}
3. 快速添加整栋楼:CareSpaceServiceImpl.builder(盖楼)与 insertBeds(建床)
「快速添加」一次插入整栋楼:N 层 × M 间 × 房型床位数,事务内完成。
bash
@Override
@Transactional(rollbackFor = Exception.class)
public Long builder(CareSpaceBuilderReqVO reqVO) {
CareRoomLayoutDO layout = careRoomLayoutMapper.selectById(reqVO.getLayoutId());
int bedCount = layout.getBedCount() == null || layout.getBedCount() < 1 ? 1 : layout.getBedCount();
CareSpaceDO building = new CareSpaceDO();
building.setName(reqVO.getBuildingName());
building.setType(TYPE_BUILDING);
building.setParentId(0L);
building.setAncestors("0");
careSpaceMapper.insert(building);
for (int i = 1; i <= reqVO.getStoreyNumber(); i++) {
CareSpaceDO floor = new CareSpaceDO();
floor.setName(i + "层");
floor.setType(TYPE_FLOOR);
floor.setParentId(building.getId());
floor.setAncestors("0," + building.getId());
careSpaceMapper.insert(floor);
for (int r = 1; r <= reqVO.getRoomNumber(); r++) {
CareSpaceDO room = new CareSpaceDO();
room.setName(pad2(r));
room.setType(TYPE_ROOM);
room.setParentId(floor.getId());
room.setAncestors(floor.getAncestors() + "," + floor.getId());
room.setLayoutId(layout.getId());
careSpaceMapper.insert(room);
insertBeds(room, building.getId(), floor.getId(), layout, bedCount);
}
}
return building.getId();
}
| 步骤 | 说明 |
|---|---|
| 读 layout | bedCount、bedPrice 决定每间生成几张床 |
| insert building | ancestors=0 根节点 |
| 双层循环 | 楼层 sort、房间 sort 与 UI 列顺序一致 |
| insertBeds | 默认 status=0 空闲、bedAttr=1 养老 |
真正「一间房几张床」在 insertBeds 里循环插入,名称 01床、02床,价格和房型表一致:
bash
private void insertBeds(CareSpaceDO room, Long buildingId, Long floorId,
CareRoomLayoutDO layout, int bedCount) {
for (int i = 1; i <= bedCount; i++) {
CareBedDO bed = new CareBedDO();
bed.setName(pad2(i) + "床");
bed.setRoomId(room.getId());
bed.setFloorId(floorId);
bed.setBuildingId(buildingId);
bed.setLayoutId(layout.getId());
bed.setBedPrice(layout.getBedPrice());
bed.setBedAttr(1);
bed.setStatus(BED_STATUS_IDLE);
bed.setSort(i);
careBedMapper.insert(bed);
}
}
单房间改房型时,createSpace 在 type=房间 && layoutId!=null 时同样调用 createBedsForRoom。
bash
@Override
@Transactional(rollbackFor = Exception.class)
public Long createSpace(CareSpaceSaveReqVO createReqVO) {
CareSpaceDO space = BeanUtils.toBean(createReqVO, CareSpaceDO.class);
fillParentAndAncestors(space);
clearLayoutIfNotRoom(space);
careSpaceMapper.insert(space);
if (Objects.equals(space.getType(), TYPE_ROOM) && space.getLayoutId() != null) {
createBedsForRoom(space);
}
return space.getId();
}
删改约束(业务层校验,无 DB 外键):有子节点不可删;房间已有床不可乱改父级/房型;在住床存在时不可改房间名。

图3 房态图 · GET /care/bed/room-state?floorId=
4. 删改床位与房态 JSON:isBedOccupied(占用判断)、getRoomState(Bridge 桥接)
hub 侧统一 isBedOccupied:在住、请假、预订、试住或 olderId!=null 时禁止删床、改房间、改养老/护理属性。
bash
private static boolean isBedOccupied(CareBedDO bed) {
return Objects.equals(bed.getStatus(), BED_STATUS_OCCUPIED)
|| Objects.equals(bed.getStatus(), BED_STATUS_LEAVE)
|| Objects.equals(bed.getStatus(), BED_STATUS_RESERVED)
|| Objects.equals(bed.getStatus(), BED_STATUS_TRYLIVE)
|| bed.getOlderId() != null;
}
@Override
public void updateBed(CareBedSaveReqVO updateReqVO) {
CareBedDO exists = validateExists(updateReqVO.getId());
if (isBedOccupied(exists) && !Objects.equals(updateReqVO.getRoomId(), exists.getRoomId())) {
throw exception(BED_OCCUPIED);
}
// ...
}
getBedTree 把 care_space 与 care_bed 拼成「楼宇→楼层→房间→床位」树,供 床位管理 与筛选;房间节点附带 layoutName、区域名(CareAreaNameBridge 可选 SPI)。
房态图数据不在 hub 硬编码:CareBedRoomStateBridge 由 institution 实现,聚合 checkin、老人姓名、预订态等。
bash
@Autowired(required = false)
private CareBedRoomStateBridge roomStateBridge;
@Override
public CareBedRoomStateRespVO getRoomState(Long floorId) {
if (roomStateBridge == null) {
throw exception(BED_ROOM_STATE_REQUIRES_INSTITUTION);
}
return roomStateBridge.buildRoomState(floorId);
}
- required=false:仅 hub 演示包时房态接口会抛业务错,提示需引入 institution JAR(与 Maven 交付一致)。
institution 里 CareInstitutionBedRoomStateBridge 会 按楼层批量查 bed ,再 join 预订/试住/在住 checkin/请假,拼成前端房卡需要的 viewState(例如请假占床、住院展示态与维修态分开):
bash
@Component
public class CareInstitutionBedRoomStateBridge implements CareBedRoomStateBridge {
@Override
public CareBedRoomStateRespVO buildRoomState(Long floorId) {
List<CareBedDO> beds = careBedMapper.selectListByFloorId(floorId);
// 统计空床/预订/在住数量 → setEmptyBedCount / setReserveCount / setLiveCount
Map<Long, CareReserveDO> reserveByBed = /* state=0 的 care_reserve 按 bedId */;
Map<Long, CareCheckinDO> checkinByBed = /* status=3 在住按 bedId */;
for (CareSpaceDO room : rooms) {
for (CareBedDO bed : bedsByRoom.get(room.getId())) {
rn.getChildren().add(toRoomStateBed(bed, olderMap, reserveByBed, ...));
}
}
return resp;
}
}
- 批量 Map:避免房态图一页 N+1 查老人姓名;姓名脱敏在前端或 VO 里做。
- 预订态 :hub 只把 bed 标成
status=1,卡片上的计划入住时间来自care_reserve(下一篇讲CareReserveServiceImpl)。

图4 床位统计 · 汇总依赖 bed.status 与在住 checkin 一致

图5 空床从房态图进预订/入住 · 颜色与 bed.status 一致
5. 后台接口:CareSpaceController(楼宇)、CareBedController(床位)
空间与床位 REST 前缀固定,Swagger 分组「机构空间 / 床位」。
bash
@RestController
@RequestMapping("/care/space")
public class CareSpaceController {
@PostMapping("/builder")
@PreAuthorize("@ss.hasPermission('care:space:create')")
public CommonResult<Long> builder(@Valid @RequestBody CareSpaceBuilderReqVO reqVO) {
return success(careSpaceService.builder(reqVO));
}
}
bash
@RestController
@RequestMapping("/care/bed")
public class CareBedController {
@GetMapping("/room-state")
@PreAuthorize("@ss.hasPermission('care:bed:query')")
public CommonResult<CareBedRoomStateRespVO> getRoomState(@RequestParam("floorId") Long floorId) {
return success(careBedService.getRoomState(floorId));
}
@GetMapping("/tree")
@PreAuthorize("@ss.hasPermission('care:bed:query')")
public CommonResult<List<CareBedTreeNodeVO>> getBedTree(@Valid CareBedTreeReqVO reqVO) {
return success(careBedService.getBedTree(reqVO));
}
}
| 接口 | 前端视图 |
|---|---|
POST /care/space/builder |
楼宇设置 · 快速添加 |
GET /care/bed/tree |
床位管理树 |
GET /care/bed/room-state |
房态图按楼层刷新 |
6. curl 联调
快速添加(演示站等价于楼宇设置弹窗提交):
bash
curl -X POST "http://localhost:48080/admin-api/care/space/builder" \
-H "Authorization: Bearer {token}" \
-H "tenant-id: 123" \
-H "Content-Type: application/json" \
-d '{"buildingName":"A栋","storeyNumber":3,"roomNumber":10,"layoutId":1}'
查床位树、房态:
bash
curl -G "http://localhost:48080/admin-api/care/bed/tree" \
-H "Authorization: Bearer {token}" \
-H "tenant-id: 123" \
--data-urlencode "layoutId="
bash
curl -G "http://localhost:48080/admin-api/care/bed/room-state" \
-H "Authorization: Bearer {token}" \
-H "tenant-id: 123" \
--data-urlencode "floorId=1001"
成功:{"code":0,"data":{"rooms":[...]}}。若 code 提示需 institution,检查 zhijian-server/pom.xml 是否依赖 zhijian-module-institution。
application-local.yaml
bash
yudao:
tenant:
enable: true
spring:
profiles:
active: local
7. 谁改床位状态:institution(机构模块)写 bed.status,hub(中台)只管楼宇结构
同一张 care_bed 行会被 institution 在 预订 / 试住 / assign 在住 / 请假 / 腾床 时改 status 和 olderId;hub 不负责写 checkin,只负责 能不能删床、能不能改房间 。我线上查房态不对,一般是 bed.status 和 checkin 不同步 (例如手工改库只改了 checkin),其次才是 tenant-id 没带 看到别园数据。
8. 踩坑
- 房态接口报需 institution :
zhijian-server/pom.xml没引zhijian-module-institution,或 Bridge 实现没进 Spring 扫描。 - 删房间报 SPACE_HAS_BEDS:房间下还有 bed 行;在住/预订要先走退住或退订释床。
- 改房间号抛 BED_OCCUPIED :在住、预订、试住、请假占用都拦
roomId变更------别和财务对账打架。 - ancestors 手工改乱 :
bed.buildingId/floorId和 room 对不上,报表按楼汇总会错;尽量走 Service 改结构。 - 快速添加后房态空 :确认
layoutId对应房型bedCount>0,且 floorId 与房态图下拉选的是同一层。
Gitee 社区版源码 :gitee.com/xinzexingji...