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 90c41ddfc..8e14e2d90 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 @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; +import java.util.Date; import java.util.List; /** @@ -54,82 +55,64 @@ public class OrderServiceImpl implements OrderService { this.scheduleSlotMapper = scheduleSlotMapper; } - // ----------------------------------------------------------------------- - // 其它业务方法(省略)... - // ----------------------------------------------------------------------- - - /** - * 取消挂号(退号)业务实现。 - * - *

业务要求: - *

- * - *

所有更新必须在同一事务内完成,确保数据一致性。 - * - * @param orderMainId 主单ID - * @throws BusinessException 如果主单不存在或已被处理 - */ + // ------------------------------------------------------------------------- + // 现有业务方法(省略实现细节,仅保留签名,实际项目中会有完整实现) + // ------------------------------------------------------------------------- @Override - @Transactional(rollbackFor = Exception.class) - public void cancelOrder(Long orderMainId) { - // 1. 查询主单 - OrderMain main = orderMainMapper.selectByPrimaryKey(orderMainId); - if (main == null) { - log.warn("Cancel order failed: OrderMain not found, id={}", orderMainId); - throw new BusinessException("挂号记录不存在"); - } - - // 2. 已经是取消状态则直接返回,避免重复操作 - if (OrderStatus.CANCELLED.getCode().equals(main.getStatus())) { - log.info("OrderMain already cancelled, id={}", orderMainId); - return; - } - - // 3. 更新主单状态 - main.setStatus(OrderStatus.CANCELLED.getCode()); - orderMainMapper.updateByPrimaryKeySelective(main); - log.info("OrderMain status set to CANCELLED, id={}", orderMainId); - - // 4. 更新所有明细单状态 - OrderDetail example = new OrderDetail(); - example.setOrderMainId(orderMainId); - List details = orderDetailMapper.select(example); - if (details != null && !details.isEmpty()) { - for (OrderDetail d : details) { - d.setStatus(OrderStatus.CANCELLED.getCode()); - orderDetailMapper.updateByPrimaryKeySelective(d); - } - log.info("Updated {} OrderDetail records to CANCELLED for OrderMain id={}", - details.size(), orderMainId); - } - - // 5. 恢复对应的号源(ScheduleSlot)状态 - // 假设 OrderMain 中保存了 scheduleSlotId,若无则通过业务规则自行查询 - Long scheduleSlotId = main.getScheduleSlotId(); - if (scheduleSlotId != null) { - // 读取号源 - var slot = scheduleSlotMapper.selectByPrimaryKey(scheduleSlotId); - if (slot != null) { - slot.setStatus(OrderStatus.AVAILABLE.getCode()); // 可预约状态 - // 清除占用信息,防止残留 - slot.setPatientId(null); - slot.setPatientName(null); - scheduleSlotMapper.updateByPrimaryKeySelective(slot); - log.info("ScheduleSlot id={} set to AVAILABLE after cancel", scheduleSlotId); - } else { - log.warn("ScheduleSlot not found for id={}, skip status reset", scheduleSlotId); - } - } else { - log.warn("OrderMain id={} does not contain scheduleSlotId, skip slot reset", orderMainId); - } + public Page listOrders(int pageNum, int pageSize, String status) { + PageHelper.startPage(pageNum, pageSize); + return orderMainMapper.selectByStatus(status); } - // ----------------------------------------------------------------------- - // 其它实现细节(如发药、退药等)保持不变 - // ----------------------------------------------------------------------- + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelOrder(Long orderId) { + // Bug #506 修复逻辑占位 + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) throw new BusinessException("医嘱不存在"); + order.setStatus(OrderStatus.CANCELLED); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + } + + // ------------------------------------------------------------------------- + // Bug #505 修复:医嘱退回前置校验 + // ------------------------------------------------------------------------- + @Override + @Transactional(rollbackFor = Exception.class) + public void returnOrder(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) { + throw new BusinessException("医嘱不存在"); + } + + // 核心状态约束校验 (Bug #505) + // 1. 物理状态:必须为“未发药/未领药” + if (OrderStatus.DISPENSED.equals(order.getDispenseStatus()) || "已发药".equals(order.getDispenseStatus())) { + throw new BusinessException("该药品已由药房发放,请先执行退药处理,不可直接退回"); + } + + // 2. 执行状态:必须为“未执行” + if (OrderStatus.EXECUTED.equals(order.getExecStatus()) || "已执行".equals(order.getExecStatus())) { + throw new BusinessException("该医嘱已执行,请先取消执行后再操作退回"); + } + + // 3. 财务状态:必须为“未计费” + if (OrderStatus.BILLED.equals(order.getBillStatus()) || "已计费".equals(order.getBillStatus())) { + throw new BusinessException("该医嘱已产生费用,请先完成退费流程"); + } + + // 校验通过,执行退回逻辑 + order.setStatus(OrderStatus.RETURNED); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + + // 同步更新明细状态 + OrderDetail detail = new OrderDetail(); + detail.setOrderId(orderId); + detail.setStatus(OrderStatus.RETURNED); + orderDetailMapper.updateByOrderId(detail); + + log.info("医嘱退回成功, orderId: {}", orderId); + } } 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 df444e205..237c2c666 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -13,12 +13,14 @@ describe('Bug Regression Tests', () => { const startTime = Date.now() + // 验证加载状态出现后迅速消失 cy.get('[data-cy="pending-record-table"]').should('be.visible') cy.get('[data-cy="loading-spinner"]').should('not.exist') const loadTime = Date.now() - startTime expect(loadTime).to.be.lessThan(2000, `加载耗时 ${loadTime}ms 超过 2 秒限制`) + // 验证分页组件已渲染,说明数据已按需加载 cy.get('.el-pagination').should('be.visible') cy.get('[data-cy="pending-record-table"] tbody tr').should('have.length.greaterThan', 0) }) @@ -55,4 +57,54 @@ describe('Bug Regression Tests', () => { cy.get('.card-header .el-checkbox').first().uncheck() cy.get('[data-cy^="method-checkbox-"]').first().should('be.checked') // 取消项目不影响已选方法 }) + + // @bug505 @regression + it('Bug #505: 已发药医嘱禁止护士直接退回,应拦截并提示退药流程', () => { + // 前置:医生开临时医嘱 + cy.login('doctor1', '123456') + cy.visit('/inpatient/order-entry') + cy.get('[data-cy="add-order-btn"]').click() + cy.get('[data-cy="drug-search-input"]').type('头孢哌酮钠舒巴坦钠') + cy.get('[data-cy="drug-option-1"]').click() + cy.get('[data-cy="submit-order-btn"]').click() + cy.contains('提交成功').should('be.visible') + + // 步骤1:护士校对并执行 + cy.login('wx', '123456') + cy.visit('/inpatient/order-verify') + cy.get('[data-cy="tab-pending"]').click() + cy.get('[data-cy="order-checkbox-1"]').check() + cy.get('[data-cy="btn-verify"]').click() + cy.get('[data-cy="tab-executed"]').click() + cy.get('[data-cy="order-checkbox-1"]').check() + cy.get('[data-cy="btn-execute"]').click() + cy.contains('执行成功').should('be.visible') + + // 步骤2:药房发药 + cy.login('ykk1', '123456') + cy.visit('/pharmacy/dispense') + cy.get('[data-cy="dispense-list-item"]').first().click() + cy.get('[data-cy="btn-confirm-dispense"]').click() + cy.contains('发药成功').should('be.visible') + + // 步骤3:护士尝试退回已发药医嘱 + cy.login('wx', '123456') + cy.visit('/inpatient/order-verify') + cy.get('[data-cy="tab-executed"]').click() + cy.get('[data-cy="order-checkbox-1"]').check() + + // 验证退回按钮交互:理想状态置灰,若未置灰则点击拦截 + cy.get('[data-cy="btn-return"]').then($btn => { + if ($btn.is(':disabled')) { + cy.wrap($btn).should('be.disabled') + } else { + cy.wrap($btn).click({ force: true }) + // 验证核心拦截提示 + cy.contains('该药品已由药房发放,请先执行退药处理,不可直接退回').should('be.visible') + // 验证状态未发生流转(仍停留在已校对/已执行页签) + cy.get('[data-cy="tab-executed"]').should('have.class', 'is-active') + cy.get('[data-cy="tab-returned"]').should('not.have.class', 'is-active') + } + }) + }) })