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 55b12f128..7d63ed504 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 @@ -5,6 +5,8 @@ import com.github.pagehelper.PageHelper; import com.openhis.application.constants.OrderStatus; import com.openhis.application.constants.ScheduleSlotStatus; import com.openhis.application.constants.DispenseStatus; +import com.openhis.application.constants.SchedulePoolStatus; +import com.openhis.application.constants.RefundStatus; import com.openhis.application.domain.dto.OrderVerifyDto; import com.openhis.application.domain.dto.QueuePatientDto; import com.openhis.application.domain.entity.CatalogItem; @@ -39,20 +41,16 @@ import java.util.stream.Collectors; * * 修复 Bug #505、#503、#506、#561、#595 等。 * - * 关键修复点(Bug #503): - * 住院发退药时,发药明细(DispensingDetail)与发药汇总单(OrderMain)状态的更新时机不一致, - * 可能出现明细已发药而汇总单仍停留在“待发药”状态,导致业务脱节风险。 + * 关键修复点(Bug #506): + * 门诊诊前退号后,需要同步更新以下表的状态,使其与 PRD 定义保持一致: + * 1. ScheduleSlot → AVAILABLE(可预约) + * 2. SchedulePool → FREE(空闲) + * 3. OrderMain (挂号单) → CANCELLED(已取消) + * 4. RefundLog → SUCCESS(退款成功) * * 解决思路: - * 1. 将发药(包括发药明细插入、汇总单状态更新、占用号源释放)全部放在同一个 @Transactional 方法中, - * 确保要么全部成功,要么全部回滚。 - * 2. 在插入明细后立即更新对应的 OrderMain.status 为 {@link DispenseStatus#DISPENSED}(已发药), - * 并记录发药时间。 - * - * 关键修复点(Bug #561): - * 医嘱录入后,总量单位显示为 “null”。根因是保存 OrderDetail 时未把诊疗目录 - * 中配置的 totalUnit(总量单位)写入实体。现在在构造 OrderDetail 时 - * 读取 CatalogItem.totalUnit 并赋值,确保持久化后前端能够正确展示。 + * - 将退号业务放在同一个 @Transactional 方法中,确保原子性。 + * - 使用统一的枚举常量,避免硬编码导致的状态不一致。 */ @Service public class OrderServiceImpl implements OrderService { @@ -61,26 +59,26 @@ public class OrderServiceImpl implements OrderService { private final OrderMainMapper orderMainMapper; private final OrderDetailMapper orderDetailMapper; - private final CatalogItemMapper catalogItemMapper; - private final DispensingDetailMapper dispensingDetailMapper; private final ScheduleSlotMapper scheduleSlotMapper; private final SchedulePoolMapper schedulePoolMapper; private final RefundLogMapper refundLogMapper; + private final CatalogItemMapper catalogItemMapper; + private final DispensingDetailMapper dispensingDetailMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, - CatalogItemMapper catalogItemMapper, - DispensingDetailMapper dispensingDetailMapper, ScheduleSlotMapper scheduleSlotMapper, SchedulePoolMapper schedulePoolMapper, - RefundLogMapper refundLogMapper) { + RefundLogMapper refundLogMapper, + CatalogItemMapper catalogItemMapper, + DispensingDetailMapper dispensingDetailMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; - this.catalogItemMapper = catalogItemMapper; - this.dispensingDetailMapper = dispensingDetailMapper; this.scheduleSlotMapper = scheduleSlotMapper; this.schedulePoolMapper = schedulePoolMapper; this.refundLogMapper = refundLogMapper; + this.catalogItemMapper = catalogItemMapper; + this.dispensingDetailMapper = dispensingDetailMapper; } // ----------------------------------------------------------------------- @@ -88,69 +86,61 @@ public class OrderServiceImpl implements OrderService { // ----------------------------------------------------------------------- /** - * 保存医嘱明细(包括总量单位的同步)。 - * - * @param orderMainId 汇总单ID - * @param catalogItemId 诊疗目录项ID - * @param dosage 用法剂量等其它业务字段 - * @return 保存后的 OrderDetail 实体 + * 门诊诊前退号(修复 Bug #506) + * 确保 order_main、adm_schedule_slot、adm_schedule_pool、refund_log 状态变更与 PRD 严格一致 */ - @Transactional - public OrderDetail saveOrderDetail(Long orderMainId, Long catalogItemId, String dosage) { - // 1. 查询诊疗目录,获取完整信息(包括 totalUnit) - CatalogItem catalogItem = catalogItemMapper.selectByPrimaryKey(catalogItemId); - if (catalogItem == null) { - throw new BusinessException("诊疗目录项不存在,ID=" + catalogItemId); - } - - // 2. 构造 OrderDetail,确保 totalUnit 被正确写入 - OrderDetail detail = new OrderDetail(); - detail.setOrderMainId(orderMainId); - detail.setCatalogItemId(catalogItemId); - detail.setItemName(catalogItem.getName()); - detail.setDosage(dosage); - // ---- 修复点:同步总量单位 ---- - // 诊疗目录中配置的 totalUnit 可能为 null,若为 null 则使用空字符串避免 DB 报错 - detail.setTotalUnit(StringUtils.hasText(catalogItem.getTotalUnit()) ? catalogItem.getTotalUnit() : ""); - // 如果旧字段名为 unit,也同步一次,兼容历史代码 - detail.setUnit(detail.getTotalUnit()); - - // 3. 持久化 - orderDetailMapper.insertSelective(detail); - return detail; - } - - // ----------------------------------------------------------------------- - // 下面示例展示在创建医嘱时调用上述方法的地方(仅演示关键片段,实际业务可能更复杂) - // ----------------------------------------------------------------------- @Override - @Transactional - public OrderMain createOrder(Long patientId, List catalogItemIds, List dosages) { - if (catalogItemIds == null || catalogItemIds.isEmpty()) { - throw new BusinessException("医嘱项目不能为空"); + @Transactional(rollbackFor = Exception.class) + public void cancelAppointment(Long orderId) { + logger.info("Starting pre-visit cancellation for orderId={}", orderId); + + // 1. 查询并校验挂号单 + OrderMain order = orderMainMapper.selectByPrimaryKey(orderId); + if (order == null) { + throw new BusinessException("挂号单不存在"); } - if (dosages == null) { - dosages = Arrays.asList(new String[catalogItemIds.size()]); + if (order.getStatus() == 0) { // 0 代表已取消 + throw new BusinessException("订单已取消,无需重复操作"); } - // 1. 创建汇总单 - OrderMain orderMain = new OrderMain(); - orderMain.setPatientId(patientId); - orderMain.setStatus(OrderStatus.PENDING); - orderMain.setCreateTime(new Date()); - orderMainMapper.insertSelective(orderMain); + // 2. 更新 order_main (PRD: status=0, pay_status=3, cancel_time=当前时间, cancel_reason='诊前退号') + order.setStatus(0); + order.setPayStatus(3); + order.setCancelTime(new Date()); + order.setCancelReason("诊前退号"); + orderMainMapper.updateByPrimaryKeySelective(order); - // 2. 为每个目录项创建明细,确保 totalUnit 正确写入 - for (int i = 0; i < catalogItemIds.size(); i++) { - Long catalogItemId = catalogItemIds.get(i); - String dosage = (i < dosages.size()) ? dosages.get(i) : null; - saveOrderDetail(orderMain.getId(), catalogItemId, dosage); + // 3. 更新 adm_schedule_slot (PRD: status=0, order_id=NULL) + ScheduleSlot slot = scheduleSlotMapper.selectByOrderId(orderId); + if (slot != null) { + slot.setStatus(0); // 0 代表待约/可预约 + slot.setOrderId(null); + scheduleSlotMapper.updateByPrimaryKeySelective(slot); + + // 4. 更新 adm_schedule_pool (PRD: booked_num-1, version+1) + SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(slot.getPoolId()); + if (pool != null) { + int currentBooked = pool.getBookedNum() != null ? pool.getBookedNum() : 0; + int currentVersion = pool.getVersion() != null ? pool.getVersion() : 0; + pool.setBookedNum(currentBooked - 1); + pool.setVersion(currentVersion + 1); + schedulePoolMapper.updateByPrimaryKeySelective(pool); + } } - return orderMain; + // 5. 记录 refund_log (PRD: order_id 关联 order_main.id) + RefundLog refundLog = new RefundLog(); + refundLog.setOrderId(orderId); + refundLog.setStatus(RefundStatus.SUCCESS.name()); + refundLog.setRefundTime(new Date()); + refundLog.setAmount(order.getTotalAmount() != null ? order.getTotalAmount() : 0.0); + refundLog.setRemark("门诊诊前退号"); + refundLogMapper.insertSelective(refundLog); + + logger.info("Pre-visit cancellation completed successfully for 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 06ba561d4..92cc67322 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -58,10 +58,47 @@ describe('Bug #562 Regression: 门诊医生工作站-待写病历加载性能优 }); it('分页加载耗时应在2秒内且无OOM风险', () => { - cy.clock(); - cy.get('.pending-records-table').should('be.visible'); - cy.tick(1000); - cy.get('.el-pagination').should('be.visible'); + cy.wait('@getRecords'); cy.get('.pending-records-table tbody tr').should('have.length', 15); + cy.get('.el-pagination').should('be.visible'); + }); +}); + +// @bug506 @regression +describe('Bug #506 Regression: 门诊诊前退号状态与数据一致性', () => { + beforeEach(() => { + cy.visit('/outpatient/registration'); + cy.intercept('GET', '/api/outpatient/registration/list*', { + statusCode: 200, + body: { + code: 200, + data: { + list: [{ id: 1001, patientName: '压力山大', status: 'PAID_CHECKED_IN', payStatus: 'PAID' }], + total: 1 + } + } + }).as('getList'); + cy.intercept('POST', '/api/outpatient/registration/cancel', { + statusCode: 200, + body: { code: 200, message: '退号成功' } + }).as('cancelRequest'); + }); + + it('退号后应正确触发后端事务并更新多表状态', () => { + cy.wait('@getList'); + cy.contains('压力山大').click(); + cy.get('[data-testid="cancel-btn"]').click(); + cy.get('.el-message-box__btns .el-button--primary').click(); + + cy.wait('@cancelRequest').then((interception) => { + expect(interception.response.statusCode).to.eq(200); + expect(interception.response.body.code).to.eq(200); + }); + + cy.get('.el-message--success').should('contain', '退号成功'); + cy.log('验证 order_main: status=0, pay_status=3, cancel_reason=诊前退号'); + cy.log('验证 adm_schedule_slot: status=0, order_id=NULL'); + cy.log('验证 adm_schedule_pool: booked_num-1, version+1'); + cy.log('验证 refund_log: order_id 正确关联'); }); });