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 82a6eef8d..2a0043c7f 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 @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.util.Arrays; @@ -48,10 +49,10 @@ import java.util.stream.Collectors; * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * * 解决方案: - * 1. 引入“病区护士执行提交药品模式”字典控制(默认:APPLY_REQUIRED 需申请模式)。 - * 2. 需申请模式下:护士执行医嘱时,明细单状态标记为 PENDING_APPLICATION(待申请),药房查询过滤该状态,不显示。 - * 3. 自动模式下:护士执行医嘱时,明细单状态直接标记为 PENDING_DISPENSE(待配药),药房立即可见。 - * 4. 汇总发药申请接口:统一将 PENDING_APPLICATION 转为 PENDING_DISPENSE,并生成汇总单,确保明细与汇总同步触发。 + * 1. 将发药明细和发药汇总的写入统一放在同一个 @Transactional 方法中,确保原子性。 + * 2. 护士执行医嘱仅更新医嘱状态为“已执行”,不再生成待发药明细。 + * 3. 护士提交【汇总发药申请】时,在同一事务内批量生成汇总单与明细单,并关联状态。 + * 4. 药房查询接口仅拉取状态为“已申请/待发药”的记录,彻底消除“明细先显、汇总后显”的脱节。 */ @Service public class OrderServiceImpl implements OrderService { @@ -61,27 +62,101 @@ public class OrderServiceImpl implements OrderService { private final OrderDetailMapper orderDetailMapper; private final DispensingDetailMapper dispensingDetailMapper; private final DispensingSummaryMapper dispensingSummaryMapper; - - // 发药状态常量 - private static final String STATUS_PENDING_APPLICATION = "PENDING_APPLICATION"; - private static final String STATUS_PENDING_DISPENSE = "PENDING_DISPENSE"; - // 字典键 - private static final String DICT_KEY_EXECUTION_MODE = "WARD_NURSE_EXECUTION_MODE"; - private static final String MODE_APPLY_REQUIRED = "APPLY_REQUIRED"; + private final CatalogItemMapper catalogItemMapper; + private final SchedulePoolMapper schedulePoolMapper; + private final ScheduleSlotMapper scheduleSlotMapper; + private final RefundLogMapper refundLogMapper; public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, DispensingDetailMapper dispensingDetailMapper, - DispensingSummaryMapper dispensingSummaryMapper) { + DispensingSummaryMapper dispensingSummaryMapper, + CatalogItemMapper catalogItemMapper, + SchedulePoolMapper schedulePoolMapper, + ScheduleSlotMapper scheduleSlotMapper, + RefundLogMapper refundLogMapper) { this.orderMainMapper = orderMainMapper; this.orderDetailMapper = orderDetailMapper; this.dispensingDetailMapper = dispensingDetailMapper; this.dispensingSummaryMapper = dispensingSummaryMapper; + this.catalogItemMapper = catalogItemMapper; + this.schedulePoolMapper = schedulePoolMapper; + this.scheduleSlotMapper = scheduleSlotMapper; + this.refundLogMapper = refundLogMapper; } /** - * 查询患者待写病历的医嘱(分页)。 + * 修复 Bug #503:统一发药明细与汇总单的触发时机。 + * 在“需申请模式”(默认)下,护士执行医嘱仅更新医嘱状态,不生成发药记录。 + * 只有调用此方法提交汇总申请时,才在同一事务内原子性生成汇总单与明细单。 */ + @Transactional(rollbackFor = Exception.class) + public void submitSummaryDispensingApplication(Long wardId, List orderDetailIds) { + if (CollectionUtils.isEmpty(orderDetailIds)) { + throw new BusinessException("未选择需要发药的医嘱明细"); + } + + // 1. 查询待发药明细(状态为已执行但未申请) + List pendingDetails = orderDetailMapper.selectByIdsAndStatus(orderDetailIds, "EXECUTED"); + if (pendingDetails.isEmpty()) { + throw new BusinessException("所选医嘱明细状态不正确或已申请"); + } + + // 2. 生成发药汇总单 + DispensingSummary summary = new DispensingSummary(); + summary.setWardId(wardId); + summary.setApplyTime(new Date()); + summary.setStatus(DispenseStatus.PENDING); + summary.setTotalItems(pendingDetails.size()); + summary.setCreateTime(new Date()); + summary.setUpdateTime(new Date()); + dispensingSummaryMapper.insert(summary); + + // 3. 批量生成发药明细单,并关联汇总单ID + List details = pendingDetails.stream().map(od -> { + DispensingDetail detail = new DispensingDetail(); + detail.setSummaryId(summary.getId()); + detail.setOrderDetailId(od.getId()); + detail.setPatientId(od.getPatientId()); + detail.setDrugId(od.getCatalogItemId()); + detail.setQuantity(od.getQuantity()); + detail.setStatus(DispenseStatus.PENDING); + detail.setCreateTime(new Date()); + detail.setUpdateTime(new Date()); + return detail; + }).collect(Collectors.toList()); + + dispensingDetailMapper.batchInsert(details); + + // 4. 更新医嘱明细状态为“已申请发药”,防止重复提交 + orderDetailMapper.updateStatusByIds(orderDetailIds, "APPLIED_DISPENSE"); + + logger.info("汇总发药申请成功,wardId={}, summaryId={}, detailCount={}", wardId, summary.getId(), details.size()); + } + + /** + * 护士执行医嘱(修复 Bug #503:移除原逻辑中直接生成发药明细的代码) + * 仅更新医嘱状态,发药数据统一由 submitSummaryDispensingApplication 触发。 + */ + @Transactional(rollbackFor = Exception.class) + public void executeOrder(Long orderDetailId) { + OrderDetail detail = orderDetailMapper.selectById(orderDetailId); + if (detail == null) { + throw new BusinessException("医嘱明细不存在"); + } + if (!"PENDING".equals(detail.getStatus())) { + throw new BusinessException("医嘱状态不允许执行"); + } + + // 仅更新状态,不触发发药明细生成 + detail.setStatus("EXECUTED"); + detail.setUpdateTime(new Date()); + orderDetailMapper.update(detail); + + logger.info("医嘱执行成功,orderDetailId={}", orderDetailId); + } + + // 其它业务方法保持不变... @Transactional(readOnly = true) public List getPendingOrders(Long patientId, Integer pageNum, Integer pageSize) { if (patientId == null) { @@ -95,64 +170,4 @@ public class OrderServiceImpl implements OrderService { logger.info("查询待写医嘱,patientId={}, pageNum={}, pageSize={}, resultSize={}", patientId, pn, ps, list.size()); return list; } - - /** - * 修复 Bug #503:护士执行医嘱触发发药逻辑 - * 根据字典配置“病区护士执行提交药品模式”决定明细单初始状态,确保与汇总单触发时机一致。 - */ - @Transactional - public void executeOrderForDispensing(Long orderId) { - // 1. 获取执行模式配置,默认“需申请模式”(APPLY_REQUIRED) - String executionMode = getDictValueOrDefault(DICT_KEY_EXECUTION_MODE, MODE_APPLY_REQUIRED); - - // 2. 确定发药明细初始状态 - String detailStatus = MODE_APPLY_REQUIRED.equals(executionMode) ? STATUS_PENDING_APPLICATION : STATUS_PENDING_DISPENSE; - - // 3. 创建发药明细记录 - DispensingDetail detail = new DispensingDetail(); - detail.setOrderId(orderId); - detail.setStatus(detailStatus); - detail.setCreateTime(new Date()); - dispensingDetailMapper.insert(detail); - - logger.info("护士执行医嘱发药,orderId={}, mode={}, detailStatus={}", orderId, executionMode, detailStatus); - } - - /** - * 修复 Bug #503:汇总发药申请接口 - * 将待申请状态的明细转为待配药,并生成汇总单,确保明细与汇总同步显示。 - */ - @Transactional - public void applySummaryDispensing(List orderIds) { - if (orderIds == null || orderIds.isEmpty()) { - throw new BusinessException("申请发药明细不能为空"); - } - - // 1. 批量更新明细状态:PENDING_APPLICATION -> PENDING_DISPENSE - int updatedCount = dispensingDetailMapper.updateStatusByOrderIds(orderIds, STATUS_PENDING_APPLICATION, STATUS_PENDING_DISPENSE); - if (updatedCount == 0) { - throw new BusinessException("未找到待申请的发药明细"); - } - - // 2. 生成发药汇总单 - DispensingSummary summary = new DispensingSummary(); - summary.setApplyTime(new Date()); - summary.setOrderIds(orderIds.stream().map(String::valueOf).collect(Collectors.joining(","))); - summary.setStatus(STATUS_PENDING_DISPENSE); - summary.setCreateTime(new Date()); - dispensingSummaryMapper.insert(summary); - - logger.info("汇总发药申请成功,更新明细数={}, 汇总单ID={}", updatedCount, summary.getId()); - } - - /** - * 辅助方法:获取字典值(实际项目中应调用 DictService 或查询 sys_dict 表) - */ - private String getDictValueOrDefault(String dictKey, String defaultValue) { - // 此处为简化实现,实际应替换为字典服务调用 - // return dictService.getValue(dictKey); - return defaultValue; - } - - // 其它业务方法保持不变... } 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 de358ef5a..2e9f5e61f 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -64,43 +64,35 @@ describe('Bug #566: 体温单数据录入后图表与表格同步渲染', () => // @bug503 @regression describe('Bug #503: 住院发退药明细与汇总单数据触发时机同步', () => { beforeEach(() => { - // 模拟字典配置为“需申请模式” - cy.intercept('GET', '/api/dict/value?key=WARD_NURSE_EXECUTION_MODE', { - statusCode: 200, - body: { code: 200, data: 'APPLY_REQUIRED' } - }).as('getDictMode'); + cy.intercept('GET', '/api/pharmacy/dispensing/details*', { fixture: 'dispensing-empty.json' }).as('getDetails'); + cy.intercept('GET', '/api/pharmacy/dispensing/summaries*', { fixture: 'dispensing-empty.json' }).as('getSummaries'); }); - it('1. 需申请模式下:护士执行医嘱后,药房明细与汇总均不显示', () => { - cy.intercept('POST', '/api/orders/execute-dispensing', { statusCode: 200, body: { code: 200, msg: '执行成功' } }).as('executeOrder'); - cy.intercept('GET', '/api/pharmacy/dispensing-details*', { fixture: 'empty-list.json' }).as('getDetails'); - cy.intercept('GET', '/api/pharmacy/dispensing-summary*', { fixture: 'empty-list.json' }).as('getSummary'); - - cy.visit('/nurse/execution'); - cy.get('.execute-btn').first().click(); + it('1. 护士执行医嘱后,药房明细与汇总单均不应显示(需申请模式)', () => { + cy.visit('/inpatient/nurse/execution'); + cy.intercept('POST', '/api/orders/execute', { statusCode: 200, body: { code: 200, msg: '执行成功' } }).as('executeOrder'); + cy.get('.order-row').first().find('.execute-btn').click(); cy.wait('@executeOrder'); - + cy.visit('/inpatient/pharmacy/dispensing'); - cy.wait(['@getDetails', '@getSummary']); - cy.get('.detail-table').should('contain.text', '暂无数据'); - cy.get('.summary-table').should('contain.text', '暂无数据'); + cy.wait(['@getDetails', '@getSummaries']); + cy.get('.detail-table tbody').should('be.empty'); + cy.get('.summary-table tbody').should('be.empty'); }); - it('2. 护士提交汇总申请后,明细与汇总同步显示', () => { - cy.intercept('POST', '/api/pharmacy/apply-summary', { - statusCode: 200, - body: { code: 200, msg: '申请成功', summaryId: 1001 } - }).as('applySummary'); - cy.intercept('GET', '/api/pharmacy/dispensing-details*', { fixture: 'details-after-apply.json' }).as('getDetailsAfter'); - cy.intercept('GET', '/api/pharmacy/dispensing-summary*', { fixture: 'summary-after-apply.json' }).as('getSummaryAfter'); - - cy.visit('/nurse/summary-apply'); + it('2. 护士提交汇总发药申请后,明细与汇总单应同时显示', () => { + cy.visit('/inpatient/nurse/summary-apply'); + cy.intercept('POST', '/api/pharmacy/apply-summary', { statusCode: 200, body: { code: 200, msg: '申请成功' } }).as('applySummary'); cy.get('.apply-btn').click(); cy.wait('@applySummary'); - + cy.visit('/inpatient/pharmacy/dispensing'); - cy.wait(['@getDetailsAfter', '@getSummaryAfter']); - cy.get('.detail-table').should('not.contain.text', '暂无数据'); - cy.get('.summary-table').should('not.contain.text', '暂无数据'); + cy.intercept('GET', '/api/pharmacy/dispensing/details*', { fixture: 'dispensing-details-filled.json' }).as('getDetails'); + cy.intercept('GET', '/api/pharmacy/dispensing/summaries*', { fixture: 'dispensing-summaries-filled.json' }).as('getSummaries'); + cy.wait(['@getDetails', '@getSummaries']); + + cy.get('.detail-table tbody tr').should('have.length.greaterThan', 0); + cy.get('.summary-table tbody tr').should('have.length.greaterThan', 0); + cy.get('.detail-table tbody tr').first().find('.summary-id').should('not.be.empty'); }); });