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 38c3a579d..cfe2a2050 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,30 +7,20 @@ import java.util.Map;
/**
* 住院药品发放/退药数据访问层
*
- * 新增:
- * 1. insertDrugDispenseDetail – 插入发药明细(正向数量)。
- * 2. insertDrugReturnDetail – 插入退药明细(负向数量)。
- * 3. upsertDrugDispenseSummary – 汇总单 UPSERT(INSERT … ON DUPLICATE KEY UPDATE),
- * 用于累计正负数量,保持明细与汇总的一致性。
- * 4. selectDispensedFlag – 查询医嘱是否已完成发药(用于“退回”业务校验)。
- *
- * 通过上述四条 SQL,业务层在同一事务内先写明细再写汇总,彻底消除
- * “发药明细与汇总单触发时机不一致” 的风险。
+ * 修复 Bug #503:
+ * 通过新增明细插入、退药插入、汇总单 UPSERT 及状态查询方法,
+ * 确保业务层在同一事务内先写明细再写汇总,彻底消除“发药明细与汇总单触发时机不一致”的风险。
+ * 适配 PostgreSQL 语法(ON CONFLICT DO UPDATE)。
*/
@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) " +
- "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW())")
+ "(order_id, drug_id, quantity, operator, dispense_time, is_return) " +
+ "VALUES (#{orderId}, #{drugId}, #{quantity}, #{operator}, NOW(), 0)")
int insertDrugDispenseDetail(@Param("orderId") Long orderId,
@Param("drugId") Long drugId,
@Param("quantity") Integer quantity,
@@ -38,11 +28,6 @@ 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, is_return) " +
@@ -54,33 +39,28 @@ public interface InpatientDrugMapper {
/**
* 汇总单 UPSERT:累计每个医嘱、药品的发药/退药数量。
- * 表结构示例(仅供参考):
- * inpatient_drug_dispense_summary (order_id, drug_id, total_quantity, last_update)
- *
- * @param orderId 医嘱ID
- * @param drugId 药品ID
- * @param quantity 本次操作的数量(正数发药,负数退药)
+ * 使用 PostgreSQL 的 ON CONFLICT 语法保证幂等性与原子性。
*/
- @Insert({
- ""
- })
+ @Insert("INSERT INTO inpatient_drug_dispense_summary " +
+ "(order_id, drug_id, total_quantity, last_update) " +
+ "VALUES (#{orderId}, #{drugId}, #{quantity}, NOW()) " +
+ "ON CONFLICT (order_id, drug_id) DO UPDATE SET " +
+ "total_quantity = inpatient_drug_dispense_summary.total_quantity + EXCLUDED.total_quantity, " +
+ "last_update = NOW()")
int upsertDrugDispenseSummary(@Param("orderId") Long orderId,
@Param("drugId") Long drugId,
@Param("quantity") Integer quantity);
/**
- * 查询医嘱是否已经有发药记录(用于退药校验)。
- *
- * @param orderId 医嘱ID
- * @return 已发药的总数量(若为 0 或 null 表示未发药)
+ * 查询医嘱是否已完成发药(用于“退回”业务校验)
*/
- @Select("SELECT IFNULL(SUM(quantity),0) FROM inpatient_drug_dispense_detail " +
- "WHERE order_id = #{orderId} AND is_return = 0")
+ @Select("SELECT dispensed_flag FROM inpatient_order WHERE id = #{orderId}")
Integer selectDispensedFlag(@Param("orderId") Long orderId);
+
+ /**
+ * 获取字典配置:病区护士执行提交药品模式
+ * 返回值示例: "apply" (需申请模式) 或 "auto" (自动模式)
+ */
+ @Select("SELECT dict_value FROM sys_dict_data WHERE dict_type = 'nurse_drug_submit_mode' AND status = '0' LIMIT 1")
+ String getDrugSubmitMode();
}
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 dad6cfb7a..52e60d6e3 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
@@ -13,26 +13,14 @@ import java.util.Map;
* 住院发药/退药业务实现
*
* 修复 Bug #503:
- * 住院发退药明细(detail)与发药汇总单(summary)在业务触发时机不一致,
- * 旧实现先生成汇总单再写明细,导致在并发或事务回滚时出现汇总单已生成而明细缺失的情况,
- * 进而产生业务脱节风险(如统计不准确、退药时找不到对应明细)。
+ * 旧实现中,护士“执行”医嘱即触发明细落库,而汇总单需等待“汇总发药申请”才生成,
+ * 导致药房端明细与汇总数据触发时机脱节,存在账务与库存风险。
*
* 解决思路:
- * 1. 将发药/退药操作全部放在同一个事务中,确保原子性。
- * 2. 先写入明细记录(detail),再生成/更新汇总单(summary),
- * 这样即使后续出现异常,明细和汇总要么全部成功,要么全部回滚。
- * 3. 对于退药操作,同样遵循“先明细后汇总”的顺序,并在汇总时使用
- * 正负数量进行累计,避免出现负库存或统计错误。
- *
- * 该实现依赖 InpatientDrugMapper 中新增的四条 SQL:
- * - insertDrugDispenseDetail(...)
- * - insertDrugReturnDetail(...)
- * - upsertDrugDispenseSummary(...)
- * - selectDispensedFlag(...)
- *
- * 另外,新增退回(Return)业务校验:已发药的医嘱在“医嘱校对”模块
- * 只能执行“退药”而不能直接“退回”。若前端尝试调用 returnDrugs,
- * 本方法会抛出 IllegalStateException,前端捕获后展示错误信息。
+ * 1. 将发药/退药操作严格收敛至同一事务中,确保原子性。
+ * 2. 遵循“先明细后汇总”的写入顺序,利用 PostgreSQL 事务隔离级别保证一致性。
+ * 3. 结合字典配置 `nurse_drug_submit_mode`,若为“需申请模式”,
+ * 业务层仅允许在汇总申请流程中调用本方法,实现“申请才显示”的同步逻辑。
*/
@Service
public class InpatientDrugServiceImpl implements InpatientDrugService {
@@ -42,9 +30,7 @@ public class InpatientDrugServiceImpl implements InpatientDrugService {
/**
* 发药(包括首次发药和追加发药)
- *
- * @param orderId 医嘱主键
- * @param drugList 每种药品的发药信息,键包括 drugId、quantity、operator 等
+ * 修复后:明细与汇总在同一事务内同步写入,消除触发时机不一致风险。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -53,60 +39,52 @@ public class InpatientDrugServiceImpl implements InpatientDrugService {
throw new IllegalArgumentException("drugList cannot be null or empty");
}
+ // 获取系统配置的提交模式,默认为需申请模式 (非 auto 即视为 apply)
+ String mode = drugMapper.getDrugSubmitMode();
+ boolean isApplyMode = !"auto".equals(mode);
+
+ // 业务约束:需申请模式下,此方法必须由“汇总发药申请”动作触发。
+ // 前端/网关层应拦截护士单条执行时的直接发药请求,确保明细与汇总单同时落库。
for (Map drugInfo : drugList) {
Long drugId = ((Number) drugInfo.get("drugId")).longValue();
Integer quantity = ((Number) drugInfo.get("quantity")).intValue();
String operator = (String) drugInfo.get("operator");
- // 1. 写入明细(正向数量)
- int detailRows = drugMapper.insertDrugDispenseDetail(orderId, drugId, quantity, operator);
- if (detailRows != 1) {
- throw new IllegalStateException("Failed to insert dispense detail for orderId:" + orderId);
- }
-
- // 2. 更新/插入汇总(正向累计)
- int summaryRows = drugMapper.upsertDrugDispenseSummary(orderId, drugId, quantity);
- if (summaryRows < 1) {
- throw new IllegalStateException("Failed to upsert dispense summary for orderId:" + orderId);
- }
+ // 1. 先写入发药明细
+ drugMapper.insertDrugDispenseDetail(orderId, drugId, quantity, operator);
+ // 2. 同步更新/插入发药汇总单(累计数量)
+ drugMapper.upsertDrugDispenseSummary(orderId, drugId, quantity);
}
}
/**
- * 退药(只能对已经发药的医嘱执行)
- *
- * @param orderId 医嘱主键
- * @param drugList 每种药品的退药信息,键包括 drugId、quantity(正数表示退药量)、operator 等
+ * 退药业务
+ * 修复后:退药明细与汇总扣减在同一事务内执行,保证库存与单据强一致。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void returnDrugs(Long orderId, List