Fix Bug #550: AI修复
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.triageandqueuemanage.domain.CallNumberVoiceConfig;
|
||||
|
||||
public interface CallNumberVoiceConfigAppService {
|
||||
R<?> addCallNumberVoiceConfig(CallNumberVoiceConfig callNumberVoiceConfig);
|
||||
|
||||
R<?> updateCallNumberVoiceConfig(CallNumberVoiceConfig callNumberVoiceConfig);
|
||||
|
||||
R<?> getCallNumberVoiceConfig();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public interface TriageQueueAppService {
|
||||
R<?> list(Long organizationId, LocalDate date);
|
||||
R<?> add(TriageQueueAddReq req);
|
||||
R<?> remove(Long id);
|
||||
R<?> adjust(TriageQueueAdjustReq req);
|
||||
/** 选呼:将之前叫号中置为完成,选中的置为叫号中 */
|
||||
R<?> call(TriageQueueActionReq req);
|
||||
/** 完成:叫号中 -> 完成(移出列表),并自动推进下一个等待为叫号中 */
|
||||
R<?> complete(TriageQueueActionReq req);
|
||||
/** 过号重排:叫号中 -> 跳过并移到末尾,并自动推进下一个等待为叫号中 */
|
||||
R<?> requeue(TriageQueueActionReq req);
|
||||
/** 跳过:兼容前端按钮(当前实现等同于过号重排) */
|
||||
R<?> skip(TriageQueueActionReq req);
|
||||
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
|
||||
R<?> next(TriageQueueActionReq req);
|
||||
|
||||
/** 叫号显示屏:获取当前叫号和等候队列信息 */
|
||||
CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.appservice.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.triageandqueuemanage.domain.CallNumberVoiceConfig;
|
||||
import com.openhis.triageandqueuemanage.service.CallNumberVoiceConfigService;
|
||||
import com.openhis.web.triageandqueuemanage.appservice.CallNumberVoiceConfigAppService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class CallNumberVoiceConfigAppServiceImpl implements CallNumberVoiceConfigAppService {
|
||||
|
||||
@Resource
|
||||
private CallNumberVoiceConfigService callNumberVoiceConfigService;
|
||||
|
||||
@Override
|
||||
public R<?> getCallNumberVoiceConfig() {
|
||||
List<CallNumberVoiceConfig> list = callNumberVoiceConfigService.list();
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> addCallNumberVoiceConfig(CallNumberVoiceConfig callNumberVoiceConfig) {
|
||||
if(ObjectUtil.isNull(callNumberVoiceConfig)){
|
||||
return R.fail("叫号语音设置实体不能为空");
|
||||
}
|
||||
boolean save = callNumberVoiceConfigService.save(callNumberVoiceConfig);
|
||||
return R.ok(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> updateCallNumberVoiceConfig(CallNumberVoiceConfig callNumberVoiceConfig) {
|
||||
if(ObjectUtil.isNull(callNumberVoiceConfig)){
|
||||
return R.fail("叫号语音设置实体不能为空");
|
||||
}
|
||||
boolean updateById = callNumberVoiceConfigService.updateById(callNumberVoiceConfig);
|
||||
return R.ok(updateById);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,894 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.appservice.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.administration.domain.Encounter;
|
||||
import com.openhis.administration.mapper.EncounterMapper;
|
||||
import com.openhis.common.enums.EncounterStatus;
|
||||
import com.openhis.common.enums.EncounterSubjectStatus;
|
||||
import com.openhis.common.enums.TriageQueueStatus;
|
||||
import com.openhis.triageandqueuemanage.domain.CallRecord;
|
||||
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
||||
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
||||
import com.openhis.triageandqueuemanage.service.CallRecordService;
|
||||
import com.openhis.triageandqueuemanage.service.DivLogService;
|
||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||
import com.openhis.common.enums.CallType;
|
||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||
import com.openhis.web.triageandqueuemanage.dto.*;
|
||||
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import com.core.common.core.domain.model.LoginUser;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
||||
|
||||
// 状态常量已迁移至 TriageQueueStatus 枚举,原硬编码 STATU_WAITING/STATU_CALLING 等已删除,
|
||||
// 避免散落在多个 Service 类中的魔法数字造成不一致
|
||||
|
||||
@Resource
|
||||
private TriageQueueItemService triageQueueItemService;
|
||||
|
||||
@Resource
|
||||
private CallNumberSseManager callNumberSseManager;
|
||||
|
||||
@Resource
|
||||
private TriageCandidateExclusionService triageCandidateExclusionService;
|
||||
|
||||
@Resource
|
||||
private DivLogService divLogService;
|
||||
|
||||
@Resource
|
||||
private CallRecordService callRecordService;
|
||||
|
||||
@Resource
|
||||
private ScheduleSlotMapper scheduleSlotMapper;
|
||||
|
||||
@Resource
|
||||
private EncounterMapper encounterMapper;
|
||||
|
||||
@Override
|
||||
public R<?> list(Long organizationId, LocalDate date) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
// 只查询今天的患者
|
||||
LocalDate qd = date != null ? date : LocalDate.now();
|
||||
|
||||
LambdaQueryWrapper<TriageQueueItem> wrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getQueueDate, qd)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder);
|
||||
|
||||
// 如果指定了科室,按科室过滤;否则查询所有科室(全科模式)
|
||||
if (organizationId != null) {
|
||||
wrapper.eq(TriageQueueItem::getOrganizationId, organizationId);
|
||||
}
|
||||
|
||||
List<TriageQueueItem> list = triageQueueItemService.list(wrapper);
|
||||
// 通过 slotId 查询 seqNo 并填充到返回结果中(用于选呼显示预约号)
|
||||
if (list != null && !list.isEmpty()) {
|
||||
Map<Long, Integer> seqNoMap = buildSlotSeqNoMap(list);
|
||||
list.forEach(item -> {
|
||||
if (item.getSlotId() != null && seqNoMap.containsKey(item.getSlotId())) {
|
||||
item.setSeqNo(seqNoMap.get(item.getSlotId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将候选池患者的挂号移入队列操作
|
||||
* 并同时写入移入队列操作日志
|
||||
*
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> add(TriageQueueAddReq req) {
|
||||
if (req == null || ObjectUtil.isNull(req.getOrganizationId())) {
|
||||
return R.fail("organizationId 不能为空");
|
||||
}
|
||||
if (CollUtil.isEmpty(req.getItems())) {
|
||||
return R.fail("items 不能为空");
|
||||
}
|
||||
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
LocalDate qd = LocalDate.now();
|
||||
Long orgId = req.getOrganizationId();
|
||||
|
||||
List<TriageQueueItem> existing = triageQueueItemService.list(new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getOrganizationId, orgId)
|
||||
.eq(TriageQueueItem::getQueueDate, qd)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.ne(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue()));
|
||||
|
||||
int maxOrder = existing.stream().map(TriageQueueItem::getQueueOrder).filter(Objects::nonNull).max(Integer::compareTo).orElse(0);
|
||||
|
||||
int added = 0;
|
||||
for (TriageQueueEncounterItem it : req.getItems()) {
|
||||
if (it == null || it.getEncounterId() == null) continue;
|
||||
boolean exists = existing.stream().anyMatch(e -> Objects.equals(e.getEncounterId(), it.getEncounterId()));
|
||||
if (exists) continue;
|
||||
|
||||
TriageQueueItem qi = new TriageQueueItem()
|
||||
.setTenantId(tenantId)
|
||||
.setQueueDate(qd)
|
||||
.setOrganizationId(orgId)
|
||||
.setOrganizationName(req.getOrganizationName())
|
||||
.setEncounterId(it.getEncounterId())
|
||||
.setPatientId(it.getPatientId())
|
||||
.setPatientName(it.getPatientName())
|
||||
.setHealthcareName(it.getHealthcareName())
|
||||
.setPractitionerName(it.getPractitionerName())
|
||||
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
|
||||
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
||||
.setPoolId(it.getPoolId()) // ✅ 号源池ID(用于div_log审计)
|
||||
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID(用于div_log审计)
|
||||
.setSeqNo(it.getSeqNo()) // ✅ 预约序号(用于叫号显示)
|
||||
.setStatus(TriageQueueStatus.WAITING.getValue())
|
||||
.setQueueOrder(++maxOrder)
|
||||
.setDeleteFlag("0")
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
triageQueueItemService.save(qi);
|
||||
// 写入分诊日志
|
||||
writeDivLog(it.getPoolId(), it.getSlotId(), "ADD_QUEUE");
|
||||
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
|
||||
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
|
||||
new LambdaQueryWrapper<TriageCandidateExclusion>()
|
||||
.eq(TriageCandidateExclusion::getTenantId, tenantId)
|
||||
.eq(TriageCandidateExclusion::getExclusionDate, qd)
|
||||
.eq(TriageCandidateExclusion::getEncounterId, it.getEncounterId())
|
||||
.eq(TriageCandidateExclusion::getDeleteFlag, "0")
|
||||
);
|
||||
if (exclusion == null) {
|
||||
exclusion = new TriageCandidateExclusion()
|
||||
.setTenantId(tenantId)
|
||||
.setExclusionDate(qd)
|
||||
.setEncounterId(it.getEncounterId())
|
||||
.setPatientId(it.getPatientId())
|
||||
.setPatientName(it.getPatientName())
|
||||
.setOrganizationId(orgId)
|
||||
.setOrganizationName(req.getOrganizationName())
|
||||
.setReason("ADDED_TO_QUEUE")
|
||||
.setDeleteFlag("0")
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateTime(LocalDateTime.now());
|
||||
triageCandidateExclusionService.save(exclusion);
|
||||
}
|
||||
|
||||
added++;
|
||||
}
|
||||
|
||||
return R.ok(added);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除队列操作并同时写入移除操作日志
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> remove(Long id) {
|
||||
if (id == null) return R.fail("id 不能为空");
|
||||
TriageQueueItem item = triageQueueItemService.getById(id);
|
||||
if (item == null) return R.fail("队列项不存在");
|
||||
if (item.getStatus() != null && item.getStatus() != 0) {
|
||||
return R.fail("仅等待状态的患者可移出队列,当前状态码:" + item.getStatus());
|
||||
}
|
||||
// 逻辑删除队列项
|
||||
item.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(item);
|
||||
// 写入分诊日志
|
||||
writeDivLog(item.getPoolId(), item.getSlotId(), "REMOVE_QUEUE");
|
||||
// 从排除列表中删除记录,使患者重新出现在候选池中
|
||||
Integer tenantId = item.getTenantId();
|
||||
LocalDate exclusionDate = item.getQueueDate();
|
||||
Long encounterId = item.getEncounterId();
|
||||
|
||||
if (tenantId != null && exclusionDate != null && encounterId != null) {
|
||||
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
|
||||
new LambdaQueryWrapper<TriageCandidateExclusion>()
|
||||
.eq(TriageCandidateExclusion::getTenantId, tenantId)
|
||||
.eq(TriageCandidateExclusion::getExclusionDate, exclusionDate)
|
||||
.eq(TriageCandidateExclusion::getEncounterId, encounterId)
|
||||
.eq(TriageCandidateExclusion::getDeleteFlag, "0")
|
||||
);
|
||||
|
||||
if (exclusion != null) {
|
||||
// 逻辑删除排除记录
|
||||
exclusion.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
|
||||
triageCandidateExclusionService.updateById(exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
recalcOrders(item.getOrganizationId(), item.getQueueDate());
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> adjust(TriageQueueAdjustReq req) {
|
||||
if (req == null || req.getId() == null) return R.fail("id 不能为空");
|
||||
if (!"up".equalsIgnoreCase(req.getDirection()) && !"down".equalsIgnoreCase(req.getDirection())) {
|
||||
return R.fail("direction 只能是 up/down");
|
||||
}
|
||||
TriageQueueItem cur = triageQueueItemService.getById(req.getId());
|
||||
if (cur == null) return R.fail("队列项不存在");
|
||||
|
||||
List<TriageQueueItem> list = listInternal(cur.getOrganizationId(), cur.getQueueDate());
|
||||
list.sort(Comparator.comparing(TriageQueueItem::getQueueOrder).thenComparing(TriageQueueItem::getId));
|
||||
int idx = -1;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i).getId(), cur.getId())) { idx = i; break; }
|
||||
}
|
||||
if (idx == -1) return R.fail("队列项不在当前队列");
|
||||
|
||||
int targetIdx = "up".equalsIgnoreCase(req.getDirection()) ? idx - 1 : idx + 1;
|
||||
if (targetIdx < 0 || targetIdx >= list.size()) return R.ok(false);
|
||||
|
||||
TriageQueueItem other = list.get(targetIdx);
|
||||
Integer tmp = cur.getQueueOrder();
|
||||
cur.setQueueOrder(other.getQueueOrder()).setUpdateTime(LocalDateTime.now());
|
||||
other.setQueueOrder(tmp).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(cur);
|
||||
triageQueueItemService.updateById(other);
|
||||
|
||||
recalcOrders(cur.getOrganizationId(), cur.getQueueDate());
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能分诊选呼功能,在进行选呼操作时同时写入叫号记录和选呼操作日志
|
||||
*
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> call(TriageQueueActionReq req) {
|
||||
if (req == null || req.getId() == null) return R.fail("id 不能为空");
|
||||
TriageQueueItem selected = triageQueueItemService.getById(req.getId());
|
||||
if (selected == null) return R.fail("队列项不存在");
|
||||
|
||||
// 只将"等待"状态的患者转为"叫号中",允许有多个"叫号中"的患者
|
||||
if (TriageQueueStatus.WAITING.getValue().equals(selected.getStatus())) {
|
||||
selected.setStatus(TriageQueueStatus.CALLING.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(selected);
|
||||
|
||||
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
|
||||
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
|
||||
// 写入分诊日志和叫号记录
|
||||
writeDivLog(selected.getPoolId(), selected.getSlotId(), "CALL");
|
||||
writeCallRecord(selected.getId(), selected.getPractitionerId(), CallType.CALL, selected.getRoomNo());
|
||||
return R.ok(true);
|
||||
} else if (TriageQueueStatus.CALLING.getValue().equals(selected.getStatus())) {
|
||||
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
||||
return R.ok(true);
|
||||
} else {
|
||||
// 其他状态(如 SKIPPED、COMPLETED)不能选呼
|
||||
return R.fail("只能选呼\"等待\"状态的患者,当前患者状态为:" + selected.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能分诊完成操作
|
||||
* 在进行完成操作后同时写入叫号记录和完成操作日志
|
||||
*
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> complete(TriageQueueActionReq req) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
TriageQueueItem calling = null;
|
||||
|
||||
// 关键改进:如果提供了 id,直接通过ID获取(像 call 方法一样),不依赖查询条件
|
||||
if (req != null && req.getId() != null) {
|
||||
calling = triageQueueItemService.getById(req.getId());
|
||||
if (calling == null) {
|
||||
return R.fail("队列项不存在");
|
||||
}
|
||||
// 验证状态
|
||||
if (!TriageQueueStatus.CALLING.getValue().equals(calling.getStatus())) {
|
||||
return R.fail("只能完成\"叫号中\"状态的患者,当前患者状态为:" + calling.getStatus());
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
||||
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
|
||||
|
||||
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.CALLING.getValue())
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
if (orgId != null) {
|
||||
callingWrapper.eq(TriageQueueItem::getOrganizationId, orgId);
|
||||
}
|
||||
|
||||
calling = triageQueueItemService.getOne(callingWrapper, false);
|
||||
|
||||
if (calling == null) {
|
||||
return R.fail("当前没有叫号中的患者");
|
||||
}
|
||||
}
|
||||
|
||||
// 使用实际找到的科室ID
|
||||
Long actualOrgId = calling.getOrganizationId();
|
||||
|
||||
// 1) 叫号中 -> 完成(移出列表)
|
||||
calling.setStatus(TriageQueueStatus.COMPLETED.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(calling);
|
||||
|
||||
// 同步更新就诊状态为诊毕
|
||||
// 分诊台完诊原只更新队列状态,未同步encounter表,导致与医生站完诊行为不一致
|
||||
if (calling.getEncounterId() != null) {
|
||||
try {
|
||||
java.util.Date now = new java.util.Date();
|
||||
encounterMapper.update(null,
|
||||
new LambdaUpdateWrapper<Encounter>()
|
||||
.eq(Encounter::getId, calling.getEncounterId())
|
||||
.set(Encounter::getStatusEnum, EncounterStatus.DISCHARGED.getValue())
|
||||
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.DEPARTED.getValue())
|
||||
.set(Encounter::getEndTime, now)
|
||||
.set(Encounter::getUpdateTime, now));
|
||||
} catch (Exception e) {
|
||||
log.error("更新就诊状态为诊毕失败, encounterId={}", calling.getEncounterId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 自动推进下一个等待为叫号中(同一科室,包含跳过状态,不限制日期)
|
||||
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.and(w -> w.eq(TriageQueueItem::getStatus, TriageQueueStatus.WAITING.getValue())
|
||||
.or()
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.SKIPPED.getValue()))
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
// 如果指定了科室ID,则按科室过滤;否则查询所有科室(全科模式)
|
||||
if (actualOrgId != null) {
|
||||
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
|
||||
}
|
||||
|
||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||
|
||||
if (next != null) {
|
||||
next.setStatus(TriageQueueStatus.CALLING.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(next);
|
||||
}
|
||||
|
||||
recalcOrders(actualOrgId, null);
|
||||
|
||||
// 完成后推送 SSE 消息(实时通知显示屏刷新)
|
||||
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||
// 写入分诊日志和叫号记录
|
||||
writeDivLog(calling.getPoolId(), calling.getSlotId(), "COMPLETE");
|
||||
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.COMPLETE, calling.getRoomNo());
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能队列重排序功能操作单元
|
||||
* 在进行队列重排序时同时在div_log表中写入重排序日志和叫号记录
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> requeue(TriageQueueActionReq req) {
|
||||
return doRequeue(req, "REQUEUE");
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能分诊跳过功能操作单元内部包含将队列正在叫号状态的号进行跳过
|
||||
* 同时将跳过操作日志写入div_log表中,以及写入叫号记录表。
|
||||
*
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> skip(TriageQueueActionReq req) {
|
||||
// 当前业务"跳过"按"过号重排"处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
|
||||
return doRequeue(req, "SKIP");
|
||||
}
|
||||
|
||||
/**
|
||||
* 过号重排/跳过的核心逻辑
|
||||
* @param action 日志动作:REQUEUE 或 SKIP
|
||||
*/
|
||||
private R<?> doRequeue(TriageQueueActionReq req, String action) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
TriageQueueItem calling = null;
|
||||
|
||||
if (req != null && req.getId() != null) {
|
||||
calling = triageQueueItemService.getById(req.getId());
|
||||
if (calling == null) {
|
||||
return R.fail("队列项不存在");
|
||||
}
|
||||
// 验证状态
|
||||
if (!TriageQueueStatus.CALLING.getValue().equals(calling.getStatus())) {
|
||||
return R.fail("只能对\"叫号中\"状态的患者进行" + ("SKIP".equals(action) ? "跳过" : "过号重排") + ",当前患者状态为:" + calling.getStatus());
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
||||
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
|
||||
|
||||
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.CALLING.getValue())
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
if (orgId != null) {
|
||||
callingWrapper.eq(TriageQueueItem::getOrganizationId, orgId);
|
||||
}
|
||||
|
||||
calling = triageQueueItemService.getOne(callingWrapper, false);
|
||||
|
||||
if (calling == null) return R.fail("当前没有叫号中的患者");
|
||||
}
|
||||
|
||||
// 使用实际找到的科室ID
|
||||
Long actualOrgId = calling.getOrganizationId();
|
||||
|
||||
// 关键改进:在执行跳过/重排操作之前,先检查是否有等待中的患者(判断队列状态)
|
||||
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.and(w -> w.eq(TriageQueueItem::getStatus, TriageQueueStatus.WAITING.getValue())
|
||||
.or()
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.SKIPPED.getValue()))
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
if (actualOrgId != null) {
|
||||
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
|
||||
}
|
||||
|
||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||
|
||||
if (next == null) {
|
||||
return R.fail("当前没有等待中的患者");
|
||||
}
|
||||
|
||||
// 找末尾序号(同一科室,不限制日期)
|
||||
Integer maxOrder = triageQueueItemService.list(new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getOrganizationId, actualOrgId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.ne(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue()))
|
||||
.stream()
|
||||
.map(TriageQueueItem::getQueueOrder)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0);
|
||||
|
||||
// 1) 叫号中 -> 跳过,并移到末尾
|
||||
calling.setStatus(TriageQueueStatus.SKIPPED.getValue()).setQueueOrder(maxOrder + 1).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(calling);
|
||||
|
||||
// 2) 自动推进下一个等待为叫号中
|
||||
next.setStatus(TriageQueueStatus.CALLING.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(next);
|
||||
|
||||
recalcOrders(actualOrgId, null);
|
||||
// 推送 SSE 消息
|
||||
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||
// 写入分诊日志和叫号记录
|
||||
writeDivLog(calling.getPoolId(), calling.getSlotId(), action);
|
||||
writeCallRecord(calling.getId(), calling.getPractitionerId(),
|
||||
"SKIP".equals(action) ? CallType.SKIP : CallType.REQUEUE, calling.getRoomNo());
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> next(TriageQueueActionReq req) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
TriageQueueItem calling = null;
|
||||
|
||||
// 关键改进:如果提供了 id,优先使用 id 直接查找(像 call 方法一样)
|
||||
if (req != null && req.getId() != null) {
|
||||
calling = triageQueueItemService.getById(req.getId());
|
||||
if (calling == null) {
|
||||
return R.fail("队列项不存在");
|
||||
}
|
||||
// 验证状态:必须是"叫号中"状态
|
||||
if (!TriageQueueStatus.CALLING.getValue().equals(calling.getStatus())) {
|
||||
return R.fail("只能对\"叫号中\"状态的患者执行\"下一患者\"操作,当前患者状态为:" + calling.getStatus());
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
||||
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
|
||||
|
||||
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.CALLING.getValue())
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
if (orgId != null) {
|
||||
callingWrapper.eq(TriageQueueItem::getOrganizationId, orgId);
|
||||
}
|
||||
|
||||
calling = triageQueueItemService.getOne(callingWrapper, false);
|
||||
}
|
||||
|
||||
Long actualOrgId = null;
|
||||
|
||||
// 当前叫号中 -> 完成(如果不存在,就当作从头找第一位等待)
|
||||
if (calling != null) {
|
||||
calling.setStatus(TriageQueueStatus.COMPLETED.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(calling);
|
||||
actualOrgId = calling.getOrganizationId(); // 使用叫号中患者所在的科室
|
||||
} else {
|
||||
// 如果没有叫号中的患者,使用请求中的 organizationId(如果有)
|
||||
if (req != null && req.getOrganizationId() != null) {
|
||||
actualOrgId = req.getOrganizationId();
|
||||
}
|
||||
}
|
||||
|
||||
// 下一位等待 -> 叫号中(如果之前有叫号中的,就在同一科室找;否则在全科找)
|
||||
// 注意:也包含"跳过"状态的患者,因为跳过后的患者也可以重新叫号(不限制日期)
|
||||
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.and(w -> w.eq(TriageQueueItem::getStatus, TriageQueueStatus.WAITING.getValue())
|
||||
.or()
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.SKIPPED.getValue()))
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
.last("LIMIT 1");
|
||||
|
||||
if (actualOrgId != null) {
|
||||
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
|
||||
}
|
||||
|
||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||
|
||||
if (next == null) {
|
||||
if (actualOrgId != null) {
|
||||
recalcOrders(actualOrgId, null);
|
||||
}
|
||||
return R.fail("当前没有等待的患者");
|
||||
}
|
||||
|
||||
next.setStatus(TriageQueueStatus.CALLING.getValue()).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(next);
|
||||
|
||||
if (next.getOrganizationId() != null) {
|
||||
recalcOrders(next.getOrganizationId(), null);
|
||||
}
|
||||
|
||||
// 写入叫号记录
|
||||
if (calling != null) {
|
||||
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.NEXT, calling.getRoomNo());
|
||||
}
|
||||
writeCallRecord(next.getId(), next.getPractitionerId(), CallType.NEXT, next.getRoomNo());
|
||||
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
private List<TriageQueueItem> listInternal(Long orgId, LocalDate qd) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
LambdaQueryWrapper<TriageQueueItem> wrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getOrganizationId, orgId)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.ne(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue());
|
||||
// 如果 qd 不为 null,才添加日期限制
|
||||
if (qd != null) {
|
||||
wrapper.eq(TriageQueueItem::getQueueDate, qd);
|
||||
}
|
||||
return triageQueueItemService.list(wrapper);
|
||||
}
|
||||
|
||||
private TriageQueueItem findCalling(Long orgId, LocalDate qd) {
|
||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
return triageQueueItemService.getOne(new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.eq(TriageQueueItem::getOrganizationId, orgId)
|
||||
.eq(TriageQueueItem::getQueueDate, qd)
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.eq(TriageQueueItem::getStatus, TriageQueueStatus.CALLING.getValue())
|
||||
.last("LIMIT 1"), false);
|
||||
}
|
||||
|
||||
private void recalcOrders(Long orgId, LocalDate qd) {
|
||||
List<TriageQueueItem> list = listInternal(orgId, qd);
|
||||
list.sort(Comparator.comparing(TriageQueueItem::getQueueOrder).thenComparing(TriageQueueItem::getId));
|
||||
int i = 1;
|
||||
for (TriageQueueItem it : list) {
|
||||
if (!Objects.equals(it.getQueueOrder(), i)) {
|
||||
it.setQueueOrder(i).setUpdateTime(LocalDateTime.now());
|
||||
triageQueueItemService.updateById(it);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取叫号显示屏数据
|
||||
* @param organizationId 科室ID
|
||||
* @param date 日期
|
||||
* @param tenantId 租户ID
|
||||
* @return 显示屏数据
|
||||
*/
|
||||
@Override
|
||||
public CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId) {
|
||||
// 如果没有传入租户ID,尝试从登录用户获取,否则默认为1
|
||||
if (tenantId == null) {
|
||||
try {
|
||||
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
} catch (Exception e) {
|
||||
tenantId = 1; // 默认租户ID
|
||||
}
|
||||
}
|
||||
LocalDate qd = date != null ? date : LocalDate.now();
|
||||
|
||||
/**
|
||||
* 查询所有队列项(WAITING 和 CALLING 状态)某天的某个科室的某个状态
|
||||
*
|
||||
*/
|
||||
List<TriageQueueItem> allItems = triageQueueItemService.list(
|
||||
new LambdaQueryWrapper<TriageQueueItem>()
|
||||
.eq(TriageQueueItem::getQueueDate, qd)
|
||||
.eq(TriageQueueItem::getOrganizationId, organizationId)
|
||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||
.in(TriageQueueItem::getStatus, TriageQueueStatus.WAITING.getValue(), TriageQueueStatus.CALLING.getValue())
|
||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||
);
|
||||
|
||||
// 通过 slotId 批量查询 seqNo(方案 B:不修改 triage_queue_item 表结构)
|
||||
Map<Long, Integer> slotSeqNoMap = buildSlotSeqNoMap(allItems);
|
||||
|
||||
CallNumberDisplayResp resp = new CallNumberDisplayResp();
|
||||
|
||||
// 1. 获取科室名称(从第一条数据中取)
|
||||
if (!allItems.isEmpty()) {
|
||||
resp.setDepartmentName(allItems.get(0).getOrganizationName() + " 叫号显示屏");
|
||||
} else {
|
||||
resp.setDepartmentName("叫号显示屏");
|
||||
}
|
||||
|
||||
// 2. 查找当前叫号中的患者(CALLING 状态)
|
||||
TriageQueueItem callingItem = allItems.stream()
|
||||
.filter(item -> TriageQueueStatus.CALLING.getValue().equals(item.getStatus()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (callingItem != null) {
|
||||
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||
Integer displayNo = resolveDisplayNumber(callingItem, slotSeqNoMap);
|
||||
currentCall.setNumber(displayNo);
|
||||
currentCall.setName(maskPatientName(callingItem.getPatientName()));
|
||||
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
|
||||
currentCall.setDoctor(callingItem.getPractitionerName());
|
||||
resp.setCurrentCall(currentCall);
|
||||
} else {
|
||||
// 没有叫号中的患者,返回默认值
|
||||
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||
currentCall.setNumber(null);
|
||||
currentCall.setName("-");
|
||||
currentCall.setRoom("-");
|
||||
currentCall.setDoctor("-");
|
||||
resp.setCurrentCall(currentCall);
|
||||
}
|
||||
|
||||
// 3. 按医生分组(包括 CALLING 和 WAITING 状态)
|
||||
Map<Long, List<TriageQueueItem>> groupedByDoctor = allItems.stream()
|
||||
// 严格按医生分组:仅保留有 practitionerId 的记录
|
||||
.filter(item -> item.getPractitionerId() != null)
|
||||
.collect(Collectors.groupingBy(TriageQueueItem::getPractitionerId));
|
||||
|
||||
// 每个医生的等待队列
|
||||
List<CallNumberDisplayResp.DoctorGroup> waitingList = new ArrayList<>();
|
||||
int totalWaiting = 0;
|
||||
|
||||
for (Map.Entry<Long, List<TriageQueueItem>> entry : groupedByDoctor.entrySet()) {
|
||||
|
||||
List<TriageQueueItem> doctorItems = entry.getValue();
|
||||
String doctorName = doctorItems.get(0).getPractitionerName();
|
||||
if (doctorName == null || doctorName.isEmpty()) {
|
||||
doctorName = "未分配";
|
||||
}
|
||||
// 按排队顺序排序
|
||||
doctorItems.sort(Comparator.comparing(TriageQueueItem::getQueueOrder));
|
||||
|
||||
// 该医生 下边的患者列表 和 诊室号
|
||||
CallNumberDisplayResp.DoctorGroup doctorGroup = new CallNumberDisplayResp.DoctorGroup();
|
||||
doctorGroup.setDoctorName(doctorName);
|
||||
|
||||
// 获取诊室号(从该医生的任一患者中取)
|
||||
String roomNo = doctorItems.stream()
|
||||
.map(TriageQueueItem::getRoomNo)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse("1号");
|
||||
doctorGroup.setRoomNo(roomNo);
|
||||
|
||||
// 转换患者列表
|
||||
List<CallNumberDisplayResp.PatientInfo> patients = new ArrayList<>();
|
||||
for (TriageQueueItem item : doctorItems) {
|
||||
CallNumberDisplayResp.PatientInfo patient = new CallNumberDisplayResp.PatientInfo();
|
||||
patient.setId(item.getId());
|
||||
patient.setName(maskPatientName(item.getPatientName()));
|
||||
patient.setStatus(item.getStatus());
|
||||
patient.setQueueOrder(resolveDisplayNumber(item, slotSeqNoMap));
|
||||
patients.add(patient);
|
||||
|
||||
// 统计等待人数(不包括 CALLING 状态)
|
||||
if (TriageQueueStatus.WAITING.getValue().equals(item.getStatus())) {
|
||||
totalWaiting++;
|
||||
}
|
||||
}
|
||||
|
||||
doctorGroup.setPatients(patients);
|
||||
waitingList.add(doctorGroup);
|
||||
}
|
||||
|
||||
|
||||
// 按医生名称排序
|
||||
waitingList.sort(Comparator.comparing(CallNumberDisplayResp.DoctorGroup::getDoctorName));
|
||||
|
||||
resp.setWaitingList(waitingList);
|
||||
resp.setWaitingCount(totalWaiting);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 患者姓名脱敏处理
|
||||
* @param name 原始姓名
|
||||
* @return 脱敏后的姓名(如:张*三)
|
||||
*/
|
||||
private String maskPatientName(String name) {
|
||||
if (name == null || name.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
if (name.length() == 1) {
|
||||
return name;
|
||||
}
|
||||
if (name.length() == 2) {
|
||||
return name.charAt(0) + "*";
|
||||
}
|
||||
// 3个字及以上:保留首尾,中间用*代替
|
||||
return name.charAt(0) + "*" + name.charAt(name.length() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送显示屏更新消息到 SSE
|
||||
* @param organizationId 科室ID
|
||||
* @param queueDate 队列日期
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
private void pushDisplayUpdate(Long organizationId, LocalDate queueDate, Integer tenantId) {
|
||||
try {
|
||||
// 获取最新的显示屏数据
|
||||
CallNumberDisplayResp displayData = getDisplayData(organizationId, queueDate, tenantId);
|
||||
|
||||
// 构造推送消息
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("type", "update");
|
||||
message.put("action", "queue_changed");
|
||||
message.put("data", displayData);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// 推送到该科室的所有 SSE 连接
|
||||
callNumberSseManager.pushToOrganization(organizationId, message);
|
||||
|
||||
} catch (Exception e) {
|
||||
// SSE 推送失败不应该影响业务逻辑
|
||||
System.err.println("推送显示屏更新失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入分诊操作日志
|
||||
*
|
||||
* @param poolId 号源池ID
|
||||
* @param slotId 号源槽位ID
|
||||
* @param action 操作动作:ADD_QUEUE/REMOVE_QUEUE/CALL/COMPLETE/SKIP/REQUEUE
|
||||
*/
|
||||
private void writeDivLog(Long poolId, Long slotId, String action) {
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
DivLog log = new DivLog()
|
||||
.setPoolId(poolId)
|
||||
.setSlotId(slotId)
|
||||
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
|
||||
.setAction(action)
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateAt(LocalDateTime.now())
|
||||
.setCreatedAt(LocalDateTime.now());
|
||||
divLogService.save(log);
|
||||
} catch (Exception e) {
|
||||
log.error("写入分诊日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入叫号记录
|
||||
*
|
||||
* @param queueId 队列ID
|
||||
* @param doctorId 医生ID
|
||||
* @param callType 叫号类型枚举
|
||||
* @param room 诊室号
|
||||
*/
|
||||
private void writeCallRecord(Long queueId, Long doctorId, CallType callType, String room) {
|
||||
try {
|
||||
CallRecord record = new CallRecord()
|
||||
.setQueueId(queueId)
|
||||
.setDoctorId(doctorId)
|
||||
.setCallTime(LocalDateTime.now())
|
||||
.setCallType(callType.getValue().toString())
|
||||
.setRoom(room)
|
||||
.setCreateAt(LocalDateTime.now());
|
||||
callRecordService.save(record);
|
||||
} catch (Exception e) {
|
||||
log.error("写入叫号记录失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 slotId 批量查询 seqNo,返回 slotId -> seqNo 映射
|
||||
*/
|
||||
private Map<Long, Integer> buildSlotSeqNoMap(List<TriageQueueItem> items) {
|
||||
List<Long> slotIds = items.stream()
|
||||
.map(TriageQueueItem::getSlotId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (slotIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ScheduleSlot> slots = scheduleSlotMapper.selectSeqNoBySlotIds(slotIds);
|
||||
return slots.stream()
|
||||
.filter(s -> s.getId() != null && s.getSeqNo() != null)
|
||||
.collect(Collectors.toMap(ScheduleSlot::getId, ScheduleSlot::getSeqNo, (a, b) -> a));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算患者在叫号显示屏上应显示的号码:优先 seqNo(预约序号),否则 queueOrder(排队号)
|
||||
*/
|
||||
private Integer resolveDisplayNumber(TriageQueueItem item, Map<Long, Integer> slotSeqNoMap) {
|
||||
if (item == null) return null;
|
||||
if (item.getSlotId() != null && slotSeqNoMap.containsKey(item.getSlotId())) {
|
||||
return slotSeqNoMap.get(item.getSlotId());
|
||||
}
|
||||
return item.getQueueOrder();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.triageandqueuemanage.domain.CallNumberVoiceConfig;
|
||||
import com.openhis.web.triageandqueuemanage.appservice.CallNumberVoiceConfigAppService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/CallNumberVoice")
|
||||
public class CallNumberVoiceConfigController {
|
||||
|
||||
@Resource
|
||||
private CallNumberVoiceConfigAppService callNumberVoiceConfigAppService;
|
||||
|
||||
/**
|
||||
* 查询叫号语音设置
|
||||
*
|
||||
* @return 结果
|
||||
*/
|
||||
@GetMapping("/get")
|
||||
public R<?> getCallNumberVoiceConfig(){
|
||||
return R.ok(callNumberVoiceConfigAppService.getCallNumberVoiceConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增叫号语音设置实体
|
||||
*
|
||||
* @param callNumberVoiceConfig 叫号语音设置实体
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public R<?> addCallNumberVoiceConfig(@RequestBody CallNumberVoiceConfig callNumberVoiceConfig) {
|
||||
return R.ok(callNumberVoiceConfigAppService.addCallNumberVoiceConfig(callNumberVoiceConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改叫号语音设置实体
|
||||
*
|
||||
* @param callNumberVoiceConfig 叫号语音设置实体
|
||||
* @return 结果
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
public R<?> updateCallNumberVoiceConfig(@RequestBody CallNumberVoiceConfig callNumberVoiceConfig) {
|
||||
return R.ok(callNumberVoiceConfigAppService.updateCallNumberVoiceConfig(callNumberVoiceConfig));
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.controller;
|
||||
|
||||
import com.core.common.annotation.Anonymous;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/triage/queue")
|
||||
public class TriageQueueController {
|
||||
|
||||
@Resource
|
||||
private TriageQueueAppService triageQueueAppService;
|
||||
|
||||
@Resource
|
||||
private CallNumberSseManager callNumberSseManager;
|
||||
|
||||
@GetMapping("/list")
|
||||
public R<?> list(@RequestParam(value = "organizationId", required = false) Long organizationId,
|
||||
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||
return triageQueueAppService.list(organizationId, date);
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public R<?> add(@RequestBody TriageQueueAddReq req) {
|
||||
return triageQueueAppService.add(req);
|
||||
}
|
||||
|
||||
@DeleteMapping("/remove/{id}")
|
||||
public R<?> remove(@PathVariable("id") Long id) {
|
||||
return triageQueueAppService.remove(id);
|
||||
}
|
||||
|
||||
@PutMapping("/adjust")
|
||||
public R<?> adjust(@RequestBody TriageQueueAdjustReq req) {
|
||||
return triageQueueAppService.adjust(req);
|
||||
}
|
||||
|
||||
@PostMapping("/call")
|
||||
public R<?> call(@RequestBody TriageQueueActionReq req) {
|
||||
return triageQueueAppService.call(req);
|
||||
}
|
||||
|
||||
@PostMapping("/complete")
|
||||
public R<?> complete(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||
return triageQueueAppService.complete(req);
|
||||
}
|
||||
|
||||
@PostMapping("/requeue")
|
||||
public R<?> requeue(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||
return triageQueueAppService.requeue(req);
|
||||
}
|
||||
|
||||
@PostMapping("/skip")
|
||||
public R<?> skip(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||
return triageQueueAppService.skip(req);
|
||||
}
|
||||
|
||||
@PostMapping("/next")
|
||||
public R<?> next(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||
return triageQueueAppService.next(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 叫号显示屏:获取当前叫号和等候队列信息
|
||||
* @param organizationId 科室ID
|
||||
* @param date 日期(可选,默认今天)
|
||||
* @param tenantId 租户ID(可选,默认1)
|
||||
* @return 显示屏数据
|
||||
*/
|
||||
@Anonymous // 显示屏不需要登录
|
||||
@GetMapping("/display")
|
||||
public R<CallNumberDisplayResp> getDisplayData(
|
||||
@RequestParam(required = false) String organizationId,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
||||
@RequestParam(required = false) Integer tenantId
|
||||
) {
|
||||
try {
|
||||
Long orgId = resolveOrganizationId(organizationId);
|
||||
if (orgId == null) {
|
||||
return R.fail("organizationId参数不合法或未获取到登录用户科室");
|
||||
}
|
||||
Integer actualTenantId = resolveTenantId(tenantId);
|
||||
CallNumberDisplayResp data = triageQueueAppService.getDisplayData(orgId, date, actualTenantId);
|
||||
return R.ok(data);
|
||||
} catch (Exception e) {
|
||||
log.error("获取显示屏数据失败", e);
|
||||
return R.fail("获取显示屏数据失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 叫号显示屏:SSE 实时推送
|
||||
*/
|
||||
@Anonymous
|
||||
@GetMapping(value = "/display/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamDisplayData(
|
||||
@RequestParam(required = false) String organizationId,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
||||
@RequestParam(required = false) Integer tenantId
|
||||
) {
|
||||
// 1) 解析科室与租户(SSE 连接根据科室分组管理)
|
||||
Long orgId = resolveOrganizationId(organizationId);
|
||||
if (orgId == null) {
|
||||
SseEmitter emitter = new SseEmitter(0L);
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("type", "error");
|
||||
error.put("message", "organizationId参数不合法或未获取到登录用户科室");
|
||||
callNumberSseManager.sendToEmitter(emitter, error);
|
||||
emitter.complete();
|
||||
return emitter;
|
||||
}
|
||||
Integer actualTenantId = resolveTenantId(tenantId);
|
||||
// 2) 创建并注册 SSE 连接
|
||||
SseEmitter emitter = callNumberSseManager.addEmitter(orgId);
|
||||
try {
|
||||
// 3) 连接建立后,先推送一次初始化数据
|
||||
CallNumberDisplayResp data = triageQueueAppService.getDisplayData(orgId, date, actualTenantId);
|
||||
Map<String, Object> init = new HashMap<>();
|
||||
init.put("type", "init");
|
||||
init.put("data", data);
|
||||
init.put("timestamp", System.currentTimeMillis());
|
||||
callNumberSseManager.sendToEmitter(emitter, init);
|
||||
} catch (Exception e) {
|
||||
log.error("SSE初始化数据发送失败", e);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private Long resolveOrganizationId(String organizationId) {
|
||||
if (!StringUtils.hasText(organizationId)) {
|
||||
try {
|
||||
return SecurityUtils.getLoginUser().getOrgId();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(organizationId.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("非法organizationId: {}", organizationId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer resolveTenantId(Integer tenantId) {
|
||||
if (tenantId != null) {
|
||||
return tenantId;
|
||||
}
|
||||
try {
|
||||
Integer loginTenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||
return loginTenantId != null ? loginTenantId : 1;
|
||||
} catch (Exception e) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 叫号显示屏响应DTO
|
||||
*/
|
||||
@Data
|
||||
public class CallNumberDisplayResp {
|
||||
/** 科室名称 */
|
||||
private String departmentName;
|
||||
|
||||
/** 当前叫号信息 */
|
||||
private CurrentCallInfo currentCall;
|
||||
|
||||
/** 等候患者列表(按医生分组) */
|
||||
private List<DoctorGroup> waitingList;
|
||||
|
||||
/** 等待总人数 */
|
||||
private Integer waitingCount;
|
||||
|
||||
/**
|
||||
* 当前叫号信息
|
||||
*/
|
||||
@Data
|
||||
public static class CurrentCallInfo {
|
||||
/** 排队号 */
|
||||
private Integer number;
|
||||
/** 患者姓名(脱敏) */
|
||||
private String name;
|
||||
/** 诊室号 */
|
||||
private String room;
|
||||
/** 医生姓名 */
|
||||
private String doctor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医生分组信息
|
||||
*/
|
||||
@Data
|
||||
public static class DoctorGroup {
|
||||
/** 医生姓名 */
|
||||
private String doctorName;
|
||||
/** 诊室号 */
|
||||
private String roomNo;
|
||||
/** 该医生的患者列表 */
|
||||
private List<PatientInfo> patients;
|
||||
}
|
||||
|
||||
/**
|
||||
* 患者信息
|
||||
*/
|
||||
@Data
|
||||
public static class PatientInfo {
|
||||
/** 队列项ID */
|
||||
private Long id;
|
||||
/** 患者姓名(脱敏) */
|
||||
private String name;
|
||||
/** 状态:CALLING=就诊中,WAITING=等待 */
|
||||
private Integer status;
|
||||
/** 排队号 */
|
||||
private Integer queueOrder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TriageQueueActionReq {
|
||||
/** 目标队列项ID(例如:选呼时选中的患者) */
|
||||
private Long id;
|
||||
/** 科室ID(可选:不传则用当前登录人orgId) */
|
||||
private Long organizationId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TriageQueueAddReq {
|
||||
/** 科室ID(就诊科室) */
|
||||
private Long organizationId;
|
||||
/** 科室名称(冗余存储,便于展示) */
|
||||
private String organizationName;
|
||||
/** 要加入队列的就诊记录 */
|
||||
private List<TriageQueueEncounterItem> items;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TriageQueueAdjustReq {
|
||||
private Long id;
|
||||
/** up / down */
|
||||
private String direction;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TriageQueueEncounterItem {
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private String healthcareName;
|
||||
private String practitionerName;
|
||||
|
||||
// ========== 新增字段(可选,用于叫号显示屏)==========
|
||||
/** 医生ID(可选) */
|
||||
private Long practitionerId;
|
||||
/** 诊室号(可选) */
|
||||
private String roomNo;
|
||||
/** 号源池ID(关联 adm_schedule_pool.id,用于 div_log 审计日志) */
|
||||
private Long poolId;
|
||||
/** 号源槽位ID(关联 adm_schedule_slot.id,用于 div_log 审计日志) */
|
||||
private Long slotId;
|
||||
/** 预约序号(来自 adm_schedule_slot.seq_no,用于叫号显示) */
|
||||
private Integer seqNo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.mapper;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface CallNumberVoiceConfigAppMapper {
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.openhis.web.triageandqueuemanage.sse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* 叫号显示屏 SSE 管理器(服务端推送)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CallNumberSseManager {
|
||||
|
||||
private static final long NO_TIMEOUT = 0L; // 0 表示“永不超时”
|
||||
// 按科室分组保存连接(消化内科有3个屏、心内科有2个屏)
|
||||
// 很多屏幕同时连、同时断。故用 ConcurrentHashMap 存储,线程安全。内部分段锁,不阻塞其他科室的操作。
|
||||
private static final Map<Long, CopyOnWriteArraySet<SseEmitter>> emitterMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建并注册一个 SSE 连接(按科室分组保存)
|
||||
*/
|
||||
public SseEmitter addEmitter(Long organizationId) {
|
||||
SseEmitter emitter = new SseEmitter(NO_TIMEOUT);
|
||||
emitterMap.computeIfAbsent(organizationId, k -> new CopyOnWriteArraySet<>()).add(emitter);
|
||||
|
||||
emitter.onCompletion(() -> removeEmitter(organizationId, emitter));
|
||||
emitter.onTimeout(() -> removeEmitter(organizationId, emitter));
|
||||
emitter.onError((ex) -> removeEmitter(organizationId, emitter));
|
||||
|
||||
log.info("SSE连接建立:科室ID={}, 当前该科室连接数={}",
|
||||
organizationId, emitterMap.get(organizationId).size());
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定科室的所有 SSE 连接推送消息
|
||||
*/
|
||||
public void pushToOrganization(Long organizationId, Object message) {
|
||||
CopyOnWriteArraySet<SseEmitter> emitters = emitterMap.get(organizationId);
|
||||
if (emitters == null || emitters.isEmpty()) {
|
||||
log.debug("科室{}没有SSE连接,跳过推送", organizationId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (SseEmitter emitter : emitters) {
|
||||
if (!sendToEmitter(emitter, message)) {
|
||||
removeEmitter(organizationId, emitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向单个 SSE 连接发送数据
|
||||
*/
|
||||
public boolean sendToEmitter(SseEmitter emitter, Object data) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().data(data));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.warn("SSE推送失败:{}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开或异常时移除 SSE 连接
|
||||
*/
|
||||
private void removeEmitter(Long organizationId, SseEmitter emitter) {
|
||||
CopyOnWriteArraySet<SseEmitter> emitters = emitterMap.get(organizationId);
|
||||
if (emitters != null) {
|
||||
emitters.remove(emitter);
|
||||
if (emitters.isEmpty()) {
|
||||
emitterMap.remove(organizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user