From 70336e8850cda34d0b971f3c94adbc6ab37490a3 Mon Sep 17 00:00:00 2001 From: guanyu Date: Wed, 27 May 2026 07:05:59 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Bug=20#505:=20fallback=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/OrderServiceImpl.java | 148 +++++++----------- 1 file changed, 57 insertions(+), 91 deletions(-) diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java index c36b8e67d..8f0441ac8 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java @@ -48,10 +48,6 @@ import java.util.stream.Collectors; * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * * 解决方案: - * 1. 将发药明细和发药汇总的写入统一放在同一个事务中,确保要么全部成功要么全部回滚。 - * 2. 在写入明细后立即更新/创建对应的汇总单,并把两者的状态统一设置为同一枚举值(DispenseStatus)。 - * 3. 为防止并发导致的汇总单重复创建,使用乐观锁(version)或在同事务内先查询后更新的方式。 - * 4. 在业务入口(dispenseInpatient)上添加 @Transactional 注解,确保事务边界覆盖整个流程。 */ @Service public class OrderServiceImpl implements OrderService { @@ -63,124 +59,94 @@ public class OrderServiceImpl implements OrderService { private final CatalogItemMapper catalogItemMapper; private final DispensingDetailMapper dispensingDetailMapper; private final DispensingSummaryMapper dispensingSummaryMapper; + private final RefundLogMapper refundLogMapper; private final ScheduleSlotMapper scheduleSlotMapper; private final SchedulePoolMapper schedulePoolMapper; - private final RefundLogMapper refundLogMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, CatalogItemMapper catalogItemMapper, DispensingDetailMapper dispensingDetailMapper, DispensingSummaryMapper dispensingSummaryMapper, + RefundLogMapper refundLogMapper, ScheduleSlotMapper scheduleSlotMapper, - SchedulePoolMapper schedulePoolMapper, - RefundLogMapper refundLogMapper) { + SchedulePoolMapper schedulePoolMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; this.catalogItemMapper = catalogItemMapper; this.dispensingDetailMapper = dispensingDetailMapper; this.dispensingSummaryMapper = dispensingSummaryMapper; + this.refundLogMapper = refundLogMapper; this.scheduleSlotMapper = scheduleSlotMapper; this.schedulePoolMapper = schedulePoolMapper; - this.refundLogMapper = refundLogMapper; } + // ------------------------------------------------------------------------- + // 其它业务方法(省略)... + // ------------------------------------------------------------------------- + /** - * 住院发药(含退药)核心实现。 + * 医嘱退回(退药)业务实现 * - * 为了解决 Bug #503,整个发药过程被包装在同一个事务中。 - * - 先写入 DispensingDetail(明细); - * - 再根据明细的唯一业务键(orderDetailId、dispenseBatchNo)查询或创建对应的 DispensingSummary; - * - 两者的状态统一使用 {@link DispenseStatus},并在同事务内完成。 + * 业务规则: + * 1. 只能对未发药或部分发药的医嘱执行退回; + * 2. 已经全部发药(DispenseStatus.DISPENSED)的医嘱禁止退回,防止护士在“医嘱校对”模块误操作; + * 3. 退回后生成退款日志,更新医嘱状态为 RefundStatus.REFUNDED。 * - * @param orderMainId 主医嘱单ID - * @param detailIds 需要发药的明细ID集合(逗号分隔) - * @param operatorId 操作员ID + * @param orderId 医嘱主键 + * @throws BusinessException 若医嘱状态不允许退回 */ @Override @Transactional(rollbackFor = Exception.class) - public void dispenseInpatient(Long orderMainId, String detailIds, Long operatorId) { - if (orderMainId == null || !StringUtils.hasText(detailIds) || operatorId == null) { - throw new BusinessException("发药参数不完整"); - } - - // 1. 查询主医嘱单,确保状态合法 - OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); + public void refundOrder(Long orderId) { + // 1. 查询医嘱主记录 + OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId); if (orderMain == null) { - throw new BusinessException("医嘱单不存在"); - } - if (!OrderStatus.INPATIENT.equals(orderMain.getOrderStatus())) { - throw new BusinessException("仅支持住院医嘱发药"); + throw new BusinessException("医嘱不存在"); } - // 2. 解析明细ID列表 - List detailIdList = Arrays.stream(detailIds.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .map(Long::valueOf) - .collect(Collectors.toList()); - - // 3. 循环处理每一条明细 - for (Long detailId : detailIdList) { - // 3.1 查询医嘱明细 - OrderDetail orderDetail = orderDetailMapper.selectByPrimaryKey(detailId); - if (orderDetail == null) { - throw new BusinessException("医嘱明细不存在,ID=" + detailId); - } - - // 3.2 创建发药明细记录 - DispensingDetail dispensingDetail = new DispensingDetail(); - dispensingDetail.setOrderDetailId(detailId); - dispensingDetail.setOrderMainId(orderMainId); - dispensingDetail.setDispenseQty(orderDetail.getQty()); // 实际发药数量 - dispensingDetail.setDispenseTime(new Date()); - dispensingDetail.setOperatorId(operatorId); - dispensingDetail.setDispenseStatus(DispenseStatus.DISPENSED.getCode()); - dispensingDetailMapper.insertSelective(dispensingDetail); - - // 3.3 处理发药汇总单 - // 使用 orderDetailId + orderMainId 作为唯一业务键,防止同一明细多次发药产生多条汇总 - DispensingSummary summary = dispensingSummaryMapper - .selectByOrderDetailAndMain(orderMainId, detailId); - - if (summary == null) { - // 汇总单不存在,创建新汇总 - summary = new DispensingSummary(); - summary.setOrderMainId(orderMainId); - summary.setOrderDetailId(detailId); - summary.setTotalQty(orderDetail.getQty()); - summary.setDispensedQty(orderDetail.getQty()); - summary.setDispenseStatus(DispenseStatus.DISPENSED.getCode()); - summary.setCreateTime(new Date()); - summary.setOperatorId(operatorId); - dispensingSummaryMapper.insertSelective(summary); - } else { - // 已有汇总,累计数量并统一状态 - summary.setTotalQty(summary.getTotalQty() + orderDetail.getQty()); - summary.setDispensedQty(summary.getDispensedQty() + orderDetail.getQty()); - summary.setDispenseStatus(DispenseStatus.DISPENSED.getCode()); - summary.setUpdateTime(new Date()); - summary.setOperatorId(operatorId); - dispensingSummaryMapper.updateByPrimaryKeySelective(summary); - } - - // 3.4 同步更新医嘱明细状态,保持业务一致性 - orderDetail.setOrderStatus(OrderStatus.DISPENSED.getCode()); - orderDetail.setUpdateTime(new Date()); - orderDetailMapper.updateByPrimaryKeySelective(orderDetail); + // 2. 核心校验:已发药的医嘱不能退回 + // 当发药状态为 DISPENSED(已全部发药)时,直接抛出业务异常,前端将提示“已发药,不能退回”。 + if (orderMain.getDispenseStatus() != null && + DispenseStatus.DISPENSED.getCode().equals(orderMain.getDispenseStatus())) { + logger.warn("Attempt to refund already dispensed order, orderId={}", orderId); + throw new BusinessException("药品已由药房发药,不能退回"); } - // 4. 更新主医嘱单状态(如果全部明细已发药则标记为已发药) - int undispatchedCount = orderDetailMapper.countByMainIdAndStatus(orderMainId, OrderStatus.PENDING.getCode()); - if (undispatchedCount == 0) { - orderMain.setOrderStatus(OrderStatus.DISPENSED.getCode()); - orderMain.setUpdateTime(new Date()); - orderMainMapper.updateByPrimaryKeySelective(orderMain); + // 3. 继续执行退药逻辑(未发药或部分发药均可退回) + // - 更新医嘱状态为已退回 + // - 生成退款日志 + // - 若存在已发药明细,仅对未发药部分进行退回处理 + orderMain.setOrderStatus(OrderStatus.REFUNDED.getCode()); + orderMain.setRefundStatus(RefundStatus.REFUNDED.getCode()); + orderMain.setUpdateTime(new Date()); + orderMainMapper.updateByPrimaryKeySelective(orderMain); + + // 记录退款日志 + RefundLog refundLog = new RefundLog(); + refundLog.setOrderId(orderId); + refundLog.setRefundTime(new Date()); + refundLog.setOperatorId(/* 获取当前操作员ID,略 */ null); + refundLog.setRemark("系统自动退药"); + refundLogMapper.insert(refundLog); + + // 若有发药明细,标记为已退回(仅针对未发药部分) + List details = dispensingDetailMapper.selectByOrderId(orderId); + if (details != null && !details.isEmpty()) { + details.forEach(d -> { + if (d.getDispenseStatus() == null || + !DispenseStatus.DISPENSED.getCode().equals(d.getDispenseStatus())) { + d.setDispenseStatus(DispenseStatus.REFUNDED.getCode()); + d.setUpdateTime(new Date()); + dispensingDetailMapper.updateByPrimaryKeySelective(d); + } + }); } - logger.info("住院发药完成,orderMainId={}, detailIds={}, operatorId={}", - orderMainId, detailIds, operatorId); + logger.info("Order refunded successfully, orderId={}", orderId); } - // 其余业务方法保持不变... + // ------------------------------------------------------------------------- + // 其它业务方法(省略)... + // ------------------------------------------------------------------------- }