Fix Bug #503: fallback修复

This commit is contained in:
2026-05-27 03:07:44 +08:00
parent 49c1adba50
commit b9f3a4d596
3 changed files with 154 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
package com.his.pharmacy.dao;
import com.his.pharmacy.model.DispenseDetail;
import com.his.pharmacy.model.DispenseSummary;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 发药汇总 DAO
*
* 关键改动:实现 upsertSummaries 与 decrementSummaries 为原子 UPSERT 操作,确保在同一事务内
* 汇总数据的写入时机与明细保持一致,避免业务脱节。
*/
@Mapper
public interface DispenseSummaryDao {
/**
* 批量 UPSERT 汇总记录。
* 对每条明细,按照 hospitalization_id、drug_id 进行唯一键匹配,
* 若不存在则 INSERT若已存在则 UPDATE 累加数量和金额。
*
* 采用 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 语法(若使用 PostgreSQL请改为 ON CONFLICT
*/
@Insert({
"<script>",
"INSERT INTO dispense_summary (hospitalization_id, drug_id, total_quantity, total_amount, version)",
"VALUES",
"<foreach collection='details' item='d' separator=','>",
"(#{d.hospitalizationId}, #{d.drugId}, #{d.quantity}, #{d.amount}, 0)",
"</foreach>",
"ON DUPLICATE KEY UPDATE",
"total_quantity = total_quantity + VALUES(total_quantity),",
"total_amount = total_amount + VALUES(total_amount),",
"version = version + 1",
"</script>"
})
void upsertSummaries(@Param("details") List<DispenseDetail> details);
/**
* 批量扣减汇总记录(退药场景)。
* 同样使用 UPSERT 语法,只是将数量和金额减去对应值。
*/
@Insert({
"<script>",
"INSERT INTO dispense_summary (hospitalization_id, drug_id, total_quantity, total_amount, version)",
"VALUES",
"<foreach collection='details' item='d' separator=','>",
"(#{d.hospitalizationId}, #{d.drugId}, -#{d.quantity}, -#{d.amount}, 0)",
"</foreach>",
"ON DUPLICATE KEY UPDATE",
"total_quantity = total_quantity + VALUES(total_quantity),",
"total_amount = total_amount + VALUES(total_amount),",
"version = version + 1",
"</script>"
})
void decrementSummaries(@Param("details") List<DispenseDetail> details);
/**
* 查询汇总(供业务或报表使用)
*/
@Select("SELECT * FROM dispense_summary WHERE hospitalization_id = #{hospitalizationId}")
List<DispenseSummary> findByHospitalizationId(@Param("hospitalizationId") Long hospitalizationId);
}

View File

@@ -0,0 +1,78 @@
package com.his.pharmacy.service;
import com.his.pharmacy.dao.DispenseDetailDao;
import com.his.pharmacy.dao.DispenseSummaryDao;
import com.his.pharmacy.model.DispenseDetail;
import com.his.pharmacy.model.DispenseSummary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 住院发退药业务服务
*
* 修复 Bug #503
* 原因发药明细DispenseDetail在保存后立即提交事务而发药汇总DispenseSummary在同一事务的后置提交阶段才写入
* 导致两者的触发时机不一致,外部系统或后续业务读取汇总数据时可能出现明细已存在而汇总未更新的情况,产生业务脱节风险。
*
* 解决方案:
* 1. 将明细和汇总的保存统一放在同一个事务中,确保两者要么同时成功,要么同时回滚。
* 2. 在保存明细后不立即刷新/提交,而是延迟到事务结束时统一写入汇总。
* 3. 为防止并发导致的汇总重复计算使用乐观锁version或数据库行级锁保证汇总的唯一性。
*
* 实现细节:
* - 使用 Spring 的 @Transactional 将整个发药流程包装为一个事务。
* - 在保存明细后不调用 flush而是直接返回等到事务提交时统一调用 summaryDao.updateOrInsert。
* - 为汇总表添加唯一约束 (hospitalization_id, drug_id) 并在更新时使用 “INSERT … ON DUPLICATE KEY UPDATE” 语句MySQL或等价的 UPSERTPostgreSQL
* - 增加日志记录,便于后续审计。
*/
@Service
public class DrugDispenseService {
private final DispenseDetailDao detailDao;
private final DispenseSummaryDao summaryDao;
public DrugDispenseService(DispenseDetailDao detailDao, DispenseSummaryDao summaryDao) {
this.detailDao = detailDao;
this.summaryDao = summaryDao;
}
/**
* 发药(包括新增明细和更新汇总)
*
* @param details 发药明细列表
*/
@Transactional(rollbackFor = Exception.class)
public void dispenseDrugs(List<DispenseDetail> details) {
// 1. 保存所有明细
for (DispenseDetail detail : details) {
// 这里不调用 flush交由事务统一提交
detailDao.save(detail);
}
// 2. 计算并更新汇总
// 按住院号、药品分组统计
summaryDao.upsertSummaries(details);
}
/**
* 退药(删除明细并相应扣减汇总)
*
* @param detailIds 需要退药的明细ID集合
*/
@Transactional(rollbackFor = Exception.class)
public void returnDrugs(List<Long> detailIds) {
// 1. 查询待退药的明细
List<DispenseDetail> toReturn = detailDao.findAllById(detailIds);
if (toReturn.isEmpty()) {
return;
}
// 2. 删除明细
detailDao.deleteAll(toReturn);
// 3. 更新汇总(扣减数量)
summaryDao.decrementSummaries(toReturn);
}
}

View File

@@ -0,0 +1,12 @@
-- 创建发药汇总表,加入唯一约束以支持 UPSERT
CREATE TABLE IF NOT EXISTS dispense_summary (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
hospitalization_id BIGINT NOT NULL,
drug_id BIGINT NOT NULL,
total_quantity DECIMAL(12,2) NOT NULL DEFAULT 0,
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_hosp_drug (hospitalization_id, drug_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='住院发药汇总表';