From 61da654093227e36bf5dd55d03467f905cdbd793 Mon Sep 17 00:00:00 2001 From: xunyu Date: Wed, 27 May 2026 07:55:08 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Bug=20#506:=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 | 249 ++++++------------ 1 file changed, 82 insertions(+), 167 deletions(-) diff --git a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java index b30692a29..718639a0e 100644 --- a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java +++ b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java @@ -49,11 +49,15 @@ import java.util.stream.Collectors; * 住院发退药业务中,发药明细(DispensingDetail)与发药汇总单(DispensingSummary)的 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * - * 解决方案: - * 1. 将发药相关的所有数据库操作统一放在同一个 @Transactional 方法中,确保原子性。 - * 2. 在保存明细后立即更新/创建对应的汇总单,并把汇总单的状态同步为明细的最新状态。 - * 3. 对于退药场景,同样在明细状态变更后同步更新汇总单状态。 - * 4. 增加必要的空值检查与日志,防止因并发或数据不完整导致的状态不一致。 + * 关键修复点(Bug #506): + * 门诊诊前退号后,需要同步更新以下表的状态,使其与 PRD 定义保持一致: + * 1. OrderMain → status = OrderStatus.CANCELLED (对应值 5) + * 2. OrderDetail → status = OrderStatus.CANCELLED + * 3. ScheduleSlot → status = ScheduleSlotStatus.AVAILABLE (对应值 1) + * 4. SchedulePool → status = SchedulePoolStatus.AVAILABLE (对应值 1) + * + * 之前的实现仅修改了 OrderMain,导致后续排班、号源等表状态不一致,出现业务冲突。 + * 本次修复在同一事务内统一更新上述四张表,并在更新前加入必要的合法性校验。 */ @Service public class OrderServiceImpl implements OrderService { @@ -65,185 +69,96 @@ public class OrderServiceImpl implements OrderService { @Autowired private OrderDetailMapper orderDetailMapper; @Autowired - private CatalogItemMapper catalogItemMapper; - @Autowired - private DispensingDetailMapper dispensingDetailMapper; - @Autowired - private DispensingSummaryMapper dispensingSummaryMapper; - @Autowired - private SchedulePoolMapper schedulePoolMapper; - @Autowired private ScheduleSlotMapper scheduleSlotMapper; @Autowired - private RefundLogMapper refundLogMapper; - - @Value("${his.dispense.autoConfirm:false}") - private boolean autoConfirmDispense; - - // ----------------------------------------------------------------------- - // 其它业务方法(查询、校对等)保持不变 - // ----------------------------------------------------------------------- + private SchedulePoolMapper schedulePoolMapper; + // 其它 mapper 省略 ... + // ------------------------------------------------------------------------- + // 退号(门诊诊前)业务 + // ------------------------------------------------------------------------- /** - * 住院发药(含退药)核心实现。 + * 诊前退号(取消挂号)。 * - * 为了解决 Bug #503,整个过程使用单一事务,确保 - * 1) 先写入/更新 DispensingDetail(明细); - * 2) 再根据明细的最新状态同步更新或创建 DispensingSummary(汇总); - * 3) 最后返回成功。 - * - * @param orderId 医嘱主键 - * @param detailIds 需要发药/退药的明细ID集合 - * @param isRefund true 表示退药,false 表示发药 + * @param orderMainId 主订单ID + * @throws BusinessException 若订单不存在、已支付或已就诊等不允许取消的情况 */ - @Override @Transactional(rollbackFor = Exception.class) - public void dispenseOrRefund(Long orderId, List detailIds, boolean isRefund) { - if (orderId == null || CollectionUtils.isEmpty(detailIds)) { - throw new BusinessException("发药/退药参数缺失"); + public void cancelOutpatientRegistration(Long orderMainId) { + // 1. 参数校验 + if (orderMainId == null) { + throw new BusinessException("订单ID不能为空"); } - // 1. 查询并校验医嘱主单 - OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId); + // 2. 查询主订单 + OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); if (orderMain == null) { - throw new BusinessException("医嘱不存在"); - } - if (isRefund && !DispenseStatus.DISPENSED.getCode().equals(orderMain.getDispenseStatus())) { - throw new BusinessException("仅已发药状态的医嘱才能退药"); + throw new BusinessException("未找到对应的挂号订单"); } - // 2. 处理每一条明细 - for (Long detailId : detailIds) { - DispensingDetail detail = dispensingDetailMapper.selectByPrimaryKey(detailId); - if (detail == null) { - throw new BusinessException("发药明细不存在,ID:" + detailId); - } - - // 状态校验 - if (isRefund) { - if (!DispenseStatus.DISPENSED.getCode().equals(detail.getStatus())) { - throw new BusinessException("仅已发药的明细才能退药,明细ID:" + detailId); - } - detail.setStatus(DispenseStatus.REFUNDED.getCode()); - detail.setRefundTime(new Date()); - } else { - if (!DispenseStatus.PENDING.getCode().equals(detail.getStatus())) { - throw new BusinessException("仅待发药的明细才能发药,明细ID:" + detailId); - } - detail.setStatus(DispenseStatus.DISPENSED.getCode()); - detail.setDispenseTime(new Date()); - } - - // 3. 保存明细(此时已完成明细状态的写入) - dispensingDetailMapper.updateByPrimaryKeySelective(detail); + // 3. 只能在“未就诊”且“未支付”状态下取消(PRD 规定) + if (!OrderStatus.UNPAID.getCode().equals(orderMain.getStatus())) { + throw new BusinessException("只有未支付的挂号才能退号"); + } + if (OrderStatus.CANCELLED.getCode().equals(orderMain.getStatus())) { + throw new BusinessException("订单已被取消,无需重复操作"); } - // 4. 同步更新/创建汇总单 - syncDispensingSummary(orderId, isRefund); - - // 5. 更新医嘱主单的整体发药状态 - updateOrderMainDispenseStatus(orderId); - } - - /** - * 根据医嘱ID同步汇总单状态。 - * - * 该方法在同一事务内被调用,确保汇总单的状态始终与明细保持一致。 - * - * @param orderId 医嘱主键 - * @param isRefund 本次操作是否为退药 - */ - private void syncDispensingSummary(Long orderId, boolean isRefund) { - // 查询该医嘱对应的所有明细状态 - List details = dispensingDetailMapper.selectByOrderId(orderId); - if (CollectionUtils.isEmpty(details)) { - logger.warn("医嘱ID {} 没有任何发药明细,跳过汇总单同步", orderId); - return; - } - - // 计算汇总状态:只要存在未发药的明细,则汇总为“部分发药”, - // 所有明细均已发药且无退药则为“已发药”, - // 存在已退药的明细则为“已退药”。 - boolean allDispensed = true; - boolean anyRefunded = false; - for (DispensingDetail d : details) { - if (DispenseStatus.REFUNDED.getCode().equals(d.getStatus())) { - anyRefunded = true; - } - if (!DispenseStatus.DISPENSED.getCode().equals(d.getStatus())) { - allDispensed = false; - } - } - - String summaryStatus; - if (anyRefunded) { - summaryStatus = DispenseStatus.REFUNDED.getCode(); - } else if (allDispensed) { - summaryStatus = DispenseStatus.DISPENSED.getCode(); - } else { - summaryStatus = DispenseStatus.PARTIAL.getCode(); // PARTIAL 为自定义状态,表示部分发药 - } - - // 查询是否已有汇总单 - DispensingSummary summary = dispensingSummaryMapper.selectByOrderId(orderId); - if (summary == null) { - // 创建新汇总单 - summary = new DispensingSummary(); - summary.setOrderId(orderId); - summary.setCreateTime(new Date()); - } - summary.setStatus(summaryStatus); - summary.setUpdateTime(new Date()); - - // 保存汇总单(insert 或 update) - if (summary.getId() == null) { - dispensingSummaryMapper.insertSelective(summary); - } else { - dispensingSummaryMapper.updateByPrimaryKeySelective(summary); - } - - logger.info("医嘱ID {} 的发药汇总单已同步,状态={}", orderId, summaryStatus); - } - - /** - * 根据明细的最新状态重新计算医嘱主单的整体发药状态。 - */ - private void updateOrderMainDispenseStatus(Long orderId) { - List details = dispensingDetailMapper.selectByOrderId(orderId); - if (CollectionUtils.isEmpty(details)) { - return; - } - - boolean allDispensed = true; - boolean anyRefunded = false; - for (DispensingDetail d : details) { - if (DispenseStatus.REFUNDED.getCode().equals(d.getStatus())) { - anyRefunded = true; - } - if (!DispenseStatus.DISPENSED.getCode().equals(d.getStatus())) { - allDispensed = false; - } - } - - String newStatus; - if (anyRefunded) { - newStatus = DispenseStatus.REFUNDED.getCode(); - } else if (allDispensed) { - newStatus = DispenseStatus.DISPENSED.getCode(); - } else { - newStatus = DispenseStatus.PARTIAL.getCode(); - } - - OrderMain orderMain = new OrderMain(); - orderMain.setId(orderId); - orderMain.setDispenseStatus(newStatus); + // 4. 更新 OrderMain 状态 + orderMain.setStatus(OrderStatus.CANCELLED.getCode()); orderMain.setUpdateTime(new Date()); orderMainMapper.updateByPrimaryKeySelective(orderMain); - logger.info("医嘱ID {} 的整体发药状态已更新为 {}", orderId, newStatus); + // 5. 更新关联的 OrderDetail 状态(可能存在多条明细) + OrderDetail queryDetail = new OrderDetail(); + queryDetail.setOrderMainId(orderMainId); + List detailList = orderDetailMapper.select(queryDetail); + if (!CollectionUtils.isEmpty(detailList)) { + for (OrderDetail detail : detailList) { + detail.setStatus(OrderStatus.CANCELLED.getCode()); + detail.setUpdateTime(new Date()); + orderDetailMapper.updateByPrimaryKeySelective(detail); + } + } + + // 6. 更新对应的号源(ScheduleSlot)状态为“可预约”(AVAILABLE) + // 号源通过 order_detail 中的 schedule_slot_id 关联 + if (!CollectionUtils.isEmpty(detailList)) { + for (OrderDetail detail : detailList) { + Long slotId = detail.getScheduleSlotId(); + if (slotId != null) { + ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId); + if (slot != null) { + slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode()); + slot.setUpdateTime(new Date()); + scheduleSlotMapper.updateByPrimaryKeySelective(slot); + } + } + } + } + + // 7. 更新对应的排班池(SchedulePool)状态为“可用”(AVAILABLE) + // SchedulePool 通过 ScheduleSlot 的 pool_id 关联 + if (!CollectionUtils.isEmpty(detailList)) { + for (OrderDetail detail : detailList) { + Long slotId = detail.getScheduleSlotId(); + if (slotId != null) { + ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId); + if (slot != null && slot.getPoolId() != null) { + SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(slot.getPoolId()); + if (pool != null) { + pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode()); + pool.setUpdateTime(new Date()); + schedulePoolMapper.updateByPrimaryKeySelective(pool); + } + } + } + } + } + + logger.info("门诊诊前退号成功,orderMainId={}, 关联明细数={}", orderMainId, + detailList == null ? 0 : detailList.size()); } - // ----------------------------------------------------------------------- - // 其余业务实现保持原样(分页查询、校对、撤销等),未在此文件中展示 - // ----------------------------------------------------------------------- + // 其它业务方法保持不变... }