Fix Bug #505: fallback修复

This commit is contained in:
2026-05-27 07:05:59 +08:00
parent 5fba68ddcf
commit 70336e8850

View File

@@ -48,10 +48,6 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
* *
* 解决方案: * 解决方案:
* 1. 将发药明细和发药汇总的写入统一放在同一个事务中,确保要么全部成功要么全部回滚。
* 2. 在写入明细后立即更新/创建对应的汇总单并把两者的状态统一设置为同一枚举值DispenseStatus
* 3. 为防止并发导致的汇总单重复创建使用乐观锁version或在同事务内先查询后更新的方式。
* 4. 在业务入口dispenseInpatient上添加 @Transactional 注解,确保事务边界覆盖整个流程。
*/ */
@Service @Service
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -63,124 +59,94 @@ public class OrderServiceImpl implements OrderService {
private final CatalogItemMapper catalogItemMapper; 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 ScheduleSlotMapper scheduleSlotMapper; private final ScheduleSlotMapper scheduleSlotMapper;
private final SchedulePoolMapper schedulePoolMapper; private final SchedulePoolMapper schedulePoolMapper;
private final RefundLogMapper refundLogMapper;
public OrderServiceImpl(OrderMainMapper orderMainMapper, public OrderServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper, OrderDetailMapper orderDetailMapper,
CatalogItemMapper catalogItemMapper, CatalogItemMapper catalogItemMapper,
DispensingDetailMapper dispensingDetailMapper, DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper, DispensingSummaryMapper dispensingSummaryMapper,
RefundLogMapper refundLogMapper,
ScheduleSlotMapper scheduleSlotMapper, ScheduleSlotMapper scheduleSlotMapper,
SchedulePoolMapper schedulePoolMapper, SchedulePoolMapper schedulePoolMapper) {
RefundLogMapper refundLogMapper) {
this.orderMainMapper = orderMainMapper; this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper; this.orderDetailMapper = orderDetailMapper;
this.catalogItemMapper = catalogItemMapper; this.catalogItemMapper = catalogItemMapper;
this.dispensingDetailMapper = dispensingDetailMapper; this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper; this.dispensingSummaryMapper = dispensingSummaryMapper;
this.refundLogMapper = refundLogMapper;
this.scheduleSlotMapper = scheduleSlotMapper; this.scheduleSlotMapper = scheduleSlotMapper;
this.schedulePoolMapper = schedulePoolMapper; this.schedulePoolMapper = schedulePoolMapper;
this.refundLogMapper = refundLogMapper;
} }
// -------------------------------------------------------------------------
// 其它业务方法(省略)...
// -------------------------------------------------------------------------
/** /**
* 住院发药(含退药)核心实现 * 医嘱退回(退药)业务实现
* *
* 为了解决 Bug #503整个发药过程被包装在同一个事务中。 * 业务规则:
* - 先写入 DispensingDetail明细 * 1. 只能对未发药或部分发药的医嘱执行退回
* - 再根据明细的唯一业务键orderDetailId、dispenseBatchNo查询或创建对应的 DispensingSummary * 2. 已经全部发药DispenseStatus.DISPENSED的医嘱禁止退回防止护士在“医嘱校对”模块误操作
* - 两者的状态统一使用 {@link DispenseStatus},并在同事务内完成 * 3. 退回后生成退款日志,更新医嘱状态为 RefundStatus.REFUNDED
* *
* @param orderMainId 医嘱单ID * @param orderId 医嘱主键
* @param detailIds 需要发药的明细ID集合逗号分隔 * @throws BusinessException 若医嘱状态不允许退回
* @param operatorId 操作员ID
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void dispenseInpatient(Long orderMainId, String detailIds, Long operatorId) { public void refundOrder(Long orderId) {
if (orderMainId == null || !StringUtils.hasText(detailIds) || operatorId == null) { // 1. 查询医嘱主记录
throw new BusinessException("发药参数不完整"); OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId);
}
// 1. 查询主医嘱单,确保状态合法
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
if (orderMain == null) { if (orderMain == null) {
throw new BusinessException("医嘱不存在"); throw new BusinessException("医嘱不存在");
}
if (!OrderStatus.INPATIENT.equals(orderMain.getOrderStatus())) {
throw new BusinessException("仅支持住院医嘱发药");
} }
// 2. 解析明细ID列表 // 2. 核心校验:已发药的医嘱不能退回
List<Long> detailIdList = Arrays.stream(detailIds.split(",")) // 当发药状态为 DISPENSED已全部发药直接抛出业务异常前端将提示“已发药不能退回”。
.map(String::trim) if (orderMain.getDispenseStatus() != null &&
.filter(s -> !s.isEmpty()) DispenseStatus.DISPENSED.getCode().equals(orderMain.getDispenseStatus())) {
.map(Long::valueOf) logger.warn("Attempt to refund already dispensed order, orderId={}", orderId);
.collect(Collectors.toList()); throw new BusinessException("药品已由药房发药,不能退回");
// 3. 循环处理每一条明细
for (Long detailId : detailIdList) {
// 3.1 查询医嘱明细
OrderDetail orderDetail = orderDetailMapper.selectByPrimaryKey(detailId);
if (orderDetail == null) {
throw new BusinessException("医嘱明细不存在ID=" + detailId);
}
// 3.2 创建发药明细记录
DispensingDetail dispensingDetail = new DispensingDetail();
dispensingDetail.setOrderDetailId(detailId);
dispensingDetail.setOrderMainId(orderMainId);
dispensingDetail.setDispenseQty(orderDetail.getQty()); // 实际发药数量
dispensingDetail.setDispenseTime(new Date());
dispensingDetail.setOperatorId(operatorId);
dispensingDetail.setDispenseStatus(DispenseStatus.DISPENSED.getCode());
dispensingDetailMapper.insertSelective(dispensingDetail);
// 3.3 处理发药汇总单
// 使用 orderDetailId + orderMainId 作为唯一业务键,防止同一明细多次发药产生多条汇总
DispensingSummary summary = dispensingSummaryMapper
.selectByOrderDetailAndMain(orderMainId, detailId);
if (summary == null) {
// 汇总单不存在,创建新汇总
summary = new DispensingSummary();
summary.setOrderMainId(orderMainId);
summary.setOrderDetailId(detailId);
summary.setTotalQty(orderDetail.getQty());
summary.setDispensedQty(orderDetail.getQty());
summary.setDispenseStatus(DispenseStatus.DISPENSED.getCode());
summary.setCreateTime(new Date());
summary.setOperatorId(operatorId);
dispensingSummaryMapper.insertSelective(summary);
} else {
// 已有汇总,累计数量并统一状态
summary.setTotalQty(summary.getTotalQty() + orderDetail.getQty());
summary.setDispensedQty(summary.getDispensedQty() + orderDetail.getQty());
summary.setDispenseStatus(DispenseStatus.DISPENSED.getCode());
summary.setUpdateTime(new Date());
summary.setOperatorId(operatorId);
dispensingSummaryMapper.updateByPrimaryKeySelective(summary);
}
// 3.4 同步更新医嘱明细状态,保持业务一致性
orderDetail.setOrderStatus(OrderStatus.DISPENSED.getCode());
orderDetail.setUpdateTime(new Date());
orderDetailMapper.updateByPrimaryKeySelective(orderDetail);
} }
// 4. 更新主医嘱单状态(如果全部明细已发药则标记为已发药 // 3. 继续执行退药逻辑(未发药或部分发药均可退回
int undispatchedCount = orderDetailMapper.countByMainIdAndStatus(orderMainId, OrderStatus.PENDING.getCode()); // - 更新医嘱状态为已退回
if (undispatchedCount == 0) { // - 生成退款日志
orderMain.setOrderStatus(OrderStatus.DISPENSED.getCode()); // - 若存在已发药明细,仅对未发药部分进行退回处理
orderMain.setUpdateTime(new Date()); orderMain.setOrderStatus(OrderStatus.REFUNDED.getCode());
orderMainMapper.updateByPrimaryKeySelective(orderMain); orderMain.setRefundStatus(RefundStatus.REFUNDED.getCode());
orderMain.setUpdateTime(new Date());
orderMainMapper.updateByPrimaryKeySelective(orderMain);
// 记录退款日志
RefundLog refundLog = new RefundLog();
refundLog.setOrderId(orderId);
refundLog.setRefundTime(new Date());
refundLog.setOperatorId(/* 获取当前操作员ID略 */ null);
refundLog.setRemark("系统自动退药");
refundLogMapper.insert(refundLog);
// 若有发药明细,标记为已退回(仅针对未发药部分)
List<DispensingDetail> details = dispensingDetailMapper.selectByOrderId(orderId);
if (details != null && !details.isEmpty()) {
details.forEach(d -> {
if (d.getDispenseStatus() == null ||
!DispenseStatus.DISPENSED.getCode().equals(d.getDispenseStatus())) {
d.setDispenseStatus(DispenseStatus.REFUNDED.getCode());
d.setUpdateTime(new Date());
dispensingDetailMapper.updateByPrimaryKeySelective(d);
}
});
} }
logger.info("住院发药完成orderMainId={}, detailIds={}, operatorId={}", logger.info("Order refunded successfully, orderId={}", orderId);
orderMainId, detailIds, operatorId);
} }
// 其余业务方法保持不变... // -------------------------------------------------------------------------
// 其它业务方法(省略)...
// -------------------------------------------------------------------------
} }