| | |
| | | |
| | | |
| | | AtomicInteger openingFlag = new AtomicInteger(0); |
| | | AtomicInteger curSeqNum = new AtomicInteger(0); |
| | | |
| | | // 【号统一(本需求)】原 curSeqNum(分流时给患者编"内部序号"的内存计数器)已删除。 |
| | | // 排队号改为签到时一次性写入 queue.seq_num,之后任何环节都不再赋号: |
| | | // 分流、叫号后补齐候诊、过号、召回、加急、装机都只改状态/诊室/工位。 |
| | | |
| | | // 维护 开放的工位 |
| | | ConcurrentHashMap<String, BedBO> mapOpeningBed = new ConcurrentHashMap<>(); |
| | |
| | | return null == bedBO ? null : Boolean.TRUE.equals(bedBO.getStopReady()); |
| | | } |
| | | |
| | | public ErrorCode bedDoctorOn(Long roomId, String bedNo, Long docId, String docName) { |
| | | /** |
| | | * 入座(把工位主人设为我)。 |
| | | * |
| | | * @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; |
| | | } |
| | | |
| | | // 【本需求·一人多工位】入座成功后**不再释放**该医生的其它工位: |
| | | // 节假日值班时一位医生可能同时占「检查工位 + 领用工位 + 装机工位」, |
| | | // 由前端「我的工位」下拉在各工位间切换(写操作仍由 @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) { |
| | |
| | | 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) { |
| | |
| | | |
| | | // 常规 或者 领用 时 医生 取下一位患者 |
| | | public ErrorCode nextPatient(Long roomId, String bedNo) { |
| | | QueueDO firstReadyQueueItem = queueMapper.getBedFirstQueueItemByStatus(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()); |
| | | |
| | | // 分流队列中 该工位 【候诊中】患者数量 减一 |
| | | BedBO bo = mapOpeningBed.get( Utils.formatRoomBed( roomId, bedNo ) ); |
| | | bo.readyPatNum--; // 可能已经在 hurry-up 中改变了值 |
| | | |
| | | refreshBedPriorityLevelInRoutingQueue(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; |
| | | } |
| | | |
| | |
| | | } |
| | | }); |
| | | |
| | | Integer num = queueMapper.getMaxSeqNum(); |
| | | curSeqNum = new AtomicInteger(null == num ? 0 : num); |
| | | |
| | | // 【号统一(本需求)】不再初始化内存计数器 curSeqNum: |
| | | // 排队号来自签到时的 queue.seq_num,分流不再分配号。 |
| | | return GlobalErrorCodeConstants.SUCCESS; |
| | | } |
| | | |
| | |
| | | * 把医生候诊的队列塞满 |
| | | */ |
| | | public void hurryupOneBed(Long roomId, String bedNo) { |
| | | if (0 == openingFlag.get()) |
| | | if (0 == openingFlag.get()) { |
| | | // 【现场排查】未开诊时**整个自动补人都是关闭的**(排队中的病人不会被搬进候诊区)。 |
| | | // 原实现是静默 return,现场只能看到"人都在,就是不进来",故这里明确记一条。 |
| | | log.warn("[补人] 系统未处于【开诊】状态(openingFlag=0),跳过自动补人. 工位={}/{}(可在监控页点「开诊」)", |
| | | roomId, bedNo); |
| | | return; |
| | | } |
| | | |
| | | 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 不检查本开关), |
| | |
| | | |
| | | // 处理 过号-回来 的人 |
| | | while (!stopReady && bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) { |
| | | // 查看 当前工位 是否有过号-回来的患者 |
| | | Integer updateNum = queueMapper.queueRecalledPatient( |
| | | bedBO.getRoomId(), |
| | | bedBO.getRoomName(), |
| | | bedBO.getBedNo(), |
| | | curSeqNum.get() + 1, // 过号回来,排到当前的下一位 |
| | | QueueStatusEnum.RECALLED.getStatus(), // 不需要处理 [安装召回], 原因: 安装工位 不进入分流队列 |
| | | QueueStatusEnum.READY.getStatus()); |
| | | if (null == updateNum || 0 == updateNum) |
| | | // 查看 当前工位 是否有「已召回(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++; |
| | |
| | | return; |
| | | |
| | | // 查看 是否有排队中的患者 |
| | | Integer updateNum = queueMapper.preemptWaitingPatientWithBedCheckTypes( |
| | | bedBO.getRoomId(), |
| | | bedBO.getRoomName(), |
| | | bedBO.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(), |
| | | 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() ); |
| | | |
| | | // 抢到排队患者后,处理 检查项目.亲和性 逻辑 |
| | | if ( checkTypeDO.getAffinityCheckTypes().length > 0) { |
| | | procAffinityWhenPreempt(preemptQueueItem, checkTypeDO.getAffinityCheckTypes()); |
| | | } |
| | | |
| | | curSeqNum.getAndIncrement(); |
| | | |
| | | // 可能已经在 nextPatient 中改变了值 |
| | | bedBO.readyPatNum++; |
| | |
| | | // 【需求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( |
| | | bedBO.getRoomId(), |
| | | bedBO.getRoomName(), |
| | | bedBO.getBedNo(), |
| | | curSeqNum.get() + 1, |
| | | // |
| | | // 【号统一(本需求)】与 hurryupOneCheckType 相同:只搬迁状态,**不再分配排队号**。 |
| | | QueueDO preemptQueueItem = queueMapper.getFirstWaitingByCheckTypes( |
| | | QueueStatusEnum.WAITING.getStatus(), |
| | | QueueStatusEnum.READY.getStatus(), |
| | | 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(); |
| | | |
| | | bedBO.readyPatNum++; |
| | | pulledNum++; |
| | | } |
| | | |
| | | 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( |
| | |
| | | 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.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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 工位「候诊区容量上限」= 该工位开展的**各检查项目** {@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 = mapCheckTypeVsReadyMax.get(checkType); |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | | * 实时读取「检查项目值 → 检查项目配置」映射(每次装配工位时读一次全表,检查项目只有十几个,量级很小)。 |
| | | * 这样管理员在「检查类型管理」里改了「候诊准备人数」**立刻生效,不用重启服务**。 |
| | | * 读库失败时回落到启动时加载的缓存({@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) { |
| | |
| | | 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 -> { |
| | |
| | | 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); |
| | | } |