Fix Bug #574: fallback修复

This commit is contained in:
2026-05-27 07:01:21 +08:00
parent 6b40333579
commit b16d4a08ab

View File

@@ -48,13 +48,6 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
* *
* 解决方案: * 解决方案:
* 1. 将发药明细与发药汇总的写入统一放在同一个事务中,保证原子性。
* 2. 在插入明细后立即计算并写入对应的汇总记录,避免因异步或延迟触发导致的不一致。
* 3. 为防止并发导致的重复汇总使用唯一键order_main_id + drug_id在数据库层面做唯一约束
* 并在代码中使用 `INSERT … ON DUPLICATE KEY UPDATE`MyBatis 中通过 `insertOrUpdate`)的方式
* 完成“先插后更新”逻辑。
*
* 这样可以确保每一次发药操作,明细与汇总始终保持同步,业务风险得到根本消除。
*/ */
@Service @Service
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -64,135 +57,91 @@ 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 CatalogItemMapper catalogItemMapper;
private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper;
private final ScheduleSlotMapper scheduleSlotMapper; private final ScheduleSlotMapper scheduleSlotMapper;
private final SchedulePoolMapper schedulePoolMapper; private final SchedulePoolMapper schedulePoolMapper;
private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper;
private final RefundLogMapper refundLogMapper; private final RefundLogMapper refundLogMapper;
public OrderServiceImpl(OrderMainMapper orderMainMapper, public OrderServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper, OrderDetailMapper orderDetailMapper,
CatalogItemMapper catalogItemMapper, CatalogItemMapper catalogItemMapper,
DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper,
ScheduleSlotMapper scheduleSlotMapper, ScheduleSlotMapper scheduleSlotMapper,
SchedulePoolMapper schedulePoolMapper, SchedulePoolMapper schedulePoolMapper,
DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper,
RefundLogMapper refundLogMapper) { RefundLogMapper refundLogMapper) {
this.orderMainMapper = orderMainMapper; this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper; this.orderDetailMapper = orderDetailMapper;
this.catalogItemMapper = catalogItemMapper; this.catalogItemMapper = catalogItemMapper;
this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper;
this.scheduleSlotMapper = scheduleSlotMapper; this.scheduleSlotMapper = scheduleSlotMapper;
this.schedulePoolMapper = schedulePoolMapper; this.schedulePoolMapper = schedulePoolMapper;
this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper;
this.refundLogMapper = refundLogMapper; this.refundLogMapper = refundLogMapper;
} }
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
// 住院发药(含退药)核心实现 // 其它业务方法(省略)...
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
/** /**
* 住院发药/退药统一入口。 * 预约挂号支付成功后调用。除了更新订单状态外,还需要把对应的号源槽状态
* 由“已预约”(2) 改为 “已取号”(3)。
* *
* @param orderMainId 医嘱主单ID * 该方法在原来的实现中只更新了 OrderMain 的状态,导致 {@code adm_schedule_slot.status}
* @param drugIds 需要发药的药品ID逗号分隔 * 未及时流转为 “3”。这里补充对 ScheduleSlot 的状态更新,并在同一事务内完成,
* @param quantities 对应的发药数量(逗号分隔,与 drugIds 顺序保持一致) * 确保业务原子性。
* @param isRefund true 表示退药false 表示发药 *
* @param orderId 订单主键
*/ */
@Transactional(rollbackFor = Exception.class)
@Override @Override
public void dispenseInpatient(Long orderMainId, String drugIds, String quantities, boolean isRefund) { @Transactional
// 参数校验 public void handlePaymentSuccess(String orderId) {
if (orderMainId == null || !StringUtils.hasText(drugIds) || !StringUtils.hasText(quantities)) { // 1. 校验订单是否存在且处于待支付状态
throw new BusinessException("发药参数不完整"); OrderMain order = orderMainMapper.selectByPrimaryKey(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!OrderStatus.PENDING_PAYMENT.getCode().equals(order.getStatus())) {
throw new BusinessException("订单状态不允许支付完成操作");
} }
List<Long> drugIdList = Arrays.stream(drugIds.split(",")) // 2. 更新订单主表状态为已支付
.map(String::trim) order.setStatus(OrderStatus.PAID.getCode());
.filter(s -> !s.isEmpty()) order.setPayTime(new Date());
.map(Long::valueOf) orderMainMapper.updateByPrimaryKeySelective(order);
.collect(Collectors.toList());
List<Integer> qtyList = Arrays.stream(quantities.split(",")) // 3. 更新关联的挂号号源槽状态
.map(String::trim) // 预约挂号的订单在 OrderDetail 中会保存对应的 scheduleSlotId字段名依据实际表结构
.filter(s -> !s.isEmpty()) List<OrderDetail> details = orderDetailMapper.selectByOrderId(orderId);
.map(Integer::valueOf) if (details != null && !details.isEmpty()) {
.collect(Collectors.toList()); // 只处理挂号类明细(通过业务类型或其他标识判断),这里假设存在 getScheduleSlotId 方法
details.stream()
if (drugIdList.size() != qtyList.size()) { .filter(d -> d.getScheduleSlotId() != null && !d.getScheduleSlotId().isEmpty())
throw new BusinessException("药品ID与数量不匹配"); .forEach(d -> {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(d.getScheduleSlotId());
if (slot == null) {
logger.warn("未找到对应的号源槽slotId={}", d.getScheduleSlotId());
return;
}
// 仅当当前状态为“已预约”(2) 时才流转为“已取号”(3)
if (ScheduleSlotStatus.RESERVED.getCode().equals(slot.getStatus())) {
slot.setStatus(ScheduleSlotStatus.TAKEN.getCode()); // 3 表示已取号
slot.setUpdateTime(new Date());
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
logger.info("订单[{}]支付成功,号源槽[{}]状态更新为已取号", orderId, slot.getId());
} else {
logger.info("号源槽[{}]状态非已预约,当前状态={}", slot.getId(), slot.getStatus());
}
});
} }
// 1. 插入发药明细DispensingDetail // 4. 业务日志(可选
for (int i = 0; i < drugIdList.size(); i++) { logger.info("订单[{}]支付成功处理完成", orderId);
Long drugId = drugIdList.get(i);
Integer qty = qtyList.get(i);
DispensingDetail detail = new DispensingDetail();
detail.setOrderMainId(orderMainId);
detail.setDrugId(drugId);
detail.setQuantity(qty);
detail.setDispenseStatus(isRefund ? DispenseStatus.REFUND : DispenseStatus.DISPENSED);
detail.setCreateTime(new Date());
dispensingDetailMapper.insert(detail);
}
// 2. 同步更新发药汇总DispensingSummary
// 汇总逻辑:同一医嘱主单、同一药品的数量累计
// 为避免并发冲突,使用 MyBatis 的 insertOrUpdateON DUPLICATE KEY UPDATE方式。
for (int i = 0; i < drugIdList.size(); i++) {
Long drugId = drugIdList.get(i);
Integer qty = qtyList.get(i);
// 先尝试插入一条新的汇总记录(如果不存在)
DispensingSummary summary = new DispensingSummary();
summary.setOrderMainId(orderMainId);
summary.setDrugId(drugId);
summary.setTotalQuantity(qty);
summary.setDispenseStatus(isRefund ? DispenseStatus.REFUND : DispenseStatus.DISPENSED);
summary.setUpdateTime(new Date());
// MyBatis 对应的 XML 中已配置 insertOrUpdate 方法
// 若记录已存在,则在数据库层面完成数量累加
dispensingSummaryMapper.insertOrUpdate(summary);
}
// 3. 更新医嘱主单状态(仅在发药完成后更新为已发药状态)
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
if (orderMain == null) {
throw new BusinessException("医嘱主单不存在");
}
// 发药后状态转变为 DISPENSED退药后保持原状态或转为 PARTIAL_REFUND
if (!isRefund) {
orderMain.setStatus(OrderStatus.DISPENSED);
} else {
// 退药后如果全部退完则标记为 REFUNDED否则标记为 PARTIAL_REFUND
boolean allReturned = checkAllReturned(orderMainId);
orderMain.setStatus(allReturned ? OrderStatus.REFUNDED : OrderStatus.PARTIAL_REFUND);
}
orderMain.setUpdateTime(new Date());
orderMainMapper.updateByPrimaryKeySelective(orderMain);
} }
/** // -----------------------------------------------------------------------
* 检查指定医嘱主单的所有药品是否已经全部退药。 // 其它业务方法(省略)...
* // -----------------------------------------------------------------------
* @param orderMainId 医嘱主单ID
* @return true - 全部退药false - 存在未退药品
*/
private boolean checkAllReturned(Long orderMainId) {
// 统计该医嘱主单的发药明细总量与退药明细总量是否相等
Integer totalDispensed = dispensingDetailMapper.sumQuantityByOrderAndStatus(orderMainId, DispenseStatus.DISPENSED);
Integer totalRefunded = dispensingDetailMapper.sumQuantityByOrderAndStatus(orderMainId, DispenseStatus.REFUND);
return totalDispensed != null && totalRefunded != null && totalDispensed.equals(totalRefunded);
}
// -------------------------------------------------------------------------
// 其余业务方法保持不变(分页查询、医嘱验证等)
// -------------------------------------------------------------------------
// ... 其余代码保持原样
} }