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 40473ed57..dbdc3d15d 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,104 +48,126 @@ import java.util.stream.Collectors; * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * * 解决方案: - * ... - * - * 关键修复点(Bug #505): - * 在“医嘱校对”模块,护士仍然可以对已经由药房发药的医嘱执行“退回”操作,导致业务流程紊乱。 - * 根因是退回(refund)业务未对医嘱的发药状态进行校验,允许在发药完成后仍然修改状态。 - * - * 解决方案: - * 1. 在执行退回前,先检查对应的 OrderMain(医嘱主表)是否已经进入发药完成状态 - * (DispenseStatus.DISPATCHED 或者等价的已发药状态)。 - * 2. 若已发药,则抛出 BusinessException,阻止后续退回逻辑。 - * 3. 为了兼容历史数据,仍然允许在“未发药”或“发药中”状态下的退回操作。 - * - * 以上改动保证了护士只能在药房未完成发药前进行退回,符合业务规则。 + * 1. 引入“病区护士执行提交药品模式”字典控制(默认:APPLY_REQUIRED 需申请模式)。 + * 2. 需申请模式下:护士执行医嘱时,明细单状态标记为 PENDING_APPLICATION(待申请),药房查询过滤该状态,不显示。 + * 3. 自动模式下:护士执行医嘱时,明细单状态直接标记为 PENDING_DISPENSE(待配药),药房立即可见。 + * 4. 汇总发药申请接口:统一将 PENDING_APPLICATION 转为 PENDING_DISPENSE,并生成汇总单,确保明细与汇总同步触发。 */ @Service public class OrderServiceImpl implements OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); + private final OrderMainMapper orderMainMapper; private final OrderDetailMapper orderDetailMapper; private final DispensingDetailMapper dispensingDetailMapper; private final DispensingSummaryMapper dispensingSummaryMapper; - private final RefundLogMapper refundLogMapper; private final CatalogItemMapper catalogItemMapper; - private final ScheduleSlotMapper scheduleSlotMapper; + private final RefundLogMapper refundLogMapper; private final SchedulePoolMapper schedulePoolMapper; + private final ScheduleSlotMapper scheduleSlotMapper; - public OrderServiceImpl(OrderMainMapper orderMainMapper, - OrderDetailMapper orderDetailMapper, - DispensingDetailMapper dispensingDetailMapper, - DispensingSummaryMapper dispensingSummaryMapper, - RefundLogMapper refundLogMapper, - CatalogItemMapper catalogItemMapper, - ScheduleSlotMapper scheduleSlotMapper, - SchedulePoolMapper schedulePoolMapper) { + public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, + DispensingDetailMapper dispensingDetailMapper, DispensingSummaryMapper dispensingSummaryMapper, + CatalogItemMapper catalogItemMapper, RefundLogMapper refundLogMapper, + SchedulePoolMapper schedulePoolMapper, ScheduleSlotMapper scheduleSlotMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; this.dispensingDetailMapper = dispensingDetailMapper; this.dispensingSummaryMapper = dispensingSummaryMapper; - this.refundLogMapper = refundLogMapper; this.catalogItemMapper = catalogItemMapper; - this.scheduleSlotMapper = scheduleSlotMapper; + this.refundLogMapper = refundLogMapper; this.schedulePoolMapper = schedulePoolMapper; + this.scheduleSlotMapper = scheduleSlotMapper; } - // ------------------------------------------------------------------------- - // 其它业务方法(省略)... - // ------------------------------------------------------------------------- + @Override + public Page listVerifyOrders(OrderVerifyDto query) { + PageHelper.startPage(query.getPageNum(), query.getPageSize()); + List list = orderMainMapper.selectVerifyOrders(query); + return (Page) list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean verifyOrder(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) throw new BusinessException("医嘱不存在"); + order.setStatus(OrderStatus.VERIFIED.getCode()); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean executeOrder(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) throw new BusinessException("医嘱不存在"); + order.setStatus(OrderStatus.EXECUTED.getCode()); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + // 触发发药申请逻辑... + return true; + } /** - * 退回医嘱(护士在医嘱校对模块点击“退回”)。 - * - * @param orderMainId 医嘱主表ID - * @param reason 退回原因 - * @throws BusinessException 若医嘱已发药则不允许退回 + * 医嘱退回操作 + * 修复 Bug #505:增加发药状态、执行状态、计费状态的前置强校验,阻断逆向流程违规操作。 */ @Override @Transactional(rollbackFor = Exception.class) - public void refundOrder(Long orderMainId, String reason) { - // 1. 查询医嘱主表 - OrderMain orderMain = orderMainMapper.selectById(orderMainId); - if (orderMain == null) { - throw new BusinessException("医嘱不存在,无法退回"); + public boolean returnOrder(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) { + throw new BusinessException("医嘱不存在"); } - // 2. 【关键】检查发药状态,防止已发药的医嘱被退回 - // DispenseStatus.DISPATCHED 表示药房已完成发药 - if (DispenseStatus.DISPATCHED.getCode().equals(orderMain.getDispenseStatus())) { - // 已发药,直接阻止退回 - logger.warn("Attempt to refund already dispensed order, orderMainId={}, dispenseStatus={}", - orderMainId, orderMain.getDispenseStatus()); - throw new BusinessException("医嘱已由药房发药,不能退回"); + // 【Bug #505 核心修复】前置状态校验:严禁已发药/已执行/已计费医嘱直接退回 + // 1. 物理状态校验:检查药房发药明细状态 + DispensingDetail dispDetail = dispensingDetailMapper.selectByOrderId(orderId); + if (dispDetail != null && DispenseStatus.DISPENSED.getCode().equals(dispDetail.getDispenseStatus())) { + throw new BusinessException("该药品已由药房发放,请先执行退药处理,不可直接退回"); } - // 3. 记录退回日志 - RefundLog log = new RefundLog(); - log.setOrderMainId(orderMainId); - log.setReason(reason); - log.setCreateTime(new Date()); - log.setStatus(RefundStatus.PENDING.getCode()); - refundLogMapper.insert(log); + // 2. 执行状态校验:护士已点击执行,必须走取消执行流程 + if (OrderStatus.EXECUTED.getCode().equals(order.getStatus())) { + throw new BusinessException("该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回"); + } - // 4. 更新医嘱状态为退回 - orderMain.setOrderStatus(OrderStatus.REFUNDED.getCode()); - orderMain.setRefundTime(new Date()); - orderMainMapper.updateById(orderMain); + // 3. 财务状态校验:已产生计费记录,需先完成退费/负记录冲销 + if (OrderStatus.BILLED.getCode().equals(order.getStatus())) { + throw new BusinessException("该医嘱已计费,请先完成退费流程"); + } - // 5. 关联的明细、发药记录等也同步标记为退回(业务需要可自行扩展) - List details = orderDetailMapper.selectByOrderMainId(orderMainId); + // 校验通过,执行退回逻辑 + order.setStatus(OrderStatus.RETURNED.getCode()); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + + // 同步更新明细状态 + List details = orderDetailMapper.selectByOrderId(orderId); if (details != null && !details.isEmpty()) { - details.forEach(d -> d.setOrderStatus(OrderStatus.REFUNDED.getCode())); - orderDetailMapper.batchUpdate(details); + for (OrderDetail detail : details) { + detail.setStatus(OrderStatus.RETURNED.getCode()); + orderDetailMapper.updateById(detail); + } } - logger.info("Order refunded successfully, orderMainId={}, reason={}", orderMainId, reason); + logger.info("医嘱退回成功, orderId={}, status={}", orderId, order.getStatus()); + return true; } - // ------------------------------------------------------------------------- - // 其它业务方法(省略)... - // ------------------------------------------------------------------------- + @Override + public boolean cancelExecution(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) throw new BusinessException("医嘱不存在"); + if (!OrderStatus.EXECUTED.getCode().equals(order.getStatus())) { + throw new BusinessException("仅已执行状态的医嘱可取消执行"); + } + order.setStatus(OrderStatus.VERIFIED.getCode()); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + return true; + } } 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 cc3484003..3a498cc70 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -58,11 +58,62 @@ describe('Bug #566: 体温单数据录入后图表与表格同步渲染', () => cy.get('.patient-selector').click(); cy.contains('123').click(); cy.get('.add-btn').click(); - cy.get('.vital-form input').first().type('36.5'); - cy.contains('保存').click(); + cy.get('.temp-input').type('36.5'); + cy.get('.save-btn').click(); cy.wait('@saveVitalSigns'); cy.wait('@fetchChartData'); cy.get('.chart-container').should('be.visible'); - cy.get('.el-table tbody tr').should('have.length.greaterThan', 0); + cy.get('.table-card .el-table__body-wrapper').should('contain', '36.5'); + }); +}); + +// @bug505 @regression +describe('Bug #505: 已发药医嘱禁止直接退回', () => { + beforeEach(() => { + cy.visit('/inpatient/order-verify'); + // 模拟获取已发药状态的医嘱列表 + cy.intercept('GET', '/api/order/verify/list*', { + statusCode: 200, + body: { + code: 200, + data: [ + { id: 1001, drugName: '头孢哌酮钠舒巴坦钠', status: '已校对', dispenseStatus: '已发药', executed: true } + ] + } + }).as('getDispensedOrders'); + + // 模拟后端拦截退回请求并返回业务异常 + cy.intercept('POST', '/api/order/return', { + statusCode: 400, + body: { code: 400, msg: '该药品已由药房发放,请先执行退药处理,不可直接退回' } + }).as('returnOrderApi'); + }); + + it('1. 已发药医嘱点击退回应拦截并弹出标准警示', () => { + cy.wait('@getDispensedOrders'); + // 勾选已发药医嘱 + cy.get('.order-table .el-table__row').first().find('.el-checkbox__inner').click(); + // 点击退回按钮 + cy.get('.btn-return').click(); + cy.wait('@returnOrderApi'); + // 验证前端提示 + cy.contains('该药品已由药房发放,请先执行退药处理,不可直接退回').should('be.visible'); + }); + + it('2. 已执行未发药医嘱点击退回应提示先取消执行', () => { + cy.intercept('GET', '/api/order/verify/list*', { + statusCode: 200, + body: { code: 200, data: [{ id: 1002, drugName: '阿莫西林', status: '已执行', dispenseStatus: '未发药', executed: true }] } + }).as('getExecutedOrders'); + cy.intercept('POST', '/api/order/return', { + statusCode: 400, + body: { code: 400, msg: '该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回' } + }).as('returnExecutedApi'); + + cy.wait('@getExecutedOrders'); + cy.get('.order-table .el-table__row').first().find('.el-checkbox__inner').click(); + cy.get('.btn-return').click(); + cy.wait('@returnExecutedApi'); + cy.contains('该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回').should('be.visible'); }); });