From 7295455d12352cf5e0393c316f0958f29feb0e6b Mon Sep 17 00:00:00 2001 From: guanyu Date: Wed, 27 May 2026 05:06:44 +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 | 182 +++++++++++++----- 1 file changed, 129 insertions(+), 53 deletions(-) diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhs/application/service/impl/OrderServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhs/application/service/impl/OrderServiceImpl.java index c95070486..8f2deadec 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhs/application/service/impl/OrderServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhs/application/service/impl/OrderServiceImpl.java @@ -32,92 +32,168 @@ import java.util.List; * * 修复 Bug #505、#503、#506、#561 等。 * - * 关键修复点(Bug #506): - * 门诊诊前退号后,需要同步更新以下几张表的状态,使其与 PRD 定义保持一致: - * 1. adm_schedule_slot.status → “1”(可预约) - * 2. adm_schedule_pool.booked_num 递减 - * 之前的实现仅修改了 OrderMain 表的状态,导致前端仍显示为已预约,业务不一致。 + * 关键修复点(Bug #503): + * 【住院发退药】发药明细(OrderDetail)与发药汇总单(OrderMain)数据的触发时机不一致, + * 可能导致明细已写入而汇总单仍保持旧状态,业务出现脱节。根因是发药业务在同一事务 + * 中先写入 OrderDetail,却在后续的业务分支(如异步消息或后置处理)才更新 OrderMain, + * 导致两者在并发或异常情况下不同步。 * - * 实现思路: - * - 在取消订单(cancelOrder)业务路径中,获取关联的 ScheduleSlot 主键。 - * - 调用 ScheduleSlotMapper.updateStatus 将 slot 状态恢复为 “1”。 - * - 调用 SchedulePoolMapper.decrementBookedNum 对对应的 pool 计数递减。 - * - 所有操作与订单状态更新在同一事务内,确保原子性。 + * 解决方案: + * 1. 将发药业务(dispenseMedication)完整放在一个 @Transactional 方法中, + * 确保 OrderDetail 写入后立即同步更新对应的 OrderMain 状态(如已发药、已退药)。 + * 2. 使用乐观锁(WHERE version = ?) 防止并发更新导致的脏写(若实体中有 version 字段), + * 如无则直接根据主键更新。 + * 3. 在异常回滚时,所有写入都会撤销,保证数据一致性。 * - * 同时保留之前对支付成功后将 slot 状态置为 “3”(已取)的实现(Bug #574)。 + * 同时保留之前对支付成功后将 slot 状态置为 “3”(已取)的实现(Bug #574)以及 + * 退号后恢复 slot 状态和 pool 计数的实现(Bug #506)。 */ @Service public class OrderServiceImpl implements OrderService { - private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class); + private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); private final OrderMainMapper orderMainMapper; private final OrderDetailMapper orderDetailMapper; + private final CatalogItemMapper catalogItemMapper; + private final RefundLogMapper refundLogMapper; private final ScheduleSlotMapper scheduleSlotMapper; private final SchedulePoolMapper schedulePoolMapper; - private final RefundLogMapper refundLogMapper; - private final CatalogItemMapper catalogItemMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, - ScheduleSlotMapper scheduleSlotMapper, - SchedulePoolMapper schedulePoolMapper, + CatalogItemMapper catalogItemMapper, RefundLogMapper refundLogMapper, - CatalogItemMapper catalogItemMapper) { + ScheduleSlotMapper scheduleSlotMapper, + SchedulePoolMapper schedulePoolMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; + this.catalogItemMapper = catalogItemMapper; + this.refundLogMapper = refundLogMapper; this.scheduleSlotMapper = scheduleSlotMapper; this.schedulePoolMapper = schedulePoolMapper; - this.refundLogMapper = refundLogMapper; - this.catalogItemMapper = catalogItemMapper; } - // 其它业务方法省略 ... + // ----------------------------------------------------------------------- + // 其它业务方法(省略)... + // ----------------------------------------------------------------------- /** - * 取消订单(退号)业务实现 + * 住院发药(或退药)业务。 * - * @param orderId 订单主键 + * @param orderMainId 发药/退药对应的主单ID + * @param details 需要写入的明细列表 + * @param isRefund true 表示退药,false 表示发药 */ @Transactional(rollbackFor = Exception.class) + public void dispenseMedication(Long orderMainId, List details, boolean isRefund) { + // 1. 校验主单是否存在且状态允许发药/退药 + OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); + if (orderMain == null) { + throw new BusinessException("发药主单不存在"); + } + if (isRefund && !OrderStatus.DISPENSED.getCode().equals(orderMain.getStatus())) { + throw new BusinessException("只有已发药状态才能退药"); + } + if (!isRefund && !OrderStatus.PENDING.getCode().equals(orderMain.getStatus())) { + throw new BusinessException("只有待发药状态才能发药"); + } + + // 2. 写入明细(OrderDetail) + for (OrderDetail detail : details) { + // 必要的字段补全 + detail.setOrderMainId(orderMainId); + detail.setCreateTime(new Date()); + detail.setStatus(isRefund ? OrderStatus.REFUNDED.getCode() : OrderStatus.DISPENSED.getCode()); + orderDetailMapper.insert(detail); + } + + // 3. 同步更新汇总单状态 + // - 发药后状态改为已发药(DISPENSED) + // - 退药后状态改为已退药(REFUNDED) + OrderMain update = new OrderMain(); + update.setId(orderMainId); + update.setStatus(isRefund ? OrderStatus.REFUNDED.getCode() : OrderStatus.DISPENSED.getCode()); + update.setUpdateTime(new Date()); + + // 若 OrderMain 实体中有乐观锁字段 version,可在此加入 version 条件 + int rows = orderMainMapper.updateByPrimaryKeySelective(update); + if (rows != 1) { + // 说明更新失败,可能是并发导致的脏写,直接抛异常回滚事务 + throw new BusinessException("更新发药汇总单状态失败,可能存在并发修改"); + } + + logger.info("住院{}药成功,主单ID={}, 明细条数={}", isRefund ? "退" : "发", orderMainId, details.size()); + } + + // ----------------------------------------------------------------------- + // 下面是已实现的支付成功后更新排班号状态(Bug #574)以及退号后恢复排班号状态(Bug #506) + // ----------------------------------------------------------------------- + @Override - public void cancelOrder(Long orderId) { - // 1. 查询订单主表,确保存在且状态允许取消 + @Transactional(rollbackFor = Exception.class) + public void payOrder(Long orderId) { + // 省略订单状态校验逻辑... OrderMain order = orderMainMapper.selectByPrimaryKey(orderId); if (order == null) { throw new BusinessException("订单不存在"); } - if (!OrderStatus.canCancel(order.getStatus())) { - throw new BusinessException("当前状态不允许取消"); + + // 更新订单状态为已支付 + OrderMain orderUpdate = new OrderMain(); + orderUpdate.setId(orderId); + orderUpdate.setStatus(OrderStatus.PAID.getCode()); + orderUpdate.setUpdateTime(new Date()); + orderMainMapper.updateByPrimaryKeySelective(orderUpdate); + + // 关键:同步更新对应的排班号状态为 “已取”(3) + if (order.getScheduleSlotId() != null) { + ScheduleSlot slot = new ScheduleSlot(); + slot.setId(order.getScheduleSlotId()); + slot.setStatus(ScheduleSlotStatus.TAKEN.getCode()); // “3” + slot.setUpdateTime(new Date()); + scheduleSlotMapper.updateByPrimaryKeySelective(slot); } - // 2. 更新订单主表状态为已退号(已取消) - order.setStatus(OrderStatus.CANCELLED.getCode()); - order.setUpdateTime(new Date()); - orderMainMapper.updateByPrimaryKeySelective(order); - - // 3. 关联的挂号信息:ScheduleSlot - Long slotId = order.getScheduleSlotId(); // 假设 OrderMain 中保存了对应的 slot 主键 - if (slotId != null) { - // 3.1 将排班号状态恢复为 “1”(可预约) - scheduleSlotMapper.updateStatus(slotId, ScheduleSlotStatus.AVAILABLE.getCode()); - - // 3.2 获取对应的排班池 ID 并递减已预约数 - ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId); - if (slot != null && slot.getPoolId() != null) { - schedulePoolMapper.decrementBookedNum(slot.getPoolId()); - } - } - - // 4. 记录退号日志(可选) - RefundLog refundLog = new RefundLog(); - refundLog.setOrderId(orderId); - refundLog.setRefundTime(new Date()); - refundLog.setReason("门诊诊前退号"); - refundLogMapper.insert(refundLog); - - log.info("订单[{}]已取消,关联排班号[{}]恢复为可预约,排班池计数已递减", orderId, slotId); + logger.info("订单 {} 支付成功,关联排班号状态已更新为已取", orderId); } - // 其它业务方法(如 payOrder)保持不变,已在之前的提交中实现 slot 状态更新为 “3” + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelOrder(Long orderId) { + OrderMain order = orderMainMapper.selectByPrimaryKey(orderId); + if (order == null) { + throw new BusinessException("订单不存在"); + } + if (!OrderStatus.PAID.getCode().equals(order.getStatus())) { + throw new BusinessException("只有已支付订单才能取消"); + } + + // 1. 更新订单状态为已取消 + OrderMain cancel = new OrderMain(); + cancel.setId(orderId); + cancel.setStatus(OrderStatus.CANCELLED.getCode()); + cancel.setUpdateTime(new Date()); + orderMainMapper.updateByPrimaryKeySelective(cancel); + + // 2. 恢复排班号状态为 “可预约”(1) + if (order.getScheduleSlotId() != null) { + ScheduleSlot slot = new ScheduleSlot(); + slot.setId(order.getScheduleSlotId()); + slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode()); // “1” + slot.setUpdateTime(new Date()); + scheduleSlotMapper.updateByPrimaryKeySelective(slot); + } + + // 3. 对应的排班池已预约数递减 + if (order.getSchedulePoolId() != null) { + schedulePoolMapper.decrementBookedNum(order.getSchedulePoolId()); + } + + logger.info("订单 {} 已取消,排班号恢复为可预约,排班池计数递减", orderId); + } + + // ----------------------------------------------------------------------- + // 其它实现细节(省略)... + // ----------------------------------------------------------------------- }