Fix Bug #503: fallback修复

This commit is contained in:
2026-05-27 00:32:15 +08:00
parent 9990542f56
commit 3ed5f8819b
4 changed files with 177 additions and 0 deletions

View File

@@ -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
* - typeDISPENSE / RETURN
* @return 实际插入的记录数
*/
@Insert({
"<script>",
"INSERT INTO his_dispensing_detail (prescription_id, drug_id, quantity, amount, type, created_at)",
"VALUES",
"<foreach collection='detailList' item='item' separator=','>",
"(#{prescriptionId}, #{item.drugId}, #{item.quantity}, #{item.amount}, #{item.type}, NOW())",
"</foreach>",
"</script>"
})
int batchInsertDetail(@Param("prescriptionId") Long prescriptionId,
@Param("detailList") List<Map<String, Object>> detailList);
}

View File

@@ -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({
"<script>",
"UPDATE his_dispensing_summary s",
"SET",
" s.total_quantity = (SELECT IFNULL(SUM(d.quantity),0) FROM his_dispensing_detail d WHERE d.prescription_id = #{prescriptionId}),",
" s.total_amount = (SELECT IFNULL(SUM(d.amount),0) FROM his_dispensing_detail d WHERE d.prescription_id = #{prescriptionId}),",
" s.status = CASE",
" WHEN EXISTS (SELECT 1 FROM his_dispensing_detail d WHERE d.prescription_id = #{prescriptionId} AND d.type = 'RETURN')",
" THEN 'PARTIAL_RETURN'",
" ELSE 'DISPENSED'",
" END",
"WHERE s.prescription_id = #{prescriptionId}",
// 加锁,防止并发更新导致汇总不一致
"AND EXISTS (SELECT 1 FROM his_dispensing_summary s2 WHERE s2.id = s.id FOR UPDATE)",
"</script>"
})
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);
}

View File

@@ -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<Map<String, Object>> detailList);
}

View File

@@ -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<Map<String, Object>> 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);
}
}
}