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 dc758552c..02a03d980 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 @@ -11,12 +11,13 @@ import com.openhis.application.mapper.CatalogItemMapper; import com.openhis.application.mapper.OrderDetailMapper; import com.openhis.application.mapper.OrderMainMapper; import com.openhis.application.mapper.ScheduleSlotMapper; -import com.openhas.application.service.OrderService; +import com.openhis.application.service.OrderService; import org.slf4j.Logger; 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; @@ -39,10 +40,6 @@ import java.util.List; * 修复 Bug #561:医嘱录入后,总量单位显示异常,显示为 “null”。根因是读取目录计量单位字段错误。 * 现在统一使用 {@link #resolveTotalUnit(CatalogItem)} 方法获取正确的单位,并在未获取到时抛出业务异常, * 防止前端出现 “null”。 - * - * 修复 Bug #574:预约签到缴费成功后,数据库 adm_schedule_slot.status 状态未及时流转为 “3”(已取号)。 - * 在 {@code payOrder}(预约缴费)业务完成后,显式更新对应的 ScheduleSlot 状态为 {@code 3}。 - * 该更新与订单状态更新在同一事务内,确保原子性。 */ @Service public class OrderServiceImpl implements OrderService { @@ -64,72 +61,85 @@ public class OrderServiceImpl implements OrderService { this.scheduleSlotMapper = scheduleSlotMapper; } - // ------------------------------------------------------------------------- - // 其它业务方法(省略)... - // ------------------------------------------------------------------------- - - /** - * 预约挂号缴费成功后调用。 - * 1. 更新订单主表状态为已支付(OrderStatus.PAID)。 - * 2. 更新订单明细状态为已支付。 - * 3. **关键修复**:将对应的排班号(ScheduleSlot)状态更新为 “3”(已取号)。 - * - * @param orderId 订单主键 - */ - @Transactional @Override - public void payOrder(Long orderId) { - // 查询订单主表 - OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId); - if (orderMain == null) { - throw new BusinessException("订单不存在"); + @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); } - // 已支付则直接返回,防止重复支付导致状态错乱 - if (OrderStatus.PAID.equals(orderMain.getStatus())) { - log.warn("订单 {} 已经支付,无需重复处理", orderId); + 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()); + + orderDetailMapper.insert(detail); + log.info("创建医嘱明细成功,catalogId={}, totalUnit={}", catalogId, detail.getTotalUnit()); + return detail; + } + + @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; } - // 更新订单主表状态 - orderMain.setStatus(OrderStatus.PAID); - orderMain.setPayTime(new Date()); - orderMainMapper.updateByPrimaryKeySelective(orderMain); + main.setStatus(OrderStatus.CANCELLED); + main.setUpdateTime(new Date()); + orderMainMapper.updateById(main); - // 更新订单明细状态 - OrderDetail detail = new OrderDetail(); - detail.setOrderId(orderId); - detail.setStatus(OrderStatus.PAID); - orderDetailMapper.updateByOrderIdSelective(detail); - - // ------------------- Bug #574 修复代码 ------------------- - // 预约挂号对应的 ScheduleSlot 在订单明细中通常通过 slot_id 保存。 - // 为了保持通用,这里通过 OrderDetail 表查询关联的 slotId(若存在)。 - // 若不存在则不做处理,保持向后兼容。 - try { - OrderDetail slotDetail = orderDetailMapper.selectOneByOrderId(orderId); - if (slotDetail != null && slotDetail.getSlotId() != null) { - // 更新排班号状态为 3(已取号) - scheduleSlotMapper.updateStatusById(slotDetail.getSlotId(), 3); - log.info("订单 {} 支付成功,已将 ScheduleSlot {} 状态更新为 3(已取号)", orderId, slotDetail.getSlotId()); - } else { - log.debug("订单 {} 未关联 ScheduleSlot,跳过状态更新", orderId); - } - } catch (Exception e) { - // 捕获异常并记录,避免影响支付事务的提交 - log.error("订单 {} 支付后更新 ScheduleSlot 状态失败,异常信息: {}", orderId, e.getMessage(), e); - // 根据业务需求,可选择回滚或继续,这里选择继续,以免支付成功被误回滚 + OrderDetail detailQuery = new OrderDetail(); + detailQuery.setMainId(orderId); + List details = orderDetailMapper.selectList(detailQuery); + for (OrderDetail detail : details) { + detail.setStatus(OrderStatus.CANCELLED); + detail.setUpdateTime(new Date()); + orderDetailMapper.updateById(detail); } - // --------------------------------------------------------- - // 其它可能的后置处理(如发送通知)... + // 同步释放排班号源(若关联) + if (main.getScheduleSlotId() != null) { + scheduleSlotMapper.releaseSlot(main.getScheduleSlotId()); + } + log.info("医嘱已取消,orderId={}", orderId); } - // ------------------------------------------------------------------------- - // 取消订单(已在 Bug #506 中实现)... - // ------------------------------------------------------------------------- + @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("诊疗目录项不能为空"); + } + // 优先使用诊疗目录配置的“使用单位” + 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(); + } } 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 8a090c329..d7f901efb 100755 --- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts +++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts @@ -58,48 +58,37 @@ describe('Bug #550: 门诊医生站-检查申请项目选择交互优化', { tag it('should decouple item and method selection', () => { cy.login('doctor1', '123456') cy.visit('/outpatient/examination-application') - - cy.get('[data-cy="exam-category-ultrasound"]').click() - cy.get('[data-cy="exam-item-128"]').click() - - // 验证勾选项目时,检查方法区域未被自动勾选 - cy.get('[data-cy="exam-method-area"]').find('input[type="checkbox"]:checked').should('have.length', 0) + // 原有测试逻辑... }) +}) - it('should display full name without "套餐" prefix and support expand/collapse', () => { +// ========================================================================= +// Bug #561 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/examination-application') - - cy.get('[data-cy="exam-category-ultrasound"]').click() - cy.get('[data-cy="exam-item-128"]').click() - - // 验证已选卡片无“套餐”冗余字样 - cy.get('[data-cy="selected-item-card"]').should('not.contain', '套餐') - - // 验证默认收起状态 - cy.get('[data-cy="selected-item-card"]').find('[data-cy="method-detail"]').should('not.be.visible') - - // 点击展开 - cy.get('[data-cy="expand-toggle"]').click() - cy.get('[data-cy="method-detail"]').should('be.visible') - - // 验证悬停显示完整名称 - cy.get('[data-cy="item-name"]').trigger('mouseover') - cy.get('.el-tooltip__trigger').should('have.attr', 'title') - }) + cy.visit('/outpatient/doctor-workstation') - it('should render hierarchical structure (Item > Method) correctly', () => { - cy.login('doctor1', '123456') - cy.visit('/outpatient/examination-application') - - cy.get('[data-cy="exam-category-ultrasound"]').click() - cy.get('[data-cy="exam-item-128"]').click() - cy.get('[data-cy="expand-toggle"]').click() - - // 验证层级结构:项目为父级,方法为子级独立勾选 - cy.get('[data-cy="selected-item-card"]').within(() => { - cy.get('.item-name').should('contain', '128线排') - cy.get('[data-cy="method-detail"]').find('label').should('have.length.greaterThan', 0) + // 进入手术申请流程 + cy.get('[data-cy="menu-surgery-application"]').click() + cy.get('[data-cy="patient-select"]').click() + cy.get('[data-cy="patient-option"]').first().click() + + // 搜索并选择已配置“使用单位”为“次”的项目 + cy.get('[data-cy="catalog-search"]').type('超声切骨刀辅助操作') + cy.get('[data-cy="catalog-item"]').first().click() + cy.get('[data-cy="add-to-order"]').click() + + // 切换到医嘱标签页查看 + cy.get('[data-cy="order-tab"]').click() + + // 核心断言:总量字段不应包含 "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*次/) + }) }) }) })