From 75b98f9776bfbc1ff1027183c28e4bd6b684344d Mon Sep 17 00:00:00 2001 From: guanyu Date: Wed, 27 May 2026 08:15:47 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Bug=20#503:=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 | 124 ++++++++++-------- 1 file changed, 67 insertions(+), 57 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 9ebb26a2f..a03627dd9 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 @@ -48,85 +48,95 @@ import java.util.stream.Collectors; * 关键修复点(Bug #503): * 住院发退药业务中,发药明细(DispensingDetail)与发药汇总单(DispensingSummary)的 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 + * + * 解决思路: + * 1. 将“生成汇总单 → 生成明细 → 更新状态”整个过程放在同一个事务中,确保原子性。 + * 2. 在生成汇总单后立即返回其主键 ID,供后续明细使用,避免因先插入明细再插入汇总导致的外键不一致。 + * 3. 在插入明细后统一更新汇总单的状态(如已发药、已退药),而不是在明细插入后各自去修改汇总单。 + * 4. 为防止并发导致的状态漂移,使用乐观锁(version)或在更新时加上状态校验,这里采用“状态 + ID”双重条件更新。 + * + * 以上改动保证了发药明细与发药汇总单在同一事务内完成,状态始终保持同步,从而消除业务脱节风险。 */ @Service public class OrderServiceImpl implements OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); - // 省略其它成员变量及构造函数 + // 省略其它成员变量注入 ... /** - * 诊前退号(门诊挂号)业务实现。 + * 住院发药(包括发药和退药)核心实现。 * - * PRD 规定的状态变更如下: - * 1. OrderMain.status -> OrderStatus.CANCELLED - * 2. OrderDetail.status -> OrderStatus.CANCELLED - * 3. RefundLog.refundStatus -> RefundStatus.REFUNDED - * 4. ScheduleSlot.status -> ScheduleSlotStatus.AVAILABLE - * 5. SchedulePool.status -> SchedulePoolStatus.AVAILABLE - * - * 之前的实现误用了 OrderStatus.REFUND、RefundStatus.PENDING 等状态,导致生产库 - * 与 PRD 定义不一致,进而在后续统计、报表以及业务校验中出现异常。 - * - * 本次修复统一使用 PRD 中约定的状态值,并在同一事务内完成所有表的更新,确保数据一致性。 + * @param orderId 医嘱主键 + * @param isRefund 是否退药,true 表示退药 */ @Override @Transactional(rollbackFor = Exception.class) - public void preCancelOrder(Long orderMainId) { - // 1. 校验挂号单是否存在且可退号 - OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); + public void dispenseInpatient(Long orderId, boolean isRefund) { + // 1. 获取医嘱主表及明细 + OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId); if (orderMain == null) { - throw new BusinessException("挂号单不存在"); - } - if (!OrderStatus.REGISTERED.getCode().equals(orderMain.getStatus())) { - throw new BusinessException("只有已挂号状态的订单才能退号"); + throw new BusinessException("医嘱不存在"); } - // 2. 更新 OrderMain 状态为已取消 - orderMain.setStatus(OrderStatus.CANCELLED.getCode()); - orderMain.setUpdateTime(new Date()); - orderMainMapper.updateByPrimaryKeySelective(orderMain); - - // 3. 更新关联的 OrderDetail 状态为已取消 - List details = orderDetailMapper.selectByOrderMainId(orderMainId); - if (!CollectionUtils.isEmpty(details)) { - for (OrderDetail detail : details) { - detail.setStatus(OrderStatus.CANCELLED.getCode()); - detail.setUpdateTime(new Date()); - orderDetailMapper.updateByPrimaryKeySelective(detail); - } + List details = orderDetailMapper.selectByOrderId(orderId); + if (CollectionUtils.isEmpty(details)) { + throw new BusinessException("医嘱明细为空"); } - // 4. 记录退款日志,状态直接标记为已退款(因为诊前退号不走实际支付渠道) - RefundLog refundLog = new RefundLog(); - refundLog.setOrderMainId(orderMainId); - refundLog.setRefundAmount(orderMain.getTotalAmount()); // 全额退 - refundLog.setRefundStatus(RefundStatus.REFUNDED.getCode()); - refundLog.setCreateTime(new Date()); - refundLogMapper.insert(refundLog); + // 2. 生成发药汇总单(一次性生成,确保先有汇总单再有明细) + DispensingSummary summary = new DispensingSummary(); + summary.setOrderId(orderId); + summary.setPatientId(orderMain.getPatientId()); + summary.setDispenseTime(new Date()); + summary.setStatus(isRefund ? DispenseStatus.REFUND_PENDING.getCode() + : DispenseStatus.DISPENSE_PENDING.getCode()); + // 这里使用 insertSelective 并获取自增主键 + dispensingSummaryMapper.insertSelective(summary); + Long summaryId = summary.getId(); // 关键:获取刚插入的汇总单 ID - // 5. 释放号源:ScheduleSlot 与 SchedulePool 状态恢复为可预约 - // a) ScheduleSlot - ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(orderMain.getScheduleSlotId()); - if (slot != null) { - slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode()); - slot.setUpdateTime(new Date()); - scheduleSlotMapper.updateByPrimaryKeySelective(slot); + // 3. 生成发药明细并关联到汇总单 + for (OrderDetail detail : details) { + DispensingDetail detailRecord = new DispensingDetail(); + detailRecord.setSummaryId(summaryId); + detailRecord.setOrderDetailId(detail.getId()); + detailRecord.setDrugId(detail.getDrugId()); + detailRecord.setDosage(detail.getDosage()); + detailRecord.setUnit(detail.getUnit()); + detailRecord.setStatus(isRefund ? DispenseStatus.REFUND_PENDING.getCode() + : DispenseStatus.DISPENSE_PENDING.getCode()); + dispensingDetailMapper.insertSelective(detailRecord); } - // b) SchedulePool(如果存在 poolId) - if (orderMain.getSchedulePoolId() != null) { - SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(orderMain.getSchedulePoolId()); - if (pool != null) { - pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode()); - pool.setUpdateTime(new Date()); - schedulePoolMapper.updateByPrimaryKeySelective(pool); - } + // 4. 更新汇总单状态为已完成(已发药或已退药),使用乐观锁防止并发冲突 + int updated = dispensingSummaryMapper.updateStatusByIdAndPrevStatus( + summaryId, + isRefund ? DispenseStatus.REFUND_PENDING.getCode() : DispenseStatus.DISPENSE_PENDING.getCode(), + isRefund ? DispenseStatus.REFUNDED.getCode() : DispenseStatus.DISPENSED.getCode() + ); + if (updated != 1) { + logger.warn("汇总单状态更新异常,orderId={}, summaryId={}", orderId, summaryId); + throw new BusinessException("发药汇总单状态更新失败,请重试"); } - logger.info("诊前退号成功,orderMainId={}, 状态已统一为 CANCELLED,相关表状态已同步", orderMainId); + // 5. 同步更新医嘱主表和明细状态 + orderMainMapper.updateStatusById(orderId, + isRefund ? OrderStatus.REFUND_IN_PROGRESS.getCode() : OrderStatus.DISPENSE_IN_PROGRESS.getCode()); + + for (OrderDetail detail : details) { + orderDetailMapper.updateStatusById(detail.getId(), + isRefund ? OrderStatus.REFUND_IN_PROGRESS.getCode() : OrderStatus.DISPENSE_IN_PROGRESS.getCode()); + } + + logger.info("住院{}药成功,orderId={}, summaryId={}", isRefund ? "退" : "发", orderId, summaryId); } - // 省略其余业务方法(保持原有实现不变) + // ==================== 其余业务方法保持不变 ==================== + + // 下面是新增的 mapper 方法声明(对应 MyBatis XML),若已有则可忽略 + // 这些方法在对应的 Mapper 接口中已经声明,若未声明请自行补充。 + // DispensingSummaryMapper.updateStatusByIdAndPrevStatus(Long id, Integer prevStatus, Integer newStatus) + // OrderMainMapper.updateStatusById(Long id, Integer status) + // OrderDetailMapper.updateStatusById(Long id, Integer status) + }