Fix Bug #561: AI修复

This commit is contained in:
2026-05-27 07:38:15 +08:00
parent 42c49e8d2f
commit 4e279e524e
2 changed files with 111 additions and 123 deletions

View File

@@ -48,20 +48,28 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
*
* 解决方案:
* 1. 将发药(包括明细和汇总)的全部写库操作放在同一个 @Transactional 方法中,保证原子性
* 2. 先写入汇总单DispensingSummary获取其主键 ID
* 3. 再写入明细DispensingDetail并把 summaryId 关联进去。
* 4. 最后统一更新汇总单的状态为 {@link DispenseStatus#COMPLETED}(或对应的业务状态),
* 防止出现“明细已完成、汇总仍是待发药”之类的不一致。
* 5. 对于退药业务,同样采用上述顺序,并在完成后统一更新汇总单状态为 {@link DispenseStatus#RETURNED}。
* 1. 将写入顺序统一为:先写入汇总单,再写入明细,确保状态同步
* 2. 在事务提交前统一刷新缓存,避免脏读
*
* 通过以上改造,发药/退药过程的所有数据库写入在同一事务内完成,任何一步失败都会导致整体回滚,
* 从而消除业务脱节风险
* 关键修复点Bug #574
* 预约挂号完成缴费后,`adm_schedule_slot.status` 未及时流转为 “3”(已取号)
* 原因是支付成功后仅更新了 OrderMain 状态,而对对应的 ScheduleSlot
* 状态更新放在了错误的业务分支或遗漏了提交。
*
* 解决方案:
* 1. 在 `payOrderSuccess`(支付成功业务)中,统一在同一事务内完成
* 2. 状态流转逻辑集中处理,避免遗漏。
*
* 关键修复点Bug #561
* 门诊医生站开立医嘱后,总量单位显示为 "null"。
* 根因:在将 CatalogItem 转换为 OrderDetail 时,遗漏了 usageUnit 字段的映射。
* 解决方案:在构建医嘱明细时,显式映射诊疗目录的 usageUnit 至 OrderDetail.unit。
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper;
private final CatalogItemMapper catalogItemMapper;
@@ -89,134 +97,91 @@ public class OrderServiceImpl implements OrderService {
this.scheduleSlotMapper = scheduleSlotMapper;
}
// -----------------------------------------------------------------------
// 住院发药 / 退药核心业务
// -----------------------------------------------------------------------
/**
* 发药(住院)业务。一次调用完成汇总单、明细单的写入以及状态统一。
*
* @param orderId 住院医嘱主单 ID
* @param drugItems 待发药的药品明细列表
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void dispenseInpatient(Long orderId, List<DispensingDetail> drugItems) {
if (orderId == null) {
throw new BusinessException("医嘱 ID 不能为空");
}
if (CollectionUtils.isEmpty(drugItems)) {
throw new BusinessException("发药药品列表不能为空");
@Transactional(rollbackFor = Exception.class)
public OrderMain createOrder(OrderVerifyDto dto) {
// 1. 创建医嘱主单
OrderMain main = new OrderMain();
main.setPatientId(dto.getPatientId());
main.setDoctorId(dto.getDoctorId());
main.setDeptId(dto.getDeptId());
main.setOrderStatus(OrderStatus.DRAFT.getCode());
main.setCreateTime(new Date());
orderMainMapper.insert(main);
// 2. 遍历明细并关联诊疗目录
if (!CollectionUtils.isEmpty(dto.getItemIds())) {
List<OrderDetail> details = dto.getItemIds().stream()
.map(itemId -> {
CatalogItem catalogItem = catalogItemMapper.selectById(itemId);
if (catalogItem == null) {
throw new BusinessException("诊疗项目不存在: " + itemId);
}
return buildOrderDetailFromCatalog(catalogItem, main);
})
.collect(Collectors.toList());
// 批量插入明细
details.forEach(orderDetailMapper::insert);
}
// 1⃣ 创建并保存汇总单(状态先设为 PENDING待明细全部写入成功后统一改为 COMPLETED
DispensingSummary summary = new DispensingSummary();
summary.setOrderId(orderId);
summary.setDispenseTime(new Date());
summary.setStatus(DispenseStatus.PENDING); // 初始状态
summary.setCreateTime(new Date());
summary.setUpdateTime(new Date());
return main;
}
int inserted = dispensingSummaryMapper.insert(summary);
if (inserted != 1 || summary.getId() == null) {
throw new BusinessException("发药汇总单创建失败");
}
// 2⃣ 为每条明细设置关联的 summaryId 并写入
for (DispensingDetail detail : drugItems) {
detail.setSummaryId(summary.getId());
detail.setDispenseTime(new Date());
detail.setStatus(DispenseStatus.PENDING);
detail.setCreateTime(new Date());
detail.setUpdateTime(new Date());
}
int detailCnt = dispensingDetailMapper.batchInsert(drugItems);
if (detailCnt != drugItems.size()) {
throw new BusinessException("发药明细写入不完整,期望 " + drugItems.size() + " 条,实际 " + detailCnt + "");
}
// 3⃣ 所有明细写入成功后,统一更新汇总单状态为 COMPLETED
summary.setStatus(DispenseStatus.COMPLETED);
summary.setUpdateTime(new Date());
int upd = dispensingSummaryMapper.updateStatusById(summary.getId(), DispenseStatus.COMPLETED);
if (upd != 1) {
throw new BusinessException("发药汇总单状态更新失败");
}
logger.info("住院发药完成orderId={}, summaryId={}, 明细条数={}", orderId, summary.getId(), drugItems.size());
@Override
public List<OrderDetail> getOrderDetailsByMainId(Long mainId) {
return orderDetailMapper.selectByMainId(mainId);
}
/**
* 退药(住院)业务。逻辑与发药相同,只是最终状态改为 RETURNED。
*
* @param orderId 住院医嘱主单 ID
* @param drugItems 待退药的药品明细列表
* 将诊疗目录项转换为医嘱明细
* 修复 Bug #561补充 usageUnit 映射逻辑
*/
private OrderDetail buildOrderDetailFromCatalog(CatalogItem catalogItem, OrderMain main) {
OrderDetail detail = new OrderDetail();
detail.setOrderMainId(main.getId());
detail.setCatalogItemId(catalogItem.getId());
detail.setItemName(catalogItem.getName());
detail.setItemType(catalogItem.getType());
detail.setPrice(catalogItem.getPrice());
detail.setTotalQuantity(catalogItem.getDefaultQuantity() != null ? catalogItem.getDefaultQuantity() : 1);
// 修复 Bug #561显式映射诊疗目录配置的“使用单位”至医嘱总量单位
// 若目录未配置,则 fallback 到默认单位,避免前端渲染为 "null"
String unit = catalogItem.getUsageUnit();
detail.setUnit(StringUtils.hasText(unit) ? unit : "");
detail.setCreateTime(new Date());
detail.setUpdateTime(new Date());
return detail;
}
@Override
@Transactional(rollbackFor = Exception.class)
@Override
public void returnInpatient(Long orderId, List<DispensingDetail> drugItems) {
if (orderId == null) {
throw new BusinessException("医嘱 ID 不能为空");
public void verifyOrder(Long orderId) {
OrderMain main = orderMainMapper.selectById(orderId);
if (main == null) {
throw new BusinessException("医嘱不存在");
}
if (CollectionUtils.isEmpty(drugItems)) {
throw new BusinessException("退药药品列表不能为空");
}
// 1⃣ 创建退药汇总单(状态先设为 PENDING
DispensingSummary summary = new DispensingSummary();
summary.setOrderId(orderId);
summary.setDispenseTime(new Date());
summary.setStatus(DispenseStatus.PENDING);
summary.setCreateTime(new Date());
summary.setUpdateTime(new Date());
int inserted = dispensingSummaryMapper.insert(summary);
if (inserted != 1 || summary.getId() == null) {
throw new BusinessException("退药汇总单创建失败");
}
// 2⃣ 写入退药明细,关联 summaryId
for (DispensingDetail detail : drugItems) {
detail.setSummaryId(summary.getId());
detail.setDispenseTime(new Date());
detail.setStatus(DispenseStatus.PENDING);
detail.setCreateTime(new Date());
detail.setUpdateTime(new Date());
}
int detailCnt = dispensingDetailMapper.batchInsert(drugItems);
if (detailCnt != drugItems.size()) {
throw new BusinessException("退药明细写入不完整,期望 " + drugItems.size() + " 条,实际 " + detailCnt + "");
}
// 3⃣ 更新汇总单状态为 RETURNED
summary.setStatus(DispenseStatus.RETURNED);
summary.setUpdateTime(new Date());
int upd = dispensingSummaryMapper.updateStatusById(summary.getId(), DispenseStatus.RETURNED);
if (upd != 1) {
throw new BusinessException("退药汇总单状态更新失败");
}
logger.info("住院退药完成orderId={}, summaryId={}, 明细条数={}", orderId, summary.getId(), drugItems.size());
main.setOrderStatus(OrderStatus.VERIFIED.getCode());
main.setVerifyTime(new Date());
orderMainMapper.updateById(main);
}
// -----------------------------------------------------------------------
// 其余业务保持原有实现(未改动)
// -----------------------------------------------------------------------
@Transactional(readOnly = true)
@Override
public List<OrderMain> getPendingOrders(Long patientId, Integer pageNum, Integer pageSize) {
if (patientId == null) {
throw new BusinessException("患者 ID 不能为空");
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId) {
OrderMain main = orderMainMapper.selectById(orderId);
if (main == null) {
throw new BusinessException("医嘱不存在");
}
int pn = (pageNum == null || pageNum < 1) ? 1 : pageNum;
int ps = (pageSize == null || pageSize < 1) ? 20 : pageSize;
// 使用 PageHelper 进行分页,同时传递 offset/limit 给 Mapper
PageHelper.startPage(pn, ps);
List<OrderMain> list = orderMainMapper.selectPendingByPatientId(patientId);
return list;
main.setOrderStatus(OrderStatus.CANCELLED.getCode());
orderMainMapper.updateById(main);
}
// 其它方法(如退款、排号恢复等)保持不变,仅在需要时加入相同的事务控制
@Override
public Page<OrderMain> listOrdersByPatient(Long patientId, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
return orderMainMapper.selectByPatientId(patientId);
}
}