From 3ed5f8819bb086c49a049213fa7a21f633f50a52 Mon Sep 17 00:00:00 2001 From: guanyu Date: Wed, 27 May 2026 00:32:15 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Bug=20#503:=20fallback=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/DispensingDetailMapper.java | 38 +++++++++++ .../mapper/DispensingSummaryMapper.java | 54 +++++++++++++++ .../inpatient/service/DispensingService.java | 18 +++++ .../service/impl/DispensingServiceImpl.java | 67 +++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 com/openhis/web/inpatient/mapper/DispensingDetailMapper.java create mode 100644 com/openhis/web/inpatient/mapper/DispensingSummaryMapper.java create mode 100644 com/openhis/web/inpatient/service/DispensingService.java create mode 100644 com/openhis/web/inpatient/service/impl/DispensingServiceImpl.java diff --git a/com/openhis/web/inpatient/mapper/DispensingDetailMapper.java b/com/openhis/web/inpatient/mapper/DispensingDetailMapper.java new file mode 100644 index 000000000..577c1911f --- /dev/null +++ b/com/openhis/web/inpatient/mapper/DispensingDetailMapper.java @@ -0,0 +1,38 @@ +package com.openhis.web.inpatient.mapper; + +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; + +/** + * 发药明细 Mapper + * + * 新增 batchInsertDetail 方法,统一使用 Map 参数,便于前端传递任意字段。 + */ +@Mapper +public interface DispensingDetailMapper { + + /** + * 批量插入发药明细。 + * + * @param prescriptionId 处方主键 + * @param detailList 明细数据列表,每条记录必须包含: + * - drug_id + * - quantity + * - amount + * - type(DISPENSE / RETURN) + * @return 实际插入的记录数 + */ + @Insert({ + "" + }) + int batchInsertDetail(@Param("prescriptionId") Long prescriptionId, + @Param("detailList") List> detailList); +} diff --git a/com/openhis/web/inpatient/mapper/DispensingSummaryMapper.java b/com/openhis/web/inpatient/mapper/DispensingSummaryMapper.java new file mode 100644 index 000000000..5eb1ddabc --- /dev/null +++ b/com/openhis/web/inpatient/mapper/DispensingSummaryMapper.java @@ -0,0 +1,54 @@ +package com.openhis.web.inpatient.mapper; + +import org.apache.ibatis.annotations.*; + +import java.util.Map; + +/** + * 发药汇总单 Mapper + * + * 新增: + * 1. {@code recalculateSummaryByPrescriptionId}:基于明细表重新计算汇总信息,并使用 FOR UPDATE 锁定行。 + * 2. {@code insertInitialSummary}:在首次发药时插入空的汇总记录,防止后续更新失败。 + */ +@Mapper +public interface DispensingSummaryMapper { + + /** + * 重新计算指定处方的发药汇总信息。 + * + * 该 SQL 会: + * - 对 his_dispensing_detail 按 prescription_id 汇总数量、金额等。 + * - 使用 SELECT ... FOR UPDATE 锁定对应的 his_dispensing_summary 行,确保并发安全。 + * - 更新汇总表的 total_quantity、total_amount、status 等字段。 + * + * @param prescriptionId 处方主键 + * @return 更新的记录数(通常为 1) + */ + @Update({ + "" + }) + int recalculateSummaryByPrescriptionId(@Param("prescriptionId") Long prescriptionId); + + /** + * 首次发药时插入一条空的汇总记录。 + * + * @param prescriptionId 处方主键 + */ + @Insert("INSERT INTO his_dispensing_summary (prescription_id, total_quantity, total_amount, status) " + + "VALUES (#{prescriptionId}, 0, 0, 'INIT')") + void insertInitialSummary(@Param("prescriptionId") Long prescriptionId); +} diff --git a/com/openhis/web/inpatient/service/DispensingService.java b/com/openhis/web/inpatient/service/DispensingService.java new file mode 100644 index 000000000..b4005c831 --- /dev/null +++ b/com/openhis/web/inpatient/service/DispensingService.java @@ -0,0 +1,18 @@ +package com.openhis.web.inpatient.service; + +import java.util.List; +import java.util.Map; + +/** + * 住院发退药业务接口 + */ +public interface DispensingService { + + /** + * 发药或退药核心业务,确保明细与汇总单同步。 + * + * @param prescriptionId 处方ID + * @param detailList 本次操作的明细列表 + */ + void dispense(Long prescriptionId, List> detailList); +} diff --git a/com/openhis/web/inpatient/service/impl/DispensingServiceImpl.java b/com/openhis/web/inpatient/service/impl/DispensingServiceImpl.java new file mode 100644 index 000000000..d33f9c9cb --- /dev/null +++ b/com/openhis/web/inpatient/service/impl/DispensingServiceImpl.java @@ -0,0 +1,67 @@ +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); + } + } +}