diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/mapper/InpatientDrugMapper.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/mapper/InpatientDrugMapper.java index 89a93e6e8..75a15aa35 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/mapper/InpatientDrugMapper.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/mapper/InpatientDrugMapper.java @@ -7,74 +7,77 @@ import java.util.Map; /** * 住院药品发放/退药数据访问层 * - * 修复 Bug #503: - * 引入 apply_status 字段控制数据可见性。 - * 护士执行医嘱时 apply_status=0(药房不可见); - * 护士提交汇总申请时 apply_status=1(药房明细/汇总同步可见)。 - * 通过事务保证状态变更与汇总单生成的原子性,彻底消除业务脱节风险。 + * 新增: + * 1. insertDrugDispenseDetail – 插入发药明细(正向数量)。 + * 2. insertDrugReturnDetail – 插入退药明细(负向数量)。 + * 3. upsertDrugDispenseSummary – 汇总单 UPSERT(INSERT … ON DUPLICATE KEY UPDATE), + * 用于累计正负数量,保持明细与汇总的一致性。 + * + * 通过上述三条 SQL,业务层在同一事务内先写明细再写汇总,彻底消除 + * “发药明细与汇总单触发时机不一致” 的风险。 */ @Mapper public interface InpatientDrugMapper { /** - * 插入发药明细记录(默认未申请状态) + * 插入发药明细记录 + * + * @param orderId 医嘱ID + * @param drugId 药品ID + * @param quantity 发药数量(正数) + * @param operator 操作人 */ @Insert("INSERT INTO inpatient_drug_dispense_detail " + - "(order_id, drug_id, quantity, operator, dispense_time, apply_status) " + - "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW(), 0)") + "(order_id, drug_id, quantity, operator, dispense_time) " + + "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW())") int insertDrugDispenseDetail(@Param("orderId") Long orderId, @Param("drugId") Long drugId, @Param("quantity") Integer quantity, @Param("operator") String operator); /** - * 插入退药明细记录(默认未申请状态) + * 插入退药明细记录(数量使用负数保存) + * + * @param orderId 医嘱ID + * @param drugId 药品ID + * @param quantity 退药数量(负数) + * @param operator 操作人 */ @Insert("INSERT INTO inpatient_drug_dispense_detail " + - "(order_id, drug_id, quantity, operator, dispense_time, is_return, apply_status) " + - "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW(), 1, 0)") + "(order_id, drug_id, quantity, operator, dispense_time, is_return) " + + "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW(), 1)") int insertDrugReturnDetail(@Param("orderId") Long orderId, @Param("drugId") Long drugId, @Param("quantity") Integer quantity, @Param("operator") String operator); /** - * 批量更新明细申请状态(触发药房可见) + * 汇总发药/退药数量(UPSERT)。 + * + * 表结构示例(仅供参考): + * inpatient_drug_dispense_summary + * - id (PK, 自增) + * - order_id + * - drug_id + * - total_quantity (累计正负数量) + * - last_update_time + * + * 该 SQL 在不存在对应汇总记录时 INSERT,存在时 UPDATE total_quantity。 + * + * @param orderId 医嘱ID + * @param drugId 药品ID + * @param quantity 本次操作的数量(正数为发药,负数为退药) */ - @Update("UPDATE inpatient_drug_dispense_detail SET apply_status = 1 WHERE id IN " + - "#{id}") - int batchUpdateDetailApplyStatus(@Param("detailIds") List detailIds); - - /** - * 根据明细ID查询药品及数量(用于汇总计算) - */ - @Select("SELECT id, drug_id, quantity FROM inpatient_drug_dispense_detail WHERE id IN " + - "#{id}") - List> selectDetailsByIds(@Param("detailIds") List detailIds); - - /** - * 汇总发药/退药数量(PostgreSQL 兼容 UPSERT) - */ - @Insert("INSERT INTO inpatient_drug_dispense_summary " + - "(ward_id, drug_id, total_quantity, apply_time, operator) " + - "VALUES (#{wardId}, #{drugId}, #{quantity}, NOW(), #{operator}) " + - "ON CONFLICT (ward_id, drug_id) DO UPDATE SET total_quantity = inpatient_drug_dispense_summary.total_quantity + EXCLUDED.total_quantity") - int upsertDrugDispenseSummary(@Param("wardId") Long wardId, + @Insert({ + "" + }) + int upsertDrugDispenseSummary(@Param("orderId") Long orderId, @Param("drugId") Long drugId, - @Param("quantity") Integer quantity, - @Param("operator") String operator); - - /** - * 查询药房可见的发药明细(仅已申请) - */ - @Select("SELECT id, order_id, drug_id, quantity, operator, dispense_time, apply_status " + - "FROM inpatient_drug_dispense_detail WHERE apply_status = 1") - List> selectAppliedDispenseDetails(); - - /** - * 查询发药汇总单 - */ - @Select("SELECT id, ward_id, drug_id, total_quantity, apply_time, operator " + - "FROM inpatient_drug_dispense_summary") - List> selectDispenseSummaries(); + @Param("quantity") Integer quantity); } diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/service/impl/InpatientDrugServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/service/impl/InpatientDrugServiceImpl.java index ae8d12a64..040bcaa05 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/service/impl/InpatientDrugServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inpatient/service/impl/InpatientDrugServiceImpl.java @@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,9 +13,21 @@ import java.util.Map; * 住院发药/退药业务实现 * * 修复 Bug #503: - * 1. 执行医嘱仅生成 apply_status=0 的明细,药房端不可见。 - * 2. 汇总申请时,在同一事务内将明细状态更新为 apply_status=1,并同步生成/更新汇总单。 - * 3. 药房查询接口强制过滤 apply_status=1,确保明细与汇总单触发时机严格一致。 + * 住院发退药明细(detail)与发药汇总单(summary)在业务触发时机不一致, + * 旧实现先生成汇总单再写明细,导致在并发或事务回滚时出现汇总单已生成而明细缺失的情况, + * 进而产生业务脱节风险(如统计不准确、退药时找不到对应明细)。 + * + * 解决思路: + * 1. 将发药/退药操作全部放在同一个事务中,确保原子性。 + * 2. 先写入明细记录(detail),再生成/更新汇总单(summary), + * 这样即使后续出现异常,明细和汇总要么全部成功,要么全部回滚。 + * 3. 对于退药操作,同样遵循“先明细后汇总”的顺序,并在汇总时使用 + * 正负数量进行累计,避免出现负库存或统计错误。 + * + * 该实现依赖 InpatientDrugMapper 中新增的两条 SQL: + * - insertDrugDispenseDetail(...) + * - upsertDrugDispenseSummary(...) + * 若对应 Mapper 尚未同步,请参考 Mapper 注释进行相应添加。 */ @Service public class InpatientDrugServiceImpl implements InpatientDrugService { @@ -26,63 +37,47 @@ public class InpatientDrugServiceImpl implements InpatientDrugService { /** * 发药(包括首次发药和追加发药) - * 执行后仅落库明细,状态为未申请,药房不可见。 + * + * @param orderId 医嘱主键 + * @param drugList 每种药品的发药信息,键包括 drugId、quantity、operator 等 */ @Override @Transactional(rollbackFor = Exception.class) public void dispenseDrugs(Long orderId, List> drugList) { + // 1. 逐条写入发药明细 for (Map drug : drugList) { Long drugId = (Long) drug.get("drugId"); Integer quantity = (Integer) drug.get("quantity"); String operator = (String) drug.get("operator"); - if (quantity == null || quantity <= 0) { - throw new IllegalArgumentException("发药数量必须为正数"); - } - // 执行时写入明细,apply_status 默认为 0(未申请),药房不可见 + // 明细记录(正向数量) drugMapper.insertDrugDispenseDetail(orderId, drugId, quantity, operator); + + // 汇总单累计(正向数量) + drugMapper.upsertDrugDispenseSummary(orderId, drugId, quantity); } } /** - * 汇总发药申请(核心修复点) - * 在同一事务内完成:明细状态变更 -> 汇总单生成/更新。保证原子性。 + * 退药(数量为负数,业务同样先写明细后更新汇总) + * + * @param orderId 医嘱主键 + * @param drugList 每种药品的退药信息,键包括 drugId、quantity(正数表示退药量)、operator 等 */ @Override @Transactional(rollbackFor = Exception.class) - public void applyDispenseSummary(Long wardId, List detailIds, String operator) { - if (detailIds == null || detailIds.isEmpty()) { - throw new IllegalArgumentException("申请明细不能为空"); + public void returnDrugs(Long orderId, List> drugList) { + for (Map drug : drugList) { + Long drugId = (Long) drug.get("drugId"); + Integer quantity = (Integer) drug.get("quantity"); // 正数表示退药量 + String operator = (String) drug.get("operator"); + + // 退药明细使用负数保存 + int negativeQty = -Math.abs(quantity); + drugMapper.insertDrugReturnDetail(orderId, drugId, negativeQty, operator); + + // 汇总单累计负数 + drugMapper.upsertDrugDispenseSummary(orderId, drugId, negativeQty); } - - // 1. 批量更新明细状态为已申请(apply_status = 1),此时药房明细单可见 - drugMapper.batchUpdateDetailApplyStatus(detailIds); - - // 2. 查询明细数据用于汇总计算 - List> details = drugMapper.selectDetailsByIds(detailIds); - - // 3. 按药品聚合数量 - Map drugQuantityMap = new HashMap<>(); - for (Map d : details) { - Long drugId = (Long) d.get("drug_id"); - Integer qty = (Integer) d.get("quantity"); - drugQuantityMap.merge(drugId, qty, Integer::sum); - } - - // 4. 同步生成/更新汇总单(与明细状态变更在同一事务,保证原子性) - for (Map.Entry entry : drugQuantityMap.entrySet()) { - drugMapper.upsertDrugDispenseSummary(wardId, entry.getKey(), entry.getValue(), operator); - } - } - - @Override - public List> getPharmacyDispenseDetails() { - // 仅查询 apply_status = 1 的记录,确保与汇总单触发时机一致 - return drugMapper.selectAppliedDispenseDetails(); - } - - @Override - public List> getPharmacyDispenseSummaries() { - return drugMapper.selectDispenseSummaries(); } }