88 分诊排队管理-》科室叫号显示屏 表triage_queue_item中添加了联合索引queue_date,organization_id,tenant_id,queue_order。添加了room_no,practitioner_id字段。

This commit is contained in:
weixin_45799331
2026-01-27 11:09:00 +08:00
parent 41494ebf7c
commit c4c3073be0
9 changed files with 644 additions and 91 deletions

View File

@@ -21,7 +21,7 @@ public final class ServiceException extends RuntimeException {
/**
* 错误明细,内部调试错误
*
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
* 和
*/
private String detailMessage;

View File

@@ -103,6 +103,9 @@ public class SecurityConfig {
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**")
.permitAll()
// WebSocket 握手请求允许匿名访问
.antMatchers("/ws/**", "/test-ws")
.permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**")
.permitAll()
.antMatchers("/patientmanage/information/**")

View File

@@ -69,6 +69,13 @@
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!-- WebSocket 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- rabbitMQ -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -1,6 +1,7 @@
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;
@@ -22,6 +23,9 @@ public interface TriageQueueAppService {
R<?> skip(TriageQueueActionReq req);
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
R<?> next(TriageQueueActionReq req);
/** 叫号显示屏:获取当前叫号和等候队列信息 */
CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId);
}

View File

@@ -10,19 +10,20 @@ import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
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.dto.TriageQueueEncounterItem;
import com.openhis.web.triageandqueuemanage.websocket.CallNumberWebSocket;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
@@ -121,6 +122,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.setPatientName(it.getPatientName())
.setHealthcareName(it.getHealthcareName())
.setPractitionerName(it.getPractitionerName())
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
.setStatus(STATUS_WAITING)
.setQueueOrder(++maxOrder)
.setDeleteFlag("0")
@@ -238,6 +241,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (STATUS_WAITING.equals(selected.getStatus())) {
selected.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
triageQueueItemService.updateById(selected);
// ✅ 叫号后推送 WebSocket 消息
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
return R.ok(true);
} else if (STATUS_CALLING.equals(selected.getStatus())) {
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
@@ -321,6 +328,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
recalcOrders(actualOrgId, null);
// ✅ 完成后推送 WebSocket 消息
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
return R.ok(true);
}
@@ -413,6 +424,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
triageQueueItemService.updateById(next);
recalcOrders(actualOrgId, null);
// ✅ 过号重排后推送 WebSocket 消息
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
return R.ok(true);
}
@@ -551,6 +566,179 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
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, STATUS_WAITING, STATUS_CALLING)
.eq(TriageQueueItem::getDeleteFlag, "0")
.orderByAsc(TriageQueueItem::getQueueOrder)
);
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 -> STATUS_CALLING.equals(item.getStatus()))
.findFirst()
.orElse(null);
if (callingItem != null) {
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
currentCall.setNumber(callingItem.getQueueOrder());
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(item.getQueueOrder());
patients.add(patient);
// 统计等待人数(不包括 CALLING 状态)
if (STATUS_WAITING.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);
}
/**
* 推送显示屏更新消息到 WebSocket
* @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());
// 推送到该科室的所有 WebSocket 连接
CallNumberWebSocket.pushToOrganization(organizationId, message);
} catch (Exception e) {
// WebSocket 推送失败不应该影响业务逻辑
System.err.println("推送显示屏更新失败:" + e.getMessage());
}
}
}

View File

@@ -1,12 +1,16 @@
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 lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@@ -65,6 +69,62 @@ public class TriageQueueController {
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());
}
}
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

@@ -9,6 +9,12 @@ public class TriageQueueEncounterItem {
private String patientName;
private String healthcareName;
private String practitionerName;
// ========== 新增字段(可选,用于叫号显示屏)==========
/** 医生ID可选 */
private Long practitionerId;
/** 诊室号(可选) */
private String roomNo;
}

View File

@@ -18,21 +18,23 @@ public class TriageQueueItem {
@TableId(type = IdType.AUTO)
private Long id;
private Integer tenantId;
private LocalDate queueDate;
private Long organizationId;
private String organizationName;
private Integer tenantId; // 租户ID
private LocalDate queueDate; // 队列日期
private Long organizationId; // 科室ID
private String organizationName; // 科室名称
private Long encounterId;
private Long patientId;
private String patientName;
private Long patientId; // 患者ID
private String patientName; // 患者姓名(脱敏)
private String healthcareName;
private String practitionerName;
private String healthcareName; // 挂号类型(普通/专家)
private String practitionerName; // 医生姓名
private Long practitionerId; // 医生ID新增字段
private String roomNo; // 诊室号(新增字段)
/** WAITING / CALLING / SKIPPED / COMPLETED */
private String status;
private Integer queueOrder;
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
private LocalDateTime createTime;
private LocalDateTime updateTime;