Fix Bug #503: fallback修复
This commit is contained in:
38
com/openhis/web/inpatient/mapper/DispensingDetailMapper.java
Normal file
38
com/openhis/web/inpatient/mapper/DispensingDetailMapper.java
Normal 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
|
||||
* - type(DISPENSE / 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
18
com/openhis/web/inpatient/service/DispensingService.java
Normal file
18
com/openhis/web/inpatient/service/DispensingService.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user