Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常

根因分析:
1. 耗材库存报错:lotNumberMatch() 按 encounterId 查询 ALL 待发放 DeviceDispense,
   不区分是否为本次执行的医嘱。若该就诊存在其他未执行的耗材记录且库存为零,
   整个调用就会失败,导致非耗材类医嘱执行也被拦截。
2. 全选联动异常:toggleRowSelection() 程序化选中会触发 @select 事件,
   handleRowSelect 中调用 selectAllCheckboxesInRow 导致级联全选。

修复方案:
- 后端:lotNumberMatch 新增 requestIdList 可选参数,当传入时通过 DeviceRequest.basedOnId
  过滤仅校验与本次执行医嘱关联的耗材记录,避免其他未执行医嘱干扰
- 前端:handleExecute 传入 selectedRequestIds(仅诊疗类医嘱的 requestId)
- 前端:新增 skipSelectCascade 标志,程序化 toggleRowSelection 时阻止 handleRowSelect
  触发 selectAllCheckboxesInRow,消除级联反馈环

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
关羽
2026-05-14 22:21:16 +08:00
committed by 荀彧
parent f41b86a143
commit 1124b1010d
4 changed files with 54 additions and 11 deletions

View File

@@ -200,9 +200,10 @@ public interface ICommonService {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
R<?> lotNumberMatch(List<Long> encounterIdList); R<?> lotNumberMatch(List<Long> encounterIdList, List<Long> requestIdList);
/** /**
* 根据机构ID获取机构名称 * 根据机构ID获取机构名称

View File

@@ -39,8 +39,10 @@ import com.openhis.web.common.dto.*;
import com.openhis.web.common.mapper.CommonAppMapper; import com.openhis.web.common.mapper.CommonAppMapper;
import com.openhis.web.pharmacymanage.dto.InventoryDetailDto; import com.openhis.web.pharmacymanage.dto.InventoryDetailDto;
import com.openhis.workflow.domain.DeviceDispense; import com.openhis.workflow.domain.DeviceDispense;
import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.InventoryItem; import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.service.IDeviceDispenseService; import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IInventoryItemService; import com.openhis.workflow.service.IInventoryItemService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -99,6 +101,9 @@ public class CommonServiceImpl implements ICommonService {
@Resource @Resource
private IDeviceDispenseService deviceDispenseService; private IDeviceDispenseService deviceDispenseService;
@Resource
private IDeviceRequestService deviceRequestService;
/** /**
* 获取药房列表 * 获取药房列表
* *
@@ -678,10 +683,11 @@ public class CommonServiceImpl implements ICommonService {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
@Override @Override
public R<?> lotNumberMatch(List<Long> encounterIdList) { public R<?> lotNumberMatch(List<Long> encounterIdList, List<Long> requestIdList) {
// 查询患者待发放的药品信息 // 查询患者待发放的药品信息
List<MedicationDispense> medicationDispenseList = medicationDispenseService List<MedicationDispense> medicationDispenseList = medicationDispenseService
.list(new LambdaQueryWrapper<MedicationDispense>().in(MedicationDispense::getEncounterId, encounterIdList) .list(new LambdaQueryWrapper<MedicationDispense>().in(MedicationDispense::getEncounterId, encounterIdList)
@@ -798,10 +804,27 @@ public class CommonServiceImpl implements ICommonService {
} }
} }
// 查询患者待发放的耗材信息 // 查询患者待发放的耗材信息
List<DeviceDispense> deviceDispenseList = deviceDispenseService LambdaQueryWrapper<DeviceDispense> deviceDispenseQuery = new LambdaQueryWrapper<DeviceDispense>()
.list(new LambdaQueryWrapper<DeviceDispense>().in(DeviceDispense::getEncounterId, encounterIdList) .in(DeviceDispense::getEncounterId, encounterIdList)
.eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue()) .eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue())
.eq(DeviceDispense::getDeleteFlag, DelFlag.NO.getCode())); .eq(DeviceDispense::getDeleteFlag, DelFlag.NO.getCode());
// 若传入requestIdList则仅查询与指定医嘱请求关联的耗材避免其他未执行医嘱的耗材记录干扰
if (requestIdList != null && !requestIdList.isEmpty()) {
List<Long> deviceReqIds = deviceRequestService
.list(new LambdaQueryWrapper<DeviceRequest>()
.in(DeviceRequest::getBasedOnId, requestIdList)
.eq(DeviceRequest::getBasedOnTable, CommonConstants.TableName.WOR_SERVICE_REQUEST))
.stream()
.map(DeviceRequest::getId)
.collect(java.util.stream.Collectors.toList());
if (!deviceReqIds.isEmpty()) {
deviceDispenseQuery.in(DeviceDispense::getDeviceReqId, deviceReqIds);
} else {
// 无关联的耗材请求,直接跳过耗材校验
deviceDispenseQuery.eq(DeviceDispense::getId, -1L);
}
}
List<DeviceDispense> deviceDispenseList = deviceDispenseService.list(deviceDispenseQuery);
// 耗材批号匹配 // 耗材批号匹配
if (deviceDispenseList != null && !deviceDispenseList.isEmpty()) { if (deviceDispenseList != null && !deviceDispenseList.isEmpty()) {
// 获取待发放的耗材id // 获取待发放的耗材id

View File

@@ -274,10 +274,13 @@ public class CommonAppController {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
@GetMapping("/lot-number-match") @GetMapping("/lot-number-match")
public R<?> lotNumberMatch(@RequestParam(value = "encounterIdList") List<Long> encounterIdList) { public R<?> lotNumberMatch(
return commonService.lotNumberMatch(encounterIdList); @RequestParam(value = "encounterIdList") List<Long> encounterIdList,
@RequestParam(value = "requestIdList", required = false) List<Long> requestIdList) {
return commonService.lotNumberMatch(encounterIdList, requestIdList);
} }
} }

View File

@@ -241,6 +241,8 @@ const loading = ref(false);
const chooseAll = ref(false); const chooseAll = ref(false);
// 独立维护选中行ID集合避免el-table内部selection状态异常导致联动全选 // 独立维护选中行ID集合避免el-table内部selection状态异常导致联动全选
const selectedRowIds = ref(new Set()); const selectedRowIds = ref(new Set());
// 跳过选中事件级联:程序化调用 toggleRowSelection 时阻止 handleRowSelect 触发 selectAllCheckboxesInRow
const skipSelectCascade = ref(false);
const props = defineProps({ const props = defineProps({
exeStatus: { exeStatus: {
type: Number, type: Number,
@@ -484,8 +486,13 @@ function handleExecute() {
if (hasServiceRequest) { if (hasServiceRequest) {
// 仅传入选中医嘱对应的 encounterId避免其他患者的耗材记录干扰 // 仅传入选中医嘱对应的 encounterId避免其他患者的耗材记录干扰
const selectedEncounterIds = [...new Set(list.map((item) => item.encounterId).filter(Boolean))]; const selectedEncounterIds = [...new Set(list.map((item) => item.encounterId).filter(Boolean))];
// 仅传入诊疗类医嘱的 requestId让后端仅校验与本次执行相关的耗材避免其他未执行医嘱的耗材记录干扰
const selectedRequestIds = list
.filter((item) => String(item.adviceTable || '') === 'wor_service_request')
.map((item) => item.requestId)
.filter(Boolean);
if (selectedEncounterIds.length > 0) { if (selectedEncounterIds.length > 0) {
lotNumberMatch({ encounterIdList: selectedEncounterIds }, { skipErrorMsg: true }) lotNumberMatch({ encounterIdList: selectedEncounterIds, requestIdList: selectedRequestIds }, { skipErrorMsg: true })
.then((matchRes) => { .then((matchRes) => {
if (matchRes && matchRes.code !== 200) { if (matchRes && matchRes.code !== 200) {
console.warn('lotNumberMatch returned error:', matchRes.msg); console.warn('lotNumberMatch returned error:', matchRes.msg);
@@ -650,6 +657,7 @@ function handleRateChange(value, date, time, row, rateItem) {
} }
function handelSwicthChange(value) { function handelSwicthChange(value) {
skipSelectCascade.value = true;
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item, index) => {
const tableRef = proxy.$refs['tableRef' + index]; const tableRef = proxy.$refs['tableRef' + index];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
@@ -670,12 +678,15 @@ function handelSwicthChange(value) {
} }
} }
}); });
skipSelectCascade.value = false;
} }
// 默认选中全部行 // 默认选中全部行
function defaultSelectAllRows() { function defaultSelectAllRows() {
// 清空并重建选中集合 // 清空并重建选中集合
selectedRowIds.value.clear(); selectedRowIds.value.clear();
// 阻止 toggleRowSelection 触发 handleRowSelect 中的 selectAllCheckboxesInRow 级联
skipSelectCascade.value = true;
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item, index) => {
const tableRef = proxy.$refs['tableRef' + index]; const tableRef = proxy.$refs['tableRef' + index];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
@@ -688,6 +699,7 @@ function defaultSelectAllRows() {
}); });
} }
}); });
skipSelectCascade.value = false;
// 更新全选开关状态 // 更新全选开关状态
chooseAll.value = true; chooseAll.value = true;
} }
@@ -764,6 +776,7 @@ function checkAndToggleRowSelection(row) {
const isCurrentlySelected = selectedRowIds.value.has(row.requestId); const isCurrentlySelected = selectedRowIds.value.has(row.requestId);
// 根据checkbox状态更新表格行选中状态 // 根据checkbox状态更新表格行选中状态
skipSelectCascade.value = true;
if (isAllSelected && !isCurrentlySelected) { if (isAllSelected && !isCurrentlySelected) {
selectedRowIds.value.add(row.requestId); selectedRowIds.value.add(row.requestId);
tableRef[0].toggleRowSelection(row, true); tableRef[0].toggleRowSelection(row, true);
@@ -771,6 +784,7 @@ function checkAndToggleRowSelection(row) {
selectedRowIds.value.delete(row.requestId); selectedRowIds.value.delete(row.requestId);
tableRef[0].toggleRowSelection(row, false); tableRef[0].toggleRowSelection(row, false);
} }
skipSelectCascade.value = false;
} }
} }
}); });
@@ -782,8 +796,10 @@ function handleRowSelect(selection, row, tableIndex) {
if (isSelected) { if (isSelected) {
selectedRowIds.value.add(row.requestId); selectedRowIds.value.add(row.requestId);
// 选中选中该行内部的所有checkbox // 仅在非程序化选中时,联动选中该行内部的所有checkbox
selectAllCheckboxesInRow(row); if (!skipSelectCascade.value) {
selectAllCheckboxesInRow(row);
}
} else { } else {
selectedRowIds.value.delete(row.requestId); selectedRowIds.value.delete(row.requestId);
// 取消选中行时取消选中该行内部的所有checkbox // 取消选中行时取消选中该行内部的所有checkbox