Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 03:00:08 +08:00
parent 8e6cb5c79f
commit 16c42ca108
5433 changed files with 171 additions and 778731 deletions

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,7 +0,0 @@
package com.openhis.web.triageandqueuemanage.mapper;
import org.springframework.stereotype.Repository;
@Repository
public interface CallNumberVoiceConfigAppMapper {
}

View File

@@ -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);
}
}
}
}