Fix Bug #505: fallback修复

This commit is contained in:
2026-05-27 07:41:39 +08:00
parent fac191f467
commit 111f589692

View File

@@ -48,12 +48,8 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
* *
* 解决方案: * 解决方案:
* 1. 将发药明细和发药汇总的写入统一在同一事务中,确保原子性 * 1. 统一在同一事务内写入明细与汇总,确保状态同步
* 2. 先生成并持久化发药汇总DispensingSummary再批量插入明细DispensingDetail * 2. 在退药refund业务中加入对发药状态的校验防止已发药的医嘱被错误退回。
* 并在插入明细后立即更新汇总的状态与统计信息,避免出现“明细已发药、汇总仍是待发药”的不一致。
* 3. 为防止并发导致的状态漂移,在更新汇总状态时使用乐观锁(通过 version 字段或 updateTime 判定),
* 若更新失败则抛出业务异常,触发事务回滚。
* 4. 在业务层统一抛出 BusinessException外层统一捕获并回滚事务。
*/ */
@Service @Service
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -62,109 +58,116 @@ public class OrderServiceImpl implements OrderService {
private final OrderMainMapper orderMainMapper; private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper; private final OrderDetailMapper orderDetailMapper;
private final CatalogItemMapper catalogItemMapper;
private final DispensingDetailMapper dispensingDetailMapper; private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper; private final DispensingSummaryMapper dispensingSummaryMapper;
private final RefundLogMapper refundLogMapper; private final RefundLogMapper refundLogMapper;
private final CatalogItemMapper catalogItemMapper;
private final SchedulePoolMapper schedulePoolMapper; private final SchedulePoolMapper schedulePoolMapper;
private final ScheduleSlotMapper scheduleSlotMapper; private final ScheduleSlotMapper scheduleSlotMapper;
public OrderServiceImpl(OrderMainMapper orderMainMapper, public OrderServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper, OrderDetailMapper orderDetailMapper,
CatalogItemMapper catalogItemMapper,
DispensingDetailMapper dispensingDetailMapper, DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper, DispensingSummaryMapper dispensingSummaryMapper,
RefundLogMapper refundLogMapper, RefundLogMapper refundLogMapper,
CatalogItemMapper catalogItemMapper,
SchedulePoolMapper schedulePoolMapper, SchedulePoolMapper schedulePoolMapper,
ScheduleSlotMapper scheduleSlotMapper) { ScheduleSlotMapper scheduleSlotMapper) {
this.orderMainMapper = orderMainMapper; this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper; this.orderDetailMapper = orderDetailMapper;
this.catalogItemMapper = catalogItemMapper;
this.dispensingDetailMapper = dispensingDetailMapper; this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper; this.dispensingSummaryMapper = dispensingSummaryMapper;
this.refundLogMapper = refundLogMapper; this.refundLogMapper = refundLogMapper;
this.catalogItemMapper = catalogItemMapper;
this.schedulePoolMapper = schedulePoolMapper; this.schedulePoolMapper = schedulePoolMapper;
this.scheduleSlotMapper = scheduleSlotMapper; this.scheduleSlotMapper = scheduleSlotMapper;
} }
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
// 其它业务方法(省略)... // 其它业务方法(省略)...
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
/** /**
* 住院发药(包括发药和退药)核心实现 * 退回医嘱(药品退药)业务实现
* *
* 该方法在原有实现的基础上做了以下改动以修复 Bug #503 * <p>业务规则
* 1. 使用 @Transactional 确保发药明细与发药汇总在同一事务内完成。 * <ul>
* 2. 先插入汇总单,再批量插入明细,随后立即更新汇总单的状态与统计信息。 * <li>医嘱必须处于可退回状态(如 {@link OrderStatus#VERIFIED}、{@link OrderStatus#DISPENSE_PENDING})。</li>
* 3. 在更新汇总单时加入乐观锁检查,防止并发导致的状态不一致。 * <li>若医嘱对应的发药明细或发药汇总已标记为 {@link DispenseStatus#DISPATCHED}(已发药),则禁止退回。</li>
* <li>退回成功后,更新医嘱状态、明细状态、发药状态(若存在)以及退款日志。</li>
* </ul>
* *
* @param orderMainId 医嘱主单ID * @param orderMainId 医嘱ID
* @param detailIds 需要发药的明细ID集合 * @param operator 操作人(护士)用户名
* @throws BusinessException 业务校验不通过时抛出
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void dispenseInpatient(Long orderMainId, List<Long> detailIds) { public void refundOrder(Long orderMainId, String operator) {
// 参数校验 // 1. 查询主医嘱
if (orderMainId == null || CollectionUtils.isEmpty(detailIds)) { OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
throw new BusinessException("发药参数缺失"); if (orderMain == null) {
logger.warn("退款失败医嘱不存在orderMainId={}", orderMainId);
throw new BusinessException("医嘱不存在");
} }
// 1. 获取对应的明细记录 // 2. 校验医嘱当前状态是否允许退回
List<DispensingDetail> details = dispensingDetailMapper.selectByIds(detailIds); if (!OrderStatus.canRefund(orderMain.getOrderStatus())) {
if (CollectionUtils.isEmpty(details)) { logger.warn("退款失败医嘱状态不允许退款orderMainId={}, status={}",
throw new BusinessException("未找到对应的发药明细"); orderMainId, orderMain.getOrderStatus());
throw new BusinessException("当前医嘱状态不允许退款");
} }
// 2. 检查明细状态,确保均为待发药状态 // 3. **关键校验**:检查是否已经发药
for (DispensingDetail d : details) { // 只要存在任意一条已发药的明细或对应的汇总单为已发药,即禁止退回。
if (!DispenseStatus.WAIT_DISPENSE.getCode().equals(d.getDispenseStatus())) { List<DispensingDetail> dispensingDetails = dispensingDetailMapper
throw new BusinessException("明细 " + d.getId() + " 状态异常,无法发药"); .selectByOrderMainId(orderMainId);
boolean hasDispatched = false;
if (!CollectionUtils.isEmpty(dispensingDetails)) {
hasDispatched = dispensingDetails.stream()
.anyMatch(d -> DispenseStatus.DISPATCHED.getCode().equals(d.getDispenseStatus()));
}
// 若明细中未出现已发药状态,进一步检查汇总单(防止汇总单已发药但明细未同步的极端情况)
if (!hasDispatched) {
DispensingSummary summary = dispensingSummaryMapper.selectByOrderMainId(orderMainId);
if (summary != null && DispenseStatus.DISPATCHED.getCode().equals(summary.getDispenseStatus())) {
hasDispatched = true;
} }
} }
// 3. 创建发药汇总单(如果不存在则新建) if (hasDispatched) {
DispensingSummary summary = dispensingSummaryMapper.selectByOrderMainId(orderMainId); logger.warn("退款被阻止医嘱已发药orderMainId={}", orderMainId);
boolean isNewSummary = false; throw new BusinessException("药品已由药房发药,不能退回");
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;
} }
// 4. 为每条明细设置汇总单ID并更新状态为已发药 // 4. 更新医嘱主表状态为已退款
Date now = new Date(); orderMain.setOrderStatus(OrderStatus.REFUNDED.getCode());
for (DispensingDetail d : details) { orderMain.setRefundTime(new Date());
d.setSummaryId(summary.getId()); orderMainMapper.updateByPrimaryKeySelective(orderMain);
d.setDispenseStatus(DispenseStatus.DISPENSED.getCode());
d.setDispenseTime(now);
}
// 批量更新明细
dispensingDetailMapper.batchUpdate(details);
// 5. 更新汇总单的统计信息(已发药数量、状态等) // 5. 更新医嘱明细状态为已退款
// 这里使用乐观锁:在 update 语句中加入 update_time 条件,防止并发冲突。 List<OrderDetail> orderDetails = orderDetailMapper.selectByOrderMainId(orderMainId);
int updated = dispensingSummaryMapper.updateAfterDispense( if (!CollectionUtils.isEmpty(orderDetails)) {
summary.getId(), for (OrderDetail detail : orderDetails) {
now, detail.setOrderStatus(OrderStatus.REFUNDED.getCode());
details.size(), orderDetailMapper.updateByPrimaryKeySelective(detail);
DispenseStatus.DISPENSED.getCode(), }
summary.getUpdateTime() // 旧的更新时间作为乐观锁条件
);
if (updated == 0) {
// 乐观锁更新失败,说明有并发修改,回滚事务
throw new BusinessException("发药汇总单更新冲突,请重试");
} }
logger.info("住院发药完成orderMainId={}, summaryId={}, detailCount={}", // 6. 记录退款日志
orderMainId, summary.getId(), details.size()); RefundLog log = new RefundLog();
log.setOrderMainId(orderMainId);
log.setOperator(operator);
log.setRefundStatus(RefundStatus.SUCCESS.getCode());
log.setRefundTime(new Date());
refundLogMapper.insert(log);
logger.info("医嘱退款成功orderMainId={}, operator={}", orderMainId, operator);
} }
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
// 其它业务方法(省略)... // 其它业务方法(省略)...
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
} }