| | |
| | | import cn.lihu.jh.module.ecg.controller.admin.room.vo.MonitorInfoVO; |
| | | import cn.lihu.jh.module.ecg.dal.dataobject.checktype.CheckTypeDO; |
| | | import cn.lihu.jh.module.ecg.dal.dataobject.queue.QueueDO; |
| | | import cn.lihu.jh.module.ecg.dal.dataobject.queue.QueueStatisticDO; |
| | | import cn.lihu.jh.module.ecg.dal.dataobject.room.RoomDO; |
| | | import cn.lihu.jh.module.ecg.dal.mysql.call.CallMapper; |
| | | import cn.lihu.jh.module.ecg.dal.mysql.checktype.CheckTypeMapper; |
| | | import cn.lihu.jh.module.ecg.dal.mysql.devrent.DevRentMapper; |
| | | import cn.lihu.jh.module.ecg.dal.mysql.queue.QueueMapper; |
| | | import cn.lihu.jh.module.ecg.dal.mysql.room.RoomMapper; |
| | | import cn.lihu.jh.module.ecg.enums.BedOpTypeEnum; |
| | | import cn.lihu.jh.module.ecg.enums.BedStatusEnum; |
| | | import cn.lihu.jh.module.ecg.enums.QueueStatusEnum; |
| | | import cn.lihu.jh.module.infra.api.config.ConfigApi; |
| | |
| | | |
| | | |
| | | AtomicInteger openingFlag = new AtomicInteger(0); |
| | | AtomicInteger curSeqNum = new AtomicInteger(0); |
| | | |
| | | ConcurrentHashMap<String, BedQueueBO > mapBedVsQueue = new ConcurrentHashMap<>(); |
| | | // 【号统一(本需求)】原 curSeqNum(分流时给患者编"内部序号"的内存计数器)已删除。 |
| | | // 排队号改为签到时一次性写入 queue.seq_num,之后任何环节都不再赋号: |
| | | // 分流、叫号后补齐候诊、过号、召回、加急、装机都只改状态/诊室/工位。 |
| | | |
| | | // 装机工位..不进入优先队列 |
| | | Map<Integer, PriorityBlockingQueue<BedQueueBO>> mapCheckTypePriorityQueue = new HashMap(); |
| | | // 维护 开放的工位 |
| | | ConcurrentHashMap<String, BedBO> mapOpeningBed = new ConcurrentHashMap<>(); |
| | | |
| | | // 基于类型的, 装机准备的优先队列 |
| | | // Map<Integer, PriorityBlockingQueue<BedQueueBO>> mapReadyPriorityQueue = new HashMap(); |
| | | |
| | | // 装机工位..不进入分流队列 |
| | | ConcurrentHashMap<Integer, PriorityBlockingQueue<BedBO>> mapCheckTypeRoutingQueue = new ConcurrentHashMap<>(); |
| | | |
| | | Map<Integer, CheckTypeDO> mapCheckTypeVsReadyMax = null; |
| | | |
| | | /** |
| | |
| | | * @return |
| | | */ |
| | | public ErrorCode bedOpen(Long roomId, String roomName, String bedNo) { |
| | | BedQueueBO bedQueueBO2 = mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null != bedQueueBO2) { |
| | | BedBO bedBO2 = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null != bedBO2) { |
| | | log.error("bedOpen mapBedVsQueue has existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_EXIST; |
| | | } |
| | |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | List<Integer> queueStatusList = new ArrayList<>(); |
| | | queueStatusList.add(QueueStatusEnum.READY.getStatus()); |
| | | List<QueueDO> queueDOList = queueMapper.getBedQueueByStatus(roomId, bedNo, queueStatusList); |
| | | |
| | | RoomDO roomDO = roomMapper.getRoom(roomId, bedNo); |
| | | |
| | | // 新增..工位队列 |
| | | BedQueueBO bedQueueBO = new BedQueueBO(); |
| | | bedQueueBO.setRoomId(roomId); |
| | | bedQueueBO.setRoomName(roomName); |
| | | bedQueueBO.setBedNo(bedNo); |
| | | bedQueueBO.setMaxQueueNum(getBedReadyMax(roomId, bedNo)); |
| | | bedQueueBO.setQueueNum(new AtomicInteger(queueDOList.size())); |
| | | bedQueueBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | bedQueueBO.setCheckTypes( roomDO.getCheckTypes() ); |
| | | bedQueueBO.setOpType( roomDO.getOpType() ); |
| | | mapBedVsQueue.put( Utils.formatRoomBed(roomId, bedNo), bedQueueBO); |
| | | BedBO bedBO = new BedBO(); |
| | | bedBO.setRoomId(roomId); |
| | | bedBO.setRoomName(roomName); |
| | | bedBO.setBedNo(bedNo); |
| | | bedBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | bedBO.setCheckTypes( roomDO.getCheckTypes() ); |
| | | bedBO.setOpType( roomDO.getOpType() ); |
| | | mapOpeningBed.put( Utils.formatRoomBed(roomId, bedNo), bedBO); |
| | | |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | public ErrorCode bedClose(Long roomId, String bedNo) { |
| | | BedQueueBO bedQueueBO = mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedQueueBO) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedBO) { |
| | | log.error("bedClose mapBedVsQueue DONOT existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | // 除了判断 准备候诊中 的人数,还需要 判断 过号的人数 |
| | | //if (bedQueueBO.getQueueNum().get() >0) |
| | | // return QUEUE_HAVE_PATIENT; |
| | | // 检查是否还有患者需要处理 |
| | | List<Integer> queueStatusList = new ArrayList<>(); |
| | | queueStatusList.add(QueueStatusEnum.READY.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.ONSTAGE.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.INSTALLING.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.PASSED.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.PASSED_INSTALL.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.RECALLED.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.RECALLED_INSTALL.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.AFFINITY.getStatus()); |
| | | queueStatusList.add(QueueStatusEnum.AFFINITY_INSTALL.getStatus()); |
| | | List<QueueDO> queueDOList = queueMapper.getBedQueueByStatus(roomId, bedNo, queueStatusList); |
| | | if (queueDOList.size() > 0) |
| | | return QUEUE_HAVE_PATIENT; |
| | |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.OPENING); |
| | | statusList.add(BedStatusEnum.DOCTOR_ON); |
| | | statusList.add(BedStatusEnum.PAUSE); |
| | | statusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | Integer updateNum = roomMapper.setBedOpeningClosed(roomId, bedNo, |
| | | BedStatusEnum.CLOSED, statusList); |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | mapBedVsQueue.remove( Utils.formatRoomBed(roomId, bedNo) ); |
| | | removePriorityQueue(bedQueueBO); |
| | | mapOpeningBed.remove( Utils.formatRoomBed(roomId, bedNo) ); |
| | | removeBedFromRoutingQueue(bedBO); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | public ErrorCode bedDoctorPause(Long roomId, String bedNo, Long docId, String docName) { |
| | | BedQueueBO bedQueueBO = mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedQueueBO) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedBO) { |
| | | log.error("bedDoctorPause mapBedVsQueue DONOT existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | |
| | | // DB update |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.DOCTOR_ON); |
| | | Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId, BedStatusEnum.PAUSE, statusList); |
| | | Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId, BedStatusEnum.DOCTOR_PAUSE, statusList); |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | | log.error("bedDoctorPause DB invalid status. " + roomId + " " + bedNo); |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | bedQueueBO.setStatus(BedStatusEnum.PAUSE.getStatus()); |
| | | removePriorityQueue(bedQueueBO); |
| | | bedBO.setStatus(BedStatusEnum.DOCTOR_PAUSE.getStatus()); |
| | | removeBedFromRoutingQueue(bedBO); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | public ErrorCode bedDoctorResume(Long roomId, String bedNo, Long docId, String docName) { |
| | | BedQueueBO bedQueueBO = mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedQueueBO || !bedQueueBO.getStatus().equals(BedStatusEnum.PAUSE.getStatus())) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedBO || !bedBO.getStatus().equals(BedStatusEnum.DOCTOR_PAUSE.getStatus())) { |
| | | log.error("bedDoctorResume mapBedVsQueue DONOT existed OR NOT Paused. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | // DB update |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.PAUSE); |
| | | statusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId, |
| | | BedStatusEnum.DOCTOR_ON, statusList); |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | bedQueueBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus()); |
| | | addPriorityQueue(bedQueueBO); |
| | | bedBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus()); |
| | | addBedToRoutingQueue(bedBO); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | public ErrorCode bedDoctorOn(Long roomId, String bedNo, Long docId, String docName) { |
| | | BedQueueBO bedQueueBO = mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedQueueBO) { |
| | | // ==================== 需求3:停止候诊开关 ==================== |
| | | |
| | | /** |
| | | * 设置「停止候诊」开关。 |
| | | * <p> |
| | | * {@code stop=true}:该工位**不再新增**候诊患者(移出分流队列), |
| | | * 但**已有候诊患者仍可正常叫号**({@code nextPatient} 不检查此开关)。 |
| | | * <p> |
| | | * {@code stop=false}:恢复正常,重新加入分流队列开始接收患者 |
| | | * (前提是工位状态为"医生在座",与 {@code bedReload} 的判定一致)。 |
| | | * <p> |
| | | * 状态写入 {@code clinic_room.stop_ready},以在 {@code bedReload} 后保持。 |
| | | * |
| | | * @return 成功返回 SUCCESS;工位不存在返回 {@link #QUEUE_BED_NOT_EXIST} |
| | | */ |
| | | public ErrorCode setStopReady(Long roomId, String bedNo, boolean stop) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedBO) { |
| | | log.error("[setStopReady] 工位不存在. roomId={} bedNo={}", roomId, bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | // 先落库(失败则不改内存,避免状态不一致) |
| | | // |
| | | // 【部署保护】若数据库尚未执行升级脚本(缺 clinic_room.stop_ready 列), |
| | | // roomMapper.setBedStopReady(...) 会在 Mapper 层直接抛 SQL 异常 |
| | | // (@Update 注解方法没有返回值可判断,异常直接冒泡)。 |
| | | // 这里显式捕获并转成明确的业务错误码,避免把原始 SQL 错误暴露给前端, |
| | | // 便于现场一眼看出是"部署遗漏"而不是"功能坏了"。 |
| | | Integer updateNum; |
| | | try { |
| | | updateNum = roomMapper.setBedStopReady(roomId, bedNo, stop ? 1 : 0); |
| | | } catch (Exception e) { |
| | | log.error("[setStopReady] 写库失败,请确认已执行 _prep/11_需求升级_汇总SQL.sql" |
| | | + "(clinic_room.stop_ready). roomId={} bedNo={}", roomId, bedNo, e); |
| | | return QUEUE_STOP_READY_NOT_CONFIGURED; |
| | | } |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 工位可能尚未在 clinic_room 登记,仅告警但仍允许改内存, |
| | | // 以免因 DB 记录缺失导致医生完全无法使用该功能。 |
| | | log.warn("[setStopReady] DB 未更新到行,仅更新内存. roomId={} bedNo={} stop={}", roomId, bedNo, stop); |
| | | } |
| | | |
| | | bedBO.setStopReady(stop); |
| | | |
| | | // 同步分流队列:停止 → 移出;恢复 → 加入(仅在医生在座时) |
| | | if (stop) { |
| | | removeBedFromRoutingQueue(bedBO); |
| | | } else if (BedStatusEnum.DOCTOR_ON.getStatus().equals(bedBO.getStatus())) { |
| | | addBedToRoutingQueue(bedBO); |
| | | } |
| | | log.info("[setStopReady] roomId={} bedNo={} stopReady={}", roomId, bedNo, stop); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** 查询当前「停止候诊」状态(工位不存在返回 null) */ |
| | | public Boolean getStopReady(Long roomId, String bedNo) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | return null == bedBO ? null : Boolean.TRUE.equals(bedBO.getStopReady()); |
| | | } |
| | | |
| | | /** |
| | | * 入座(把工位主人设为我)。 |
| | | * |
| | | * @param bedId 工位主键(前端请求里带;**优先按它定位**,见下)——可为空,为空时按 roomId+bedNo 定位 |
| | | */ |
| | | public ErrorCode bedDoctorOn(Long roomId, String bedNo, Long docId, String docName, Integer bedId) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | if (null == bedBO) { |
| | | log.error("bedDoctorOn mapBedVsQueue DONOT existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | // DB update |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.OPENING); |
| | | Integer updateNum = roomMapper.setBedDoctorOn(roomId, bedNo, docId, docName, BedStatusEnum.DOCTOR_ON, statusList); |
| | | // 可以"成为主人"的状态:已开通 / 有医生 / 暂停(**排除"已关闭"**)。 |
| | | // 【本次放宽】原来"无主人"时只认"已开通":一旦数据出现"doc_id 为空但状态仍是有医生/暂停" |
| | | // 这种不一致,医生站在工位前却始终坐不上,只报"状态异常",现场无从处理。 |
| | | List<BedStatusEnum> claimableStatusList = new ArrayList<>(); |
| | | claimableStatusList.add(BedStatusEnum.OPENING); |
| | | claimableStatusList.add(BedStatusEnum.DOCTOR_ON); |
| | | claimableStatusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | |
| | | // 【本次修复】优先按**主键**更新:现场出现过"工位在库里明明空闲、入座却更新 0 行", |
| | | // 已排除"被占用";剩下的可能是 bed_no 与请求值有肉眼难辨的差异 |
| | | // (结尾空格 / 大小写 / 全角,JSON 与复制粘贴都看不出来)。前端本来就带 id,用它最稳。 |
| | | Integer updateNum = null; |
| | | if (null != bedId) { |
| | | updateNum = roomMapper.setBedDoctorOnById(bedId, docId, docName, |
| | | BedStatusEnum.DOCTOR_ON, claimableStatusList); |
| | | if (null != updateNum && updateNum > 0) { |
| | | // 校验一下 id 指向的行确实就是请求的工位(不一致说明前端缓存与库不同步,记日志便于排查) |
| | | RoomDO seatRow = roomMapper.getRoom(roomId, bedNo); |
| | | RoomDO idRow = roomMapper.selectById(bedId); |
| | | if (null != idRow && null != seatRow && !idRow.getId().equals(seatRow.getId())) { |
| | | log.warn("[bedDoctorOn] 按 id 入座成功,但 id 指向的工位与请求的 roomId/bedNo 不是同一行!" |
| | | + " id={} → {}/{}, 请求 → {}/{}", |
| | | bedId, idRow.getRoomId(), idRow.getBedNo(), roomId, bedNo); |
| | | } |
| | | } |
| | | } |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 回退:按 roomId + bedNo 定位(兼容不带 id 的调用方,如自动入座的老路径) |
| | | updateNum = roomMapper.setBedDoctorOn(roomId, bedNo, docId, docName, |
| | | BedStatusEnum.DOCTOR_ON, claimableStatusList); |
| | | } |
| | | |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | | log.error("bedDoctorOn DB invalid status. " + roomId + " " + bedNo); |
| | | // 仍失败 → 把库里**真实状态**摊开,并区分原因(原来一律"状态异常",现场看不出所以然) |
| | | RoomDO rowById = (null == bedId) ? null : roomMapper.selectById(bedId); |
| | | RoomDO rowByRoomBed = roomMapper.getRoom(roomId, bedNo); |
| | | log.warn("[bedDoctorOn] 入座更新 0 行. 请求 roomId={} bedNo=[{}] bedId={} docId={};" |
| | | + "按id查到={} 按roomId+bedNo查到={}", |
| | | roomId, bedNo, bedId, docId, |
| | | null == rowById ? "无" : describeBedRow(rowById), |
| | | null == rowByRoomBed ? "无" : describeBedRow(rowByRoomBed)); |
| | | |
| | | RoomDO occupant = (null != rowById) ? rowById : rowByRoomBed; |
| | | if (null != occupant && null != occupant.getDocId() && !occupant.getDocId().equals(docId)) { |
| | | log.warn("[bedDoctorOn] 工位已被他人占用,拒绝入座(不挤掉). 占用人={}({})", |
| | | occupant.getDocName(), occupant.getDocId()); |
| | | return ROOM_OCCUPIED_BY_OTHER; |
| | | } |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | bedQueueBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus()); |
| | | addPriorityQueue(bedQueueBO); |
| | | // 【本需求·一人多工位】入座成功后**不再释放**该医生的其它工位: |
| | | // 节假日值班时一位医生可能同时占「检查工位 + 领用工位 + 装机工位」, |
| | | // 由前端「我的工位」下拉在各工位间切换(写操作仍由 @RequireBedOwner 按工位 doc_id 校验)。 |
| | | // 原实现:roomMapper.releaseOtherBedsOfDoc(docId, roomId, bedNo, BedStatusEnum.OPENING); |
| | | // 工位释放改为三种明确途径:① 医生点「离座」;② 闭诊统一清空;③ 管理员「清空工位」/接班医生「接管」。 |
| | | |
| | | bedBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus()); |
| | | addBedToRoutingQueue(bedBO); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** |
| | | * 手工把某位「排队中」患者拉入指定工位(工作台"本诊室排队中(未分配)"分区的「拉入」按钮)。 |
| | | * |
| | | * <p>与自动分流({@code hurryupOneCheckType})走**同一套**动作: |
| | | * 条件更新「排队中(10) → 准备中(20)」并把 room/bed 写成本工位,然后处理亲和项。 |
| | | * 号不变(号在签到时已定)。 |
| | | * |
| | | * <p>三重校验:工位存在 → 该行仍是「排队中」→ 检查项目在本工位开展范围内。 |
| | | * 条件更新自带并发保护:两个工位同时点「拉入」,只有一个能成功。 |
| | | */ |
| | | public ErrorCode pullWaitingPatient(Long roomId, String bedNo, Integer queueId) { |
| | | RoomDO bed = (null == roomId || null == bedNo) ? null : roomMapper.getRoom(roomId, bedNo); |
| | | if (null == bed) { |
| | | log.warn("[pullWaitingPatient] 工位不存在. roomId={} bedNo={}", roomId, bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | QueueDO item = (null == queueId) ? null : queueMapper.selectById(queueId); |
| | | if (null == item) { |
| | | log.warn("[pullWaitingPatient] 排队行不存在. queueId={}", queueId); |
| | | return QUEUE_NOT_EXISTS; |
| | | } |
| | | if (!QueueStatusEnum.WAITING.getStatus().equals(item.getStatus())) { |
| | | log.warn("[pullWaitingPatient] 该患者已不在排队中. queueId={} patId={} status={}", |
| | | queueId, item.getPatId(), item.getStatus()); |
| | | return QUEUE_NOT_WAITING; |
| | | } |
| | | Integer[] bedCheckTypes = bed.getCheckTypes(); |
| | | if (null == bedCheckTypes || 0 == bedCheckTypes.length |
| | | || null == item.getBookCheckType() |
| | | || !java.util.Arrays.asList(bedCheckTypes).contains(item.getBookCheckType())) { |
| | | log.warn("[pullWaitingPatient] 检查项目不在本工位范围. queueId={} checkType={} 本工位={}", |
| | | queueId, null == item ? null : item.getBookCheckType(), java.util.Arrays.toString(bedCheckTypes)); |
| | | return QUEUE_CHECK_TYPE_NOT_MATCH; |
| | | } |
| | | |
| | | Integer updateNum = queueMapper.preemptWaitingPatientById(queueId, roomId, bed.getRoomName(), bedNo, |
| | | QueueStatusEnum.WAITING.getStatus(), QueueStatusEnum.READY.getStatus()); |
| | | if (null == updateNum || 0 == updateNum) { |
| | | log.warn("[pullWaitingPatient] 拉入未生效(已被其他工位拉走). queueId={}", queueId); |
| | | return QUEUE_NOT_WAITING; |
| | | } |
| | | |
| | | // 内存对象与库内保持一致(亲和项要跟随主项的诊室/工位) |
| | | item.setStatus(QueueStatusEnum.READY.getStatus()); |
| | | item.setRoomId(roomId); |
| | | item.setRoomName(bed.getRoomName()); |
| | | item.setBedNo(bedNo); |
| | | |
| | | try { |
| | | CheckTypeDO checkTypeDO = getCheckTypeItem(item.getBookCheckType()); |
| | | if (null != checkTypeDO && null != checkTypeDO.getAffinityCheckTypes() |
| | | && checkTypeDO.getAffinityCheckTypes().length > 0) { |
| | | procAffinityWhenPreempt(item, checkTypeDO.getAffinityCheckTypes()); |
| | | } |
| | | } catch (Exception e) { |
| | | // 亲和项处理失败不影响主项已成功拉入 |
| | | log.error("[pullWaitingPatient] 亲和项处理失败(主项已拉入). queueId={}", queueId, e); |
| | | } |
| | | |
| | | log.info("[pullWaitingPatient] 已手工拉入. roomId={} bedNo={} queueId={} patId={} seqNum={}", |
| | | roomId, bedNo, queueId, item.getPatId(), item.getSeqNum()); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** 拼一行工位关键字段,供日志/排查快速对比(尤其看 bed_no 是否与请求一致) */ |
| | | private String describeBedRow(RoomDO row) { |
| | | return String.format("id=%s,room_id=%s,bed_no=[%s](len=%d),status=%s,doc_id=%s,doc_name=%s", |
| | | row.getId(), row.getRoomId(), row.getBedNo(), |
| | | null == row.getBedNo() ? -1 : row.getBedNo().length(), |
| | | row.getStatus(), row.getDocId(), row.getDocName()); |
| | | } |
| | | |
| | | public ErrorCode bedDoctorOff(Long roomId, String bedNo, Long docId, String docName) { |
| | | BedQueueBO bedQueueBO = mapBedVsQueue.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | if (null == bedQueueBO) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | if (null == bedBO) { |
| | | log.error("bedDoctorOff mapBedVsQueue DONOT existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | |
| | | // DB update |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.DOCTOR_ON); |
| | | statusList.add(BedStatusEnum.PAUSE); |
| | | statusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | Integer updateNum = roomMapper.setBedDoctorOff(roomId, bedNo, docId, BedStatusEnum.OPENING, statusList); |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | | log.error("bedDoctorOff DB invalid status. " + roomId + " " + bedNo); |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | removePriorityQueue(bedQueueBO); |
| | | removeBedFromRoutingQueue(bedBO); |
| | | |
| | | bedQueueBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | bedBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** |
| | | * 【本次需求】管理员强制清空工位(不要求是本人)。 |
| | | * <p> |
| | | * 与 {@link #bedDoctorOff} 的区别:不清 {@code doc_id=?} 条件, |
| | | * 因此可把「别人占用中」的工位清掉 —— 用于医生下班忘离座、换人使用同一工位的场景。 |
| | | * 仍然只清「医生在岗 / 暂停」且 doc_id 非空的工位,空闲工位不会被误动。 |
| | | */ |
| | | public ErrorCode bedDoctorOffByAdmin(Long roomId, String bedNo) { |
| | | BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | if (null == bedBO) { |
| | | log.error("bedDoctorOffByAdmin mapBedVsQueue DONOT existed. " + roomId + " " + bedNo); |
| | | return QUEUE_BED_NOT_EXIST; |
| | | } |
| | | |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.DOCTOR_ON); |
| | | statusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | Integer updateNum = roomMapper.setBedDoctorOffAdmin(roomId, bedNo, BedStatusEnum.OPENING, statusList); |
| | | if ( null==updateNum || 0 == updateNum ) { |
| | | log.error("bedDoctorOffByAdmin DB invalid status. " + roomId + " " + bedNo); |
| | | return ROOM_INVALID_STATUS; |
| | | } |
| | | |
| | | removeBedFromRoutingQueue(bedBO); |
| | | |
| | | bedBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | log.info("[管理员清空工位] roomId={} bedNo={} 已置为空闲", roomId, bedNo); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** |
| | | * 【本次需求】闭诊清空:把所有「医生在岗 / 暂停」的工位置为空闲(doc_id / doc_name 清空)。 |
| | | * <p> |
| | | * 与 {@link #closeBiz()} 配合:先清库,随后 {@code startBedReload()} 会按库重新装载内存, |
| | | * 保证内存(mapOpeningBed / 分流队列)与库一致。 |
| | | * |
| | | * @return 清空的工位数 |
| | | */ |
| | | public int closeBizClearBeds() { |
| | | List statusList = new ArrayList<BedStatusEnum>(); |
| | | statusList.add(BedStatusEnum.DOCTOR_ON); |
| | | statusList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | Integer updateNum = roomMapper.clearAllBedDoctors(BedStatusEnum.OPENING, statusList); |
| | | |
| | | // 内存同步:把工位状态改回空闲,并从分流队列摘除(避免闭诊后仍有工位被派患者) |
| | | // 注意:status 是 Integer 包装类型,必须用 equals 比较(== 只在 -128~127 缓存区间内偶然成立) |
| | | for (BedBO bedBO : mapOpeningBed.values()) { |
| | | if (Objects.equals(BedStatusEnum.DOCTOR_ON.getStatus(), bedBO.getStatus()) |
| | | || Objects.equals(BedStatusEnum.DOCTOR_PAUSE.getStatus(), bedBO.getStatus())) { |
| | | removeBedFromRoutingQueue(bedBO); |
| | | bedBO.setStatus(BedStatusEnum.OPENING.getStatus()); |
| | | } |
| | | } |
| | | log.info("[闭诊] 已清空当班工位 {} 个(doc_id / doc_name 置空,状态改为空闲)", |
| | | updateNum == null ? 0 : updateNum); |
| | | return updateNum == null ? 0 : updateNum; |
| | | } |
| | | |
| | | /* 2024.11.11 |
| | | // 常规 或者 领用 时 医生 取下一位患者 |
| | | public ErrorCode nextPatient(Long roomId, String bedNo) { |
| | | // 从 DB 把该工位.序号最小的 [就诊准备中] 的人 设置为 [就诊中] (或领用中) |
| | | // 从 DB 把该工位.序号最小的 [候诊中中] 的人 设置为 [就诊中] (或领用中) |
| | | Integer updateNum = queueMapper.updateBedQueueStatus(roomId, bedNo, |
| | | QueueStatusEnum.READY.getStatus(), QueueStatusEnum.ONSTAGE.getStatus()); |
| | | // 该工位 没有 [就诊准备中] 人员 |
| | | // 该工位 没有 [候诊中中] 人员 |
| | | if (null == updateNum || 0 == updateNum) { |
| | | return QUEUE_NOT_READY_PATIENT; |
| | | } |
| | | |
| | | // 优先队列中 该工位 就诊准备中人的数量 减一 |
| | | // 分流队列中 该工位 候诊中中人的数量 减一 |
| | | BedQueueBO bo = mapBedVsQueue.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | bo.queueNum.getAndDecrement(); // 可能已经【并发的】在 hurry-up 中改变了值 |
| | | |
| | | refreshPriorityQueue(bo); |
| | | refreshRoutingQueue(bo); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | */ |
| | | |
| | | // 常规 或者 领用 时 医生 取下一位患者 |
| | | public ErrorCode nextPatient(Long roomId, String bedNo) { |
| | | QueueDO firstReadyQueueItem = queueMapper.getFirstItemByBedAndStatus(roomId, bedNo, QueueStatusEnum.READY.getStatus()); |
| | | // 该工位 没有 [就诊准备中] 人员 |
| | | if (null == firstReadyQueueItem) { |
| | | // ⓪ 【并发加固·本需求】本工位**已有「就诊中(30)」患者时不再叫下一位**。 |
| | | // |
| | | // 原先这个判断只在 `QueueServiceImpl#finishNextPatient` 里做过,而它位于 |
| | | // singleThreadExecutor 串行区**之外**:两个并发请求(双击 / 多端)都可能读到 0, |
| | | // 随后各自提交一次 nextPatient,串行执行时会各推进一条 READY ⇒ |
| | | // 同一工位出现 2 个「就诊中」,患者被"卡"在就诊中、工位也不再补人。 |
| | | // 这里在串行区内再判一次(本方法只由该串行线程池调用)。 |
| | | List<Integer> onstageStatusList = new ArrayList<>(); |
| | | onstageStatusList.add(QueueStatusEnum.ONSTAGE.getStatus()); |
| | | Integer onstageNum = queueMapper.bedQueueStatisticByStatus(roomId, bedNo, onstageStatusList); |
| | | if (null != onstageNum && onstageNum > 0) { |
| | | log.warn("[nextPatient] 本工位已有 {} 个「就诊中」患者,跳过本次叫号(防并发双就诊中). 工位={}/{}", |
| | | onstageNum, roomId, bedNo); |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | // ① 候诊中(20) 优先 |
| | | QueueDO firstQueueItem = queueMapper.getBedFirstQueueItemByStatus( |
| | | roomId, bedNo, QueueStatusEnum.READY.getStatus()); |
| | | boolean fromRecalled = false; |
| | | |
| | | // ② 候诊区没人时,回退取「已召回(15)」里号最小的一位 |
| | | // |
| | | // 【缺陷修复】过号患者"召回"后会先置为 RECALLED(15),再靠 hurryupOneBed 的补人循环 |
| | | // 提升成 READY(20)。但补人循环有三道门闸(openingFlag=0 / stopReady=true / 候诊区已满), |
| | | // 任一条成立患者就**一直停在 15**;而叫号此前只认 READY ⇒ 出现死结: |
| | | // 现场表现为"排队号 A2/A3 状态是已召回,点叫号加载不出患者信息,无法检查"(2026-10-29 反馈)。 |
| | | // 这里按与 nextInstallPatient(已含 RECALLED_INSTALL)一致的思路直接叫到, |
| | | // 不再依赖补人是否成功。 |
| | | if (null == firstQueueItem) { |
| | | firstQueueItem = queueMapper.getBedFirstQueueItemByStatus( |
| | | roomId, bedNo, QueueStatusEnum.RECALLED.getStatus()); |
| | | fromRecalled = null != firstQueueItem; |
| | | if (fromRecalled) { |
| | | log.info("[nextPatient] 候诊区无人,改叫「已召回」患者. 工位={}/{} patId={} seqNum={}", |
| | | roomId, bedNo, firstQueueItem.getPatId(), firstQueueItem.getSeqNum()); |
| | | } |
| | | } |
| | | |
| | | // 该工位既没有 [候诊中] 也没有 [已召回] 人员 |
| | | if (null == firstQueueItem) { |
| | | return QUEUE_NOT_READY_PATIENT; |
| | | } |
| | | |
| | | // QueueStatusEnum.READY => QueueStatusEnum.ONSTAGE |
| | | firstReadyQueueItem.setStatus(QueueStatusEnum.ONSTAGE.getStatus()); |
| | | queueMapper.updateById( firstReadyQueueItem ); |
| | | // ③ READY / RECALLED => ONSTAGE:用「主键 + 当前状态」条件更新(并发加固), |
| | | // 0 行说明该行已被另一请求推进,本次放弃(下次再取)。 |
| | | int curStatus = fromRecalled |
| | | ? QueueStatusEnum.RECALLED.getStatus() : QueueStatusEnum.READY.getStatus(); |
| | | Integer updated = queueMapper.updateStatusByIdIfCurrent( |
| | | firstQueueItem.getId(), curStatus, QueueStatusEnum.ONSTAGE.getStatus()); |
| | | if (null == updated || 0 == updated) { |
| | | log.warn("[nextPatient] 候诊行已被并发推进,放弃本次叫号. 工位={}/{} queueId={} 期望状态={}", |
| | | roomId, bedNo, firstQueueItem.getId(), curStatus); |
| | | return QUEUE_NOT_READY_PATIENT; |
| | | } |
| | | firstQueueItem.setStatus(QueueStatusEnum.ONSTAGE.getStatus()); |
| | | |
| | | // 优先队列中 该工位 就诊准备中人的数量 减一 |
| | | BedQueueBO bo = mapBedVsQueue.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | bo.queueNum.getAndDecrement(); // 可能已经【并发的】在 hurry-up 中改变了值 |
| | | |
| | | refreshPriorityQueue(bo); |
| | | // 分流队列中 该工位 【候诊中】患者数量 减一 |
| | | // 注意:召回的这位本来就不在候诊计数里,不能减;另外 bo 可能为 null(工位不在开诊列表), |
| | | // 原实现直接 bo.readyPatNum-- 会 NPE。 |
| | | BedBO bo = mapOpeningBed.get(Utils.formatRoomBed(roomId, bedNo)); |
| | | if (null != bo) { |
| | | if (!fromRecalled) { |
| | | bo.readyPatNum--; // 可能已经在 hurry-up 中改变了值 |
| | | } |
| | | refreshBedPriorityLevelInRoutingQueue(bo); |
| | | } else { |
| | | log.warn("[nextPatient] 工位不在开诊列表,跳过内存计数与分流刷新. 工位={}/{}", roomId, bedNo); |
| | | } |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | // 装机时 医生 取下一位患者 |
| | | public ErrorCode nextInstallPatient(Long roomId, String bedNo) { |
| | | // 从 DB 把 序号最小的 [已领用] [已召回-安装] 的人 设置为 [安装中] |
| | | QueueDO firstItem = queueMapper.getFirstItemByRoomAndStatus(roomId, List.of(QueueStatusEnum.RECEIVED.getStatus(), QueueStatusEnum.RECALLED_INSTALL.getStatus())); |
| | | QueueDO firstItem = queueMapper.getRoomFirstQueueItemByStatus(roomId, List.of(QueueStatusEnum.RECEIVED.getStatus(), QueueStatusEnum.RECALLED_INSTALL.getStatus())); |
| | | // 该工位 没有 [已领用] | [已召回-安装] 人员 |
| | | if (null == firstItem) { |
| | | return QUEUE_NOT_READY_PATIENT; |
| | | } |
| | | |
| | | firstItem.setStatus( QueueStatusEnum.INSTALLING.getStatus() ); |
| | | firstItem.setBedNo( bedNo ); |
| | | firstItem.setBedNo( bedNo ); // 转到 安装工位 |
| | | queueMapper.updateById( firstItem ); |
| | | |
| | | // 检查项目.亲和性 处理逻辑 |
| | | // 检查项目.亲和性 处理逻辑: 变为【安装中】后 拉 其他检查项来亲和 |
| | | CheckTypeDO checkTypeDO = getCheckTypeItem( firstItem.getBookCheckType() ); |
| | | if (checkTypeDO.getAffinityCheckTypes().length > 0) { |
| | | procAffinityWhenSomeOneEntryInstalling(firstItem.getPatId(), roomId, bedNo, checkTypeDO.getAffinityCheckTypes() ); |
| | | } |
| | | |
| | | // 装机工位 无优先队列 |
| | | // 优先队列中 该工位 [已领用]人的数量 减一 |
| | | //BedQueueBO bo = mapBedVsQueue.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | //bo.queueNum.getAndDecrement(); // 可能已经【并发的】在 hurry-up 中改变了值 |
| | | //refreshPriorityQueue(bo); |
| | | // 装机工位 不参与分流 |
| | | |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | | /** |
| | | * 1. 每天开诊前 从DB同步工位的患者队列数据到 工位优先队列 |
| | | * 1. 每天开诊前 从DB同步工位的患者队列数据到 工位分流队列 |
| | | * 2. 服务运维重启时 |
| | | */ |
| | | public ErrorCode bedReload() { |
| | | mapCheckTypePriorityQueue.clear(); |
| | | mapBedVsQueue.clear(); |
| | | mapCheckTypeRoutingQueue.clear(); |
| | | mapOpeningBed.clear(); |
| | | |
| | | // 清除非当天的排队人员 |
| | | queueMapper.clearQueue(); |
| | |
| | | List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>(); |
| | | bedStatusEnumList.add(BedStatusEnum.OPENING); |
| | | bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON); |
| | | bedStatusEnumList.add(BedStatusEnum.PAUSE); |
| | | bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList); |
| | | List<BedQueueBO> bedQueueBOList = roomDOList.stream().map(item -> BeanUtils.toBean(item, BedQueueBO.class)).toList(); |
| | | List<BedBO> bedBOList = roomDOList.stream().map(item -> BeanUtils.toBean(item, BedBO.class)).toList(); |
| | | |
| | | // 从DB 获取 队列中 就诊准备中人员统计 列表 |
| | | List<Integer> queueStatusList = new ArrayList<>(); |
| | | queueStatusList.add(QueueStatusEnum.READY.getStatus()); |
| | | List<QueueStatisticDO> queueStatisticDOList = queueMapper.queueStatistic(queueStatusList); |
| | | |
| | | bedQueueBOList.forEach(item -> { |
| | | item.maxQueueNum = getBedReadyMax(item.roomId, item.bedNo); |
| | | Optional<QueueStatisticDO> queueStatisticDOOptional = queueStatisticDOList.stream().filter(it->it.getRoomId()==item.roomId && it.getBedNo().equals(item.getBedNo())).findFirst(); |
| | | int queueNum = queueStatisticDOOptional.isPresent() ? queueStatisticDOOptional.get().getTotalInStatus() : 0; |
| | | if ( item.maxQueueNum < queueNum ) |
| | | throw new RuntimeException("init: exceed max queue number!"); |
| | | |
| | | item.queueNum.set( queueNum ); |
| | | mapBedVsQueue.put( Utils.formatRoomBed(item.roomId, item.bedNo), item); |
| | | if (BedStatusEnum.DOCTOR_ON.getStatus() == item.getStatus()) { |
| | | addPriorityQueue(item); |
| | | bedBOList.forEach(item -> { |
| | | mapOpeningBed.put( Utils.formatRoomBed(item.roomId, item.bedNo), item); |
| | | // 【需求3】「停止候诊」的工位不加入分流队列 —— 与医生在座(status=DOCTOR_ON)同样判定, |
| | | // 否则每次 bedReload(开诊/重启/诊室重置)后开关会失效、又开始收病人。 |
| | | // 注意:stopReady 仅阻止**新增**候诊患者,已有候诊患者仍可正常叫号 |
| | | // (nextPatient 不检查该开关)。 |
| | | if (BedStatusEnum.DOCTOR_ON.getStatus() == item.getStatus() |
| | | && !Boolean.TRUE.equals(item.getStopReady())) { |
| | | addBedToRoutingQueue(item); |
| | | } |
| | | }); |
| | | |
| | | Integer num = queueMapper.getMaxSeqNum(); |
| | | curSeqNum = new AtomicInteger(null == num ? 0 : num); |
| | | |
| | | // 【号统一(本需求)】不再初始化内存计数器 curSeqNum: |
| | | // 排队号来自签到时的 queue.seq_num,分流不再分配号。 |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | |
| | | // 踢出在座的医生 |
| | | List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>(); |
| | | bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON); |
| | | bedStatusEnumList.add(BedStatusEnum.PAUSE); |
| | | bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE); |
| | | List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList); |
| | | List<Long> userIdList = roomDOList.stream().map(roomDO -> roomDO.getDocId()).toList(); |
| | | oAuth2TokenApi.tick(userIdList); |
| | |
| | | if (0 == openingFlag.get()) |
| | | return; |
| | | |
| | | mapBedVsQueue.values().forEach(bedQueueBO -> hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo)); |
| | | mapOpeningBed.values().forEach(bedQueueBO -> hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo)); |
| | | } |
| | | |
| | | /** |
| | | * 把医生候诊的队列塞满 |
| | | */ |
| | | public void hurryupOneBed(Long roomId, String bedNo) { |
| | | if (0 == openingFlag.get()) |
| | | if (0 == openingFlag.get()) { |
| | | // 【现场排查】未开诊时**整个自动补人都是关闭的**(排队中的病人不会被搬进候诊区)。 |
| | | // 原实现是静默 return,现场只能看到"人都在,就是不进来",故这里明确记一条。 |
| | | log.warn("[补人] 系统未处于【开诊】状态(openingFlag=0),跳过自动补人. 工位={}/{}(可在监控页点「开诊」)", |
| | | roomId, bedNo); |
| | | return; |
| | | } |
| | | |
| | | BedQueueBO bedQueueBO = getBedQueueBO(roomId, bedNo); |
| | | BedBO bedBO = getBedQueueBO(roomId, bedNo); |
| | | if (null == bedBO) { |
| | | log.warn("[补人] 工位不在「已开诊工位」列表中,跳过. 工位={}/{}", roomId, bedNo); |
| | | return; |
| | | } |
| | | |
| | | // 【本次加固】用库里的真实候诊人数校正内存计数: |
| | | // readyPatNum 是内存值,若某条路径(撤销/放弃/过号等)没同步,它会虚高, |
| | | // 于是 "候诊区已满" 的判断一直成立、永远不补人(而界面上候诊区其实是空的)。 |
| | | int realReadyNum = countReadyInDb(roomId, bedNo); |
| | | if (realReadyNum != bedBO.getReadyPatNum()) { |
| | | log.warn("[补人] 内存候诊人数({}) 与库内({}) 不一致,已按库内校正. 工位={}/{}", |
| | | bedBO.getReadyPatNum(), realReadyNum, roomId, bedNo); |
| | | bedBO.setReadyPatNum(realReadyNum); |
| | | } |
| | | |
| | | // 【需求3】「停止候诊」时不再把【已召回】患者放入候诊区。 |
| | | // 注意:这**不影响**已有候诊患者被正常叫号(nextPatient 不检查本开关), |
| | | // 也不影响医生对具体患者的"召回"操作本身(只影响其自动进入候诊的补位)。 |
| | | boolean stopReady = Boolean.TRUE.equals(bedBO.getStopReady()); |
| | | |
| | | // 处理 过号-回来 的人 |
| | | while (bedQueueBO.queueNum.get() < bedQueueBO.maxQueueNum) { |
| | | // 查看 当前工位 是否有过号-回来的患者 |
| | | Integer updateNum = queueMapper.queueRecalledPatient( |
| | | bedQueueBO.getRoomId(), |
| | | bedQueueBO.getRoomName(), |
| | | bedQueueBO.getBedNo(), |
| | | curSeqNum.get() + 1, // 过号回来,排到当前的下一位 |
| | | QueueStatusEnum.RECALLED.getStatus(), // 不需要处理 [安装召回], 原因: 安装工位 没有优先队列 |
| | | QueueStatusEnum.READY.getStatus()); |
| | | if (null == updateNum || 0 == updateNum) |
| | | while (!stopReady && bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) { |
| | | // 查看 当前工位 是否有「已召回(15)」的患者(排队号最小的那一位) |
| | | QueueDO recalledQueueItem = queueMapper.getBedFirstQueueItemByStatus( |
| | | bedBO.getRoomId(), bedBO.getBedNo(), QueueStatusEnum.RECALLED.getStatus()); |
| | | if (null == recalledQueueItem) |
| | | break; |
| | | |
| | | // 召回 [过号] 患者后,处理 检查项目.亲和性 逻辑 |
| | | QueueDO recalledQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1); |
| | | // 【号统一(本需求)】过号召回**不再重排队号**:号是签到时定的, |
| | | // 患者按原号回到候诊序列(需求:「病人过号了,也排着」)。 |
| | | // 旧实现(seq_num = 当前号+1,排到队尾)会让他与屏幕上/患者手里的号对不上。 |
| | | // 不需要处理 [安装召回], 原因: 安装工位 不进入分流队列 |
| | | recalledQueueItem.setStatus(QueueStatusEnum.READY.getStatus()); |
| | | queueMapper.updateById(recalledQueueItem); |
| | | |
| | | // [过号] 患者 => [候诊中] ,处理 检查项目.亲和性 逻辑 |
| | | CheckTypeDO checkTypeDO = getCheckTypeItem( recalledQueueItem.getBookCheckType() ); |
| | | if ( checkTypeDO.getAffinityCheckTypes().length > 0) { |
| | | procAffinityWhenRecalled(recalledQueueItem, checkTypeDO.getAffinityCheckTypes()); |
| | | } |
| | | |
| | | curSeqNum.getAndIncrement(); |
| | | // 可能已经在 nextPatient 中改变了值 |
| | | bedBO.readyPatNum++; |
| | | |
| | | // 可能已经【并发的】在 nextPatient 中改变了值 |
| | | bedQueueBO.queueNum.incrementAndGet(); |
| | | |
| | | // 可能已经【并发的】在 nextPatient 中改变了优先队列顺序 |
| | | refreshPriorityQueue(bedQueueBO); |
| | | // 可能已经【并发的】在 nextPatient 中改变了分流队列顺序 |
| | | refreshBedPriorityLevelInRoutingQueue(bedBO); |
| | | } |
| | | |
| | | // 处理 排队中 患者 |
| | | hurryupOneBedCheckTypeList( bedQueueBO ); |
| | | hurryupOneBedCheckTypeList(bedBO); |
| | | } |
| | | |
| | | /** |
| | | * Deprecated 代码保留,实际已经不再使用 |
| | | * 原因: 同一工位可以同时处理多个检查项目,所以需要保证多个检查项目的就诊时间先后顺序 |
| | | * 一个工位可以支持多个检查项目,所以需要保证多个检查项目的就诊时间先后顺序 |
| | | * 按照一个检查项目来处理,就不能保证同一个工位的多个检查项目的就诊时间的先后顺序 |
| | | * 所以实现逻辑是 让支持该检查项目的所有工位去抢占 【排队中】【工位支持的检查项目】的患者 |
| | | * 实际抢占的未必是 参数中的检查项目的患者,可能是其他检查项目的患者 |
| | | * @param checkType |
| | | */ |
| | | public void hurryupOneCheckType(Integer checkType) { |
| | | |
| | | PriorityBlockingQueue<BedQueueBO> priorityQueue = mapCheckTypePriorityQueue.get(checkType); |
| | | if (null == priorityQueue) |
| | | // 【修复既有缺陷】补上开诊门闸。原实现**唯独这里漏了** openingFlag 检查 |
| | | // (hurryupAllBed 与 hurryupOneBed 都有),导致手动"闭诊"后, |
| | | // 只要有人预约确认 / 加急,患者仍会被推进候诊区。 |
| | | if (0 == openingFlag.get()) |
| | | return; |
| | | |
| | | PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType); |
| | | if (null == routingQueue) |
| | | return; |
| | | |
| | | while (true) { |
| | | BedQueueBO bedQueueBO = priorityQueue.peek(); |
| | | if (null == bedQueueBO) |
| | | BedBO bedBO = routingQueue.peek(); |
| | | if (null == bedBO) |
| | | return; |
| | | |
| | | int curQueueNum = bedQueueBO.queueNum.get(); |
| | | if (curQueueNum > bedQueueBO.maxQueueNum) |
| | | // 【需求3】跳过处于「停止候诊」的工位。 |
| | | // 不能直接 return —— 该队列按 readyPatNum 升序 peek, |
| | | // 直接 return 会让其余工位也收不到患者(饿死)。 |
| | | // 这里 poll 出来后若命中停止态,仅将其从队列移除(不再 offer 回去), |
| | | // 然后 continue 处理下一个工位。 |
| | | if (Boolean.TRUE.equals(bedBO.getStopReady())) { |
| | | routingQueue.poll(); |
| | | log.info("[hurryupOneCheckType] 工位[停止候诊],移出分流队列. roomId={} bedNo={}", |
| | | bedBO.getRoomId(), bedBO.getBedNo()); |
| | | continue; |
| | | } |
| | | |
| | | int curQueueNum = bedBO.getReadyPatNum(); |
| | | if (curQueueNum > bedBO.getMaxReadyPatNum()) |
| | | throw new RuntimeException("hurryupOneCheckType: exceed max queue number!"); |
| | | |
| | | if (curQueueNum == bedQueueBO.maxQueueNum) |
| | | if (curQueueNum == bedBO.getMaxReadyPatNum()) |
| | | return; |
| | | |
| | | // 查看 是否有排队中的患者 |
| | | Integer updateNum = queueMapper.preemptWaitingPatient( |
| | | bedQueueBO.getRoomId(), |
| | | bedQueueBO.getRoomName(), |
| | | bedQueueBO.getBedNo(), |
| | | curSeqNum.get() + 1, |
| | | // |
| | | // 【号统一(本需求)】分流只做「排队中(10) → 候诊中(20)」的搬迁,**不再分配排队号**: |
| | | // 号在签到时已写入 seq_num;加急只影响这里的取人顺序(SQL 内 jump_flag 优先)。 |
| | | // 旧实现是「UPDATE ... 顺带 seq_num=当前号+1,再按号查回该行」—— |
| | | // 号不再唯一后"按号查回"会取错人,故改为:先取行 → 按 id 条件更新。 |
| | | QueueDO preemptQueueItem = queueMapper.getFirstWaitingByCheckTypes( |
| | | QueueStatusEnum.WAITING.getStatus(), |
| | | QueueStatusEnum.READY.getStatus(), |
| | | checkType); |
| | | bedBO.checkTypes); //关键点 关键点 关键点 checkType |
| | | |
| | | // 没有抢到排队患者 |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 没有排队中的患者 |
| | | if (null == preemptQueueItem) { |
| | | return; |
| | | } |
| | | |
| | | QueueDO preemptQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1); |
| | | // 条件更新:status 必须仍是「排队中」,保证并发下只有一方抢到该行 |
| | | Integer updateNum = queueMapper.preemptWaitingPatientById( |
| | | preemptQueueItem.getId(), |
| | | bedBO.getRoomId(), |
| | | bedBO.getRoomName(), |
| | | bedBO.getBedNo(), |
| | | QueueStatusEnum.WAITING.getStatus(), |
| | | QueueStatusEnum.READY.getStatus()); |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 被其它工位抢先:本轮放弃,等下一次补齐触发 |
| | | return; |
| | | } |
| | | |
| | | // 内存对象与库内保持一致 —— 亲和项要跟随主项的诊室/工位 |
| | | preemptQueueItem.setStatus(QueueStatusEnum.READY.getStatus()); |
| | | preemptQueueItem.setRoomId(bedBO.getRoomId()); |
| | | preemptQueueItem.setRoomName(bedBO.getRoomName()); |
| | | preemptQueueItem.setBedNo(bedBO.getBedNo()); |
| | | |
| | | CheckTypeDO checkTypeDO = getCheckTypeItem( preemptQueueItem.getBookCheckType() ); |
| | | |
| | | // 抢到排队患者后,处理 检查项目.亲和性 逻辑 |
| | |
| | | procAffinityWhenPreempt(preemptQueueItem, checkTypeDO.getAffinityCheckTypes()); |
| | | } |
| | | |
| | | curSeqNum.getAndIncrement(); |
| | | // 可能已经在 nextPatient 中改变了值 |
| | | bedBO.readyPatNum++; |
| | | |
| | | // 可能已经【并发的】在 nextPatient 中改变了值 |
| | | bedQueueBO.queueNum.incrementAndGet(); |
| | | |
| | | // 可能已经【并发的】在 nextPatient 中改变了优先队列顺序 |
| | | refreshPriorityQueue(bedQueueBO); |
| | | // 可能已经【并发的】在 nextPatient 中改变了分流队列顺序 |
| | | refreshBedPriorityLevelInRoutingQueue(bedBO); |
| | | } |
| | | } |
| | | |
| | |
| | | return mapCheckTypeVsReadyMax.get( checkType ); |
| | | } |
| | | |
| | | public BedQueueBO getBedQueueBO(Long roomId, String bedNo) { |
| | | return mapBedVsQueue.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | public BedBO getBedQueueBO(Long roomId, String bedNo) { |
| | | return mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) ); |
| | | } |
| | | |
| | | public MonitorInfoVO getMonitorInfo() { |
| | |
| | | public void monitorInfo() { |
| | | MonitorInfoVO monitorInfoVO = new MonitorInfoVO(); |
| | | roomBedStatistic(monitorInfoVO); |
| | | log.info(" opening " + openingFlag.get() + " " + monitorInfoVO.getQueueNum() + " " + monitorInfoVO.getActiveQueueNum() + " " + monitorInfoVO.getCheckTypeBedInfo().toString() ); |
| | | log.info(" opening " + openingFlag.get() + " " + monitorInfoVO.getOpeningBedNum() + " " + monitorInfoVO.getDocBedNum() + " " + monitorInfoVO.getCheckTypeBedInfo().toString() ); |
| | | } |
| | | |
| | | private void hurryupOneBedCheckTypeList(BedQueueBO bedQueueBO) { |
| | | while (bedQueueBO.queueNum.get() < bedQueueBO.maxQueueNum) { |
| | | private void hurryupOneBedCheckTypeList(BedBO bedBO) { |
| | | // 【需求3】「停止候诊」的工位不再新增候诊患者。 |
| | | // 已有候诊患者仍可正常叫号(本方法只负责"补齐候诊",不参与叫号)。 |
| | | if (Boolean.TRUE.equals(bedBO.getStopReady())) { |
| | | // 【现场排查】明确记一条:工位开了「停止候诊」时不会自动补人 |
| | | log.warn("[补人] 工位处于【停止候诊】,跳过自动补人. 工位={}/{}(如需接收请关闭该开关)", |
| | | bedBO.getRoomId(), bedBO.getBedNo()); |
| | | return; |
| | | } |
| | | if (bedBO.getReadyPatNum() >= bedBO.getMaxReadyPatNum()) { |
| | | // 【现场排查】候诊区已达上限(上限=各检查项目 check_type.ready_num 的最大值) |
| | | log.info("[补人] 候诊区已达上限,跳过. 工位={}/{} 当前候诊={} 上限={}", |
| | | bedBO.getRoomId(), bedBO.getBedNo(), bedBO.getReadyPatNum(), bedBO.getMaxReadyPatNum()); |
| | | refreshBedPriorityLevelInRoutingQueue(bedBO); |
| | | return; |
| | | } |
| | | int pulledNum = 0; |
| | | while (bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) { |
| | | // 抢占 是否有排队中的患者 |
| | | Integer updateNum = queueMapper.preemptWaitingPatientWithBedCheckTypes( |
| | | bedQueueBO.getRoomId(), |
| | | bedQueueBO.getRoomName(), |
| | | bedQueueBO.getBedNo(), |
| | | curSeqNum.get() + 1, |
| | | // |
| | | // 【号统一(本需求)】与 hurryupOneCheckType 相同:只搬迁状态,**不再分配排队号**。 |
| | | QueueDO preemptQueueItem = queueMapper.getFirstWaitingByCheckTypes( |
| | | QueueStatusEnum.WAITING.getStatus(), |
| | | QueueStatusEnum.READY.getStatus(), |
| | | bedQueueBO.checkTypes); |
| | | bedBO.checkTypes); |
| | | |
| | | // 没有抢到排队患者 |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 没有排队中的患者 |
| | | if (null == preemptQueueItem) { |
| | | return; |
| | | } |
| | | |
| | | QueueDO preemptQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1); |
| | | // 条件更新:status 必须仍是「排队中」,保证并发下只有一方抢到该行 |
| | | Integer updateNum = queueMapper.preemptWaitingPatientById( |
| | | preemptQueueItem.getId(), |
| | | bedBO.getRoomId(), |
| | | bedBO.getRoomName(), |
| | | bedBO.getBedNo(), |
| | | QueueStatusEnum.WAITING.getStatus(), |
| | | QueueStatusEnum.READY.getStatus()); |
| | | if (null == updateNum || 0 == updateNum) { |
| | | // 被其它工位抢先:本轮放弃,等下一次补齐触发 |
| | | return; |
| | | } |
| | | |
| | | // 内存对象与库内保持一致 —— 亲和项要跟随主项的诊室/工位 |
| | | preemptQueueItem.setStatus(QueueStatusEnum.READY.getStatus()); |
| | | preemptQueueItem.setRoomId(bedBO.getRoomId()); |
| | | preemptQueueItem.setRoomName(bedBO.getRoomName()); |
| | | preemptQueueItem.setBedNo(bedBO.getBedNo()); |
| | | |
| | | CheckTypeDO checkTypeDO = getCheckTypeItem( preemptQueueItem.getBookCheckType() ); |
| | | |
| | | // 抢到排队患者后,处理 检查项目.亲和性 逻辑 |
| | |
| | | procAffinityWhenPreempt(preemptQueueItem, checkTypeDO.getAffinityCheckTypes()); |
| | | } |
| | | |
| | | curSeqNum.getAndIncrement(); |
| | | |
| | | bedQueueBO.queueNum.incrementAndGet(); |
| | | bedBO.readyPatNum++; |
| | | pulledNum++; |
| | | } |
| | | |
| | | // 改变了优先队列顺序 |
| | | refreshPriorityQueue(bedQueueBO); |
| | | if (pulledNum > 0) { |
| | | log.info("[补人] 已补入 {} 人. 工位={}/{} 当前候诊={} 上限={}", |
| | | pulledNum, bedBO.getRoomId(), bedBO.getBedNo(), |
| | | bedBO.getReadyPatNum(), bedBO.getMaxReadyPatNum()); |
| | | } |
| | | |
| | | // 改变了分流队列顺序 |
| | | refreshBedPriorityLevelInRoutingQueue(bedBO); |
| | | } |
| | | |
| | | /** 库里该工位当前「准备中(20)」的真实人数(用于校正内存计数与现场诊断) */ |
| | | private int countReadyInDb(Long roomId, String bedNo) { |
| | | List<Integer> readyStatus = new ArrayList<>(); |
| | | readyStatus.add(QueueStatusEnum.READY.getStatus()); |
| | | List<QueueDO> readyList = queueMapper.getBedQueueByStatus(roomId, bedNo, readyStatus); |
| | | return null == readyList ? 0 : readyList.size(); |
| | | } |
| | | |
| | | /** |
| | | * 立即补人(现场自救 + 自助诊断):手动触发一次该工位的自动分流,并把"为什么没补/补了几人"一起返回。 |
| | | * |
| | | * <p>用于工作台候诊区为空时的「立即补人」按钮:不必等定时任务、也不用重启去猜是哪道门闸挡住的。 |
| | | */ |
| | | public Map<String, Object> hurryUpNow(Long roomId, String bedNo) { |
| | | Map<String, Object> data = new LinkedHashMap<>(); |
| | | int flag = openingFlag.get(); |
| | | data.put("系统开诊状态", 0 == flag ? "已闭诊(此时不会自动补人,请先在监控页点「开诊」)" : "开诊中"); |
| | | |
| | | BedBO bedBO = getBedQueueBO(roomId, bedNo); |
| | | if (null == bedBO) { |
| | | data.put("结论", "该工位不在「已开诊工位」列表里(可能已关闭/本次未开诊),无法补人;请先到选工位页入座或让管理员开诊"); |
| | | log.warn("[补人] hurryUpNow 工位不存在. 工位={}/{}", roomId, bedNo); |
| | | return data; |
| | | } |
| | | |
| | | boolean stopReady = Boolean.TRUE.equals(bedBO.getStopReady()); |
| | | int before = countReadyInDb(roomId, bedNo); |
| | | int memoryBefore = bedBO.getReadyPatNum(); |
| | | // 先按库内真实值校正内存(内存虚高会导致永久"已满") |
| | | bedBO.setReadyPatNum(before); |
| | | |
| | | data.put("工位", bedBO.getRoomName() + " " + bedNo); |
| | | data.put("检查项目", null == bedBO.getCheckTypes() ? "[]" : java.util.Arrays.toString(bedBO.getCheckTypes())); |
| | | data.put("停止候诊", stopReady); |
| | | data.put("候诊上限", bedBO.getMaxReadyPatNum()); |
| | | data.put("补人前候诊人数", before + (memoryBefore == before ? "" : "(内存曾为 " + memoryBefore + ",已校正)")); |
| | | |
| | | if (0 == flag) { |
| | | data.put("结论", "系统已闭诊,本次未补人;请先在监控页点「开诊」"); |
| | | return data; |
| | | } |
| | | if (stopReady) { |
| | | data.put("结论", "本工位开了「停止候诊」,本次未补人;如需接收患者请关闭该开关"); |
| | | return data; |
| | | } |
| | | |
| | | hurryupOneBed(roomId, bedNo); |
| | | |
| | | int after = countReadyInDb(roomId, bedNo); |
| | | int pulled = Math.max(0, after - before); |
| | | data.put("补人后候诊人数", after); |
| | | data.put("本次补入", pulled); |
| | | if (pulled > 0) { |
| | | data.put("结论", "已补入 " + pulled + " 人"); |
| | | } else if (after >= bedBO.getMaxReadyPatNum()) { |
| | | data.put("结论", "候诊区已达上限(" + after + "/" + bedBO.getMaxReadyPatNum() + "),不再补人"); |
| | | } else { |
| | | data.put("结论", "没有匹配本工位检查项目的「排队中」患者(可到工作台查看「本诊室排队中(未分配)」分区)"); |
| | | } |
| | | log.info("[补人] hurryUpNow 工位={}/{} 前={} 后={} 结论={}", roomId, bedNo, before, after, data.get("结论")); |
| | | return data; |
| | | } |
| | | |
| | | /** |
| | | * 有检查项分流到诊室后,也就是进入[候诊中]后,拉 [排队中] + 具有亲和性的 检查项 来依附该检查项。 |
| | | * |
| | | * <p>【亲和项与排队号】亲和项目(同一患者、可在一个工位一起做的多个检查项)在候诊区 |
| | | * <b>跟随主项显示</b>:这里把亲和项的排队号改成**主项的号**、并搬到主项所在诊室/工位, |
| | | * 界面(工作台/诊间屏/大屏)才能把两者排在一起。 |
| | | * <p>注意这**不是**"重新赋号":号仍是签到时定的那个,只是亲和项沿用主项的号, |
| | | * 与"分流/叫号/召回不再改号"并不冲突。 |
| | | */ |
| | | private void procAffinityWhenPreempt(QueueDO preemptQueueItem, Integer[] affinityCheckTypes) { |
| | | List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus( |
| | | preemptQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.WAITING.getStatus()); |
| | | for (int i = 0; i < affinityItems.size(); i++) { |
| | | QueueDO queueItem = affinityItems.get(i); |
| | | queueItem.setStatus(QueueStatusEnum.AFFINITY_WAITING.getStatus()); //改变 排队状态 |
| | | queueItem.setStatus(QueueStatusEnum.AFFINITY.getStatus()); //改变 排队状态 |
| | | queueItem.setAffinityItem( 1 ); |
| | | queueItem.setRoomId(preemptQueueItem.getRoomId()); |
| | | queueItem.setRoomName(preemptQueueItem.getRoomName()); |
| | | queueItem.setBedNo(preemptQueueItem.getBedNo()); |
| | | queueItem.setSeqNum( preemptQueueItem.getSeqNum()); // 使用同一个 内部序号 |
| | | queueItem.setSeqNum( preemptQueueItem.getSeqNum()); // 亲和项沿用主项的排队号(跟随显示) |
| | | queueMapper.updateById(queueItem); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * [过号] 患者 => [候诊中] ,处理 检查项目.亲和性 逻辑。 |
| | | * |
| | | * <p>召回时主项的号不变,因此只需让已经跟随的亲和项保持一致(保持同号 → 界面仍排在一起)。 |
| | | */ |
| | | private void procAffinityWhenRecalled(QueueDO recalledQueueItem, Integer[] affinityCheckTypes) { |
| | | List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus( |
| | | recalledQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.AFFINITY_WAITING.getStatus()); |
| | | recalledQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.AFFINITY.getStatus()); |
| | | for (int i = 0; i < affinityItems.size(); i++) { |
| | | QueueDO queueItem = affinityItems.get(i); |
| | | queueItem.setSeqNum( recalledQueueItem.getSeqNum() ); // 召回后,原内部序号更新 |
| | | queueItem.setSeqNum( recalledQueueItem.getSeqNum() ); // 亲和项与主项保持同一个号 |
| | | queueMapper.updateById(queueItem); |
| | | } |
| | | } |
| | | |
| | | private Integer getBedReadyMax(Long roomId, String bedNo) { |
| | | RoomDO roomDO = roomMapper.getRoom(roomId, bedNo); |
| | | Integer[] checkTypes = roomDO.getCheckTypes(); |
| | | Integer ret = 0; |
| | | for (int i=0; i<checkTypes.length; i++) { |
| | | CheckTypeDO checkTypeDO = mapCheckTypeVsReadyMax.get(checkTypes[i]); |
| | | /** |
| | | * 工位「候诊区容量上限」= 该工位开展的**各检查项目** {@code check_type.ready_num} |
| | | * (页面字段:「检查类型管理」→「候诊准备人数」)的**最大值**。 |
| | | * |
| | | * <p><b>返回 0 表示"不自动分流"</b>:此时排队中(10) 的患者**不会**被主动搬进候诊区(20), |
| | | * 需要医生在候诊区用「拉入」手动补人(现场确认的口径)。 |
| | | * |
| | | * <p>【本次加固】 |
| | | * <ul> |
| | | * <li><b>实时读库</b>:原来这份配置只在服务启动时加载一次 |
| | | * ({@code MySpringEventListener} → {@code initCheckType()}), |
| | | * 在页面上改了「候诊准备人数」必须**重启服务**才生效;</li> |
| | | * <li><b>容错</b>:检查项目在配置里查不到、或 {@code ready_num} 为空 → **跳过并记警告**, |
| | | * 不再抛 NPE。原先这个 NPE 发生在 {@code addBedToRoutingQueue}(入座/开诊/重载都会走), |
| | | * 会连带把入座弄失败。</li> |
| | | * </ul> |
| | | */ |
| | | private Integer getBedReadyMax(BedBO bedBO) { |
| | | Integer[] checkTypes = bedBO.getCheckTypes(); |
| | | if (null == checkTypes || 0 == checkTypes.length) { |
| | | return 0; |
| | | } |
| | | Map<Integer, CheckTypeDO> fresh = readCheckTypeMapFresh(); |
| | | int ret = 0; |
| | | for (Integer checkType : checkTypes) { |
| | | CheckTypeDO checkTypeDO = fresh.get(checkType); |
| | | if (null == checkTypeDO) { |
| | | // 兼容:实时读失败时回落到启动缓存,两边都没有就跳过(不再 NPE) |
| | | checkTypeDO = getCheckTypeItem(checkType); |
| | | } |
| | | if (null == checkTypeDO) { |
| | | log.warn("[getBedReadyMax] 检查项目在 check_type 里不存在,跳过(按 0 计). checkType={} 工位={}/{}", |
| | | checkType, bedBO.getRoomId(), bedBO.getBedNo()); |
| | | continue; |
| | | } |
| | | Integer tmp = checkTypeDO.getReadyNum(); |
| | | ret = ret < tmp ? tmp : ret; //取大值 |
| | | if (null == tmp) { |
| | | log.warn("[getBedReadyMax] 检查项目[候诊准备人数]为空,跳过. checkType={} name={}", |
| | | checkType, checkTypeDO.getName()); |
| | | continue; |
| | | } |
| | | ret = Math.max(ret, tmp); //【候诊中】数量 取大值 |
| | | } |
| | | return ret; |
| | | } |
| | | |
| | | private void addPriorityQueue(BedQueueBO bedQueueBO) { |
| | | Integer[] checkTypes = bedQueueBO.getCheckTypes(); |
| | | /** |
| | | * 实时读取「检查项目值 → 检查项目配置」映射(每次装配工位时读一次全表,检查项目只有十几个,量级很小)。 |
| | | * 这样管理员在「检查类型管理」里改了「候诊准备人数」**立刻生效,不用重启服务**。 |
| | | * 读库失败时回落到启动时加载的缓存({@link #initCheckType()})。 |
| | | */ |
| | | private Map<Integer, CheckTypeDO> readCheckTypeMapFresh() { |
| | | try { |
| | | List<CheckTypeDO> list = checkTypeMapper.simpleCheckTypeList(); |
| | | Map<Integer, CheckTypeDO> map = new HashMap<>(); |
| | | for (CheckTypeDO item : list) { |
| | | if (null != item && null != item.getValue()) { |
| | | map.put(item.getValue(), item); |
| | | } |
| | | } |
| | | return map; |
| | | } catch (Exception e) { |
| | | log.warn("[readCheckTypeMapFresh] 实时读检查项目失败,改用启动时缓存", e); |
| | | return null == mapCheckTypeVsReadyMax ? new HashMap<>() : mapCheckTypeVsReadyMax; |
| | | } |
| | | } |
| | | |
| | | private void addBedToRoutingQueue(BedBO bedBO) { |
| | | // 装机工位 不进 分流队列,不接收 【排队中】患者分流 |
| | | if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType())) |
| | | return; |
| | | |
| | | // 【需求3】「停止候诊」的工位不进分流队列。 |
| | | // 这是**唯一入口**的统一门闸:只要工位被标记为停止候诊, |
| | | // 无论从哪条路径(开诊 addBedToRoutingQueue、医生恢复、bedReload、 |
| | | // 或手动恢复开关)试图加入,都会被拒绝,从而保证: |
| | | // · 已有候诊患者仍可正常叫号(nextPatient 不检查该开关) |
| | | // · 但不会再被分配【排队中】的新患者 |
| | | if (Boolean.TRUE.equals(bedBO.getStopReady())) { |
| | | log.info("[addBedToRoutingQueue] 工位处于[停止候诊],跳过加入分流队列. roomId={} bedNo={}", |
| | | bedBO.getRoomId(), bedBO.getBedNo()); |
| | | return; |
| | | } |
| | | |
| | | // 同步DB, 设置工位的分流控制信息 |
| | | List<Integer> queueStatusList = new ArrayList<>(); |
| | | queueStatusList.add(QueueStatusEnum.READY.getStatus()); //[候诊中] |
| | | List<QueueDO> queueDOList = queueMapper.getBedQueueByStatus(bedBO.getRoomId(), bedBO.getBedNo(), queueStatusList); |
| | | bedBO.setMaxReadyPatNum( getBedReadyMax(bedBO) ); |
| | | bedBO.setReadyPatNum( queueDOList.size() ); |
| | | |
| | | // 【现场排查】把"候诊区上限 / 当前人数 / 来源"打出来: |
| | | // 上限=0 表示**不自动分流**(排队中的患者不会被主动搬进候诊区,需在候诊区手动「拉入」)。 |
| | | // 现场反馈过"有这么多排队病人,为什么没主动加载到候诊区",一条日志即可自证。 |
| | | log.info("[addBedToRoutingQueue] 工位 {}/{} 候诊上限={} 当前候诊={} 检查项目={}" |
| | | + "(上限=各检查项目 check_type.ready_num 的最大值;0=不自动分流,靠工位手动「拉入」)", |
| | | bedBO.getRoomId(), bedBO.getBedNo(), bedBO.getMaxReadyPatNum(), bedBO.getReadyPatNum(), |
| | | null == bedBO.getCheckTypes() ? "[]" : java.util.Arrays.toString(bedBO.getCheckTypes())); |
| | | |
| | | Integer[] checkTypes = bedBO.getCheckTypes(); |
| | | Arrays.stream(checkTypes).forEach(checkType -> { |
| | | CheckTypeDO checkTypeDO = mapCheckTypeVsReadyMax.get(checkType); |
| | | // 装机工位 不进 优先队列,不从排队中接收人 |
| | | if (checkTypeDO.getNeedDevReady() == 1 && bedQueueBO.getOpType() == 2) { |
| | | return; |
| | | } |
| | | |
| | | PriorityBlockingQueue<BedQueueBO> priorityQueue = mapCheckTypePriorityQueue.get(checkType); |
| | | if (null == priorityQueue) { |
| | | priorityQueue = new PriorityBlockingQueue<BedQueueBO>(); |
| | | mapCheckTypePriorityQueue.put( checkType, priorityQueue ); |
| | | PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType); |
| | | if (null == routingQueue) { |
| | | routingQueue = new PriorityBlockingQueue<BedBO>(); |
| | | mapCheckTypeRoutingQueue.put( checkType, routingQueue ); |
| | | } |
| | | |
| | | if (!priorityQueue.contains(bedQueueBO)) { |
| | | priorityQueue.offer(bedQueueBO); |
| | | if (!routingQueue.contains(bedBO)) { |
| | | routingQueue.offer(bedBO); |
| | | } else { |
| | | log.error("bedDoctorResume priorityQueue still exist!. " + bedQueueBO.getRoomId() + " " + bedQueueBO.getBedNo()); |
| | | log.error("bedDoctorResume routingQueue still exist!. " + bedBO.getRoomId() + " " + bedBO.getBedNo()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void removePriorityQueue(BedQueueBO bedQueueBO) { |
| | | Integer[] checkTypes = bedQueueBO.getCheckTypes(); |
| | | private void removeBedFromRoutingQueue(BedBO bedBO) { |
| | | // 装机工位 不进 分流队列,不接收 【排队中】患者分流 |
| | | if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType())) |
| | | return; |
| | | |
| | | Integer[] checkTypes = bedBO.getCheckTypes(); |
| | | Arrays.stream(checkTypes).forEach(checkType -> { |
| | | PriorityBlockingQueue<BedQueueBO> priorityBlockingQueue = mapCheckTypePriorityQueue.get(checkType); |
| | | if (null != priorityBlockingQueue) |
| | | priorityBlockingQueue.remove(bedQueueBO); |
| | | PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType); |
| | | if (null != routingQueue) |
| | | routingQueue.remove(bedBO); |
| | | }); |
| | | } |
| | | |
| | | private void refreshPriorityQueue(BedQueueBO bedQueueBO) { |
| | | Integer[] checkTypes = bedQueueBO.getCheckTypes(); |
| | | private void refreshBedPriorityLevelInRoutingQueue(BedBO bedBO) { |
| | | // 装机工位 不进 分流队列,不接收 【排队中】患者分流 |
| | | if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType())) |
| | | return; |
| | | |
| | | Integer[] checkTypes = bedBO.getCheckTypes(); |
| | | Arrays.stream(checkTypes).forEach(checkType -> { |
| | | PriorityBlockingQueue<BedQueueBO> priorityQueue = mapCheckTypePriorityQueue.get(checkType); |
| | | priorityQueue.remove(bedQueueBO); |
| | | priorityQueue.offer(bedQueueBO); |
| | | PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType); |
| | | if (null != routingQueue) { |
| | | routingQueue.remove(bedBO); |
| | | routingQueue.offer(bedBO); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 检查项进入到 [安装中] 时,拉 [已领用] + 具有亲和性的 检查项 来依附该检查项 |
| | | */ |
| | | private void procAffinityWhenSomeOneEntryInstalling(String patId, Long roomId_operator, String bedNo_operator, Integer[] affinityCheckTypes) { |
| | | // 对于 刚进入到 [安装中]的项, 如果存在 [已领用] 的亲和项,则需要其跟随 |
| | | List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus( |
| | | patId, affinityCheckTypes, QueueStatusEnum.RECEIVED.getStatus()); |
| | | for (int i = 0; i < affinityItems.size(); i++) { |
| | | QueueDO queueItem = affinityItems.get(i); |
| | | queueItem.setStatus(QueueStatusEnum.AFFINITY_RECEIVED.getStatus()); //改变 排队状态 |
| | | queueItem.setStatus(QueueStatusEnum.AFFINITY_INSTALL.getStatus()); //改变 排队状态 |
| | | queueItem.setAffinityItem(1); |
| | | queueItem.setRoomId(roomId_operator); |
| | | queueItem.setBedNo(bedNo_operator); // 跟随到 装机 工位 |
| | | queueMapper.updateById(queueItem); |
| | |
| | | private void roomBedStatistic(MonitorInfoVO monitorInfoVO) { |
| | | Map<Integer, Integer> mapOpeningCheckBedStatInfo = new HashMap<>(); |
| | | Map<Integer, Integer> mapOpeningInstallBedStatInfo = new HashMap<>(); |
| | | Map<Integer, Integer> mapOperatingCheckBedStatInfo = new HashMap<>(); |
| | | Map<Integer, Integer> mapOperatingInstallBedStatInfo = new HashMap<>(); |
| | | Map<Integer, Integer> mapDocCheckBedStatInfo = new HashMap<>(); |
| | | Map<Integer, Integer> mapDocInstallBedStatInfo = new HashMap<>(); |
| | | mapCheckTypeVsReadyMax.keySet().forEach( checkType -> { |
| | | mapOpeningCheckBedStatInfo.put(checkType, 0); |
| | | mapOpeningInstallBedStatInfo.put(checkType, 0); |
| | | mapOperatingCheckBedStatInfo.put(checkType, 0); |
| | | mapOperatingInstallBedStatInfo.put(checkType, 0); |
| | | mapDocCheckBedStatInfo.put(checkType, 0); |
| | | mapDocInstallBedStatInfo.put(checkType, 0); |
| | | }); |
| | | |
| | | //开通的 和 运营中 的工位统计 |
| | | mapBedVsQueue.values().forEach( bedQueueBO -> { |
| | | mapOpeningBed.values().forEach(bedQueueBO -> { |
| | | Integer[] checkTypes = bedQueueBO.getCheckTypes(); |
| | | Arrays.stream(checkTypes).forEach(checkType -> { |
| | | if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1) |
| | |
| | | else |
| | | mapOpeningInstallBedStatInfo.put(checkType, mapOpeningInstallBedStatInfo.get(checkType) + 1 ); |
| | | |
| | | if (bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus()) { |
| | | if (bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus() || |
| | | bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_PAUSE.getStatus()) { |
| | | if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1) |
| | | mapOperatingCheckBedStatInfo.put(checkType, mapOperatingCheckBedStatInfo.get(checkType) + 1); |
| | | mapDocCheckBedStatInfo.put(checkType, mapDocCheckBedStatInfo.get(checkType) + 1); |
| | | else |
| | | mapOperatingInstallBedStatInfo.put(checkType, mapOperatingInstallBedStatInfo.get(checkType) + 1); |
| | | mapDocInstallBedStatInfo.put(checkType, mapDocInstallBedStatInfo.get(checkType) + 1); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // 接收..排队中患者..的优先队列 |
| | | HashSet<BedQueueBO> hashSetOperatingBedQueue = new HashSet<>(); |
| | | mapCheckTypePriorityQueue.values().forEach(priorityQueue->{ |
| | | priorityQueue.stream().forEach(bedQueueBO -> hashSetOperatingBedQueue.add(bedQueueBO)); |
| | | // 接收..排队中患者..的分流队列 |
| | | HashSet<BedBO> hashSetOperatingBedQueue = new HashSet<>(); |
| | | mapCheckTypeRoutingQueue.values().forEach(routingQueue->{ |
| | | routingQueue.stream().forEach(bedQueueBO -> hashSetOperatingBedQueue.add(bedQueueBO)); |
| | | }); |
| | | |
| | | |
| | |
| | | mapCheckTypeVsReadyMax.keySet().forEach( checkType -> { |
| | | String str = mapOpeningCheckBedStatInfo.get(checkType) + " " + |
| | | mapOpeningInstallBedStatInfo.get(checkType) + " " + |
| | | mapOperatingCheckBedStatInfo.get(checkType) + " " + |
| | | mapOperatingInstallBedStatInfo.get(checkType) + " "; |
| | | mapDocCheckBedStatInfo.get(checkType) + " " + |
| | | mapDocInstallBedStatInfo.get(checkType) + " "; |
| | | mapCheckTypeBedInfo.put( checkType, str); |
| | | }); |
| | | |
| | | monitorInfoVO.setCheckTypeBedInfo(mapCheckTypeBedInfo); |
| | | monitorInfoVO.setQueueNum(mapBedVsQueue.size()); |
| | | monitorInfoVO.setActiveQueueNum((int) mapBedVsQueue.values().stream().filter(item -> item.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus()).count()); |
| | | monitorInfoVO.setPriorityQueueNum( hashSetOperatingBedQueue.size() ); |
| | | monitorInfoVO.setMapBedVsQueue( mapBedVsQueue ); |
| | | monitorInfoVO.setOpeningBedNum(mapOpeningBed.size()); |
| | | monitorInfoVO.setDocBedNum((int) mapOpeningBed.values().stream().filter(item -> |
| | | item.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus() || |
| | | item.getStatus() == BedStatusEnum.DOCTOR_PAUSE.getStatus()) |
| | | .count()); |
| | | // 【本需求·统计按人合并】在座**医生数**:一人多工位时上面的 docBedNum 会按工位累计, |
| | | // 这里按 doc_id 去重,给监控页一个"到底几个医生在岗"的指标。 |
| | | List<Integer> seatedStatusList = new ArrayList<>(); |
| | | seatedStatusList.add(BedStatusEnum.DOCTOR_ON.getStatus()); |
| | | seatedStatusList.add(BedStatusEnum.DOCTOR_PAUSE.getStatus()); |
| | | try { |
| | | Integer docNum = roomMapper.countSeatedDoctors(seatedStatusList); |
| | | monitorInfoVO.setDocNum(null == docNum ? 0 : docNum); |
| | | } catch (Exception e) { |
| | | // 统计失败不影响监控页其它数据 |
| | | log.warn("[roomBedStatistic] 统计在座医生数失败(不影响其它指标)", e); |
| | | monitorInfoVO.setDocNum(0); |
| | | } |
| | | monitorInfoVO.setRoutingBedNum( hashSetOperatingBedQueue.size() ); |
| | | monitorInfoVO.setMapBedVsQueue(mapOpeningBed); |
| | | } |
| | | |
| | | } |