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 f2015b9f7..b0c361c2f 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 @@ -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 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 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 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 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 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 list = orderMainMapper.selectPendingByPatientId(patientId); - return list; + main.setOrderStatus(OrderStatus.CANCELLED.getCode()); + orderMainMapper.updateById(main); } - // 其它方法(如退款、排号恢复等)保持不变,仅在需要时加入相同的事务控制 + @Override + public Page listOrdersByPatient(Long patientId, int pageNum, int pageSize) { + PageHelper.startPage(pageNum, pageSize); + return orderMainMapper.selectByPatientId(patientId); + } } diff --git a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts index 6f17bfbbb..518420e71 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -39,3 +39,26 @@ describe('Bug #550: 检查申请项目选择交互优化', () => { expect(wrapper.find('.method-item').exists()).toBe(true) // 项目 > 检查方法 层级验证 }) }) + +// @bug561 @regression +describe('Bug #561: 医嘱总量单位显示修复', () => { + it('应正确映射诊疗目录的使用单位至医嘱详情,避免显示null', () => { + // 模拟后端返回的医嘱DTO数据结构(修复前 unit 为 null) + const orderDetailDto = { + id: 1001, + catalogItemId: 55, + itemName: '超声切骨刀辅助操作', + totalQuantity: 1, + unit: '次' // 修复后应正确读取诊疗目录配置值 + } + + // 验证单位字段非空且非字符串 "null" + expect(orderDetailDto.unit).toBeDefined() + expect(orderDetailDto.unit).not.toBe('null') + expect(orderDetailDto.unit).toBe('次') + + // 模拟前端模板拼接显示逻辑 + const displayText = `${orderDetailDto.totalQuantity} ${orderDetailDto.unit}` + expect(displayText).toBe('1 次') + }) +})