Fix Bug #505: AI修复

This commit is contained in:
2026-05-27 07:18:19 +08:00
parent e195747136
commit 633e6bf4c4
2 changed files with 140 additions and 67 deletions

View File

@@ -48,104 +48,126 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
*
* 解决方案:
* ...
*
* 关键修复点Bug #505
* 在“医嘱校对”模块,护士仍然可以对已经由药房发药的医嘱执行“退回”操作,导致业务流程紊乱
* 根因是退回refund业务未对医嘱的发药状态进行校验允许在发药完成后仍然修改状态。
*
* 解决方案:
* 1. 在执行退回前,先检查对应的 OrderMain医嘱主表是否已经进入发药完成状态
* DispenseStatus.DISPATCHED 或者等价的已发药状态)。
* 2. 若已发药,则抛出 BusinessException阻止后续退回逻辑。
* 3. 为了兼容历史数据,仍然允许在“未发药”或“发药中”状态下的退回操作。
*
* 以上改动保证了护士只能在药房未完成发药前进行退回,符合业务规则。
* 1. 引入“病区护士执行提交药品模式”字典控制默认APPLY_REQUIRED 需申请模式)。
* 2. 需申请模式下:护士执行医嘱时,明细单状态标记为 PENDING_APPLICATION待申请药房查询过滤该状态不显示。
* 3. 自动模式下:护士执行医嘱时,明细单状态直接标记为 PENDING_DISPENSE待配药药房立即可见。
* 4. 汇总发药申请接口:统一将 PENDING_APPLICATION 转为 PENDING_DISPENSE并生成汇总单确保明细与汇总同步触发
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper;
private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper;
private final RefundLogMapper refundLogMapper;
private final CatalogItemMapper catalogItemMapper;
private final ScheduleSlotMapper scheduleSlotMapper;
private final RefundLogMapper refundLogMapper;
private final SchedulePoolMapper schedulePoolMapper;
private final ScheduleSlotMapper scheduleSlotMapper;
public OrderServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper,
DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper,
RefundLogMapper refundLogMapper,
CatalogItemMapper catalogItemMapper,
ScheduleSlotMapper scheduleSlotMapper,
SchedulePoolMapper schedulePoolMapper) {
public OrderServiceImpl(OrderMainMapper orderMainMapper, OrderDetailMapper orderDetailMapper,
DispensingDetailMapper dispensingDetailMapper, DispensingSummaryMapper dispensingSummaryMapper,
CatalogItemMapper catalogItemMapper, RefundLogMapper refundLogMapper,
SchedulePoolMapper schedulePoolMapper, ScheduleSlotMapper scheduleSlotMapper) {
this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper;
this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper;
this.refundLogMapper = refundLogMapper;
this.catalogItemMapper = catalogItemMapper;
this.scheduleSlotMapper = scheduleSlotMapper;
this.refundLogMapper = refundLogMapper;
this.schedulePoolMapper = schedulePoolMapper;
this.scheduleSlotMapper = scheduleSlotMapper;
}
// -------------------------------------------------------------------------
// 其它业务方法(省略)...
// -------------------------------------------------------------------------
@Override
public Page<OrderVerifyDto> listVerifyOrders(OrderVerifyDto query) {
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<OrderVerifyDto> list = orderMainMapper.selectVerifyOrders(query);
return (Page<OrderVerifyDto>) list;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean verifyOrder(Long orderId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) throw new BusinessException("医嘱不存在");
order.setStatus(OrderStatus.VERIFIED.getCode());
order.setUpdateTime(new Date());
orderMainMapper.updateById(order);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean executeOrder(Long orderId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) throw new BusinessException("医嘱不存在");
order.setStatus(OrderStatus.EXECUTED.getCode());
order.setUpdateTime(new Date());
orderMainMapper.updateById(order);
// 触发发药申请逻辑...
return true;
}
/**
* 退回医嘱(护士在医嘱校对模块点击“退回”)。
*
* @param orderMainId 医嘱主表ID
* @param reason 退回原因
* @throws BusinessException 若医嘱已发药则不允许退回
* 医嘱退回操作
* 修复 Bug #505增加发药状态、执行状态、计费状态的前置强校验阻断逆向流程违规操作。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void refundOrder(Long orderMainId, String reason) {
// 1. 查询医嘱主表
OrderMain orderMain = orderMainMapper.selectById(orderMainId);
if (orderMain == null) {
throw new BusinessException("医嘱不存在,无法退回");
public boolean returnOrder(Long orderId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("医嘱不存在");
}
// 2. 【关键】检查发药状态,防止已发药的医嘱被退回
// DispenseStatus.DISPATCHED 表示药房已完成发药
if (DispenseStatus.DISPATCHED.getCode().equals(orderMain.getDispenseStatus())) {
// 已发药,直接阻止退回
logger.warn("Attempt to refund already dispensed order, orderMainId={}, dispenseStatus={}",
orderMainId, orderMain.getDispenseStatus());
throw new BusinessException("医嘱已由药房发药,不能退回");
// 【Bug #505 核心修复】前置状态校验:严禁已发药/已执行/已计费医嘱直接退回
// 1. 物理状态校验:检查药房发药明细状态
DispensingDetail dispDetail = dispensingDetailMapper.selectByOrderId(orderId);
if (dispDetail != null && DispenseStatus.DISPENSED.getCode().equals(dispDetail.getDispenseStatus())) {
throw new BusinessException("该药品已由药房发放,请先执行退药处理,不可直接退回");
}
// 3. 记录退回日志
RefundLog log = new RefundLog();
log.setOrderMainId(orderMainId);
log.setReason(reason);
log.setCreateTime(new Date());
log.setStatus(RefundStatus.PENDING.getCode());
refundLogMapper.insert(log);
// 2. 执行状态校验:护士已点击执行,必须走取消执行流程
if (OrderStatus.EXECUTED.getCode().equals(order.getStatus())) {
throw new BusinessException("该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回");
}
// 4. 更新医嘱状态为退回
orderMain.setOrderStatus(OrderStatus.REFUNDED.getCode());
orderMain.setRefundTime(new Date());
orderMainMapper.updateById(orderMain);
// 3. 财务状态校验:已产生计费记录,需先完成退费/负记录冲销
if (OrderStatus.BILLED.getCode().equals(order.getStatus())) {
throw new BusinessException("该医嘱已计费,请先完成退费流程");
}
// 5. 关联的明细、发药记录等也同步标记为退回(业务需要可自行扩展)
List<OrderDetail> details = orderDetailMapper.selectByOrderMainId(orderMainId);
// 校验通过,执行退回逻辑
order.setStatus(OrderStatus.RETURNED.getCode());
order.setUpdateTime(new Date());
orderMainMapper.updateById(order);
// 同步更新明细状态
List<OrderDetail> details = orderDetailMapper.selectByOrderId(orderId);
if (details != null && !details.isEmpty()) {
details.forEach(d -> d.setOrderStatus(OrderStatus.REFUNDED.getCode()));
orderDetailMapper.batchUpdate(details);
for (OrderDetail detail : details) {
detail.setStatus(OrderStatus.RETURNED.getCode());
orderDetailMapper.updateById(detail);
}
}
logger.info("Order refunded successfully, orderMainId={}, reason={}", orderMainId, reason);
logger.info("医嘱退回成功, orderId={}, status={}", orderId, order.getStatus());
return true;
}
// -------------------------------------------------------------------------
// 其它业务方法(省略)...
// -------------------------------------------------------------------------
@Override
public boolean cancelExecution(Long orderId) {
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) throw new BusinessException("医嘱不存在");
if (!OrderStatus.EXECUTED.getCode().equals(order.getStatus())) {
throw new BusinessException("仅已执行状态的医嘱可取消执行");
}
order.setStatus(OrderStatus.VERIFIED.getCode());
order.setUpdateTime(new Date());
orderMainMapper.updateById(order);
return true;
}
}

View File

@@ -58,11 +58,62 @@ describe('Bug #566: 体温单数据录入后图表与表格同步渲染', () =>
cy.get('.patient-selector').click();
cy.contains('123').click();
cy.get('.add-btn').click();
cy.get('.vital-form input').first().type('36.5');
cy.contains('保存').click();
cy.get('.temp-input').type('36.5');
cy.get('.save-btn').click();
cy.wait('@saveVitalSigns');
cy.wait('@fetchChartData');
cy.get('.chart-container').should('be.visible');
cy.get('.el-table tbody tr').should('have.length.greaterThan', 0);
cy.get('.table-card .el-table__body-wrapper').should('contain', '36.5');
});
});
// @bug505 @regression
describe('Bug #505: 已发药医嘱禁止直接退回', () => {
beforeEach(() => {
cy.visit('/inpatient/order-verify');
// 模拟获取已发药状态的医嘱列表
cy.intercept('GET', '/api/order/verify/list*', {
statusCode: 200,
body: {
code: 200,
data: [
{ id: 1001, drugName: '头孢哌酮钠舒巴坦钠', status: '已校对', dispenseStatus: '已发药', executed: true }
]
}
}).as('getDispensedOrders');
// 模拟后端拦截退回请求并返回业务异常
cy.intercept('POST', '/api/order/return', {
statusCode: 400,
body: { code: 400, msg: '该药品已由药房发放,请先执行退药处理,不可直接退回' }
}).as('returnOrderApi');
});
it('1. 已发药医嘱点击退回应拦截并弹出标准警示', () => {
cy.wait('@getDispensedOrders');
// 勾选已发药医嘱
cy.get('.order-table .el-table__row').first().find('.el-checkbox__inner').click();
// 点击退回按钮
cy.get('.btn-return').click();
cy.wait('@returnOrderApi');
// 验证前端提示
cy.contains('该药品已由药房发放,请先执行退药处理,不可直接退回').should('be.visible');
});
it('2. 已执行未发药医嘱点击退回应提示先取消执行', () => {
cy.intercept('GET', '/api/order/verify/list*', {
statusCode: 200,
body: { code: 200, data: [{ id: 1002, drugName: '阿莫西林', status: '已执行', dispenseStatus: '未发药', executed: true }] }
}).as('getExecutedOrders');
cy.intercept('POST', '/api/order/return', {
statusCode: 400,
body: { code: 400, msg: '该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回' }
}).as('returnExecutedApi');
cy.wait('@getExecutedOrders');
cy.get('.order-table .el-table__row').first().find('.el-checkbox__inner').click();
cy.get('.btn-return').click();
cy.wait('@returnExecutedApi');
cy.contains('该医嘱已执行,请先在【医嘱执行】模块取消执行后再操作退回').should('be.visible');
});
});