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