package cn.lihu.jh.module.ecg.service.queue;
|
|
import cn.lihu.jh.framework.common.exception.ErrorCode;
|
import cn.lihu.jh.framework.common.exception.enums.GlobalErrorCodeConstants;
|
import cn.lihu.jh.framework.common.util.object.BeanUtils;
|
import cn.lihu.jh.module.ecg.Utils;
|
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.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;
|
import cn.lihu.jh.module.system.api.oauth2.OAuth2TokenApi;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Component;
|
import org.springframework.validation.annotation.Validated;
|
|
import javax.annotation.Resource;
|
import java.util.*;
|
import java.util.concurrent.*;
|
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.stream.Collectors;
|
|
import static cn.lihu.jh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.lihu.jh.framework.common.pojo.CommonResult.error;
|
import static cn.lihu.jh.module.ecg.enums.ErrorCodeConstants.*;
|
|
/**
|
* 排队 Service 数据库事务相关的方法
|
*
|
* @author 芋道源码
|
*/
|
@Component
|
@Validated
|
@Slf4j
|
public class QueueServiceTxFunctions {
|
|
@Resource
|
private ConfigApi configApi;
|
|
@Resource
|
private OAuth2TokenApi oAuth2TokenApi;
|
|
@Resource
|
private QueueMapper queueMapper;
|
|
@Resource
|
private RoomMapper roomMapper;
|
|
@Resource
|
private CallMapper callMapper;
|
|
@Resource
|
private DevRentMapper devRentMapper;
|
|
@Resource
|
private CheckTypeMapper checkTypeMapper;
|
|
|
AtomicInteger openingFlag = new AtomicInteger(0);
|
|
// 【号统一(本需求)】原 curSeqNum(分流时给患者编"内部序号"的内存计数器)已删除。
|
// 排队号改为签到时一次性写入 queue.seq_num,之后任何环节都不再赋号:
|
// 分流、叫号后补齐候诊、过号、召回、加急、装机都只改状态/诊室/工位。
|
|
// 维护 开放的工位
|
ConcurrentHashMap<String, BedBO> mapOpeningBed = new ConcurrentHashMap<>();
|
|
// 装机工位..不进入分流队列
|
ConcurrentHashMap<Integer, PriorityBlockingQueue<BedBO>> mapCheckTypeRoutingQueue = new ConcurrentHashMap<>();
|
|
Map<Integer, CheckTypeDO> mapCheckTypeVsReadyMax = null;
|
|
/**
|
* 已关闭 或者 关闭中,可以开通工位
|
* @param roomId
|
* @param bedNo
|
* @return
|
*/
|
public ErrorCode bedOpen(Long roomId, String roomName, String bedNo) {
|
BedBO bedBO2 = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
|
if (null != bedBO2) {
|
log.error("bedOpen mapBedVsQueue has existed. " + roomId + " " + bedNo);
|
return QUEUE_BED_EXIST;
|
}
|
|
// DB update
|
List statusList = new ArrayList<BedStatusEnum>();
|
statusList.add(BedStatusEnum.CLOSED);
|
Integer updateNum = roomMapper.setBedOpeningOpening(roomId, bedNo, BedStatusEnum.OPENING, statusList);
|
if ( null==updateNum || 0 == updateNum ) {
|
log.error("bedOpen DB invalid status. " + roomId + " " + bedNo);
|
return ROOM_INVALID_STATUS;
|
}
|
|
RoomDO roomDO = roomMapper.getRoom(roomId, bedNo);
|
|
// 新增..工位队列
|
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) {
|
BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
|
if (null == bedBO) {
|
log.error("bedClose mapBedVsQueue DONOT existed. " + roomId + " " + bedNo);
|
return QUEUE_BED_NOT_EXIST;
|
}
|
|
// 检查是否还有患者需要处理
|
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;
|
|
// DB update
|
List statusList = new ArrayList<BedStatusEnum>();
|
statusList.add(BedStatusEnum.OPENING);
|
statusList.add(BedStatusEnum.DOCTOR_ON);
|
statusList.add(BedStatusEnum.DOCTOR_PAUSE);
|
Integer updateNum = roomMapper.setBedOpeningClosed(roomId, bedNo,
|
BedStatusEnum.CLOSED, statusList);
|
if ( null==updateNum || 0 == updateNum ) {
|
log.error("bedClose DB invalid status. " + roomId + " " + bedNo);
|
return ROOM_INVALID_STATUS;
|
}
|
|
mapOpeningBed.remove( Utils.formatRoomBed(roomId, bedNo) );
|
removeBedFromRoutingQueue(bedBO);
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
|
public ErrorCode bedDoctorPause(Long roomId, String bedNo, Long docId, String docName) {
|
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.DOCTOR_PAUSE, statusList);
|
if ( null==updateNum || 0 == updateNum ) {
|
log.error("bedDoctorPause DB invalid status. " + roomId + " " + bedNo);
|
return ROOM_INVALID_STATUS;
|
}
|
|
bedBO.setStatus(BedStatusEnum.DOCTOR_PAUSE.getStatus());
|
removeBedFromRoutingQueue(bedBO);
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
|
public ErrorCode bedDoctorResume(Long roomId, String bedNo, Long docId, String docName) {
|
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.DOCTOR_PAUSE);
|
Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId,
|
BedStatusEnum.DOCTOR_ON, statusList);
|
if ( null==updateNum || 0 == updateNum ) {
|
log.error("bedDoctorResume DB invalid status. " + roomId + " " + bedNo);
|
return ROOM_INVALID_STATUS;
|
}
|
|
bedBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus());
|
addBedToRoutingQueue(bedBO);
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
|
// ==================== 需求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;
|
}
|
|
// 可以"成为主人"的状态:已开通 / 有医生 / 暂停(**排除"已关闭"**)。
|
// 【本次放宽】原来"无主人"时只认"已开通":一旦数据出现"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 ) {
|
// 仍失败 → 把库里**真实状态**摊开,并区分原因(原来一律"状态异常",现场看不出所以然)
|
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) {
|
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.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;
|
}
|
|
removeBedFromRoutingQueue(bedBO);
|
|
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 把该工位.序号最小的 [候诊中中] 的人 设置为 [就诊中] (或领用中)
|
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 中改变了值
|
|
refreshRoutingQueue(bo);
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
*/
|
|
// 常规 或者 领用 时 医生 取下一位患者
|
public ErrorCode nextPatient(Long roomId, String bedNo) {
|
// ⓪ 【并发加固·本需求】本工位**已有「就诊中(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;
|
}
|
|
// ③ 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());
|
|
// 分流队列中 该工位 【候诊中】患者数量 减一
|
// 注意:召回的这位本来就不在候诊计数里,不能减;另外 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.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 ); // 转到 安装工位
|
queueMapper.updateById( firstItem );
|
|
// 检查项目.亲和性 处理逻辑: 变为【安装中】后 拉 其他检查项来亲和
|
CheckTypeDO checkTypeDO = getCheckTypeItem( firstItem.getBookCheckType() );
|
if (checkTypeDO.getAffinityCheckTypes().length > 0) {
|
procAffinityWhenSomeOneEntryInstalling(firstItem.getPatId(), roomId, bedNo, checkTypeDO.getAffinityCheckTypes() );
|
}
|
|
// 装机工位 不参与分流
|
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
|
/**
|
* 1. 每天开诊前 从DB同步工位的患者队列数据到 工位分流队列
|
* 2. 服务运维重启时
|
*/
|
public ErrorCode bedReload() {
|
mapCheckTypeRoutingQueue.clear();
|
mapOpeningBed.clear();
|
|
// 清除非当天的排队人员
|
queueMapper.clearQueue();
|
// 清除非当天的语音叫号记录
|
callMapper.clearCall();
|
|
// 从DB 获取 工位列表
|
List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>();
|
bedStatusEnumList.add(BedStatusEnum.OPENING);
|
bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON);
|
bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE);
|
List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList);
|
List<BedBO> bedBOList = roomDOList.stream().map(item -> BeanUtils.toBean(item, BedBO.class)).toList();
|
|
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);
|
}
|
});
|
|
// 【号统一(本需求)】不再初始化内存计数器 curSeqNum:
|
// 排队号来自签到时的 queue.seq_num,分流不再分配号。
|
return GlobalErrorCodeConstants.SUCCESS;
|
}
|
|
public void resetRoom(Boolean needCloseBed) {
|
if (needCloseBed) {
|
// 踢出在座的医生
|
List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>();
|
bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON);
|
bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE);
|
List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList);
|
List<Long> userIdList = roomDOList.stream().map(roomDO -> roomDO.getDocId()).toList();
|
oAuth2TokenApi.tick(userIdList);
|
|
// 所有工位状态 => 关闭
|
Integer ret = roomMapper.resetRoom(BedStatusEnum.CLOSED.getStatus());
|
}
|
}
|
|
public void hurryupAllBed() {
|
if (0 == openingFlag.get())
|
return;
|
|
mapOpeningBed.values().forEach(bedQueueBO -> hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo));
|
}
|
|
/**
|
* 【周期兜底专用】只给**有医生在座(DOCTOR_ON)**的工位补人。
|
*
|
* <p>为什么不能直接用 {@link #hurryupAllBed()}:它遍历的是 `mapOpeningBed`
|
* (含「已开通但没人」的工位),会把排队患者推进**没有医生**的工位候诊区 ——
|
* 同一检查项目有多个工位时(如常规心电图 116/117/118),患者可能被"分配"到
|
* 一个没人看的工位,真正在座的医生反而看不到。该方法原本只在开诊/重启时跑一次,
|
* 若改成每分钟跑一遍,这个副作用会被放大。因此周期兜底只补在座工位,
|
* 与自动分流队列({@code addBedToRoutingQueue} 只收 DOCTOR_ON)的口径保持一致。
|
*/
|
public void hurryupAllSeatedBeds() {
|
if (0 == openingFlag.get())
|
return;
|
|
mapOpeningBed.values().forEach(bedQueueBO -> {
|
// 只补「有医生在座」的工位
|
if (!Objects.equals(BedStatusEnum.DOCTOR_ON.getStatus(), bedQueueBO.getStatus())) {
|
return;
|
}
|
// 「停止候诊」的工位直接跳过:hurryupOneBed 内部本来也会跳过,
|
// 这里提前判断是为了避免每分钟刷一条 "[补人] 工位处于【停止候诊】" 的 WARN。
|
if (Boolean.TRUE.equals(bedQueueBO.getStopReady())) {
|
return;
|
}
|
hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo);
|
});
|
}
|
|
/**
|
* 把医生候诊的队列塞满
|
*/
|
public void hurryupOneBed(Long roomId, String bedNo) {
|
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 不检查本开关),
|
// 也不影响医生对具体患者的"召回"操作本身(只影响其自动进入候诊的补位)。
|
boolean stopReady = Boolean.TRUE.equals(bedBO.getStopReady());
|
|
// 处理 过号-回来 的人
|
while (!stopReady && bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) {
|
// 查看 当前工位 是否有「已召回(15)」的患者(排队号最小的那一位)
|
QueueDO recalledQueueItem = queueMapper.getBedFirstQueueItemByStatus(
|
bedBO.getRoomId(), bedBO.getBedNo(), QueueStatusEnum.RECALLED.getStatus());
|
if (null == recalledQueueItem)
|
break;
|
|
// 【号统一(本需求)】过号召回**不再重排队号**:号是签到时定的,
|
// 患者按原号回到候诊序列(需求:「病人过号了,也排着」)。
|
// 旧实现(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());
|
}
|
|
// 可能已经在 nextPatient 中改变了值
|
bedBO.readyPatNum++;
|
|
// 可能已经【并发的】在 nextPatient 中改变了分流队列顺序
|
refreshBedPriorityLevelInRoutingQueue(bedBO);
|
}
|
|
// 处理 排队中 患者
|
hurryupOneBedCheckTypeList(bedBO);
|
}
|
|
/**
|
* 一个工位可以支持多个检查项目,所以需要保证多个检查项目的就诊时间先后顺序
|
* 按照一个检查项目来处理,就不能保证同一个工位的多个检查项目的就诊时间的先后顺序
|
* 所以实现逻辑是 让支持该检查项目的所有工位去抢占 【排队中】【工位支持的检查项目】的患者
|
* 实际抢占的未必是 参数中的检查项目的患者,可能是其他检查项目的患者
|
* @param checkType
|
*/
|
public void hurryupOneCheckType(Integer checkType) {
|
|
// 【修复既有缺陷】补上开诊门闸。原实现**唯独这里漏了** openingFlag 检查
|
// (hurryupAllBed 与 hurryupOneBed 都有),导致手动"闭诊"后,
|
// 只要有人预约确认 / 加急,患者仍会被推进候诊区。
|
if (0 == openingFlag.get())
|
return;
|
|
PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
|
if (null == routingQueue)
|
return;
|
|
while (true) {
|
BedBO bedBO = routingQueue.peek();
|
if (null == bedBO)
|
return;
|
|
// 【需求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 == bedBO.getMaxReadyPatNum())
|
return;
|
|
// 查看 是否有排队中的患者
|
//
|
// 【号统一(本需求)】分流只做「排队中(10) → 候诊中(20)」的搬迁,**不再分配排队号**:
|
// 号在签到时已写入 seq_num;加急只影响这里的取人顺序(SQL 内 jump_flag 优先)。
|
// 旧实现是「UPDATE ... 顺带 seq_num=当前号+1,再按号查回该行」——
|
// 号不再唯一后"按号查回"会取错人,故改为:先取行 → 按 id 条件更新。
|
QueueDO preemptQueueItem = queueMapper.getFirstWaitingByCheckTypes(
|
QueueStatusEnum.WAITING.getStatus(),
|
bedBO.checkTypes); //关键点 关键点 关键点 checkType
|
|
// 没有排队中的患者
|
if (null == preemptQueueItem) {
|
return;
|
}
|
|
// 条件更新: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());
|
}
|
|
// 可能已经在 nextPatient 中改变了值
|
bedBO.readyPatNum++;
|
|
// 可能已经【并发的】在 nextPatient 中改变了分流队列顺序
|
refreshBedPriorityLevelInRoutingQueue(bedBO);
|
}
|
}
|
|
public Integer getOpeningFlag() {
|
return openingFlag.get();
|
}
|
|
public void setOpeningFlag(Integer flag) {
|
openingFlag.set(flag);
|
}
|
|
public void initCheckType() {
|
List<CheckTypeDO> checkTypeDOList = checkTypeMapper.simpleCheckTypeList();
|
mapCheckTypeVsReadyMax = checkTypeDOList.stream().collect(Collectors.toMap(CheckTypeDO::getValue, checkType -> checkType));
|
}
|
|
public CheckTypeDO getCheckTypeItem(Integer checkType) {
|
return mapCheckTypeVsReadyMax.get( checkType );
|
}
|
|
public BedBO getBedQueueBO(Long roomId, String bedNo) {
|
return mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
|
}
|
|
public MonitorInfoVO getMonitorInfo() {
|
MonitorInfoVO monitorInfoVO = new MonitorInfoVO();
|
monitorInfoVO.setOpeningFlag( openingFlag.get() );
|
roomBedStatistic(monitorInfoVO);
|
|
return monitorInfoVO;
|
}
|
|
public void monitorInfo() {
|
MonitorInfoVO monitorInfoVO = new MonitorInfoVO();
|
roomBedStatistic(monitorInfoVO);
|
log.info(" opening " + openingFlag.get() + " " + monitorInfoVO.getOpeningBedNum() + " " + monitorInfoVO.getDocBedNum() + " " + monitorInfoVO.getCheckTypeBedInfo().toString() );
|
}
|
|
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()) {
|
// 抢占 是否有排队中的患者
|
//
|
// 【号统一(本需求)】与 hurryupOneCheckType 相同:只搬迁状态,**不再分配排队号**。
|
QueueDO preemptQueueItem = queueMapper.getFirstWaitingByCheckTypes(
|
QueueStatusEnum.WAITING.getStatus(),
|
bedBO.checkTypes);
|
|
// 没有排队中的患者
|
if (null == preemptQueueItem) {
|
return;
|
}
|
|
// 条件更新: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());
|
}
|
|
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(
|
preemptQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.WAITING.getStatus());
|
for (int i = 0; i < affinityItems.size(); i++) {
|
QueueDO queueItem = affinityItems.get(i);
|
queueItem.setStatus(QueueStatusEnum.AFFINITY.getStatus()); //改变 排队状态
|
queueItem.setAffinityItem( 1 );
|
queueItem.setRoomId(preemptQueueItem.getRoomId());
|
queueItem.setRoomName(preemptQueueItem.getRoomName());
|
queueItem.setBedNo(preemptQueueItem.getBedNo());
|
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() ); // 亲和项与主项保持同一个号
|
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 = 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();
|
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) {
|
// 装机工位 不进 分流队列,不接收 【排队中】患者分流
|
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);
|
PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
|
if (null == routingQueue) {
|
routingQueue = new PriorityBlockingQueue<BedBO>();
|
mapCheckTypeRoutingQueue.put( checkType, routingQueue );
|
}
|
|
if (!routingQueue.contains(bedBO)) {
|
routingQueue.offer(bedBO);
|
} else {
|
log.error("bedDoctorResume routingQueue still exist!. " + bedBO.getRoomId() + " " + bedBO.getBedNo());
|
}
|
});
|
}
|
|
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<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
|
if (null != routingQueue)
|
routingQueue.remove(bedBO);
|
});
|
}
|
|
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<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_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> mapDocCheckBedStatInfo = new HashMap<>();
|
Map<Integer, Integer> mapDocInstallBedStatInfo = new HashMap<>();
|
mapCheckTypeVsReadyMax.keySet().forEach( checkType -> {
|
mapOpeningCheckBedStatInfo.put(checkType, 0);
|
mapOpeningInstallBedStatInfo.put(checkType, 0);
|
mapDocCheckBedStatInfo.put(checkType, 0);
|
mapDocInstallBedStatInfo.put(checkType, 0);
|
});
|
|
//开通的 和 运营中 的工位统计
|
mapOpeningBed.values().forEach(bedQueueBO -> {
|
Integer[] checkTypes = bedQueueBO.getCheckTypes();
|
Arrays.stream(checkTypes).forEach(checkType -> {
|
if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1)
|
mapOpeningCheckBedStatInfo.put(checkType, mapOpeningCheckBedStatInfo.get(checkType) + 1 );
|
else
|
mapOpeningInstallBedStatInfo.put(checkType, mapOpeningInstallBedStatInfo.get(checkType) + 1 );
|
|
if (bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus() ||
|
bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_PAUSE.getStatus()) {
|
if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1)
|
mapDocCheckBedStatInfo.put(checkType, mapDocCheckBedStatInfo.get(checkType) + 1);
|
else
|
mapDocInstallBedStatInfo.put(checkType, mapDocInstallBedStatInfo.get(checkType) + 1);
|
}
|
});
|
});
|
|
// 接收..排队中患者..的分流队列
|
HashSet<BedBO> hashSetOperatingBedQueue = new HashSet<>();
|
mapCheckTypeRoutingQueue.values().forEach(routingQueue->{
|
routingQueue.stream().forEach(bedQueueBO -> hashSetOperatingBedQueue.add(bedQueueBO));
|
});
|
|
|
Map<Integer, String> mapCheckTypeBedInfo = new HashMap<>();
|
mapCheckTypeVsReadyMax.keySet().forEach( checkType -> {
|
String str = mapOpeningCheckBedStatInfo.get(checkType) + " " +
|
mapOpeningInstallBedStatInfo.get(checkType) + " " +
|
mapDocCheckBedStatInfo.get(checkType) + " " +
|
mapDocInstallBedStatInfo.get(checkType) + " ";
|
mapCheckTypeBedInfo.put( checkType, str);
|
});
|
|
monitorInfoVO.setCheckTypeBedInfo(mapCheckTypeBedInfo);
|
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);
|
}
|
|
}
|