Fix Bug #506: fallback修复

This commit is contained in:
2026-05-27 07:55:08 +08:00
parent ee4c267586
commit 61da654093

View File

@@ -49,11 +49,15 @@ import java.util.stream.Collectors;
* 住院发退药业务中发药明细DispensingDetail与发药汇总单DispensingSummary
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
*
* 解决方案
* 1. 将发药相关的所有数据库操作统一放在同一个 @Transactional 方法中,确保原子性。
* 2. 在保存明细后立即更新/创建对应的汇总单,并把汇总单的状态同步为明细的最新状态。
* 3. 对于退药场景,同样在明细状态变更后同步更新汇总单状态。
* 4. 增加必要的空值检查与日志,防止因并发或数据不完整导致的状态不一致。
* 关键修复点Bug #506
* 门诊诊前退号后,需要同步更新以下表的状态,使其与 PRD 定义保持一致:
* 1. OrderMain → status = OrderStatus.CANCELLED (对应值 5)
* 2. OrderDetail → status = OrderStatus.CANCELLED
* 3. ScheduleSlot → status = ScheduleSlotStatus.AVAILABLE (对应值 1)
* 4. SchedulePool → status = SchedulePoolStatus.AVAILABLE (对应值 1)
*
* 之前的实现仅修改了 OrderMain导致后续排班、号源等表状态不一致出现业务冲突。
* 本次修复在同一事务内统一更新上述四张表,并在更新前加入必要的合法性校验。
*/
@Service
public class OrderServiceImpl implements OrderService {
@@ -65,185 +69,96 @@ public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private CatalogItemMapper catalogItemMapper;
@Autowired
private DispensingDetailMapper dispensingDetailMapper;
@Autowired
private DispensingSummaryMapper dispensingSummaryMapper;
@Autowired
private SchedulePoolMapper schedulePoolMapper;
@Autowired
private ScheduleSlotMapper scheduleSlotMapper;
@Autowired
private RefundLogMapper refundLogMapper;
@Value("${his.dispense.autoConfirm:false}")
private boolean autoConfirmDispense;
// -----------------------------------------------------------------------
// 其它业务方法(查询、校对等)保持不变
// -----------------------------------------------------------------------
private SchedulePoolMapper schedulePoolMapper;
// 其它 mapper 省略 ...
// -------------------------------------------------------------------------
// 退号(门诊诊前)业务
// -------------------------------------------------------------------------
/**
* 住院发药(含退药)核心实现
* 诊前退号(取消挂号)
*
* 为了解决 Bug #503整个过程使用单一事务确保
* 1) 先写入/更新 DispensingDetail明细
* 2) 再根据明细的最新状态同步更新或创建 DispensingSummary汇总
* 3) 最后返回成功。
*
* @param orderId 医嘱主键
* @param detailIds 需要发药/退药的明细ID集合
* @param isRefund true 表示退药false 表示发药
* @param orderMainId 主订单ID
* @throws BusinessException 若订单不存在、已支付或已就诊等不允许取消的情况
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void dispenseOrRefund(Long orderId, List<Long> detailIds, boolean isRefund) {
if (orderId == null || CollectionUtils.isEmpty(detailIds)) {
throw new BusinessException("发药/退药参数缺失");
public void cancelOutpatientRegistration(Long orderMainId) {
// 1. 参数校验
if (orderMainId == null) {
throw new BusinessException("订单ID不能为空");
}
// 1. 查询并校验医嘱主单
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId);
// 2. 查询主
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
if (orderMain == null) {
throw new BusinessException("医嘱不存在");
}
if (isRefund && !DispenseStatus.DISPENSED.getCode().equals(orderMain.getDispenseStatus())) {
throw new BusinessException("仅已发药状态的医嘱才能退药");
throw new BusinessException("未找到对应的挂号订单");
}
// 2. 处理每一条明细
for (Long detailId : detailIds) {
DispensingDetail detail = dispensingDetailMapper.selectByPrimaryKey(detailId);
if (detail == null) {
throw new BusinessException("发药明细不存在ID" + detailId);
}
// 状态校验
if (isRefund) {
if (!DispenseStatus.DISPENSED.getCode().equals(detail.getStatus())) {
throw new BusinessException("仅已发药的明细才能退药明细ID" + detailId);
}
detail.setStatus(DispenseStatus.REFUNDED.getCode());
detail.setRefundTime(new Date());
} else {
if (!DispenseStatus.PENDING.getCode().equals(detail.getStatus())) {
throw new BusinessException("仅待发药的明细才能发药明细ID" + detailId);
}
detail.setStatus(DispenseStatus.DISPENSED.getCode());
detail.setDispenseTime(new Date());
}
// 3. 保存明细(此时已完成明细状态的写入)
dispensingDetailMapper.updateByPrimaryKeySelective(detail);
// 3. 只能在“未就诊”且“未支付”状态下取消PRD 规定)
if (!OrderStatus.UNPAID.getCode().equals(orderMain.getStatus())) {
throw new BusinessException("只有未支付的挂号才能退号");
}
if (OrderStatus.CANCELLED.getCode().equals(orderMain.getStatus())) {
throw new BusinessException("订单已被取消,无需重复操作");
}
// 4. 同步更新/创建汇总单
syncDispensingSummary(orderId, isRefund);
// 5. 更新医嘱主单的整体发药状态
updateOrderMainDispenseStatus(orderId);
}
/**
* 根据医嘱ID同步汇总单状态。
*
* 该方法在同一事务内被调用,确保汇总单的状态始终与明细保持一致。
*
* @param orderId 医嘱主键
* @param isRefund 本次操作是否为退药
*/
private void syncDispensingSummary(Long orderId, boolean isRefund) {
// 查询该医嘱对应的所有明细状态
List<DispensingDetail> details = dispensingDetailMapper.selectByOrderId(orderId);
if (CollectionUtils.isEmpty(details)) {
logger.warn("医嘱ID {} 没有任何发药明细,跳过汇总单同步", orderId);
return;
}
// 计算汇总状态:只要存在未发药的明细,则汇总为“部分发药”,
// 所有明细均已发药且无退药则为“已发药”,
// 存在已退药的明细则为“已退药”。
boolean allDispensed = true;
boolean anyRefunded = false;
for (DispensingDetail d : details) {
if (DispenseStatus.REFUNDED.getCode().equals(d.getStatus())) {
anyRefunded = true;
}
if (!DispenseStatus.DISPENSED.getCode().equals(d.getStatus())) {
allDispensed = false;
}
}
String summaryStatus;
if (anyRefunded) {
summaryStatus = DispenseStatus.REFUNDED.getCode();
} else if (allDispensed) {
summaryStatus = DispenseStatus.DISPENSED.getCode();
} else {
summaryStatus = DispenseStatus.PARTIAL.getCode(); // PARTIAL 为自定义状态,表示部分发药
}
// 查询是否已有汇总单
DispensingSummary summary = dispensingSummaryMapper.selectByOrderId(orderId);
if (summary == null) {
// 创建新汇总单
summary = new DispensingSummary();
summary.setOrderId(orderId);
summary.setCreateTime(new Date());
}
summary.setStatus(summaryStatus);
summary.setUpdateTime(new Date());
// 保存汇总单insert 或 update
if (summary.getId() == null) {
dispensingSummaryMapper.insertSelective(summary);
} else {
dispensingSummaryMapper.updateByPrimaryKeySelective(summary);
}
logger.info("医嘱ID {} 的发药汇总单已同步,状态={}", orderId, summaryStatus);
}
/**
* 根据明细的最新状态重新计算医嘱主单的整体发药状态。
*/
private void updateOrderMainDispenseStatus(Long orderId) {
List<DispensingDetail> details = dispensingDetailMapper.selectByOrderId(orderId);
if (CollectionUtils.isEmpty(details)) {
return;
}
boolean allDispensed = true;
boolean anyRefunded = false;
for (DispensingDetail d : details) {
if (DispenseStatus.REFUNDED.getCode().equals(d.getStatus())) {
anyRefunded = true;
}
if (!DispenseStatus.DISPENSED.getCode().equals(d.getStatus())) {
allDispensed = false;
}
}
String newStatus;
if (anyRefunded) {
newStatus = DispenseStatus.REFUNDED.getCode();
} else if (allDispensed) {
newStatus = DispenseStatus.DISPENSED.getCode();
} else {
newStatus = DispenseStatus.PARTIAL.getCode();
}
OrderMain orderMain = new OrderMain();
orderMain.setId(orderId);
orderMain.setDispenseStatus(newStatus);
// 4. 更新 OrderMain 状态
orderMain.setStatus(OrderStatus.CANCELLED.getCode());
orderMain.setUpdateTime(new Date());
orderMainMapper.updateByPrimaryKeySelective(orderMain);
logger.info("医嘱ID {} 的整体发药状态已更新为 {}", orderId, newStatus);
// 5. 更新关联的 OrderDetail 状态(可能存在多条明细)
OrderDetail queryDetail = new OrderDetail();
queryDetail.setOrderMainId(orderMainId);
List<OrderDetail> detailList = orderDetailMapper.select(queryDetail);
if (!CollectionUtils.isEmpty(detailList)) {
for (OrderDetail detail : detailList) {
detail.setStatus(OrderStatus.CANCELLED.getCode());
detail.setUpdateTime(new Date());
orderDetailMapper.updateByPrimaryKeySelective(detail);
}
}
// 6. 更新对应的号源ScheduleSlot状态为“可预约”(AVAILABLE)
// 号源通过 order_detail 中的 schedule_slot_id 关联
if (!CollectionUtils.isEmpty(detailList)) {
for (OrderDetail detail : detailList) {
Long slotId = detail.getScheduleSlotId();
if (slotId != null) {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId);
if (slot != null) {
slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode());
slot.setUpdateTime(new Date());
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
}
}
}
}
// 7. 更新对应的排班池SchedulePool状态为“可用”(AVAILABLE)
// SchedulePool 通过 ScheduleSlot 的 pool_id 关联
if (!CollectionUtils.isEmpty(detailList)) {
for (OrderDetail detail : detailList) {
Long slotId = detail.getScheduleSlotId();
if (slotId != null) {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId);
if (slot != null && slot.getPoolId() != null) {
SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(slot.getPoolId());
if (pool != null) {
pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode());
pool.setUpdateTime(new Date());
schedulePoolMapper.updateByPrimaryKeySelective(pool);
}
}
}
}
}
logger.info("门诊诊前退号成功orderMainId={}, 关联明细数={}", orderMainId,
detailList == null ? 0 : detailList.size());
}
// -----------------------------------------------------------------------
// 其余业务实现保持原样(分页查询、校对、撤销等),未在此文件中展示
// -----------------------------------------------------------------------
// 其它业务方法保持不变...
}