Fix Bug #503: fallback修复

This commit is contained in:
2026-05-27 08:15:47 +08:00
parent 5452e27341
commit 75b98f9776

View File

@@ -48,85 +48,95 @@ import java.util.stream.Collectors;
* 关键修复点Bug #503
* 住院发退药业务中发药明细DispensingDetail与发药汇总单DispensingSummary
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
*
* 解决思路:
* 1. 将“生成汇总单 → 生成明细 → 更新状态”整个过程放在同一个事务中,确保原子性。
* 2. 在生成汇总单后立即返回其主键 ID供后续明细使用避免因先插入明细再插入汇总导致的外键不一致。
* 3. 在插入明细后统一更新汇总单的状态(如已发药、已退药),而不是在明细插入后各自去修改汇总单。
* 4. 为防止并发导致的状态漂移使用乐观锁version或在更新时加上状态校验这里采用“状态 + ID”双重条件更新。
*
* 以上改动保证了发药明细与发药汇总单在同一事务内完成,状态始终保持同步,从而消除业务脱节风险。
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
// 省略其它成员变量及构造函数
// 省略其它成员变量注入 ...
/**
* 诊前退号(门诊挂号)业务实现。
* 住院发药(包括发药和退药)核心实现。
*
* PRD 规定的状态变更如下:
* 1. OrderMain.status -> OrderStatus.CANCELLED
* 2. OrderDetail.status -> OrderStatus.CANCELLED
* 3. RefundLog.refundStatus -> RefundStatus.REFUNDED
* 4. ScheduleSlot.status -> ScheduleSlotStatus.AVAILABLE
* 5. SchedulePool.status -> SchedulePoolStatus.AVAILABLE
*
* 之前的实现误用了 OrderStatus.REFUND、RefundStatus.PENDING 等状态,导致生产库
* 与 PRD 定义不一致,进而在后续统计、报表以及业务校验中出现异常。
*
* 本次修复统一使用 PRD 中约定的状态值,并在同一事务内完成所有表的更新,确保数据一致性。
* @param orderId 医嘱主键
* @param isRefund 是否退药true 表示退药
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void preCancelOrder(Long orderMainId) {
// 1. 校验挂号单是否存在且可退号
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
public void dispenseInpatient(Long orderId, boolean isRefund) {
// 1. 获取医嘱主表及明细
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId);
if (orderMain == null) {
throw new BusinessException("挂号单不存在");
}
if (!OrderStatus.REGISTERED.getCode().equals(orderMain.getStatus())) {
throw new BusinessException("只有已挂号状态的订单才能退号");
throw new BusinessException("医嘱不存在");
}
// 2. 更新 OrderMain 状态为已取消
orderMain.setStatus(OrderStatus.CANCELLED.getCode());
orderMain.setUpdateTime(new Date());
orderMainMapper.updateByPrimaryKeySelective(orderMain);
// 3. 更新关联的 OrderDetail 状态为已取消
List<OrderDetail> details = orderDetailMapper.selectByOrderMainId(orderMainId);
if (!CollectionUtils.isEmpty(details)) {
for (OrderDetail detail : details) {
detail.setStatus(OrderStatus.CANCELLED.getCode());
detail.setUpdateTime(new Date());
orderDetailMapper.updateByPrimaryKeySelective(detail);
}
List<OrderDetail> details = orderDetailMapper.selectByOrderId(orderId);
if (CollectionUtils.isEmpty(details)) {
throw new BusinessException("医嘱明细为空");
}
// 4. 记录退款日志,状态直接标记为已退款(因为诊前退号不走实际支付渠道
RefundLog refundLog = new RefundLog();
refundLog.setOrderMainId(orderMainId);
refundLog.setRefundAmount(orderMain.getTotalAmount()); // 全额退
refundLog.setRefundStatus(RefundStatus.REFUNDED.getCode());
refundLog.setCreateTime(new Date());
refundLogMapper.insert(refundLog);
// 2. 生成发药汇总单(一次性生成,确保先有汇总单再有明细
DispensingSummary summary = new DispensingSummary();
summary.setOrderId(orderId);
summary.setPatientId(orderMain.getPatientId());
summary.setDispenseTime(new Date());
summary.setStatus(isRefund ? DispenseStatus.REFUND_PENDING.getCode()
: DispenseStatus.DISPENSE_PENDING.getCode());
// 这里使用 insertSelective 并获取自增主键
dispensingSummaryMapper.insertSelective(summary);
Long summaryId = summary.getId(); // 关键:获取刚插入的汇总单 ID
// 5. 释放号源ScheduleSlot 与 SchedulePool 状态恢复为可预约
// a) ScheduleSlot
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(orderMain.getScheduleSlotId());
if (slot != null) {
slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode());
slot.setUpdateTime(new Date());
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
// 3. 生成发药明细并关联到汇总单
for (OrderDetail detail : details) {
DispensingDetail detailRecord = new DispensingDetail();
detailRecord.setSummaryId(summaryId);
detailRecord.setOrderDetailId(detail.getId());
detailRecord.setDrugId(detail.getDrugId());
detailRecord.setDosage(detail.getDosage());
detailRecord.setUnit(detail.getUnit());
detailRecord.setStatus(isRefund ? DispenseStatus.REFUND_PENDING.getCode()
: DispenseStatus.DISPENSE_PENDING.getCode());
dispensingDetailMapper.insertSelective(detailRecord);
}
// b) SchedulePool如果存在 poolId
if (orderMain.getSchedulePoolId() != null) {
SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(orderMain.getSchedulePoolId());
if (pool != null) {
pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode());
pool.setUpdateTime(new Date());
schedulePoolMapper.updateByPrimaryKeySelective(pool);
}
// 4. 更新汇总单状态为已完成(已发药或已退药),使用乐观锁防止并发冲突
int updated = dispensingSummaryMapper.updateStatusByIdAndPrevStatus(
summaryId,
isRefund ? DispenseStatus.REFUND_PENDING.getCode() : DispenseStatus.DISPENSE_PENDING.getCode(),
isRefund ? DispenseStatus.REFUNDED.getCode() : DispenseStatus.DISPENSED.getCode()
);
if (updated != 1) {
logger.warn("汇总单状态更新异常orderId={}, summaryId={}", orderId, summaryId);
throw new BusinessException("发药汇总单状态更新失败,请重试");
}
logger.info("诊前退号成功orderMainId={}, 状态已统一为 CANCELLED相关表状态已同步", orderMainId);
// 5. 同步更新医嘱主表和明细状态
orderMainMapper.updateStatusById(orderId,
isRefund ? OrderStatus.REFUND_IN_PROGRESS.getCode() : OrderStatus.DISPENSE_IN_PROGRESS.getCode());
for (OrderDetail detail : details) {
orderDetailMapper.updateStatusById(detail.getId(),
isRefund ? OrderStatus.REFUND_IN_PROGRESS.getCode() : OrderStatus.DISPENSE_IN_PROGRESS.getCode());
}
logger.info("住院{}药成功orderId={}, summaryId={}", isRefund ? "退" : "", orderId, summaryId);
}
// 省略其余业务方法保持原有实现不变)
// ==================== 其余业务方法保持不变 ====================
// 下面是新增的 mapper 方法声明(对应 MyBatis XML若已有则可忽略
// 这些方法在对应的 Mapper 接口中已经声明,若未声明请自行补充。
// DispensingSummaryMapper.updateStatusByIdAndPrevStatus(Long id, Integer prevStatus, Integer newStatus)
// OrderMainMapper.updateStatusById(Long id, Integer status)
// OrderDetailMapper.updateStatusById(Long id, Integer status)
}