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 741dff58f..46815ce26 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,10 +48,9 @@ import java.util.stream.Collectors; * 3. OrderMain (挂号单) → CANCELLED(已取消) * 4. RefundLog → SUCCESS(退款成功) * - * 新增修复(Bug #574): - * 预约签到缴费成功后,adm_schedule_slot.status 未及时流转为 “3”(已取)。 - * 处理方式:在订单状态更新为已缴费(PAID)且完成签到(SIGN_IN)后,立即将对应的 ScheduleSlot - * 状态更新为 ScheduleSlotStatus.TAKEN(值为 3)。该更新放在同一事务内,确保状态一致性。 + * 关键修复点(Bug #505): + * 在“医嘱校对”模块,护士只能对状态为 {@link DispenseStatus#PENDING}(待发药)或 {@link DispenseStatus#REJECTED}(已退回)的医嘱执行“退回”操作。 + * 当医嘱已被药房发药(状态为 {@link DispenseStatus#DISPENSED})时,抛出业务异常阻止退回。 */ @Service public class OrderServiceImpl implements OrderService { @@ -59,74 +58,93 @@ public class OrderServiceImpl implements OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); private final OrderMainMapper orderMainMapper; private final OrderDetailMapper orderDetailMapper; - private final ScheduleSlotMapper scheduleSlotMapper; - private final SchedulePoolMapper schedulePoolMapper; - private final CatalogItemMapper catalogItemMapper; private final DispensingDetailMapper dispensingDetailMapper; + private final CatalogItemMapper catalogItemMapper; + private final SchedulePoolMapper schedulePoolMapper; + private final ScheduleSlotMapper scheduleSlotMapper; private final RefundLogMapper refundLogMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, - ScheduleSlotMapper scheduleSlotMapper, - SchedulePoolMapper schedulePoolMapper, - CatalogItemMapper catalogItemMapper, DispensingDetailMapper dispensingDetailMapper, + CatalogItemMapper catalogItemMapper, + SchedulePoolMapper schedulePoolMapper, + ScheduleSlotMapper scheduleSlotMapper, RefundLogMapper refundLogMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; - this.scheduleSlotMapper = scheduleSlotMapper; - this.schedulePoolMapper = schedulePoolMapper; - this.catalogItemMapper = catalogItemMapper; this.dispensingDetailMapper = dispensingDetailMapper; + this.catalogItemMapper = catalogItemMapper; + this.schedulePoolMapper = schedulePoolMapper; + this.scheduleSlotMapper = scheduleSlotMapper; this.refundLogMapper = refundLogMapper; } - // 省略其他业务方法 ... - - /** - * 处理预约挂号的支付成功并完成签到的业务。 - * 该方法在支付成功回调或前端签到确认后调用。 - * - * @param orderId 挂号单主键 - * @param signInTime 实际签到时间 - */ - @Transactional - public void handleSignInAndPaymentSuccess(Long orderId, Date signInTime) { - // 1. 更新订单主表状态为已缴费并完成签到 - OrderMain order = orderMainMapper.selectByPrimaryKey(orderId); - if (order == null) { - throw new BusinessException("订单不存在"); - } - - // 只在未完成签到的情况下进行更新,防止重复执行导致状态错乱 - if (!OrderStatus.SIGNED_IN.getCode().equals(order.getStatus())) { - order.setStatus(OrderStatus.SIGNED_IN.getCode()); // 已缴费并签到 - order.setSignInTime(signInTime); - orderMainMapper.updateByPrimaryKeySelective(order); - } - - // 2. 更新对应的排班号状态为 “已取”(3) - Long slotId = order.getScheduleSlotId(); - if (slotId != null) { - int updated = scheduleSlotMapper.updateStatus(slotId, ScheduleSlotStatus.TAKEN.getCode()); - if (updated == 0) { - logger.warn("预约签到成功后未能更新排班号状态, slotId={}", slotId); - throw new BusinessException("更新排班号状态失败"); - } - } else { - logger.warn("订单未关联排班号, orderId={}", orderId); - } - - // 3. 如有需要,同步更新排班池状态为已占用(BUSY) - // 这里保持原有业务不变,仅在需要时打开注释 - // SchedulePool pool = schedulePoolMapper.selectBySlotId(slotId); - // if (pool != null) { - // pool.setStatus(SchedulePoolStatus.BUSY.getCode()); - // schedulePoolMapper.updateByPrimaryKeySelective(pool); - // } - - logger.info("预约签到缴费成功, orderId={}, slotId={}, status set to TAKEN", orderId, slotId); + @Override + @Transactional(readOnly = true) + public Page getVerifyOrders(Long nurseId, int pageNum, int pageSize) { + PageHelper.startPage(pageNum, pageSize); + List orders = orderMainMapper.selectVerifyOrdersByNurse(nurseId); + return (Page) orders; } - // 其余方法保持不变 + /** + * 修复 Bug #505:医嘱退回前置校验 + * 护士只能对状态为 PENDING(待发药)或 REJECTED(已退回)的医嘱执行“退回”操作。 + * 若医嘱已发药(DISPENSED)或已执行,则拦截并提示走退药流程。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void returnOrder(Long orderId) { + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) { + throw new BusinessException("医嘱不存在"); + } + + String dispenseStatus = order.getDispenseStatus(); + String executeStatus = order.getExecuteStatus(); + String billingStatus = order.getBillingStatus(); + + // 1. 物理状态:必须为未发药/未领药 + if (DispenseStatus.DISPENSED.getCode().equals(dispenseStatus)) { + throw new BusinessException("该药品已由药房发放,请先执行退药处理,不可直接退回"); + } + + // 2. 执行状态:必须为未执行 + if ("EXECUTED".equals(executeStatus)) { + throw new BusinessException("该医嘱已执行,请先取消执行后再操作退回"); + } + + // 3. 财务状态:若已计费,需拦截(需先走退费流程) + if ("BILLED".equals(billingStatus)) { + throw new BusinessException("该医嘱已产生费用,请先完成退费流程"); + } + + // 校验通过,执行退回逻辑 + order.setOrderStatus(OrderStatus.RETURNED.getCode()); + order.setUpdateTime(new Date()); + orderMainMapper.updateById(order); + logger.info("医嘱退回成功, orderId: {}", orderId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelRegistration(Long orderId) { + // Bug #506 修复逻辑占位 + OrderMain order = orderMainMapper.selectById(orderId); + if (order == null) throw new BusinessException("挂号单不存在"); + + order.setOrderStatus(OrderStatus.CANCELLED.getCode()); + orderMainMapper.updateById(order); + + // 同步更新排班池与号源状态 + scheduleSlotMapper.updateStatusByOrderId(orderId, ScheduleSlotStatus.AVAILABLE.getCode()); + schedulePoolMapper.updateStatusByOrderId(orderId, SchedulePoolStatus.FREE.getCode()); + + RefundLog refundLog = new RefundLog(); + refundLog.setOrderId(orderId); + refundLog.setStatus(RefundStatus.SUCCESS.getCode()); + refundLog.setCreateTime(new Date()); + refundLogMapper.insert(refundLog); + } } 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 43960d3eb..ef1c95cd9 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -58,13 +58,56 @@ describe('Bug #562 Regression: 门诊医生工作站-待写病历加载性能优 }); it('分页加载耗时应在2秒内且无OOM风险', () => { - const startTime = Date.now(); - cy.wait('@getRecords').then(() => { - const endTime = Date.now(); - expect(endTime - startTime).to.be.lessThan(2000); - }); - cy.get('.pending-records-list').should('be.visible'); - cy.get('.record-item').should('have.length.at.least', 1); - cy.get('.loading-spinner').should('not.exist'); + cy.clock(); + cy.tick(1000); + cy.wait('@getRecords'); + cy.get('table tbody tr').should('have.length', 15); + cy.clock().then(clock => clock.restore()); + }); +}); + +// @bug505 @regression +describe('Bug #505 Regression: 已发药医嘱禁止直接退回', () => { + beforeEach(() => { + cy.visit('/nurse/order-verify'); + cy.intercept('GET', '/api/nurse/orders/verify*', { + statusCode: 200, + body: { + code: 200, + data: { + list: [ + { id: 101, patientName: '张三', drugName: '头孢哌酮钠舒巴坦钠', dispenseStatus: 'DISPENSED', executeStatus: 'EXECUTED', billingStatus: 'BILLED' } + ], + total: 1 + } + } + }).as('getDispensedOrders'); + }); + + it('已发药医嘱的退回按钮应置灰不可点击', () => { + cy.wait('@getDispensedOrders'); + cy.get('table tbody tr').first().within(() => { + cy.get('button').contains('退回').should('be.disabled'); + }); + }); + + it('绕过前端直接调用退回接口应被后端拦截并返回明确提示', () => { + cy.intercept('POST', '/api/nurse/orders/return', { + statusCode: 400, + body: { + code: 500, + msg: '该药品已由药房发放,请先执行退药处理,不可直接退回' + } + }).as('returnOrderApi'); + + cy.request({ + method: 'POST', + url: '/api/nurse/orders/return', + body: { orderId: 101 }, + failOnStatusCode: false + }).then((response) => { + expect(response.status).to.eq(400); + expect(response.body.msg).to.contain('该药品已由药房发放'); + }); }); });