| | |
| | | |
| | | import java.util.*; |
| | | import java.util.concurrent.*; |
| | | import java.util.stream.Collectors; |
| | | import javax.annotation.Resource; |
| | | |
| | | import cn.lihu.jh.module.ecg.Utils; |
| | |
| | | return ECG_INNER_ERROR; |
| | | } |
| | | |
| | | /** |
| | | * 「本诊室排队中(未分配工位)」的患者列表。 |
| | | * |
| | | * <p>工作台候诊列表按 {@code room_id + bed_no} 查,只看得到**已分流到本工位**的患者; |
| | | * 而"排队中(10)"的患者 {@code bed_no} 为空(大屏会显示、工作台看不到), |
| | | * 这里按**本工位开展的检查项目**把他们单独查出来,供医生手工拉入。 |
| | | * |
| | | * <p>工位不存在或没配检查项目时返回空列表(记警告),不抛异常 —— 列表为空不影响主流程。 |
| | | */ |
| | | @Override |
| | | public ErrorCode startBedDoctorOn(Long roomId, String bedNo, Long docId, String docName) { |
| | | Future<ErrorCode> future = singleThreadExecutor.submit(new BedDoctorOnCallable(queueServiceTxFunctions, roomId, bedNo, docId, docName)); |
| | | public List<QueueDO> getUnassignedWaitingPatient(Long roomId, String bedNo) { |
| | | RoomDO bed = (null == roomId || null == bedNo) ? null : roomMapper.getRoom(roomId, bedNo); |
| | | if (null == bed || null == bed.getCheckTypes() || 0 == bed.getCheckTypes().length) { |
| | | log.warn("[getUnassignedWaitingPatient] 工位不存在或未配置检查项目,返回空. roomId={} bedNo={}", |
| | | roomId, bedNo); |
| | | return new ArrayList<>(); |
| | | } |
| | | return queueMapper.selectUnassignedWaitingByCheckTypes( |
| | | QueueStatusEnum.WAITING.getStatus(), bed.getCheckTypes()); |
| | | } |
| | | |
| | | /** |
| | | * 手工把某位「排队中」患者拉入本工位(→ 准备中)。 |
| | | * |
| | | * <p>权限由 Controller 上的 {@code @RequireBedOwner} 保证:只有本工位主人能拉。 |
| | | * 这里只做业务校验与条件更新(并发安全:{@code and status = 10}),并复用自动分流的亲和项逻辑。 |
| | | */ |
| | | @Override |
| | | public ErrorCode pullWaitingPatient(Long roomId, String bedNo, Integer queueId) { |
| | | return queueServiceTxFunctions.pullWaitingPatient(roomId, bedNo, queueId); |
| | | } |
| | | |
| | | @Override |
| | | public ErrorCode startBedDoctorOn(Long roomId, String bedNo, Long docId, String docName, Integer bedId) { |
| | | Future<ErrorCode> future = singleThreadExecutor.submit(new BedDoctorOnCallable(queueServiceTxFunctions, roomId, bedNo, docId, docName, bedId)); |
| | | |
| | | try { |
| | | ErrorCode ret = future.get(); |
| | |
| | | } |
| | | |
| | | //System.out.println("startBedDoctorOff ========"); |
| | | return ECG_INNER_ERROR; |
| | | } |
| | | |
| | | /** |
| | | * 【本次需求】管理员强制清空工位(不要求是本人)。 |
| | | * <p> |
| | | * 与 {@link #startBedDoctorOff} 共用同一个单线程执行器,保证同一时刻只处理一个工位操作, |
| | | * 避免与「叫号 / 入座 / 离座」并发改同一行。 |
| | | */ |
| | | @Override |
| | | public ErrorCode startBedDoctorOffByAdmin(Long roomId, String bedNo) { |
| | | Future<ErrorCode> future = singleThreadExecutor.submit( |
| | | () -> queueServiceTxFunctions.bedDoctorOffByAdmin(roomId, bedNo)); |
| | | try { |
| | | return future.get(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.error("[管理员清空工位] 被中断", e); |
| | | } catch (ExecutionException e) { |
| | | log.error("[管理员清空工位] 执行失败", e.getCause() != null ? e.getCause() : e); |
| | | } |
| | | return ECG_INNER_ERROR; |
| | | } |
| | | |
| | |
| | | singleThreadExecutor.execute(() -> { |
| | | queueServiceTxFunctions.hurryupOneBed(roomId, bedNo); |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> hurryUpNow(Long roomId, String bedNo) { |
| | | // 同步执行:要立刻返回"补人前后人数 + 结论",异步就看不到结果了 |
| | | return queueServiceTxFunctions.hurryUpNow(roomId, bedNo); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<QueueDO> selectBookSeqNumByAppointIdList(List<Long> appointIdList) { |
| | | return queueMapper.selectBookSeqNumByAppointIdList(appointIdList); |
| | | public List<QueueDO> selectQueueByAppointIdList(List<Long> appointIdList) { |
| | | return queueMapper.selectQueueByAppointIdList(appointIdList); |
| | | } |
| | | |
| | | // 常规检查 叫号 |
| | |
| | | // 从 DB 把 [安装中]的人 设置为 [过号-安装] |
| | | Integer ret = queueMapper.updateBedQueueStatus(roomId, bedNo, QueueStatusEnum.INSTALLING.getStatus(), QueueStatusEnum.PASSED_INSTALL.getStatus()); |
| | | |
| | | startNextPatient(roomId, bedNo); |
| | | // 【既有笔误修复】这里原本调的是 startNextPatient(常规检查取人:候诊中 → 就诊中), |
| | | // 而装机过号后应当继续取**安装队列**里的下一位(已领用/已召回-安装 → 安装中)。 |
| | | // 由于装机工位不参与分流、没有"候诊中"患者,原调用实际上什么也不会发生, |
| | | // 表现为"装机过号后不会自动叫下一位"。 |
| | | startNextInstallPatient(roomId, bedNo); |
| | | } |
| | | |
| | | public List<QueueDO> getBedQueueByStatus(Long roomId, String bedNo, List<Integer> statusList) { |
| | |
| | | /** |
| | | * 从预约单补齐 queue 表上没有的患者信息。 |
| | | * |
| | | * queue 表不存这些字段,但医生端列表接口需要展示,所以查询后按 applyNo 回查预约单填充: |
| | | * 多耐药、性别、出生日期、登记时间、开单医师、开单日期。 |
| | | * <p>queue 表不存这些字段,但医生端列表接口需要展示,所以查询后按 applyNo 回查预约单填充: |
| | | * 多耐药、性别、出生日期、登记时间、开单医师、开单日期。</p> |
| | | * |
| | | * <h3>⚠️ 性能修复:由 N+1 改为一次批量查询</h3> |
| | | * <p><b>原实现</b>在 {@code forEach} 里对每一行调用 |
| | | * {@code appointmentMapper.getByApplyNo(applyNo)},即**每行一次 SQL**。 |
| | | * 列表每页 10 条 ⇒ 10 次查询;而该 SQL |
| | | * ({@code WHERE apply_no = ? AND deleted = 0})在修复前**用不上索引**, |
| | | * 每次都是全表扫描 appointment(实测 16.6 万行,约 549 ms/次), |
| | | * 一页就要 4.5~5.5 秒 —— 这是「队列列表打开很慢」的直接原因。</p> |
| | | * |
| | | * <p><b>现实现</b>改为:先收集本页所有 applyNo(去重)→ **一次** |
| | | * {@code getByApplyNoList} 批量取回 → 内存里按 applyNo 建映射再回填。 |
| | | * 一页 SQL 从 10 次降到 1 次。</p> |
| | | * |
| | | * <p>注:配套已于 {@code 26_add_index_appointment_apply_no.sql} |
| | | * 给 {@code appointment.apply_no} 加上索引; |
| | | * 但即便有索引,N+1 仍是 10 次往返,故两处一起改才是最优。</p> |
| | | */ |
| | | @Override |
| | | public void fillAppointmentInfo(List<QueueDO> queueDOList) { |
| | | if (CollectionUtils.isEmpty(queueDOList)) { |
| | | return; |
| | | } |
| | | // 1. 收集本次需要回查的申请单号(跳过空值)。 |
| | | // 用 LinkedHashSet 去重:同一页里理论上 apply_no 唯一,但去重后 IN 列表更短、更安全。 |
| | | Set<String> applyNoSet = new LinkedHashSet<>(); |
| | | for (QueueDO queueDO : queueDOList) { |
| | | if (queueDO != null && queueDO.getApplyNo() != null && !queueDO.getApplyNo().isEmpty()) { |
| | | applyNoSet.add(queueDO.getApplyNo()); |
| | | } |
| | | } |
| | | if (applyNoSet.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | // 2. 一次批量取回(SQL 内部已带 deleted = 0)。 |
| | | // 注意:MySQL 的 IN () 会语法错误,故必须保证列表非空 —— 上面已判空。 |
| | | List<AppointmentDO> appointmentList = appointmentMapper.getByApplyNoList(new ArrayList<>(applyNoSet)); |
| | | |
| | | // 3. 按 applyNo 建映射;个别 applyNo 查不到预约单是正常情况(历史脏数据),跳过即可。 |
| | | Map<String, AppointmentDO> appointmentMap = appointmentList.stream() |
| | | .filter(item -> item != null && item.getApplyNo() != null) |
| | | .collect(Collectors.toMap(AppointmentDO::getApplyNo, item -> item, (a, b) -> a)); |
| | | |
| | | // 4. 内存回填,不再逐条查库 |
| | | queueDOList.forEach(queueDO -> { |
| | | if (null == queueDO.getApplyNo() || queueDO.getApplyNo().isEmpty()) { |
| | | if (queueDO == null || queueDO.getApplyNo() == null || queueDO.getApplyNo().isEmpty()) { |
| | | return; |
| | | } |
| | | AppointmentDO appointment = appointmentMapper.getByApplyNo(queueDO.getApplyNo()); |
| | | AppointmentDO appointment = appointmentMap.get(queueDO.getApplyNo()); |
| | | if (null == appointment) { |
| | | return; |
| | | } |
| | |
| | | |
| | | public void closeBiz() { |
| | | queueServiceTxFunctions.setOpeningFlag(0); |
| | | |
| | | // 【本次需求】闭诊时清空当班工位(doc_id / doc_name 置空,状态改回空闲)。 |
| | | // |
| | | // 背景:工位不再因「空闲超时 / 令牌失效 / 刷新页面」被释放(避免医生离开一会儿就丢工位), |
| | | // 因此需要一个兜底 —— 每天闭诊时统一清空,防止医生下班忘离座导致工位长期被占用。 |
| | | // 先清库,再由下面的 startBedReload() 按库重新装载内存,保证二者一致。 |
| | | try { |
| | | queueServiceTxFunctions.closeBizClearBeds(); |
| | | } catch (Exception e) { |
| | | // 清空失败不能阻断闭诊流程本身 |
| | | log.error("[闭诊] 清空当班工位失败(不影响闭诊)", e); |
| | | } |
| | | |
| | | startBedReload(); |
| | | } |
| | | |
| | |
| | | // 会在 HurryUpOnBed 里面处理 检查项目.亲和性 |
| | | |
| | | startHurryUpOneBed(roomId, bedNo); |
| | | |
| | | // 【诊断】补人循环有三道门闸(未开诊 / 已停止候诊 / 候诊区已满), |
| | | // 任一条成立时患者会**停在「已召回(15)」不进候诊区**。 |
| | | // 现在 nextPatient 支持直接从「已召回」叫号(2026-10-29 修复),这里只补一条日志方便现场排查。 |
| | | if (null != updateNum && updateNum > 0) { |
| | | try { |
| | | QueueDO stillRecalled = queueMapper.getBedQueueItemByPatAndStatus( |
| | | roomId, bedNo, patId, checkType, QueueStatusEnum.RECALLED.getStatus()); |
| | | if (null != stillRecalled) { |
| | | log.warn("[recallPatient] 已置为「已召回」,但未进入候诊区" |
| | | + "(可能原因:未开诊 / 已停止候诊 / 候诊区已满),叫号时将直接从「已召回」取人. " |
| | | + "工位={}/{} patId={} seqNum={}", |
| | | roomId, bedNo, patId, stillRecalled.getSeqNum()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[recallPatient] 检查「已召回」是否进入候诊区时异常(不影响召回结果). 工位={}/{} patId={}", |
| | | roomId, bedNo, patId, e); |
| | | } |
| | | } |
| | | return updateNum; |
| | | } |
| | | |
| | |
| | | queueStatusList.add(QueueStatusEnum.AFFINITY.getStatus()); |
| | | List<QueueDO> queueDOList = getBedQueueByStatus(roomId, bedNo, queueStatusList); |
| | | |
| | | //把 [亲和的] 挪到主项后面 |
| | | //把 [亲和的] 挪到主项后面(同排队号 + 同患者才算一组,见 Utils#insertAffinityItemsAfterMain) |
| | | List<QueueDO> affinityWaitingList = new ArrayList<>(); |
| | | ListIterator<QueueDO> iterator = queueDOList.listIterator(); |
| | | while (iterator.hasNext()) { |
| | |
| | | affinityWaitingList.add(item); |
| | | } |
| | | } |
| | | affinityWaitingList.forEach(affinityWaitingItem -> { |
| | | for (int i = queueDOList.size() - 1; i >= 0; i--) { |
| | | if (affinityWaitingItem.getSeqNum() == queueDOList.get(i).getSeqNum()) { |
| | | queueDOList.add(i + 1, affinityWaitingItem); |
| | | break; |
| | | } |
| | | } |
| | | }); |
| | | Utils.insertAffinityItemsAfterMain(queueDOList, affinityWaitingList); |
| | | |
| | | return queueDOList; |
| | | } |
| | |
| | | // |
| | | // 背景:上面两处查询的条件是 `room_id = ? AND status IN (...)` |
| | | // (getRoomQueueByStatus 甚至**不含 bed_no**),**完全不看 book_check_type**。 |
| | | // 而自动分流(preemptWaitingPatientWithBedCheckTypes)是**按工位 checkTypes 过滤**的, |
| | | // 而自动分流(QueueMapper#getFirstWaitingByCheckTypes + preemptWaitingPatientById)是**按工位 checkTypes 过滤**的, |
| | | // 所以只有"人工把患者拉进来"这条路径会漏 —— 表现为 |
| | | // 「设备领用」页错误出现常规心电图(100)、平板运动心电(400)等**不需领用设备**的患者。 |
| | | // |
| | |
| | | // 故这里另起一个 final 变量,而不是复用上面的 queueDOList(它已被重新赋值)。 |
| | | final List<QueueDO> installQueueList = queueDOList; |
| | | |
| | | //把 [亲和的] 挪到主项后面 |
| | | //把 [亲和的] 挪到主项后面(同排队号 + 同患者才算一组,见 Utils#insertAffinityItemsAfterMain) |
| | | List<QueueDO> affinityReceivedList = new ArrayList<>(); |
| | | ListIterator<QueueDO> iterator = installQueueList.listIterator(); |
| | | while (iterator.hasNext()) { |
| | |
| | | affinityReceivedList.add(item); |
| | | } |
| | | } |
| | | affinityReceivedList.forEach(affinityReceivedItem -> { |
| | | for (int i = installQueueList.size() - 1; i >= 0; i--) { |
| | | if (affinityReceivedItem.getSeqNum() == installQueueList.get(i).getSeqNum()) { |
| | | installQueueList.add(i + 1, affinityReceivedItem); |
| | | break; |
| | | } |
| | | } |
| | | }); |
| | | Utils.insertAffinityItemsAfterMain(installQueueList, affinityReceivedList); |
| | | |
| | | return installQueueList; |
| | | } |