Compare commits

...

5 Commits

Author SHA1 Message Date
荀彧
43c1bf7b02 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 23:18:30 +08:00
关羽
1124b1010d 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>
2026-05-14 22:22:01 +08:00
关羽
f41b86a143 Fix Bug #476: 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等)
补充打印功能中缺失的核心临床字段:紧急程度、期望检查时间、过敏史、检查目的、病史摘要,
并对urgencyLevel等编码字段应用transformField进行值转换显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:11:25 +08:00
关羽
d3310ade51 Fix Bug #467: [住院医生工作站-检验申请] 列表显示信息不规范:标题术语错误且单据名称未展示具体检验项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:10:56 +08:00
关羽
1dbf7859ea Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
合并验证逻辑:移除独立的"配置列表是否为空"检查,改为在遍历 activityList
时统一验证每个项目的执行科室配置。所有验证在任何数据库操作之前完成,
确保验证失败时不会产生脏数据。同时增加 activityList 为空时的明确提示。
2026-05-14 22:09:06 +08:00
9 changed files with 94 additions and 48 deletions

View File

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

View File

@@ -39,8 +39,10 @@ import com.openhis.web.common.dto.*;
import com.openhis.web.common.mapper.CommonAppMapper;
import com.openhis.web.pharmacymanage.dto.InventoryDetailDto;
import com.openhis.workflow.domain.DeviceDispense;
import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IInventoryItemService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -99,6 +101,9 @@ public class CommonServiceImpl implements ICommonService {
@Resource
private IDeviceDispenseService deviceDispenseService;
@Resource
private IDeviceRequestService deviceRequestService;
/**
* 获取药房列表
*
@@ -678,10 +683,11 @@ public class CommonServiceImpl implements ICommonService {
* 批号匹配
*
* @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果
*/
@Override
public R<?> lotNumberMatch(List<Long> encounterIdList) {
public R<?> lotNumberMatch(List<Long> encounterIdList, List<Long> requestIdList) {
// 查询患者待发放的药品信息
List<MedicationDispense> medicationDispenseList = medicationDispenseService
.list(new LambdaQueryWrapper<MedicationDispense>().in(MedicationDispense::getEncounterId, encounterIdList)
@@ -798,10 +804,27 @@ public class CommonServiceImpl implements ICommonService {
}
}
// 查询患者待发放的耗材信息
List<DeviceDispense> deviceDispenseList = deviceDispenseService
.list(new LambdaQueryWrapper<DeviceDispense>().in(DeviceDispense::getEncounterId, encounterIdList)
.eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue())
.eq(DeviceDispense::getDeleteFlag, DelFlag.NO.getCode()));
LambdaQueryWrapper<DeviceDispense> deviceDispenseQuery = new LambdaQueryWrapper<DeviceDispense>()
.in(DeviceDispense::getEncounterId, encounterIdList)
.eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue())
.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()) {
// 获取待发放的耗材id

View File

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

View File

@@ -81,36 +81,35 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L;
// 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("先配置当前时间段的执行科室");
// 校验所有activityList中的项目是否都配置了执行科室并收集positionId供后续使用
// 必须在任何数据库操作之前完成全部校验,避免部分保存后异常导致脏数据
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
if (activityList == null || activityList.isEmpty()) {
throw new ServiceException("选择检查项目");
}
// 逐个校验activityList中的项目是否都配置了执行科室并收集positionId供后续使用
// 必须在任何数据库操作之前完成全部校验,避免部分保存后异常导致脏数据
// 🔧 Bug #516: 优先使用前端传入的positionId用户手动选择的发往科室仅在未选择时使用配置的执行科室
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
// 缓存校验结果,避免主循环中重复查询和可能出现的数据不一致
// 🔧 Bug #475: 查询诊疗执行科室配置
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
// 缓存校验结果,先全部验证通过后再进行数据库操作
// 优先使用前端传入的positionId用户手动选择的发往科室仅在未选择时使用配置的执行科室
java.util.Map<Long, Long> activityIdToPositionIdMap = new java.util.HashMap<>();
if (activityList != null && !activityList.isEmpty()) {
for (ActivitySaveDto activitySaveDto : activityList) {
// 优先使用前端传入的positionId用户手动选择的科室
Long frontendPositionId = activitySaveDto.getPositionId();
if (frontendPositionId != null) {
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), frontendPositionId);
continue;
}
// 前端未传入时,使用配置的执行科室
Long configPositionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (configPositionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), configPositionId);
for (ActivitySaveDto activitySaveDto : activityList) {
// 优先使用前端传入的positionId用户手动选择的科室
Long frontendPositionId = activitySaveDto.getPositionId();
if (frontendPositionId != null) {
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), frontendPositionId);
continue;
}
// 前端未传入时,使用配置的执行科室
Long configPositionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (configPositionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), configPositionId);
}
// 诊疗处方号

View File

@@ -78,7 +78,11 @@
<select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto">
SELECT wsr.quantity,
wsr.unit_code,
COALESCE(wad.NAME, wsr.content_json::jsonb->>'surgeryName') AS advice_name,
COALESCE(
wad.NAME,
wsr.content_json::jsonb->>'surgeryName',
'检验项目'
) AS advice_name,
aci.total_price
FROM wor_service_request AS wsr
LEFT JOIN wor_activity_definition AS wad ON wad.ID = wsr.activity_id
@@ -88,6 +92,7 @@
AND aci.delete_flag = '0'
WHERE wsr.delete_flag = '0'
AND wsr.prescription_no = #{prescriptionNo}
ORDER BY wsr.id
</select>
<select id="getActivityOrganizationConfig"

View File

@@ -678,15 +678,16 @@ const handlePrint = async (row) => {
}
// 构建 descJson 字段行(与详情弹窗展示的字段一致)
const fieldKeys = ['targetDepartment', 'symptom', 'sign', 'clinicalDiagnosis', 'otherDiagnosis', 'relatedResult', 'attention'];
const fieldKeys = ['targetDepartment', 'urgencyLevel', 'expectedExaminationTime', 'allergyHistory', 'examinationPurpose', 'medicalHistorySummary', 'symptom', 'sign', 'clinicalDiagnosis', 'otherDiagnosis', 'relatedResult', 'attention'];
let descFieldsHtml = '';
fieldKeys.forEach((key) => {
const label = labelMap[key] || key;
if (descData[key] != null && descData[key] !== '') {
const value = transformField(key, descData[key]);
if (value != null && value !== '') {
descFieldsHtml += `
<div class="info-row">
<span class="label">${label}</span>
<span class="value">${descData[key]}</span>
<span class="value">${value}</span>
</div>`;
}
});

View File

@@ -136,7 +136,7 @@
currentDetail.patientName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请单名称">{{
currentDetail.name || '-'
buildApplicationName(currentDetail)
}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-'

View File

@@ -910,9 +910,8 @@ function handleFocus(row, index) {
// 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 修复Bug #486当行没有显式选择医嘱类型时row.adviceType为undefined
// 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤
const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
// 当行没有显式选择医嘱类型时使用已配置的默认categoryCode确保后端能返回结果
const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : (adviceQueryParams.value.categoryCode || '');
const searchKey = row.adviceName || '';
nextTick(() => {
@@ -949,9 +948,8 @@ function handleChange(value) {
// 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 修复Bug #486当行没有显式选择医嘱类型时row?.adviceType为undefined
// 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
// 当行没有显式选择医嘱类型时使用已配置的默认categoryCode确保后端能返回结果
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : (adviceQueryParams.value.categoryCode || '');
// 修复Bug #453当adviceType为空字符串或NaN时不传具体类型让refresh函数根据searchKey决定搜索范围
const effectiveAdviceType = (adviceType && !isNaN(Number(adviceType))) ? adviceType : '';
tableRef.refresh(effectiveAdviceType, categoryCode, value);

View File

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