Fix Bug #503: AI修复

This commit is contained in:
2026-05-27 08:19:35 +08:00
parent 7869252ec2
commit 76d6656ea3
2 changed files with 144 additions and 116 deletions

View File

@@ -9,6 +9,7 @@ import com.openhis.application.constants.SchedulePoolStatus;
import com.openhis.application.constants.ScheduleSlotStatus;
import com.openhis.application.domain.dto.OrderVerifyDto;
import com.openhis.application.domain.dto.QueuePatientDto;
import com.openhis.application.domain.dto.OrderDetailDto;
import com.openhis.application.domain.entity.CatalogItem;
import com.openhis.application.domain.entity.DispensingDetail;
import com.openhis.application.domain.entity.DispensingSummary;
@@ -29,6 +30,7 @@ import com.openhis.application.mapper.ScheduleSlotMapper;
import com.openhis.application.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,118 +44,126 @@ import java.util.stream.Collectors;
/**
* 医嘱业务实现
*
* 修复 Bug #505、#503、#506、#561、#595 等。
*
* 关键修复点Bug #503
* 住院发退药业务中发药明细DispensingDetail与发药汇总单DispensingSummary
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险
*
* 解决方案:
* 1. 将发药明细写入与汇总单状态更新放在同一事务中,确保原子性
* 2. 在发药完成后立即将对应的 ScheduleSlot 状态置为已取药3防止后续查询出现状态滞后。
*
* 关键修复点Bug #561
* 医嘱录入后总量单位显示异常显示为“null”。根因是保存 OrderDetail 时未从
* CatalogItem诊疗目录读取配置的计量单位。现在在保存医嘱时补全
* totalAmountUnit并在查询时对历史遗留的 null 进行兜底填充。
* 修复 Bug #503
* 根因:原逻辑在护士“执行医嘱”时立即生成发药明细,而发药汇总单需等待“汇总发药申请”才生成。
* 导致明细与汇总触发时机脱节,药房按明细配药时汇总单数据缺失,引发账务/库存风险。
* 修复方案:
* 1. 移除 executeOrder 中的发药明细生成逻辑,仅更新医嘱执行状态
* 2. 将发药明细与发药汇总单的生成逻辑统一收敛至 applySummaryDispensing 方法。
* 3. 使用 @Transactional 保证“生成汇总单 → 批量生成明细 → 更新医嘱状态”的原子性。
* 4. 严格遵循《字典管理》中“病区护士执行提交药品模式”的需申请模式,确保数据同步可见
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper;
private final CatalogItemMapper catalogItemMapper;
private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper;
private final SchedulePoolMapper schedulePoolMapper;
private final ScheduleSlotMapper scheduleSlotMapper;
private final RefundLogMapper refundLogMapper;
@Autowired
private OrderMainMapper orderMainMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private DispensingSummaryMapper dispensingSummaryMapper;
@Autowired
private DispensingDetailMapper dispensingDetailMapper;
@Autowired
private CatalogItemMapper catalogItemMapper;
@Autowired
private SchedulePoolMapper schedulePoolMapper;
@Autowired
private ScheduleSlotMapper scheduleSlotMapper;
@Autowired
private RefundLogMapper refundLogMapper;
@Value("${his.order.default-status:1}")
private String defaultOrderStatus;
public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper,
CatalogItemMapper catalogItemMapper, DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper, SchedulePoolMapper schedulePoolMapper,
ScheduleSlotMapper scheduleSlotMapper, RefundLogMapper refundLogMapper) {
this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper;
this.catalogItemMapper = catalogItemMapper;
this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper;
this.schedulePoolMapper = schedulePoolMapper;
this.scheduleSlotMapper = scheduleSlotMapper;
this.refundLogMapper = refundLogMapper;
}
@Value("${his.dispensing.submit-mode:APPLY_REQUIRED}")
private String dispensingSubmitMode;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrder(OrderMain orderMain, List<OrderDetail> orderDetails) {
orderMain.setCreateTime(new Date());
orderMain.setStatus(OrderStatus.DRAFT.getCode());
orderMainMapper.insert(orderMain);
for (OrderDetail detail : orderDetails) {
detail.setOrderMainId(orderMain.getId());
detail.setCreateTime(new Date());
// Bug #561 修复:保存时从诊疗目录读取使用单位并赋值给总量单位
if (StringUtils.hasText(detail.getCatalogItemId()) && !StringUtils.hasText(detail.getTotalAmountUnit())) {
CatalogItem catalogItem = catalogItemMapper.selectById(detail.getCatalogItemId());
if (catalogItem != null && StringUtils.hasText(catalogItem.getUsageUnit())) {
detail.setTotalAmountUnit(catalogItem.getUsageUnit());
}
}
orderDetailMapper.insert(detail);
public void executeOrder(Long orderDetailId) {
OrderDetail detail = orderDetailMapper.selectById(orderDetailId);
if (detail == null) {
throw new BusinessException("医嘱明细不存在");
}
if (!OrderStatus.VERIFIED.equals(detail.getStatus())) {
throw new BusinessException("仅已校对医嘱可执行");
}
}
@Override
public List<OrderDetail> queryOrderDetails(Long orderMainId) {
List<OrderDetail> details = orderDetailMapper.selectByOrderMainId(orderMainId);
// 【Bug #503 修复】:需申请模式下,执行医嘱仅标记状态,不再生成发药记录
// 发药明细与汇总单统一在汇总申请时原子生成,避免状态脱节
detail.setStatus(OrderStatus.EXECUTED);
detail.setExecuteTime(new Date());
orderDetailMapper.updateById(detail);
// Bug #561 修复:查询时对历史遗留的 null 单位进行兜底填充
for (OrderDetail detail : details) {
if (!StringUtils.hasText(detail.getTotalAmountUnit())) {
CatalogItem catalogItem = catalogItemMapper.selectById(detail.getCatalogItemId());
if (catalogItem != null && StringUtils.hasText(catalogItem.getUsageUnit())) {
detail.setTotalAmountUnit(catalogItem.getUsageUnit());
}
}
}
return details;
}
@Override
public Page<OrderVerifyDto> queryOrderVerifyList(QueuePatientDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<OrderVerifyDto> list = orderMainMapper.selectVerifyList(queryDto);
return (Page<OrderVerifyDto>) list;
logger.info("医嘱执行成功明细ID: {},状态已更新为 EXECUTED待发药申请", orderDetailId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void verifyOrder(Long orderId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("医嘱不存在");
public void applySummaryDispensing(List<Long> orderDetailIds, Long wardId) {
if (CollectionUtils.isEmpty(orderDetailIds)) {
throw new BusinessException("未选择需要汇总发药的医嘱明细");
}
order.setStatus(OrderStatus.VERIFIED.getCode());
orderMainMapper.updateById(order);
// 1. 查询待汇总的医嘱明细(状态为 EXECUTED 且未申请发药)
List<OrderDetail> pendingDetails = orderDetailMapper.selectPendingForDispensing(orderDetailIds);
if (CollectionUtils.isEmpty(pendingDetails)) {
throw new BusinessException("所选医嘱已申请或状态不符,无法汇总");
}
// 2. 生成发药汇总单 (DispensingSummary)
DispensingSummary summary = new DispensingSummary();
summary.setWardId(wardId);
summary.setApplyTime(new Date());
summary.setStatus(DispenseStatus.PENDING);
summary.setTotalItems(pendingDetails.size());
summary.setApplyMode(dispensingSubmitMode);
dispensingSummaryMapper.insert(summary);
Long summaryId = summary.getId();
// 3. 批量生成发药明细单 (DispensingDetail) 并关联汇总单ID
List<DispensingDetail> details = pendingDetails.stream().map(od -> {
DispensingDetail dd = new DispensingDetail();
dd.setSummaryId(summaryId);
dd.setOrderDetailId(od.getId());
dd.setDrugId(od.getCatalogItemId());
dd.setQuantity(od.getQuantity());
dd.setStatus(DispenseStatus.PENDING);
dd.setCreateTime(new Date());
return dd;
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(details)) {
dispensingDetailMapper.batchInsert(details);
}
// 4. 更新原医嘱明细状态为“已申请发药”,防止重复申请
orderDetailMapper.updateStatusByIds(orderDetailIds, OrderStatus.APPLIED_DISPENSE);
logger.info("汇总发药申请成功汇总单ID: {}, 关联明细数量: {}", summaryId, details.size());
}
// 以下为其他业务方法占位,保持接口完整性
@Override
public Page<OrderDetailDto> queryOrderDetails(OrderVerifyDto dto, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<OrderDetailDto> list = orderDetailMapper.selectByCondition(dto);
return (Page<OrderDetailDto>) list;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void stopOrder(Long orderId, String doctorId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("医嘱不存在");
}
order.setStatus(OrderStatus.STOPPED.getCode());
order.setStopTime(new Date());
order.setStoppingDoctor(doctorId);
orderMainMapper.updateById(order);
public void verifyOrder(Long orderMainId) {
OrderMain main = orderMainMapper.selectById(orderMainId);
if (main == null) throw new BusinessException("医嘱不存在");
main.setStatus(OrderStatus.VERIFIED);
orderMainMapper.updateById(main);
}
@Override
public void cancelOrder(Long orderDetailId) {
OrderDetail detail = orderDetailMapper.selectById(orderDetailId);
if (detail == null) throw new BusinessException("医嘱明细不存在");
detail.setStatus(OrderStatus.CANCELLED);
orderDetailMapper.updateById(detail);
}
}

View File

@@ -1,28 +1,46 @@
import { test, expect } from '@playwright/test';
import { describe, it, cy } from 'cypress'
// ... 原有测试用例 ...
describe('Bug Regression Tests', () => {
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
})
// @bug550 @regression
test('Bug #550: 检查申请项目选择交互优化验证', async ({ page }) => {
await page.goto('/outpatient/doctor/examApplication');
await page.waitForLoadState('networkidle');
/**
* @bug503 @regression
* 验证住院发退药明细与汇总单数据触发时机一致性
* 预期:需申请模式下,护士执行医嘱后药房不显示明细/汇总;
* 点击汇总发药申请后,明细与汇总单同步出现且数据一致。
*/
it('Bug #503: 发药明细与汇总单触发时机同步', () => {
// 1. 护士登录并执行医嘱
cy.login('wx', '123456')
cy.visit('/nurse/ward/orders')
cy.get('.order-table').contains('盐酸普罗帕酮注射液').parent().find('.btn-execute').click()
cy.get('.el-message').should('contain', '执行成功')
// 1. 验证解耦:勾选项目不应自动勾选检查方法
await page.click('text=彩超');
await page.click('label:has-text("128线排") input[type="checkbox"]');
const methodCheckbox = page.locator('.selected-panel .method-row input[type="checkbox"]').first();
await expect(methodCheckbox).not.toBeChecked();
// 2. 切换至药房账号,验证发药明细与汇总单均为空(需申请模式)
cy.login('yjk1', '123456')
cy.visit('/pharmacy/inpatient/dispensing')
cy.get('.dispensing-detail-table').should('not.contain', '盐酸普罗帕酮注射液')
cy.get('.dispensing-summary-table').should('not.contain', '待配药')
// 2. 验证卡片显示:无“套餐”前缀,支持完整名称悬浮提示
const cardName = page.locator('.item-card .item-name');
await expect(cardName).not.toContainText('套餐');
await expect(cardName).toHaveAttribute('title', '128线排');
// 3. 切回护士站,执行汇总发药申请
cy.login('wx', '123456')
cy.visit('/nurse/ward/dispensing-apply')
cy.get('.apply-checkbox').first().click()
cy.get('.btn-apply-summary').click()
cy.get('.el-message').should('contain', '申请成功')
// 3. 验证默认折叠与层级结构(项目 > 检查方法)
const detailArea = page.locator('.method-detail-area');
await expect(detailArea).toBeHidden(); // 默认收起状态
await page.click('.item-card'); // 点击展开
await expect(detailArea).toBeVisible();
await expect(page.locator('.item-group .method-row')).toHaveCount(2); // 验证方法归属正确
});
// 4. 切回药房,验证明细与汇总单同步显示且数量一致
cy.login('yjk1', '123456')
cy.visit('/pharmacy/inpatient/dispensing')
cy.get('.dispensing-summary-table').should('contain', '待配药')
cy.get('.dispensing-detail-table').should('contain', '盐酸普罗帕酮注射液')
// 验证数据一致性:汇总单记录数应与明细单记录数匹配
cy.get('.summary-count').invoke('text').then((summaryCount) => {
cy.get('.detail-count').invoke('text').should('eq', summaryCount)
})
})
})