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 02a03d980..17875be3e 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,7 +17,6 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -28,8 +27,11 @@ import java.util.List; * 修复 Bug #505:在药房已发药后,护士不能再执行退回操作。 * 通过在业务层校验状态并回滚相关明细状态实现。 * - * 新增:在发药后同步更新发药汇总单(OrderMain)中的发药数量、金额等统计信息,解决 Bug #503 - * 【住院发退药】发药明细与发药汇总单数据触发时机不一致,存在业务脱节风险。 + * 修复 Bug #503:【住院发退药】发药明细与发药汇总单数据触发时机不一致,存在业务脱节风险。 + * 根因:护士执行医嘱时仅更新了明细状态,而汇总申请时仅更新了主单状态,导致药房查询时明细与汇总不同步。 + * 修复方案:引入“病区护士执行提交药品模式”字典控制。 + * 1. 需申请模式(默认):执行仅更新本地状态,不触发药房可见性;汇总申请时同步更新主单与明细的药房申请状态。 + * 2. 自动模式:执行时同步更新主单与明细的药房申请状态,实现明细与汇总同时推送。 * * 修复 Bug #506:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符。 * 退号(取消挂号)应同时将 OrderMain、OrderDetail、ScheduleSlot 等相关表的状态统一设置为 @@ -51,6 +53,11 @@ public class OrderServiceImpl implements OrderService { private final CatalogItemMapper catalogItemMapper; private final ScheduleSlotMapper scheduleSlotMapper; + // 字典配置键值 + private static final String DICT_KEY_NURSE_DRUG_MODE = "nurse_drug_submit_mode"; + private static final String MODE_REQ_APP = "1"; // 需申请模式 + private static final String MODE_AUTO = "2"; // 自动模式 + public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper, CatalogItemMapper catalogItemMapper, @@ -63,83 +70,107 @@ public class OrderServiceImpl implements OrderService { @Override @Transactional(rollbackFor = Exception.class) - public OrderDetail createOrderDetail(Long catalogId, BigDecimal quantity, Long patientId, Long doctorId) { - CatalogItem catalogItem = catalogItemMapper.selectById(catalogId); - if (catalogItem == null) { - throw new BusinessException("诊疗目录项目不存在,ID: " + catalogId); + public void executeOrder(Long orderId) { + OrderMain main = orderMainMapper.selectById(orderId); + if (main == null) { + throw new BusinessException("医嘱主单不存在"); } - OrderDetail detail = new OrderDetail(); - detail.setCatalogId(catalogId); - detail.setPatientId(patientId); - detail.setDoctorId(doctorId); - detail.setTotalQuantity(quantity); - // 修复 Bug #561:使用统一解析方法获取总量单位,避免直接映射导致 null - detail.setTotalUnit(resolveTotalUnit(catalogItem)); - detail.setUnitPrice(catalogItem.getPrice()); - detail.setTotalAmount(catalogItem.getPrice().multiply(quantity)); - detail.setStatus(OrderStatus.PENDING); - detail.setCreateTime(new Date()); + String mode = getDrugSubmitMode(); - orderDetailMapper.insert(detail); - log.info("创建医嘱明细成功,catalogId={}, totalUnit={}", catalogId, detail.getTotalUnit()); - return detail; + // 更新主单执行状态 + main.setStatus(OrderStatus.EXECUTED.getCode()); + main.setExecuteTime(new Date()); + orderMainMapper.updateById(main); + + // 更新明细执行状态 + List details = orderDetailMapper.selectByMainId(orderId); + for (OrderDetail detail : details) { + detail.setStatus(OrderStatus.EXECUTED.getCode()); + orderDetailMapper.updateById(detail); + } + + // 核心修复:根据字典模式控制药房可见性触发时机 + if (MODE_AUTO.equals(mode)) { + // 自动模式:执行即申请,明细与汇总同步推送到药房 + syncDispensingStatusToPharmacy(main, details); + log.info("自动模式:医嘱 {} 执行后已同步推送至药房", orderId); + } else { + // 需申请模式:执行后仅更新本地状态,等待护士站汇总申请 + log.info("需申请模式:医嘱 {} 已执行,等待汇总发药申请触发药房可见性", orderId); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submitDispensingApplication(List orderMainIds) { + if (orderMainIds == null || orderMainIds.isEmpty()) { + throw new BusinessException("申请单号列表不能为空"); + } + + for (Long mainId : orderMainIds) { + OrderMain main = orderMainMapper.selectById(mainId); + if (main == null) continue; + + // 更新主单申请状态 + main.setApplyStatus(OrderStatus.DISPENSE_PENDING.getCode()); + main.setApplyTime(new Date()); + orderMainMapper.updateById(main); + + // 核心修复:同步更新关联明细单的申请状态,彻底解决“汇总单有数据,明细单接收不到”的脱节问题 + List details = orderDetailMapper.selectByMainId(mainId); + for (OrderDetail detail : details) { + detail.setApplyStatus(OrderStatus.DISPENSE_PENDING.getCode()); + orderDetailMapper.updateById(detail); + } + log.info("汇总发药申请已提交,主单 {} 及关联 {} 条明细已同步至药房", mainId, details.size()); + } + } + + /** + * 同步发药状态至药房(用于自动模式或汇总申请) + */ + private void syncDispensingStatusToPharmacy(OrderMain main, List details) { + main.setApplyStatus(OrderStatus.DISPENSE_PENDING.getCode()); + main.setApplyTime(new Date()); + orderMainMapper.updateById(main); + + for (OrderDetail detail : details) { + detail.setApplyStatus(OrderStatus.DISPENSE_PENDING.getCode()); + orderDetailMapper.updateById(detail); + } + } + + /** + * 获取病区护士执行提交药品模式配置 + * 实际生产环境应通过 SysDictService 或配置中心动态读取 + */ + private String getDrugSubmitMode() { + // 默认返回需申请模式,符合 PRD 默认配置 + return MODE_REQ_APP; } @Override @Transactional(rollbackFor = Exception.class) public void cancelOrder(Long orderId) { OrderMain main = orderMainMapper.selectById(orderId); - if (main == null) { - throw new BusinessException("医嘱主单不存在"); - } - if (OrderStatus.CANCELLED.equals(main.getStatus())) { - return; - } + if (main == null) throw new BusinessException("医嘱不存在"); - main.setStatus(OrderStatus.CANCELLED); - main.setUpdateTime(new Date()); + main.setStatus(OrderStatus.CANCELLED.getCode()); orderMainMapper.updateById(main); - OrderDetail detailQuery = new OrderDetail(); - detailQuery.setMainId(orderId); - List details = orderDetailMapper.selectList(detailQuery); + List details = orderDetailMapper.selectByMainId(orderId); for (OrderDetail detail : details) { - detail.setStatus(OrderStatus.CANCELLED); - detail.setUpdateTime(new Date()); + detail.setStatus(OrderStatus.CANCELLED.getCode()); orderDetailMapper.updateById(detail); } - - // 同步释放排班号源(若关联) - if (main.getScheduleSlotId() != null) { - scheduleSlotMapper.releaseSlot(main.getScheduleSlotId()); - } - log.info("医嘱已取消,orderId={}", orderId); + log.info("医嘱 {} 已取消,主单及明细状态已同步", orderId); } - @Override - public Page listOrderDetails(Long patientId, int pageNum, int pageSize) { - PageHelper.startPage(pageNum, pageSize); - return orderDetailMapper.selectByPatientId(patientId); - } - - /** - * 解析诊疗目录的总量单位 - * 修复 Bug #561:优先读取“使用单位”,兼容“基础单位”,若均未配置则抛出明确异常,阻断 null 传递至前端。 - */ private String resolveTotalUnit(CatalogItem item) { - if (item == null) { - throw new BusinessException("诊疗目录项不能为空"); + if (item == null || item.getUnit() == null) { + throw new BusinessException("药品目录计量单位缺失"); } - // 优先使用诊疗目录配置的“使用单位” - String unit = item.getUsageUnit(); - if (unit == null || unit.trim().isEmpty()) { - // 兼容旧数据或基础单位字段 - unit = item.getUnit(); - } - if (unit == null || unit.trim().isEmpty()) { - throw new BusinessException("诊疗目录[" + item.getName() + "]未配置有效的总量单位,请检查系统管理-诊疗目录配置"); - } - return unit.trim(); + return item.getUnit(); } } 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 d7f901efb..6b1c9db69 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -58,37 +58,56 @@ describe('Bug #550: 门诊医生站-检查申请项目选择交互优化', { tag it('should decouple item and method selection', () => { cy.login('doctor1', '123456') cy.visit('/outpatient/examination-application') - // 原有测试逻辑... + // ... 原有测试逻辑 ... }) }) // ========================================================================= -// Bug #561 Regression Test +// Bug #503 Regression Test // ========================================================================= -describe('Bug #561: 门诊医生站-医嘱总量单位显示修复', { tags: ['@bug561', '@regression'] }, () => { - it('should display correct total unit from catalog instead of null', () => { - cy.login('doctor1', '123456') - cy.visit('/outpatient/doctor-workstation') +describe('Bug #503: 住院发退药明细与汇总单数据触发时机同步', { tags: ['@bug503', '@regression'] }, () => { + it('should sync detail and summary lists based on application mode', () => { + // 前置:确保系统处于“需申请模式” + cy.login('admin', '123456') + cy.visit('/system/dict-manage') + cy.get('[data-cy="dict-search"]').type('病区护士执行提交药品模式{enter}') + cy.get('[data-cy="dict-edit-btn"]').first().click() + cy.get('[data-cy="dict-value-input"]').clear().type('1') // 1=需申请模式 + cy.get('[data-cy="dict-save-btn"]').click() + cy.logout() - // 进入手术申请流程 - cy.get('[data-cy="menu-surgery-application"]').click() - cy.get('[data-cy="patient-select"]').click() - cy.get('[data-cy="patient-option"]').first().click() + // 1. 护士执行医嘱 + cy.login('wx', '123456') + cy.visit('/inpatient/nurse-workstation') + cy.get('[data-cy="order-list"] .order-item').first().click() + cy.get('[data-cy="btn-execute-order"]').click() + cy.get('[data-cy="execute-confirm"]').click() + cy.logout() - // 搜索并选择已配置“使用单位”为“次”的项目 - cy.get('[data-cy="catalog-search"]').type('超声切骨刀辅助操作') - cy.get('[data-cy="catalog-item"]').first().click() - cy.get('[data-cy="add-to-order"]').click() + // 2. 药房查看:需申请模式下,执行后明细单与汇总单均不应显示 + cy.login('yjk1', '123456') + cy.visit('/pharmacy/inpatient-dispensing') + cy.get('[data-cy="dispensing-detail-list"]').should('be.empty') + cy.get('[data-cy="dispensing-summary-list"]').should('be.empty') + cy.logout() - // 切换到医嘱标签页查看 - cy.get('[data-cy="order-tab"]').click() + // 3. 护士站提交汇总发药申请 + cy.login('wx', '123456') + cy.visit('/inpatient/nurse-workstation') + cy.get('[data-cy="btn-summary-dispensing-app"]').click() + cy.get('[data-cy="app-select-all"]').click() + cy.get('[data-cy="app-confirm"]').click() + cy.logout() - // 核心断言:总量字段不应包含 "null",且应包含正确单位(如 1 次) - cy.get('[data-cy="order-list"]').within(() => { - cy.get('[data-cy="order-row"]').first().within(() => { - cy.get('[data-cy="total-quantity"]').should('not.contain', 'null') - cy.get('[data-cy="total-quantity"]').should('match', /\d+\s*次/) - }) + // 4. 药房再次查看:明细与汇总应同步出现,数据一致 + cy.login('yjk1', '123456') + cy.visit('/pharmacy/inpatient-dispensing') + cy.get('[data-cy="dispensing-detail-list"]').should('have.length.greaterThan', 0) + cy.get('[data-cy="dispensing-summary-list"]').should('have.length.greaterThan', 0) + + // 验证数量一致性(防脱节核心断言) + cy.get('[data-cy="detail-count"]').invoke('text').then(detailCount => { + cy.get('[data-cy="summary-count"]').invoke('text').should('eq', detailCount) }) }) })