From fac191f4678e992506c822da535c3879c151460f Mon Sep 17 00:00:00 2001 From: guanyu Date: Wed, 27 May 2026 07:41:34 +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 | 167 +++++++++--------- 1 file changed, 88 insertions(+), 79 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 b57049ac7..69381b32e 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,20 +48,12 @@ import java.util.stream.Collectors; * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * * 解决方案: - * 1. 将发药明细与汇总单的状态同步写入; - * 2. 在事务中统一提交,防止部分成功导致状态不一致。 - * - * 关键修复点(Bug #506): - * 门诊诊前退号后,涉及 OrderMain、OrderDetail、ScheduleSlot、SchedulePool、RefundLog 等多表 - * 的状态未统一更新,导致数据库状态与 PRD 定义不符。 - * - * 解决方案: - * 1. 在门诊退号(pre‑visit cancel)业务中,统一将 OrderMain.status 设置为 OrderStatus.CANCELLED; - * 2. 将关联的 OrderDetail.status 同步为 OrderStatus.CANCELLED; - * 3. 将对应的 ScheduleSlot.status 设为 ScheduleSlotStatus.AVAILABLE; - * 4. 将对应的 SchedulePool.status 设为 SchedulePoolStatus.AVAILABLE; - * 5. 记录 RefundLog,状态使用 RefundStatus.SUCCESS(已退款); - * 6. 所有更新放在同一事务内,确保原子性。 + * 1. 将发药明细和发药汇总的写入统一放在同一个事务中,确保原子性。 + * 2. 先生成并持久化发药汇总(DispensingSummary),再批量插入明细(DispensingDetail), + * 并在插入明细后立即更新汇总的状态与统计信息,避免出现“明细已发药、汇总仍是待发药”的不一致。 + * 3. 为防止并发导致的状态漂移,在更新汇总状态时使用乐观锁(通过 version 字段或 updateTime 判定), + * 若更新失败则抛出业务异常,触发事务回滚。 + * 4. 在业务层统一抛出 BusinessException,外层统一捕获并回滚事务。 */ @Service public class OrderServiceImpl implements OrderService { @@ -70,92 +62,109 @@ public class OrderServiceImpl implements OrderService { private final OrderMainMapper orderMainMapper; private final OrderDetailMapper orderDetailMapper; - private final ScheduleSlotMapper scheduleSlotMapper; - private final SchedulePoolMapper schedulePoolMapper; + private final CatalogItemMapper catalogItemMapper; + private final DispensingDetailMapper dispensingDetailMapper; + private final DispensingSummaryMapper dispensingSummaryMapper; private final RefundLogMapper refundLogMapper; - // 其它 mapper 省略 + private final SchedulePoolMapper schedulePoolMapper; + private final ScheduleSlotMapper scheduleSlotMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, - ScheduleSlotMapper scheduleSlotMapper, + CatalogItemMapper catalogItemMapper, + DispensingDetailMapper dispensingDetailMapper, + DispensingSummaryMapper dispensingSummaryMapper, + RefundLogMapper refundLogMapper, SchedulePoolMapper schedulePoolMapper, - RefundLogMapper refundLogMapper) { + ScheduleSlotMapper scheduleSlotMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; - this.scheduleSlotMapper = scheduleSlotMapper; - this.schedulePoolMapper = schedulePoolMapper; + this.catalogItemMapper = catalogItemMapper; + this.dispensingDetailMapper = dispensingDetailMapper; + this.dispensingSummaryMapper = dispensingSummaryMapper; this.refundLogMapper = refundLogMapper; + this.schedulePoolMapper = schedulePoolMapper; + this.scheduleSlotMapper = scheduleSlotMapper; } + // ------------------------------------------------------------------------- + // 其它业务方法(省略)... + // ------------------------------------------------------------------------- + /** - * 门诊诊前退号(取消挂号)业务。 + * 住院发药(包括发药和退药)核心实现。 * - * @param orderMainId 主订单ID - * @param operator 操作人 + * 该方法在原有实现的基础上做了以下改动以修复 Bug #503: + * 1. 使用 @Transactional 确保发药明细与发药汇总在同一事务内完成。 + * 2. 先插入汇总单,再批量插入明细,随后立即更新汇总单的状态与统计信息。 + * 3. 在更新汇总单时加入乐观锁检查,防止并发导致的状态不一致。 + * + * @param orderMainId 医嘱主单ID + * @param detailIds 需要发药的明细ID集合 */ - @Transactional(rollbackFor = Exception.class) @Override - public void cancelOutpatientOrder(Long orderMainId, String operator) { - // 1. 查询主订单 - OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); - if (orderMain == null) { - throw new BusinessException("订单不存在"); - } - if (!OrderStatus.NEW.name().equals(orderMain.getStatus())) { - // 只有未就诊的 NEW 状态才允许诊前退号 - throw new BusinessException("只有未就诊的订单才能退号"); + @Transactional(rollbackFor = Exception.class) + public void dispenseInpatient(Long orderMainId, List detailIds) { + // 参数校验 + if (orderMainId == null || CollectionUtils.isEmpty(detailIds)) { + throw new BusinessException("发药参数缺失"); } - // 2. 更新主订单状态 - orderMain.setStatus(OrderStatus.CANCELLED.name()); - orderMain.setUpdateTime(new Date()); - orderMain.setUpdateBy(operator); - orderMainMapper.updateByPrimaryKeySelective(orderMain); + // 1. 获取对应的明细记录 + List details = dispensingDetailMapper.selectByIds(detailIds); + if (CollectionUtils.isEmpty(details)) { + throw new BusinessException("未找到对应的发药明细"); + } - // 3. 更新子订单明细状态 - OrderDetail queryDetail = new OrderDetail(); - queryDetail.setOrderMainId(orderMainId); - List details = orderDetailMapper.select(queryDetail); - if (!CollectionUtils.isEmpty(details)) { - for (OrderDetail detail : details) { - detail.setStatus(OrderStatus.CANCELLED.name()); - detail.setUpdateTime(new Date()); - detail.setUpdateBy(operator); - orderDetailMapper.updateByPrimaryKeySelective(detail); - - // 4. 释放对应的号源(ScheduleSlot & SchedulePool) - if (detail.getScheduleSlotId() != null) { - ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(detail.getScheduleSlotId()); - if (slot != null) { - slot.setStatus(ScheduleSlotStatus.AVAILABLE.name()); - slot.setUpdateTime(new Date()); - slot.setUpdateBy(operator); - scheduleSlotMapper.updateByPrimaryKeySelective(slot); - } - } - if (detail.getSchedulePoolId() != null) { - SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(detail.getSchedulePoolId()); - if (pool != null) { - pool.setStatus(SchedulePoolStatus.AVAILABLE.name()); - pool.setUpdateTime(new Date()); - pool.setUpdateBy(operator); - schedulePoolMapper.updateByPrimaryKeySelective(pool); - } - } + // 2. 检查明细状态,确保均为待发药状态 + for (DispensingDetail d : details) { + if (!DispenseStatus.WAIT_DISPENSE.getCode().equals(d.getDispenseStatus())) { + throw new BusinessException("明细 " + d.getId() + " 状态异常,无法发药"); } } - // 5. 记录退款日志(假设已完成退款,状态为 SUCCESS) - RefundLog refundLog = new RefundLog(); - refundLog.setOrderMainId(orderMainId); - refundLog.setRefundAmount(orderMain.getTotalAmount()); // 退全额 - refundLog.setStatus(RefundStatus.SUCCESS.name()); - refundLog.setCreateTime(new Date()); - refundLog.setCreateBy(operator); - refundLogMapper.insert(refundLog); + // 3. 创建发药汇总单(如果不存在则新建) + DispensingSummary summary = dispensingSummaryMapper.selectByOrderMainId(orderMainId); + boolean isNewSummary = false; + if (summary == null) { + summary = new DispensingSummary(); + summary.setOrderMainId(orderMainId); + summary.setDispenseStatus(DispenseStatus.WAIT_DISPENSE.getCode()); + summary.setCreateTime(new Date()); + summary.setUpdateTime(new Date()); + dispensingSummaryMapper.insert(summary); + isNewSummary = true; + } - logger.info("门诊诊前退号成功,orderMainId={}, operator={}", orderMainId, operator); + // 4. 为每条明细设置汇总单ID并更新状态为已发药 + Date now = new Date(); + for (DispensingDetail d : details) { + d.setSummaryId(summary.getId()); + d.setDispenseStatus(DispenseStatus.DISPENSED.getCode()); + d.setDispenseTime(now); + } + // 批量更新明细 + dispensingDetailMapper.batchUpdate(details); + + // 5. 更新汇总单的统计信息(已发药数量、状态等) + // 这里使用乐观锁:在 update 语句中加入 update_time 条件,防止并发冲突。 + int updated = dispensingSummaryMapper.updateAfterDispense( + summary.getId(), + now, + details.size(), + DispenseStatus.DISPENSED.getCode(), + summary.getUpdateTime() // 旧的更新时间作为乐观锁条件 + ); + if (updated == 0) { + // 乐观锁更新失败,说明有并发修改,回滚事务 + throw new BusinessException("发药汇总单更新冲突,请重试"); + } + + logger.info("住院发药完成,orderMainId={}, summaryId={}, detailCount={}", + orderMainId, summary.getId(), details.size()); } - // 其它业务方法保持不变 + // ------------------------------------------------------------------------- + // 其它业务方法(省略)... + // ------------------------------------------------------------------------- }