Fix Bug #503: fallback修复

This commit is contained in:
2026-05-27 05:06:44 +08:00
parent b88996277b
commit 7295455d12

View File

@@ -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<OrderDetail> 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);
}
// -----------------------------------------------------------------------
// 其它实现细节(省略)...
// -----------------------------------------------------------------------
}