# 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 树。
@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 |
@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 里再写一遍排序:
@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 间 × 房型床位数,事务内完成。
@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床,价格和房型表一致:
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。
@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时禁止删床、改房间、改养老/护理属性。
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、老人姓名、预订态等。
@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(例如请假占床、住院展示态与维修态分开):
@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 分组「机构空间 / 床位」。
@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)); } }@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 联调
快速添加(演示站等价于楼宇设置弹窗提交):
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}'查床位树、房态:
curl -G "http://localhost:48080/admin-api/care/bed/tree" \ -H "Authorization: Bearer {token}" \ -H "tenant-id: 123" \ --data-urlencode "layoutId="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
yudao: tenant: enable: true spring: profiles: active: local7. 谁改床位状态: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 社区版源码:https://gitee.com/xinzexingjian-data_0/older_care