package com.openhis.web.inpatient.service.impl; import com.openhis.web.inpatient.mapper.DispensingDetailMapper; import com.openhis.web.inpatient.mapper.DispensingSummaryMapper; import com.openhis.web.inpatient.service.DispensingService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * 住院发退药业务实现 * * 修复 Bug #503: * 发药明细(DispensingDetail)与发药汇总单(DispensingSummary)在数据触发时机不一致, * 可能导致明细已写入而汇总单仍保持旧状态,产生业务脱节风险。 * * 解决思路: * 1. 将明细写入与汇总单更新放在同一个事务中,确保原子性。 * 2. 在插入明细后立即调用 {@link DispensingSummaryMapper#recalculateSummaryByPrescriptionId} * 重新计算该处方的汇总信息(总数量、总金额、状态等)。 * 3. 为防止并发导致的脏读,使用数据库行级锁(FOR UPDATE)在汇总计算时锁定对应的汇总记录。 * * 这样可以保证:每一次发药/退药操作,明细与汇总单的数据始终保持同步。 */ @Service public class DispensingServiceImpl implements DispensingService { private final DispensingDetailMapper detailMapper; private final DispensingSummaryMapper summaryMapper; public DispensingServiceImpl(DispensingDetailMapper detailMapper, DispensingSummaryMapper summaryMapper) { this.detailMapper = detailMapper; this.summaryMapper = summaryMapper; } /** * 发药(或退药)核心业务 * * @param prescriptionId 处方主键 * @param detailList 本次操作的明细列表 */ @Override @Transactional(rollbackFor = Exception.class) public void dispense(Long prescriptionId, List> detailList) { if (prescriptionId == null || detailList == null || detailList.isEmpty()) { throw new IllegalArgumentException("参数非法:处方ID和明细列表不能为空"); } // 1. 批量插入发药明细 int inserted = detailMapper.batchInsertDetail(prescriptionId, detailList); if (inserted != detailList.size()) { throw new RuntimeException("发药明细插入数量不匹配,expected=" + detailList.size() + ", actual=" + inserted); } // 2. 立即重新计算并更新汇总单 // 此方法内部使用 SELECT ... FOR UPDATE 锁定对应的汇总记录,防止并发冲突。 int updated = summaryMapper.recalculateSummaryByPrescriptionId(prescriptionId); if (updated == 0) { // 汇总记录可能不存在,首次发药时需要插入一条新记录 summaryMapper.insertInitialSummary(prescriptionId); // 再次计算 summaryMapper.recalculateSummaryByPrescriptionId(prescriptionId); } } }