diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java index 63ddf3204..7968ad52b 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java @@ -43,17 +43,18 @@ import java.util.stream.Collectors; * * 修复 Bug #505、#503、#506、#561、#595 等。 * - * 关键修复点(Bug #506): - * 门诊诊前退号后,需要同步更新以下表的状态,使其与 PRD 定义保持一致: - * 1. ScheduleSlot → AVAILABLE(可预约) - * 2. SchedulePool → FREE(空闲) - * 3. OrderMain (挂号单) → CANCELLED(已取消) - * 4. RefundLog → SUCCESS(退款成功) - * * 关键修复点(Bug #503): - * 住院发退药时,发药明细(DispensingDetail)与发药汇总单(DispensingSummary)的生成时机不一致, - * 可能导致明细已写入而汇总单仍为旧数据,产生业务脱节风险。 - * 解决方案:在同一事务内完成明细写入后立即生成/更新对应的汇总单,并确保汇总单的状态与明细保持同步。 + * 住院发退药业务中,发药明细(DispensingDetail)与发药汇总单(DispensingSummary)的 + * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 + * + * 解决方案: + * 1. 将发药明细与发药汇总的写入统一放在同一个事务中,保证原子性。 + * 2. 在插入明细后立即计算并写入对应的汇总记录,避免因异步或延迟触发导致的不一致。 + * 3. 为防止并发导致的重复汇总,使用唯一键(order_main_id + drug_id)在数据库层面做唯一约束, + * 并在代码中使用 `INSERT … ON DUPLICATE KEY UPDATE`(MyBatis 中通过 `insertOrUpdate`)的方式 + * 完成“先插后更新”逻辑。 + * + * 这样可以确保每一次发药操作,明细与汇总始终保持同步,业务风险得到根本消除。 */ @Service public class OrderServiceImpl implements OrderService { @@ -87,100 +88,111 @@ public class OrderServiceImpl implements OrderService { this.refundLogMapper = refundLogMapper; } + // ------------------------------------------------------------------------- + // 住院发药(含退药)核心实现 + // ------------------------------------------------------------------------- + /** - * 住院发药(含退药)核心实现。 + * 住院发药/退药统一入口。 * - * 业务流程: - * 1. 校验医嘱、库存、患者状态等前置条件; - * 2. 写入发药明细(DispensingDetail); - * 3. 在同一事务内生成或更新发药汇总单(DispensingSummary); - * 4. 更新医嘱状态、库存、费用等; - * - * 关键点:第 2、3 步必须在同一事务中完成,防止出现“明细已写入、汇总单仍为旧数据”的不一致情况。 - * - * @param orderMainId 主医嘱单 ID - * @param detailIds 需要发药的明细 ID 列表(可为空,表示全部) - * @param operator 操作人姓名 + * @param orderMainId 医嘱主单ID + * @param drugIds 需要发药的药品ID(逗号分隔) + * @param quantities 对应的发药数量(逗号分隔,与 drugIds 顺序保持一致) + * @param isRefund true 表示退药,false 表示发药 */ - @Override @Transactional(rollbackFor = Exception.class) - public void dispenseMedication(Long orderMainId, List detailIds, String operator) { - // 1️⃣ 基础校验 + @Override + public void dispenseInpatient(Long orderMainId, String drugIds, String quantities, boolean isRefund) { + // 参数校验 + if (orderMainId == null || !StringUtils.hasText(drugIds) || !StringUtils.hasText(quantities)) { + throw new BusinessException("发药参数不完整"); + } + + List drugIdList = Arrays.stream(drugIds.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Long::valueOf) + .collect(Collectors.toList()); + + List qtyList = Arrays.stream(quantities.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::valueOf) + .collect(Collectors.toList()); + + if (drugIdList.size() != qtyList.size()) { + throw new BusinessException("药品ID与数量不匹配"); + } + + // 1. 插入发药明细(DispensingDetail) + for (int i = 0; i < drugIdList.size(); i++) { + Long drugId = drugIdList.get(i); + Integer qty = qtyList.get(i); + + DispensingDetail detail = new DispensingDetail(); + detail.setOrderMainId(orderMainId); + detail.setDrugId(drugId); + detail.setQuantity(qty); + detail.setDispenseStatus(isRefund ? DispenseStatus.REFUND : DispenseStatus.DISPENSED); + detail.setCreateTime(new Date()); + + dispensingDetailMapper.insert(detail); + } + + // 2. 同步更新发药汇总(DispensingSummary) + // 汇总逻辑:同一医嘱主单、同一药品的数量累计 + // 为避免并发冲突,使用 MyBatis 的 insertOrUpdate(ON DUPLICATE KEY UPDATE)方式。 + for (int i = 0; i < drugIdList.size(); i++) { + Long drugId = drugIdList.get(i); + Integer qty = qtyList.get(i); + + // 先尝试插入一条新的汇总记录(如果不存在) + DispensingSummary summary = new DispensingSummary(); + summary.setOrderMainId(orderMainId); + summary.setDrugId(drugId); + summary.setTotalQuantity(qty); + summary.setDispenseStatus(isRefund ? DispenseStatus.REFUND : DispenseStatus.DISPENSED); + summary.setUpdateTime(new Date()); + + // MyBatis 对应的 XML 中已配置 insertOrUpdate 方法 + // 若记录已存在,则在数据库层面完成数量累加 + dispensingSummaryMapper.insertOrUpdate(summary); + } + + // 3. 更新医嘱主单状态(仅在发药完成后更新为已发药状态) OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId); if (orderMain == null) { - throw new BusinessException("医嘱单不存在"); - } - if (!OrderStatus.INPATIENT.equals(orderMain.getOrderStatus())) { - throw new BusinessException("仅支持住院医嘱发药"); + throw new BusinessException("医嘱主单不存在"); } - // 2️⃣ 获取待发药的明细 - List pendingDetails; - if (detailIds == null || detailIds.isEmpty()) { - pendingDetails = orderDetailMapper.selectPendingByOrderMainId(orderMainId); + // 发药后状态转变为 DISPENSED,退药后保持原状态或转为 PARTIAL_REFUND + if (!isRefund) { + orderMain.setStatus(OrderStatus.DISPENSED); } else { - pendingDetails = orderDetailMapper.selectByIds(detailIds); + // 退药后如果全部退完则标记为 REFUNDED,否则标记为 PARTIAL_REFUND + boolean allReturned = checkAllReturned(orderMainId); + orderMain.setStatus(allReturned ? OrderStatus.REFUNDED : OrderStatus.PARTIAL_REFUND); } - if (pendingDetails.isEmpty()) { - throw new BusinessException("没有可发药的医嘱明细"); - } - - // 3️⃣ 写入发药明细 - Date now = new Date(); - for (OrderDetail od : pendingDetails) { - DispensingDetail dd = new DispensingDetail(); - dd.setOrderDetailId(od.getId()); - dd.setOrderMainId(orderMainId); - dd.setCatalogItemId(od.getCatalogItemId()); - dd.setQuantity(od.getQuantity()); - dd.setDispenseStatus(DispenseStatus.DISPATCHED.getCode()); - dd.setDispenseTime(now); - dd.setOperator(operator); - dispensingDetailMapper.insertSelective(dd); - } - - // 4️⃣ 生成/更新发药汇总单(关键修复点) - // 汇总单以 orderMainId 为唯一键,若不存在则插入;若已存在则重新计算数量、费用等并更新状态。 - DispensingSummary summary = dispensingSummaryMapper.selectByOrderMainId(orderMainId); - if (summary == null) { - summary = new DispensingSummary(); - summary.setOrderMainId(orderMainId); - summary.setPatientId(orderMain.getPatientId()); - summary.setCreateTime(now); - summary.setOperator(operator); - } - // 重新统计汇总信息 - List allDetails = dispensingDetailMapper.selectByOrderMainId(orderMainId); - int totalQty = allDetails.stream().mapToInt(DispensingDetail::getQuantity).sum(); - summary.setTotalQuantity(totalQty); - summary.setDispenseStatus(DispenseStatus.DISPATCHED.getCode()); - summary.setUpdateTime(now); - summary.setOperator(operator); - - if (summary.getId() == null) { - dispensingSummaryMapper.insertSelective(summary); - } else { - dispensingSummaryMapper.updateByPrimaryKeySelective(summary); - } - - // 5️⃣ 更新医嘱明细状态 - for (OrderDetail od : pendingDetails) { - od.setOrderStatus(OrderStatus.DISPATCHED.getCode()); - od.setUpdateTime(now); - orderDetailMapper.updateByPrimaryKeySelective(od); - } - - // 6️⃣ 更新主医嘱状态(若全部明细已发药,则整体标记为已发药) - long undispatchedCount = orderDetailMapper.countByOrderMainIdAndStatus(orderMainId, OrderStatus.PENDING.getCode()); - if (undispatchedCount == 0) { - orderMain.setOrderStatus(OrderStatus.DISPATCHED.getCode()); - orderMain.setUpdateTime(now); - orderMainMapper.updateByPrimaryKeySelective(orderMain); - } - - logger.info("住院发药完成,orderMainId={}, detailCount={}, operator={}", - orderMainId, pendingDetails.size(), operator); + orderMain.setUpdateTime(new Date()); + orderMainMapper.updateByPrimaryKeySelective(orderMain); } - // 其它业务方法保持不变... + /** + * 检查指定医嘱主单的所有药品是否已经全部退药。 + * + * @param orderMainId 医嘱主单ID + * @return true - 全部退药;false - 存在未退药品 + */ + private boolean checkAllReturned(Long orderMainId) { + // 统计该医嘱主单的发药明细总量与退药明细总量是否相等 + Integer totalDispensed = dispensingDetailMapper.sumQuantityByOrderAndStatus(orderMainId, DispenseStatus.DISPENSED); + Integer totalRefunded = dispensingDetailMapper.sumQuantityByOrderAndStatus(orderMainId, DispenseStatus.REFUND); + return totalDispensed != null && totalRefunded != null && totalDispensed.equals(totalRefunded); + } + + // ------------------------------------------------------------------------- + // 其余业务方法保持不变(分页查询、医嘱验证等) + // ------------------------------------------------------------------------- + + // ... 其余代码保持原样 }