16 Commits

Author SHA1 Message Date
赵云
e2f4996f47 Merge remote-tracking branch 'origin/华佗' into 华佗
# Conflicts:
#	openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue
2026-05-11 15:27:40 +08:00
赵云
37b57e8b12 Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
根因: clickRowDb 双击编辑条件为 row.statusEnum == 1 && !row.requestId,
护士退回的医嘱 statusEnum 被重置为 1(DRAFT),但 requestId 仍存在(之前已保存过),
导致不满足 !row.requestId 条件,无法进入编辑模式。

修复: 移除 !row.requestId 限制,仅保留 statusEnum == 1 条件。
保存流程已支持两种场景:
- 新建医嘱(无requestId): dbOpType = '1' 创建
- 退回医嘱(有requestId): dbOpType = '2' 更新
2026-05-11 15:26:39 +08:00
wangjian963
9bd39c06e7 Merge remote-tracking branch 'origin/develop' into develop 2026-05-11 15:25:17 +08:00
赵云
bde42d6b14 fix: 恢复 Bug #497 的后端修改 + 数据库字段同步 (ALTER TABLE doc_request_form ADD COLUMN status) 2026-05-11 14:57:06 +08:00
关羽
01bf3177c9 fix: 还原 Bug #443/#475/#477/#486/#497 引入的 getRequestForm 编译错误 2026-05-11 14:40:07 +08:00
赵云
2a9f8376e6 fix: 完整回退 Bug #497 的后端修改(SQL/Java接口/Impl/Dto) 2026-05-11 14:13:49 +08:00
关羽
0774d9f877 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:handleFocus/handleChange 中 categoryCode 的计算逻辑错误。当新增行未选择
医嘱类型时(row.adviceType 为 undefined),代码回退到 adviceQueryParams 的默认值并
匹配到具体药品分类(如西药 categoryCode='2'),导致搜索被限制在单一分类而非全局药库。

修复:简化 categoryCode 判定为 `row.adviceType !== undefined ? selectedItem.categoryCode : ''`,
未选类型时传空 categoryCode,使 searchKey 在全药库范围内模糊匹配。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:08:13 +08:00
赵云
cee38eceae Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. 非耗材类医嘱执行报"耗材库存"错误: handleExecute 中无条件调用 lotNumberMatch,
   后端会校验该就诊下所有待发放耗材库存,即使当前执行的是口服药等非耗材类医嘱
2. 全选联动异常: msgSuccess 在 handleGetPrescription 之前执行,数据刷新后
   defaultSelectAllRows 重新选中所有行,用户关闭弹窗后看到全选效果

修复方案:
1. 增加医嘱类型判断,仅当选中医嘱包含药品(med_medication_request)或耗材(device)
   类型时才调用 lotNumberMatch
2. 调整执行顺序:先刷新数据再显示成功弹窗,避免用户看到数据刷新的副作用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:07:49 +08:00
赵云
bbc740b6ce fix: 完全回退 Bug #497 引入的 drf.status 字段(数据库不存在) 2026-05-11 14:06:53 +08:00
关羽
256b986c0e Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因:handleViewDetail 为同步方法,点击详情时 getLocationInfo 尚未返回,
orgOptions 为空导致 recursionFun 无法将 targetDepartment ID 解析为科室名称。

修复:
1. 前端(4个申请组件):handleViewDetail 改为 async,解析 descJson 前确保 orgOptions 已加载
2. 前端:watch encounterId 改为 Promise.all 并行加载数据和科室列表
3. 后端:新增 keyword 关键字筛选参数(申请单号/检查项目模糊匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:03:49 +08:00
关羽
eaac16769d Fix Bug #491: 【执行科室配置】保存配置时系统报错
根因: addOrEditOrgLoc 方法中 organizationService.getById() 返回 null 时
直接调用 .getName() 导致 NullPointerException。当数据库中某条执行科室配置
关联的 organizationId 对应的科室记录已被删除时触发此问题。

修复: 在调用 getName() 前增加 null 检查,返回"未知科室"作为降级提示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:01:23 +08:00
wangjian963
df6c5f3824 506 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
CommonConstants.AppointmentOrderStatus 常量 → OrderStatus 枚举重构
   新增枚举:0=患者取消 / 1=有效 / 2=系统取消 / 3=已完成
   退号流程加乐观锁防并发,slot 状态改回待约,退号日志独立事务 修复 XML 中 Integer 比较用字符串的问题
Bug #411 — 诊室过滤栏从科室下拉框改为诊室按钮组
2026-05-11 13:51:47 +08:00
关羽
08075c90e2 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
根因分析:
1. el-collapse accordion 模式下快速切换分类时,连续的折叠/展开动画重叠,
   Element Plus 在动画过程中重新计算面板高度,导致高度跳变和白屏闪烁
2. 折叠容器缺少 overflow:hidden,动画过渡期间内容溢出造成闪烁

修复方案:
1. 添加 isAnimating 防抖标志,handleCollapseChange 中 300ms 内忽略后续点击
   (与 CSS 过渡时长一致),让当前动画完整执行后再响应下一次切换
2. .collapse-scroll 添加 overflow-x:hidden,防止水平方向溢出
3. :deep(.el-collapse-item__wrap) 添加 overflow:hidden 替代 will-change:height,
   避免强制 GPU 合成层带来的性能开销

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:50:04 +08:00
赵云
c5820fcec2 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因:get-check 接口只接收 encounterId 参数,忽略前端传递的 startDate/endDate/status 筛选参数,
导致日期筛选和状态筛选全部失效。同类型的 get-inspection 接口已正确支持这些参数。
修复:在 controller 的 get-check 方法增加 startDate、endDate、status 三个 @RequestParam,
调用 5 参数重载的 service 方法,使筛选参数正确传递到 SQL 层。
前端 examineApplication.vue 已包含状态列、parseStatus 映射、状态筛选下拉框,无需修改。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:45 +08:00
赵云
fbe7f4f41f Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空
根因:projectWithDepartment() 在提交时会清空用户手动选择的发往科室,
且项目未配置执行科室时 findTreeItem 返回 null 导致校验失败。
同时 submit() 使用 item.positionId(可能为 undefined)作为执行科室。

修复:
1. 清空科室前保存用户手动选择的值(manualDept)
2. type=2(提交)且 findItem 不存在时,若用户已手动选择科室则恢复并允许通过
3. positionId 兜底使用 form.targetDepartment

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:22 +08:00
关羽
4bd0d2dcc8 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:02:20 +08:00
16 changed files with 274 additions and 207 deletions

View File

@@ -10,7 +10,7 @@ import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -198,10 +198,11 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
dto.setStatus("已取号");
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("已预约");
}
@@ -372,10 +373,11 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
dto.setStatus("已取号");
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("已预约");
}

View File

@@ -2,6 +2,7 @@ package com.openhis.web.chargemanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
@@ -329,16 +330,14 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
}
}
// 如果本次门诊挂号来自预约签到,同步预约订单与号源槽位状态改为已退号
// 退费成功后,同步回滚预约订单状态及号源;同时移除分诊队列
Long refundOrderMainId = null;
if (result != null && result.getCode() == 200) {
syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason());
// 同步移除分诊队列中的记录
refundOrderMainId = syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason());
removeTriageQueueItem(byId.getId());
}
// 记录退号日志
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
// 退号日志独立事务写入,无论退费成功与否均记录
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon, refundOrderMainId);
// 2025/05/05 该处保存费用项后,会通过统一收费处理进行收费
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
@@ -435,8 +434,6 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
// 通过患者、科室、日期查找关联的预约订单
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.orderByDesc(Order::getUpdateTime)
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1");
@@ -590,20 +587,25 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
}
/**
* 同步预约号源状态为已退号
* 说明:
* 1) 门诊退号主流程不依赖该步骤成功与否,因此此方法内部异常仅记录日志,不向上抛出。
* 2) 通过患者、科室、日期以及状态筛选最近一条预约订单,尽量避免误匹配。
* 诊前退号:回滚预约订单、号源槽位、号源池统计
*
* <p>处理四件事:
* <ol>
* <li>order_main → status=0(患者取消), pay_status=3(已退费), 写入取消时间和原因</li>
* <li>adm_schedule_slot → status=0(待约), order_id=NULL(释放号源)</li>
* <li>adm_schedule_pool → 重算统计值 + version+1</li>
* <li>返回 order_main.id 供 refund_log 关联</li>
* </ol>
*
* <p>异常仅记录日志不向上抛,不影响主流程返回成功。
*/
private void syncAppointmentReturnStatus(Encounter encounter, String reason) {
private Long syncAppointmentReturnStatus(Encounter encounter, String reason) {
if (encounter == null || encounter.getPatientId() == null) {
return;
return null;
}
try {
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.orderByDesc(Order::getUpdateTime)
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1");
@@ -625,35 +627,55 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
Order appointmentOrder = orderService.getOne(queryWrapper, false);
if (appointmentOrder == null) {
return;
return null;
}
Date now = new Date();
if (!CommonConstants.AppointmentOrderStatus.RETURNED.equals(appointmentOrder.getStatus())) {
Order updateOrder = new Order();
updateOrder.setId(appointmentOrder.getId());
updateOrder.setStatus(CommonConstants.AppointmentOrderStatus.RETURNED);
updateOrder.setPayStatus(0);
updateOrder.setCancelTime(now);
updateOrder.setCancelReason("门诊退号");
updateOrder.setUpdateTime(now);
orderService.updateById(updateOrder);
// 只有有效订单(1)才能退号
if (!OrderStatus.ACTIVE.getValue().equals(appointmentOrder.getStatus())) {
log.warn("退号跳过:订单状态非有效, orderId={}, status={}",
appointmentOrder.getId(), appointmentOrder.getStatus());
return null;
}
// 乐观锁更新WHERE version = 旧值,防并发重复退号
boolean updated = orderService.update(
new LambdaUpdateWrapper<Order>()
.set(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue())
.set(Order::getPayStatus, PaymentStatus.REFUND_ALL.getValue())
.set(Order::getCancelTime, new Date())
.set(Order::getCancelReason,
StringUtils.isNotEmpty(reason) ? reason : "诊前退号")
.set(Order::getUpdateTime, new Date())
.setSql("version = version + 1")
.eq(Order::getId, appointmentOrder.getId())
.eq(Order::getVersion, appointmentOrder.getVersion())
);
if (!updated) {
log.warn("退号乐观锁冲突,订单已被其他操作修改, orderId={}", appointmentOrder.getId());
return null;
}
Long slotId = appointmentOrder.getSlotId();
if (slotId == null) {
return;
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.RETURNED);
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.AVAILABLE);
if (slotRows > 0) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
}
return appointmentOrder.getId();
} catch (Exception e) {
log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e);
return null;
}
}
@@ -672,22 +694,29 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
}
/**
* 记录退号日志
* 记录退号日志(独立事务)。
*
* <p>REQUIRES_NEW 确保即使主事务回滚,退号审计日志也不丢失。
* orderMainId 优先使用 order_main.id若退费失败则 fallback 到 encounterId。
*
* @param cancelRegPaymentDto 退号请求对象
* @param encounter 就诊信息
* @param result 退号结果
* @param paymentRecon 支付对账信息
* @param orderMainId 预约订单主键order_main.id用于关联业务数据
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordRefundLog(CancelRegPaymentDto cancelRegPaymentDto,
Encounter encounter,
R<?> result,
PaymentReconciliation paymentRecon) {
PaymentReconciliation paymentRecon,
Long orderMainId) {
RefundLog refundLog = new RefundLog();
try {
// 1. 订单ID唯一
String orderId = String.valueOf(cancelRegPaymentDto.getEncounterId());
// 1. 订单ID关联 order_main.id
String orderId = orderMainId != null
? String.valueOf(orderMainId)
: String.valueOf(cancelRegPaymentDto.getEncounterId());
refundLog.setOrderId(orderId);
// 已存在则不重复插入(防止唯一约束异常)

View File

@@ -1991,7 +1991,7 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
Order appointmentOrder = iOrderService.getOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounterFormData.getPatientId())
.eq(Order::getStatus, CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.eq(Order::getStatus, OrderStatus.ACTIVE.getValue()) // 有效订单(1)
.eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1")
@@ -2114,11 +2114,11 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
Long queuePoolId = null;
Long queueSlotId = null;
try {
// 查询患者当天的待签到预约订单status = 1 或 2 表示已预约或已取号)
// 查询患者当天有效订单(1);已取消(0/2)和已完成(3)的不参与排队
Order order = iOrderService.getOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, 1, 2) // 1=BOOKED 已预约, 2=CHECKED_IN 已取号
.eq(Order::getStatus, OrderStatus.ACTIVE.getValue()) // 有效(1)
.eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1")

View File

@@ -0,0 +1,63 @@
/*
* Copyright ©2023 CJB-CNIT Team. All rights reserved
*/
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态 (order_main.status)
*
* <pre>
* 状态流转:
* 创建订单 → ACTIVE(1)
* 签到 → ACTIVE(1) 不变
* 患者退号 → PATIENT_CANCELLED(0)
* 系统取消 → SYSTEM_CANCELLED(2)
* 就诊完成 → COMPLETED(3)
* </pre>
*
* @author wangjian963
* @date 2026-05-09
*/
@Getter
@AllArgsConstructor
public enum OrderStatus implements HisEnumInterface {
/**
* 患者取消
*/
PATIENT_CANCELLED(0, "0", "患者取消"),
/**
* 有效
*/
ACTIVE(1, "1", "有效"),
/**
* 系统取消
*/
SYSTEM_CANCELLED(2, "2", "系统取消"),
/**
* 已完成
*/
COMPLETED(3, "3", "已完成");
private Integer value;
private String code;
private String info;
public static OrderStatus getByValue(Integer value) {
if (value == null) {
return null;
}
for (OrderStatus val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -6,8 +6,8 @@ import com.core.common.utils.AssignSeqUtil;
import com.openhis.clinical.domain.Order;
import com.openhis.clinical.mapper.OrderMapper;
import com.openhis.clinical.service.IOrderService;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
import com.openhis.common.enums.AssignSeqEnum;
import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -124,7 +124,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
: new Date(); // 兜底:正常业务不应走到这里
order.setAppointmentDate(appointmentDateTime);
order.setAppointmentTime(appointmentDateTime);
order.setStatus(AppointmentOrderStatus.BOOKED);
// 订单状态: 0=患者取消 1=有效 2=系统取消 3=已完成
order.setStatus(OrderStatus.ACTIVE.getValue());
order.setPayStatus(0);
order.setVersion(0);
@@ -169,10 +170,13 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (AppointmentOrderStatus.CANCELLED.equals(order.getStatus())) {
// 已取消患者取消0 或 系统取消2不可再次取消
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(order.getStatus())
|| OrderStatus.SYSTEM_CANCELLED.getValue().equals(order.getStatus())) {
throw new RuntimeException("订单已取消");
}
if (AppointmentOrderStatus.CHECKED_IN.equals(order.getStatus())) {
// 已完成(3)的订单不可取消
if (OrderStatus.COMPLETED.getValue().equals(order.getStatus())) {
throw new RuntimeException("订单已完成,无法取消");
}
@@ -189,6 +193,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
.eq(Order::getPatientId, patientId)
.eq(Order::getTenantId, tenantId)
.ge(Order::getCancelTime, startTime)
.eq(Order::getStatus, AppointmentOrderStatus.CANCELLED));
// 只统计患者主动取消(0),不含系统取消(2)
.eq(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue()));
}
}

View File

@@ -13,8 +13,8 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.TicketMapper;
import com.openhis.clinical.service.IOrderService;
import com.openhis.clinical.service.ITicketService;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -195,8 +195,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
Date startTime = Date.from(periodStart.atZone(ZoneId.systemDefault()).toInstant());
Date endTime = Date.from(periodEnd.atZone(ZoneId.systemDefault()).toInstant());
// 预约去重以订单为准order_main因为预约成功会先落订单clinical_ticket 不一定在此链路写入
List<Integer> effectiveOrderStatuses = Arrays.asList(AppointmentOrderStatus.BOOKED, AppointmentOrderStatus.CHECKED_IN);
// 预约去重以订单为准order_main有效订单(1)才参与去重
List<Integer> effectiveOrderStatuses = Arrays.asList(OrderStatus.ACTIVE.getValue());
int exists = orderMapper.countPatientDeptOrdersInPeriod(dto.getPatientId(), slot.getDepartmentId(), slot.getDepartmentName(),
startTime, endTime, effectiveOrderStatuses);
if (exists > 0) {
@@ -314,9 +314,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
}
Order latestOrder = orders.get(0);
// 1. 更新订单状态为已取号,并更新支付状态和支付时间
orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN);
// 更新支付状态为已支付,记录支付时间
// 1. 签到不改变订单状态(仍为有效1),更新支付状态为已支付并记录支付时间
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 查询号源槽位信息

View File

@@ -160,11 +160,12 @@
AND delete_flag = '0'
</update>
<!-- status=0(待约)时清空order_id释放号源使退号后号源可再被预约 -->
<update id="updateSlotStatus">
UPDATE adm_schedule_slot
SET
status = #{status},
<if test="status != null and '0'.equals(status.toString())">
<if test="status != null and status == 0">
order_id = NULL,
</if>
update_time = now()

View File

@@ -117,12 +117,14 @@
</where>
</select>
<!-- status=1: 只查有效订单(0=患者取消 1=有效 2=系统取消 3=已完成) -->
<select id="selectOrderById" resultMap="OrderResult">
select * from order_main where id = #{id}
and status = 1
order by create_time desc
</select>
<!-- status=1: 只查有效订单 -->
<select id="selectOrderBySlotId" resultMap="OrderResult">
select * from order_main where slot_id = #{slotId} and status = 1
</select>
@@ -248,8 +250,9 @@
update order_main set status = #{status} where id = #{id}
</update>
<!-- status=0: 患者取消 (OrderStatus.PATIENT_CANCELLED) -->
<update id="updateOrderCancelInfoById">
update order_main set status = 3, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id}
update order_main set status = 0, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id}
</update>
<update id="updatePayStatus">

View File

@@ -2063,20 +2063,15 @@ watch(() => props.patientInfo, async (newVal) => {
}
}, { deep: true, immediate: true })
// Bug #329/#466: 监听已选择的检验项目,自动更新检验项目文本并设置默认执行科室、标本类型
// Bug #329: 监听已选择的检验项目,自动更新检验项目文本并设置默认执行科室
watch(() => selectedInspectionItems.value, async (newVal) => {
if (newVal && newVal.length > 0) {
formData.inspectionItemsText = newVal.map(item => item.itemName).join('+')
const firstItem = newVal[0]
// Bug #466: 如果标本类型为空或仍为初始化默认值,根据第一个检验项目的 sampleType 自动设置默认标本类型
if ((!formData.specimenName || formData.specimenName === '血液') && firstItem.sampleType) {
formData.specimenName = firstItem.sampleType
}
// Bug #329: 如果执行科室为空,根据第一个检验项目的检验类型自动设置默认执行科室
if (!formData.executeDepartment) {
const firstItem = newVal[0]
// 根据检验项目的 inspectionTypeId 获取默认执行科室
if (firstItem.inspectionTypeId) {
const defaultDeptCode = await getDefaultPerformDeptCode(firstItem.inspectionTypeId)
@@ -2086,10 +2081,9 @@ watch(() => selectedInspectionItems.value, async (newVal) => {
}
}
} else {
// Bug #329: 当项目被清空时,同时清空执行科室和标本类型(下次选择项目时会重新自动设置)
// Bug #329: 当项目被清空时,同时清空执行科室(下次选择项目时会重新自动设置)
formData.inspectionItemsText = ''
formData.executeDepartment = ''
formData.specimenName = ''
}
}, { deep: true })

View File

@@ -86,14 +86,14 @@ const tableColumns = computed<TableColumn[]>(() => [
* @param searchKey 搜索关键词
*/
function refresh(adviceType: any, categoryCode: string, searchKey: string) {
// 有搜索词时跨类型搜索,或adviceType为空时也跨类型搜索新行未选类型时默认搜全部避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目
if (searchKey || adviceType === undefined || adviceType === '') {
// 有搜索词时跨类型搜索,避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目
if (searchKey) {
queryParams.value.adviceTypes = '1,2,3,6';
queryParams.value.categoryCode = '';
} else {
queryParams.value.adviceTypes = String(adviceType);
queryParams.value.categoryCode = categoryCode || '';
queryParams.value.adviceTypes =
adviceType !== undefined && adviceType !== '' ? String(adviceType) : '1,2,3,6';
}
queryParams.value.categoryCode = categoryCode || '';
queryParams.value.searchKey = searchKey || '';
getList();
}

View File

@@ -281,14 +281,13 @@ const submit = () => {
accountId: patientInfo.value.accountId, // // 账户id
};
});
const itemNames = applicationListAllFilter.map(item => item.adviceName).filter(Boolean).join('、');
saveCheckd({
activityList: applicationListAllFilter,
patientId: patientInfo.value.patientId, //患者ID
encounterId: patientInfo.value.encounterId, // 就诊ID
organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID
requestFormId: '', // 申请单ID
name: itemNames || '检查申请单',
name: '检查申请单',
descJson: JSON.stringify(form),
categoryEnum: '2', // 1 检验 2 检查 3 输血 4 手术
}).then((res) => {

View File

@@ -802,8 +802,8 @@ function clickRowDb(row, column, event) {
return;
}
row.showPopover = false;
// “待签发(已保存 requestId存在)”不允许编辑;仅“待保存(无requestId)”允许编辑
if (row.statusEnum == 1 && !row.requestId) {
// statusEnum == 1 允许编辑(包含新创建的”待保存”和护士退回的”待签发”)
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true;

View File

@@ -174,6 +174,7 @@ const activeNames = ref([]);
const userStore = useUserStore();
const prescriptionList = ref([]);
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
const therapyEnum = ref(undefined);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const chooseAll = ref(false);
@@ -189,10 +190,6 @@ const props = defineProps({
deadline: {
type: String,
},
therapyEnum: {
type: Number,
default: undefined,
},
});
function handleGetPrescription() {
@@ -203,7 +200,7 @@ function handleGetPrescription() {
encounterIds: encounterIds,
pageSize: 10000,
pageNo: 1,
therapyEnum: props.therapyEnum,
therapyEnum: therapyEnum.value,
exeStatus: props.exeStatus,
requestStatus: props.requestStatus,
})

View File

@@ -58,7 +58,7 @@
:clearable="false"
@change="handleGetPrescription"
/>
<el-radio-group v-model="therapyEnum" class="ml20" @change="handleTherapyChange">
<el-radio-group v-model="therapyEnum" class="ml20" @change="handleRadioChange">
<el-radio :value="undefined">全部</el-radio>
<el-radio :value="1">长期</el-radio>
<el-radio :value="2">临时</el-radio>
@@ -79,7 +79,6 @@
:exeStatus="exeStatus"
:requestStatus="requestStatus"
:deadline="deadline"
:therapyEnum="therapyEnum"
/>
<SummaryMedicineList v-else />
<!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick">
@@ -121,7 +120,6 @@ const requestStatus = ref(RequestStatus.COMPLETED);
const chooseAll = ref(false);
const drugType = ref('1');
const isDetails = ref('1');
const therapyEnum = ref(undefined);
// 存储子组件引用的对象
const prescriptionRefs = ref();
@@ -177,10 +175,6 @@ function handleRadioChange(value) {
}
}
function handleTherapyChange() {
handleGetPrescription();
}
function handleExecute() {
proxy.$refs['prescriptionRefs'].handleMedicineSummary();
}

View File

@@ -1481,8 +1481,6 @@ function handleMedicalAdvice(row) {
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== row.visitId) return false;
// 仅保留药品adviceType=1过滤耗材(2)和项目(3)
if (item.adviceType !== 1 && item.advice_type !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
@@ -1745,8 +1743,6 @@ function handleQuoteBilling() {
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 仅保留药品adviceType=1过滤耗材(2)和项目(3)
if (item.adviceType !== 1 && item.advice_type !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
return medicineName && medicineName.trim() !== '';

View File

@@ -198,7 +198,38 @@
</el-button>
</div>
<div class="queue-actions-right">
</div>
</div>
</div>
<!-- 底部控制面板 -->
<div class="footer-section">
<!-- Bug #411诊室快速过滤栏筛选维度从科室改为诊室 -->
<div class="filter-section">
<div class="filter-left">
<div class="filter-label">
诊室快速过滤栏
</div>
<div class="filter-button-wrapper">
<el-button
:type="selectedRoom === 'all' ? 'primary' : ''"
size="small"
@click="selectedRoom = 'all'"
>
全部
</el-button>
<el-button
v-for="room in uniqueRooms"
:key="room"
:type="selectedRoom === room ? 'primary' : ''"
size="small"
@click="selectedRoom = room"
>
{{ room }}
</el-button>
</div>
</div>
<div class="filter-right">
<el-button
:type="showOnlyWaiting ? 'primary' : ''"
size="small"
@@ -215,38 +246,6 @@
</el-button>
</div>
</div>
</div>
</div>
<!-- 底部控制面板 -->
<div class="footer-section">
<!-- 诊室快速过滤栏 -->
<div class="filter-section">
<div class="filter-label">
诊室快速过滤栏
</div>
<div class="filter-select-wrapper">
<el-select
v-model="selectedClinicRoom"
placeholder="请选择诊室"
clearable
filterable
style="width: 100%"
size="default"
>
<el-option
label="全部"
value="all"
/>
<el-option
v-for="room in clinicRoomList"
:key="room"
:label="room"
:value="room"
/>
</el-select>
</div>
</div>
<!-- 叫号控制板 -->
<div class="call-control-section">
@@ -680,10 +679,8 @@ const selectedCandidates = ref([])
// 显示选项
const showOnlyWaiting = ref(false)
// 诊室过滤(按诊室维度筛选
const selectedClinicRoom = ref('all')
// 诊室列表(从数据中动态提取)
const clinicRoomList = ref([])
// Bug #411诊室过滤替代原来的科室下拉框selectedDept/departmentList 已移除
const selectedRoom = ref('all')
// 修复【#397】动态获取当前科室名称
const currentDeptName = computed(() => {
@@ -906,11 +903,12 @@ const mapFrontendStatusToBackend = (status) => {
// 从数据库加载队列
const loadQueueFromDb = async () => {
try {
// 使用当前登录人科室
// Bug #411不再按科室选筛加载后端默认按当前登录人科室查询
const organizationId = undefined
// 只查询今天的患者
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
console.log('【心内科】loadQueueFromDb 开始date=', todayStr)
const res = await getTriageQueueList({ date: todayStr }).catch((err) => {
const res = await getTriageQueueList({ organizationId, date: todayStr }).catch((err) => {
console.error('【心内科】loadQueueFromDb 请求异常:', err)
return { code: 500, msg: err?.message || '请求失败', data: null }
})
@@ -1138,8 +1136,6 @@ const loadDataFromApi = async () => {
// 同步当前呼叫(队列从 DB 加载后已同步;这里再兜底一次)
syncCurrentCallFromQueue()
// 提取诊室列表供过滤栏使用
extractClinicRooms()
console.log('【心内科】数据加载完成:候选池', originalCandidatePoolList.value.length, '条,队列', originalQueueList.value.length, '条')
ElMessage.success('【心内科】已从门诊挂号接口加载数据')
} catch (e) {
@@ -1151,42 +1147,35 @@ const loadDataFromApi = async () => {
totalSignedIn.value = originalCandidatePoolList.value.length
totalInQueue.value = originalQueueList.value.length
syncCurrentCallFromQueue()
extractClinicRooms()
}
}
// 原始数据存储(用于过滤)
const originalCandidatePoolList = ref(getInitialCandidatePoolList())
// 提取诊室列表(从队列和候选池数据中动态获取
const extractClinicRooms = () => {
const roomSet = new Set()
// 从队列中提取
originalQueueList.value.forEach(item => {
if (item.room && item.room !== '-') {
roomSet.add(item.room)
}
})
// 从候选池中提取
originalCandidatePoolList.value.forEach(item => {
if (item.room && item.room !== '-') {
roomSet.add(item.room)
}
})
clinicRoomList.value = Array.from(roomSet).sort()
}
// 过滤后的智能候选池数据
// 过滤后的智能候选池数据(按诊室过滤
const filteredCandidatePoolList = computed(() => {
if (selectedClinicRoom.value === 'all') {
if (selectedRoom.value === 'all') {
return originalCandidatePoolList.value
}
return originalCandidatePoolList.value.filter(item => item.room === selectedClinicRoom.value)
return originalCandidatePoolList.value.filter(item => item.room === selectedRoom.value)
})
// 原始队列数据存储(用于过滤)
const originalQueueList = ref(getInitialQueueList())
// 动态计算已加载数据中的唯一诊室列表(依赖上方两个 ref确保声明顺序正确
const uniqueRooms = computed(() => {
const rooms = new Set()
originalCandidatePoolList.value.forEach(item => {
if (item.room && item.room !== '-') rooms.add(item.room)
})
originalQueueList.value.forEach(item => {
if (item.room && item.room !== '-') rooms.add(item.room)
})
return Array.from(rooms).sort()
})
const parseMmSsToSeconds = (mmss) => {
if (!mmss || typeof mmss !== 'string') return 0
const [mm, ss] = mmss.split(':')
@@ -1203,7 +1192,7 @@ const formatSecondsToMmSs = (totalSeconds) => {
return `${mm}:${ss}`
}
// 过滤后的智能队列数据(同时考虑诊室过滤状态过滤)
// 过滤后的智能队列数据(Bug #411诊室过滤 + 状态过滤)
const filteredQueueList = computed(() => {
let filtered = originalQueueList.value
@@ -1211,8 +1200,8 @@ const filteredQueueList = computed(() => {
filtered = filtered.filter(item => item.status !== '已完成')
// 再按诊室过滤
if (selectedClinicRoom.value !== 'all') {
filtered = filtered.filter(item => item.room === selectedClinicRoom.value)
if (selectedRoom.value !== 'all') {
filtered = filtered.filter(item => item.room === selectedRoom.value)
}
// 再按状态过滤(只显示等待)
@@ -1723,16 +1712,12 @@ const handleNextPatient = async () => {
reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId
} else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
// 全科模式:优先用"当前叫号中/第一个等待"所在科室
let orgId = null
{
// Bug #411已移除 selectedDept改为从队列数据中动态获取科室
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
const waiting = originalQueueList.value.find((i) => i.status === '等待')
console.log('【心内科】handleNextPatient 查找:叫号中=', calling?.patientName, '等待=', waiting?.patientName)
orgId = calling?.organizationId ?? waiting?.organizationId
const orgId = calling?.organizationId ?? waiting?.organizationId
console.log('【心内科】handleNextPatient 确定的 orgId=', orgId)
}
if (orgId != null) {
reqData.organizationId = orgId
}
@@ -1761,13 +1746,9 @@ const handleSkip = async () => {
reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId
} else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
// 全科模式:优先用”当前叫号中”所在科室
let orgId = null
{
// 如果没有选中患者,使用当前叫号中的科室
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId
}
const orgId = calling?.organizationId
if (orgId != null) {
reqData.organizationId = orgId
}
@@ -1795,13 +1776,9 @@ const handleComplete = async () => {
reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId
} else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
// 全科模式:优先用”当前叫号中”所在科室
let orgId = null
{
// 如果没有选中患者,使用当前叫号中的科室
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId
}
const orgId = calling?.organizationId
if (orgId != null) {
reqData.organizationId = orgId
}
@@ -1829,13 +1806,9 @@ const handleRequeue = async () => {
reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId
} else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
// 全科模式:优先用”当前叫号中”所在科室
let orgId = null
{
// 如果没有选中患者,使用当前叫号中的科室
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId
}
const orgId = calling?.organizationId
if (orgId != null) {
reqData.organizationId = orgId
}
@@ -2219,10 +2192,6 @@ onUnmounted(() => {
gap: 10px;
}
.queue-actions-right {
display: flex;
gap: 10px;
}
}
.candidate-actions {
@@ -2244,6 +2213,12 @@ onUnmounted(() => {
.filter-section {
margin-bottom: 20px;
display: flex;
align-items: flex-start;
justify-content: space-between;
.filter-left {
flex: 1;
.filter-label {
font-size: 14px;
@@ -2252,8 +2227,18 @@ onUnmounted(() => {
margin-bottom: 10px;
}
.filter-select-wrapper {
width: 100%;
.filter-button-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.filter-right {
display: flex;
gap: 8px;
flex-shrink: 0;
align-self: flex-end;
}
}