Fix Bug #503: fallback修复

This commit is contained in:
2026-05-27 01:56:54 +08:00
parent 1d78ccf15f
commit ce325b96a5
2 changed files with 89 additions and 91 deletions

View File

@@ -7,74 +7,77 @@ import java.util.Map;
/**
* 住院药品发放/退药数据访问层
*
* 修复 Bug #503
* 引入 apply_status 字段控制数据可见性
* 护士执行医嘱时 apply_status=0药房不可见
* 护士提交汇总申请时 apply_status=1药房明细/汇总同步可见)。
* 通过事务保证状态变更与汇总单生成的原子性,彻底消除业务脱节风险
* 新增
* 1. insertDrugDispenseDetail 插入发药明细(正向数量)
* 2. insertDrugReturnDetail 插入退药明细(负向数量)。
* 3. upsertDrugDispenseSummary 汇总单 UPSERTINSERT … 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 " +
"<foreach item='id' collection='detailIds' open='(' separator=',' close=')'>#{id}</foreach>")
int batchUpdateDetailApplyStatus(@Param("detailIds") List<Long> detailIds);
/**
* 根据明细ID查询药品及数量用于汇总计算
*/
@Select("SELECT id, drug_id, quantity FROM inpatient_drug_dispense_detail WHERE id IN " +
"<foreach item='id' collection='detailIds' open='(' separator=',' close=')'>#{id}</foreach>")
List<Map<String, Object>> selectDetailsByIds(@Param("detailIds") List<Long> 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({
"<script>",
"INSERT INTO inpatient_drug_dispense_summary (order_id, drug_id, total_quantity, last_update_time) ",
"VALUES (#{orderId}, #{drugId}, #{quantity}, NOW()) ",
"ON DUPLICATE KEY UPDATE ",
"total_quantity = total_quantity + #{quantity}, ",
"last_update_time = NOW()",
"</script>"
})
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<Map<String, Object>> selectAppliedDispenseDetails();
/**
* 查询发药汇总单
*/
@Select("SELECT id, ward_id, drug_id, total_quantity, apply_time, operator " +
"FROM inpatient_drug_dispense_summary")
List<Map<String, Object>> selectDispenseSummaries();
@Param("quantity") Integer quantity);
}

View File

@@ -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<Map<String, Object>> drugList) {
// 1. 逐条写入发药明细
for (Map<String, Object> 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<Long> detailIds, String operator) {
if (detailIds == null || detailIds.isEmpty()) {
throw new IllegalArgumentException("申请明细不能为空");
public void returnDrugs(Long orderId, List<Map<String, Object>> drugList) {
for (Map<String, Object> 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<Map<String, Object>> details = drugMapper.selectDetailsByIds(detailIds);
// 3. 按药品聚合数量
Map<Long, Integer> drugQuantityMap = new HashMap<>();
for (Map<String, Object> 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<Long, Integer> entry : drugQuantityMap.entrySet()) {
drugMapper.upsertDrugDispenseSummary(wardId, entry.getKey(), entry.getValue(), operator);
}
}
@Override
public List<Map<String, Object>> getPharmacyDispenseDetails() {
// 仅查询 apply_status = 1 的记录,确保与汇总单触发时机一致
return drugMapper.selectAppliedDispenseDetails();
}
@Override
public List<Map<String, Object>> getPharmacyDispenseSummaries() {
return drugMapper.selectDispenseSummaries();
}
}