@@ -5,22 +5,32 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper ;
import com.core.common.core.domain.R ;
import com.core.common.utils.SecurityUtils ;
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 {
@@ -39,13 +49,22 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
@Resource
private TriageQueueItemService triageQueueItemService ;
@Resource
private CallNumberSseManager callNumberSseManager ;
@Resource
private TriageCandidateExclusionService triageCandidateExclusionService ;
@Resource
private DivLogService divLogService ;
@Resource
private CallRecordService callRecordService ;
@Resource
private ScheduleSlotMapper scheduleSlotMapper ;
@Override
public R < ? > list ( Long organizationId , LocalDate date ) {
Integer tenantId = SecurityUtils . getLoginUser ( ) . getTenantId ( ) ;
@@ -65,31 +84,33 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
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 ( ) ) ) ;
}
} ) ;
}
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
if ( list ! = null & & ! list . isEmpty ( ) ) {
int beforeSize = list . size ( ) ;
list = list . stream ( )
. filter ( item - > ! STATUS_COMPLETED . equals ( item . getStatus ( ) ) )
. collect ( java . util . stream . Collectors . toList ( ) ) ;
if ( beforeSize ! = list . size ( ) ) {
System . out . println ( " >>> [TriageQueue] list() 警告:过滤掉了 " + ( beforeSize - list . size ( ) ) + " 条 COMPLETED 状态的记录 " ) ;
}
}
// 调试日志:检查状态值
if ( list ! = null & & ! list . isEmpty ( ) ) {
System . out . println ( " >>> [TriageQueue] list() 返回 " + list . size ( ) + " 条记录(已排除 COMPLETED) " ) ;
for ( int i = 0 ; i < Math . min ( 3 , list . size ( ) ) ; i + + ) {
TriageQueueItem item = list . get ( i ) ;
System . out . println ( " [ " + i + " ] patientName= " + item . getPatientName ( )
+ " , status= " + item . getStatus ( )
+ " , organizationId= " + item . getOrganizationId ( ) ) ;
}
}
return R . ok ( list ) ;
}
/**
* 将候选池患者的挂号移入队列操作
* 并同时写入移入队列操作日志
*
* @param req
* @return
*/
@Override
@Transactional ( rollbackFor = Exception . class )
public R < ? > add ( TriageQueueAddReq req ) {
@@ -133,6 +154,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
. setRoomNo ( it . getRoomNo ( ) ) // ✅ 新增字段(可选)
. setPoolId ( it . getPoolId ( ) ) // ✅ 号源池ID( 用于div_log审计)
. setSlotId ( it . getSlotId ( ) ) // ✅ 号源槽位ID( 用于div_log审计)
. setSeqNo ( it . getSeqNo ( ) ) // ✅ 预约序号(用于叫号显示)
. setStatus ( STATUS_WAITING )
. setQueueOrder ( + + maxOrder )
. setDeleteFlag ( " 0 " )
@@ -140,7 +162,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
. setUpdateTime ( LocalDateTime . now ( ) ) ;
triageQueueItemService . save ( qi ) ;
// 写入分诊日志
writeDivLog ( it . getPoolId ( ) , it . getSlotId ( ) , " ADD_QUEUE " ) ;
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
TriageCandidateExclusion exclusion = triageCandidateExclusionService . getOne (
new LambdaQueryWrapper < TriageCandidateExclusion > ( )
@@ -171,17 +194,26 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
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 ( ) ;
@@ -239,6 +271,12 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
return R . ok ( true ) ;
}
/**
* 智能分诊选呼功能,在进行选呼操作时同时写入叫号记录和选呼操作日志
*
* @param req
* @return
*/
@Override
@Transactional ( rollbackFor = Exception . class )
public R < ? > call ( TriageQueueActionReq req ) {
@@ -253,7 +291,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 叫号后推送 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 ( STATUS_CALLING . equals ( selected . getStatus ( ) ) ) {
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
@@ -264,6 +304,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
}
/**
* 智能分诊完成操作
* 在进行完成操作后同时写入叫号记录和完成操作日志
*
* @param req
* @return
*/
@Override
@Transactional ( rollbackFor = Exception . class )
public R < ? > complete ( TriageQueueActionReq req ) {
@@ -283,7 +330,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
} else {
// 如果没有提供 id, 通过查询条件查找( 兼容旧逻辑)
Long orgId = req ! = null & & req . getOrganizationId ( ) ! = null ? req . getOrganizationId ( ) : null ;
System . out . println ( " >>> [TriageQueue] complete() 开始执行(不限制日期,通过查询条件), tenantId= " + tenantId + " , orgId= " + orgId ) ;
LambdaQueryWrapper < TriageQueueItem > callingWrapper = new LambdaQueryWrapper < TriageQueueItem > ( )
. eq ( TriageQueueItem : : getTenantId , tenantId )
@@ -297,8 +343,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
calling = triageQueueItemService . getOne ( callingWrapper , false ) ;
System . out . println ( " >>> [TriageQueue] complete() 查询叫号中患者(不限制日期): orgId= " + orgId + " , 结果= " + ( calling ! = null ? calling . getPatientName ( ) + " (status= " + calling . getStatus ( ) + " , queueDate= " + calling . getQueueDate ( ) + " ) " : " null " ) ) ;
if ( calling = = null ) {
return R . fail ( " 当前没有叫号中的患者 " ) ;
@@ -328,8 +372,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService . getOne ( nextWrapper , false ) ;
System . out . println ( " >>> [TriageQueue] complete() 查询等待患者(不限制日期): actualOrgId= " + actualOrgId + " , 结果= " + ( next ! = null ? next . getPatientName ( ) + " (status= " + next . getStatus ( ) + " ) " : " null " ) ) ;
if ( next ! = null ) {
next . setStatus ( STATUS_CALLING ) . setUpdateTime ( LocalDateTime . now ( ) ) ;
@@ -340,16 +382,45 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 完成后推送 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 ( ) ) ;
@@ -358,7 +429,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
// 验证状态
if ( ! STATUS_CALLING . equals ( calling . getStatus ( ) ) ) {
return R . fail ( " 只能对 \" 叫号中 \" 状态的患者进行过号重排 ,当前患者状态为: " + calling . getStatus ( ) ) ;
return R . fail ( " 只能对 \" 叫号中 \" 状态的患者进行" + ( " SKIP " . equals ( action ) ? " 跳过 " : " 过号重排 " ) + " ,当前患者状态为: " + calling . getStatus ( ) ) ;
}
} else {
// 如果没有提供 id, 通过查询条件查找( 兼容旧逻辑)
@@ -379,12 +450,11 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
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 " )
@@ -393,22 +463,14 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
. eq ( TriageQueueItem : : getStatus , STATUS_SKIPPED ) )
. orderByAsc ( TriageQueueItem : : getQueueOrder )
. last ( " LIMIT 1 " ) ;
// 如果指定了科室ID, 则按科室过滤; 否则查询所有科室( 全科模式)
if ( actualOrgId ! = null ) {
nextWrapper . eq ( TriageQueueItem : : getOrganizationId , actualOrgId ) ;
}
TriageQueueItem next = triageQueueItemService . getOne ( nextWrapper , false ) ;
// 调试日志:检查查询结果
System . out . println ( " >>> [TriageQueue] requeue() 查询等待中的患者: " ) ;
System . out . println ( " >>> - 科室ID: " + actualOrgId ) ;
System . out . println ( " >>> - 找到的等待患者: " + ( next ! = null ? next . getPatientName ( ) + " (状态: " + next . getStatus ( ) + " ) " : " null " ) ) ;
// 如果找不到等待中的患者,直接返回失败(不执行跳过操作)
if ( next = = null ) {
System . out . println ( " >>> [TriageQueue] requeue() 失败:没有等待中的患者 " ) ;
return R . fail ( " 当前没有等待中的患者 " ) ;
}
@@ -433,27 +495,20 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
triageQueueItemService . updateById ( next ) ;
recalcOrders ( actualOrgId , null ) ;
// ✅ 过号重排后推送 SSE 消息(实时通知显示屏刷新)
// 推送 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 < ? > skip ( TriageQueueActionReq req ) {
// 当前业务“跳过”按“过号重排”处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
return requeue ( req ) ;
}
@Override
@Transactional ( rollbackFor = Exception . class )
public R < ? > next ( TriageQueueActionReq req ) {
Integer tenantId = SecurityUtils . getLoginUser ( ) . getTenantId ( ) ;
TriageQueueItem calling = null ;
System . out . println ( " >>> [TriageQueue] next() 开始执行(不限制日期), tenantId= " + tenantId ) ;
// 关键改进:如果提供了 id, 优先使用 id 直接查找(像 call 方法一样)
if ( req ! = null & & req . getId ( ) ! = null ) {
@@ -513,14 +568,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService . getOne ( nextWrapper , false ) ;
// 调试日志:打印查询条件和结果
System . out . println ( " >>> [TriageQueue] next() 查询条件(不限制日期): tenantId= " + tenantId
+ " , actualOrgId= " + actualOrgId
+ " , deleteFlag=0 "
+ " , status IN (WAITING, SKIPPED) " ) ;
System . out . println ( " >>> [TriageQueue] next() 查询结果: calling= " + ( calling ! = null ? calling . getPatientName ( ) + " (status= " + calling . getStatus ( ) + " , queueDate= " + calling . getQueueDate ( ) + " ) " : " null " )
+ " , next= " + ( next ! = null ? next . getPatientName ( ) + " (status= " + next . getStatus ( ) + " , queueDate= " + next . getQueueDate ( ) + " ) " : " null " ) ) ;
if ( next = = null ) {
if ( actualOrgId ! = null ) {
@@ -535,6 +582,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
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 ) ;
}
@@ -608,7 +662,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
. eq ( TriageQueueItem : : getDeleteFlag , " 0 " )
. orderByAsc ( TriageQueueItem : : getQueueOrder )
) ;
// 通过 slotId 批量查询 seqNo( 方案 B: 不修改 triage_queue_item 表结构)
Map < Long , Integer > slotSeqNoMap = buildSlotSeqNoMap ( allItems ) ;
CallNumberDisplayResp resp = new CallNumberDisplayResp ( ) ;
// 1. 获取科室名称(从第一条数据中取)
@@ -626,7 +683,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if ( callingItem ! = null ) {
CallNumberDisplayResp . CurrentCallInfo currentCall = new CallNumberDisplayResp . CurrentCallInfo ( ) ;
currentCall . set Number( callingItem . getQueueOrder ( ) ) ;
Integer displayNo = resolveDisplay Number( callingItem , slotSeqNoMap ) ;
currentCall . setNumber ( displayNo ) ;
currentCall . setName ( maskPatientName ( callingItem . getPatientName ( ) ) ) ;
currentCall . setRoom ( callingItem . getRoomNo ( ) ! = null ? callingItem . getRoomNo ( ) : " 1号 " ) ;
currentCall . setDoctor ( callingItem . getPractitionerName ( ) ) ;
@@ -680,7 +738,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
patient . setId ( item . getId ( ) ) ;
patient . setName ( maskPatientName ( item . getPatientName ( ) ) ) ;
patient . setStatus ( item . getStatus ( ) ) ;
patient . setQueueOrder ( item . getQueueOrder ( ) ) ;
patient . setQueueOrder ( resolveDisplayNumber ( item , slotSeqNoMap ) ) ;
patients . add ( patient ) ;
// 统计等待人数(不包括 CALLING 状态)
@@ -748,6 +806,82 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
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 ( ) ;
}
}