8 Commits

Author SHA1 Message Date
关羽
66d42f415a Fix Bug #477: 住院检查申请详情弹窗中"发往科室"字段显示异常
根因:recursionFun 使用嵌套循环搜索科室树,但 API 返回扁平列表导致匹配失败。
修复:改用递归 findTreeItem 搜索(与 medicalExaminations.vue 一致),添加 API 错误处理,
并在 ID 匹配失败时回退显示原始值而非空白。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:22:12 +08:00
荀彧
fd0132ba80 Fix Bug #467: [住院医生工作站-检验申请] 列表显示信息不规范:标题术语错误且单据名称未展示具体检验项目
1. 详情弹窗中"处方号"改为"申请单号",符合住院检验业务术语规范
2. 列表"申请单名称"列改为从 requestFormDetailList 动态构建:
   - 单一项目:显示"项目名称+数量"
   - 多个项目:显示"首项目名称+数量等X项"
   解决此前统一显示"检验申请单"无法区分单据内容的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:15:10 +08:00
关羽
6907c7dbc8 Fix Bug #481: [住院护士站-医嘱执行] 药品库存充足但执行时提示库存不足
根因: AdviceUtils.checkExeMedInventory() 中硬编码 performLocation == locationId 的匹配条件,
当医嘱的 performLocation 指向的药房没有该药品库存时(库存实际在其他药房),匹配失败导致"库存不足"错误。

修复策略: 采用两步匹配法 -
1. 先按 performLocation 匹配指定药房的库存(添加 null 容错)
2. 若指定药房无匹配,则放宽条件跨所有药房聚合库存

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:14:30 +08:00
赵云
487b05c845 Fix Bug #465: [住院医生工作站-检验申请] 检验项目选择列表被限制为500项,导致医生无法检索并开立其余800多项
根因分析:
1. 前端参数名错误:getList 发送 pageNum 但后端期望 pageNo,导致分页参数被忽略
2. 后端 MyBatis Plus 分页拦截器单页最多返回500条,前端用 pageSize:9999 无效
3. 总共有1300+条检验项目,但只返回了前500条

修复方案:
- 修正参数名为 pageNo 匹配后端接口
- 使用分页循环拉取全部数据(每页500条,循环直到数据全部拉取)
- 添加 try/catch 错误处理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:12:41 +08:00
关羽
71f99da69a Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
修复 handleExecute 中 hasDevice 判断逻辑错误:原代码用 includes('device') 判断
adviceTable 是否包含耗材类医嘱,但 adviceTable 实际取值为 med_medication_request
(药品)或 wor_service_request(诊疗/耗材),均不含 "device" 字符串,导致 hasDevice
恒为 false。

改为检查 adviceTable === 'wor_service_request',确保仅当选中医嘱包含诊疗类(可能
绑定耗材)时才调用 lotNumberMatch,纯药品医嘱不再触发耗材库存校验。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:43:27 +08:00
荀彧
15a65063a3 Fix Bug #470: 住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率
添加模块级缓存(surgeryRecordsCache + surgeryMappedCache),首次打开弹窗请求API后
缓存数据,后续打开直接复用,避免重复请求500条手术项目列表导致加载缓慢。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:18:29 +08:00
关羽
72fdafb032 Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
根因:后端返回字段名为 status,而操作列条件判断使用了 scope.row.billStatus,
billStatus 为 undefined 导致所有状态条件判断失败,仅显示固定的"详情"按钮。
修复:将操作列条件中的 billStatus 统一改为 status。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:14:51 +08:00
关羽
56b8d0e98d Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
- 将requestFormId判空改为requestFormId != null && requestFormId != 0L,防止前端空字符串反序列化为0L误入编辑场景
- 在循环中逐个校验activityList中的项目是否都配置了执行科室,将所有校验前置到任何数据库操作之前,避免部分通过后在save中途抛异常
- 统一使用isEdit标志变量替代重复的null检查

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:12:06 +08:00
7 changed files with 138 additions and 66 deletions

View File

@@ -178,15 +178,26 @@ public class AdviceUtils {
// 生命提示信息集合
List<String> tipsList = new ArrayList<>();
for (MedicationRequestUseExe medicationRequestUseExe : medUseExeList) {
// 聚合同一位置所有批次的库存总量
// 第一步:按 performLocation 匹配指定药房的库存
List<AdviceInventoryDto> matchedInventories = adviceInventory.stream()
.filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId())
&& CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable())
&& medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId())
&& (medicationRequestUseExe.getPerformLocation() == null
|| medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId()))
// 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件
&& (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber())
|| medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber())))
.collect(Collectors.toList());
// 第二步:如果指定药房没有匹配到库存,则放宽条件查询所有药房的库存
if (matchedInventories.isEmpty()) {
matchedInventories = adviceInventory.stream()
.filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId())
&& CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable())
// 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件
&& (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber())
|| medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber())))
.collect(Collectors.toList());
}
// 匹配到库存信息
if (!matchedInventories.isEmpty()) {
// 聚合所有批次的可用库存

View File

@@ -76,6 +76,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 申请单ID前端空字符串可能反序列化为0L需同时判0
Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L;
// 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
@@ -83,12 +87,23 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
throw new ServiceException("请先配置当前时间段的执行科室");
}
// 逐个校验activityList中的项目是否都配置了执行科室避免部分通过后在循环中抛异常导致事务复杂化
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
if (activityList != null && !activityList.isEmpty()) {
for (ActivitySaveDto activitySaveDto : activityList) {
Long positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
}
}
// 诊疗处方号
String prescriptionNo;
// 申请单ID
Long requestFormId = requestFormSaveDto.getRequestFormId();
// 编辑场景
if (requestFormId != null) {
if (isEdit) {
RequestForm requestFormInfo = iRequestFormService.getById(requestFormId);
prescriptionNo = requestFormInfo.getPrescriptionNo();
// 该申请单存在的待发送医嘱个数
@@ -132,7 +147,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
iRequestFormService.saveOrUpdate(requestForm);
// 编辑场景时,先删除掉原有诊疗项目及账单再新增
if (requestFormId != null) {
if (isEdit) {
List<Long> serviceRequestIds = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo))
.stream().map(ServiceRequest::getId).collect(Collectors.toList());
@@ -146,8 +161,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
ServiceRequest serviceRequest;
ChargeItem chargeItem;
// 诊疗集合
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
for (ActivitySaveDto activitySaveDto : activityList) {

View File

@@ -310,32 +310,26 @@ const hasMatchedFields = computed(() => {
/** 查询科室 */
const getLocationInfo = async () => {
const res = await getDepartmentList();
orgOptions.value = res.data || [];
try {
const res = await getDepartmentList();
orgOptions.value = Array.isArray(res.data) ? res.data : [];
} catch (e) {
console.warn('科室列表加载失败:', e.message);
orgOptions.value = [];
}
};
const recursionFun = (targetDepartment) => {
if (!targetDepartment) return '';
let name = '';
for (let index = 0; index < orgOptions.value.length; index++) {
const obj = orgOptions.value[index];
if (obj.id == targetDepartment) {
name = obj.name;
break;
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
for (const item of list) {
if (item.id == id) return item;
if (item.children && item.children.length > 0) {
const found = findTreeItem(item.children, id);
if (found) return found;
}
const subObjArray = obj['children'];
if (subObjArray && subObjArray.length > 0) {
for (let i = 0; i < subObjArray.length; i++) {
const item = subObjArray[i];
if (item.id == targetDepartment) {
name = item.name;
break;
}
}
}
if (name) break;
}
return name;
return null;
};
const handleViewDetail = async (row) => {
@@ -349,7 +343,11 @@ const handleViewDetail = async (row) => {
if (row.descJson) {
try {
const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment);
// 将发往科室 ID 转换为名称
if (obj.targetDepartment) {
const deptItem = findTreeItem(orgOptions.value, obj.targetDepartment);
obj.targetDepartment = deptItem ? deptItem.name : obj.targetDepartment;
}
descJsonData.value = obj;
} catch (e) {
console.error('解析 descJson 失败:', e);

View File

@@ -82,7 +82,11 @@
</template>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" />
<el-table-column label="申请单名称" width="140">
<template #default="scope">
<span>{{ buildApplicationName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column label="单据状态" width="100" align="center">
@@ -103,11 +107,11 @@
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="160">
<template #default="scope">
<template v-if="scope.row.billStatus == 0 || scope.row.status == 0">
<template v-if="scope.row.status == 0">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
<template v-else-if="scope.row.billStatus == 1 || scope.row.status == 1">
<template v-else-if="scope.row.status == 1">
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
</template>
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
@@ -137,7 +141,7 @@
<el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="处方号">{{
<el-descriptions-item label="申请单号">{{
currentDetail.prescriptionNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请者">{{
@@ -339,6 +343,24 @@ const parseSpecimenType = (descJson) => {
}
};
/**
* 根据申请单详情构建申请单名称
* 单一项目:显示项目名称+数量
* 多个项目:显示首个项目名称+数量+"等X项"
*/
const buildApplicationName = (row) => {
const details = row.requestFormDetailList;
if (!details || details.length === 0) {
return row.name || '-';
}
if (details.length === 1) {
const item = details[0];
return `${item.adviceName}${item.quantity || ''}`;
}
const first = details[0];
return `${first.adviceName}${first.quantity || ''}${details.length}`;
};
const isFieldMatched = (key) => {
return key in labelMap;
};

View File

@@ -144,42 +144,56 @@ const applicationListAll = ref();
const applicationList = ref();
const loading = ref(false);
const orgOptions = ref([]); // 科室选项
const getList = () => {
const getList = async () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationList.value = [];
return;
}
loading.value = true;
getApplicationList({
pageSize: 9999,
pageNum: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], //1 药品 2耗材 3诊疗
})
.then((res) => {
if (res.code === 200) {
applicationListAll.value = res.data.records;
applicationList.value = res.data.records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
console.log('applicationList========>', JSON.stringify(res.data.records));
} else {
try {
const allRecords = [];
let currentPage = 1;
const pageSize = 500;
// 分页拉取全部数据后端单页最多500条
while (true) {
const res = await getApplicationList({
pageSize,
pageNo: currentPage,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
});
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationList.value = [];
return;
}
})
.finally(() => {
loading.value = false;
const records = res.data?.records || [];
allRecords.push(...records);
// 当前页不足 pageSize 或已无数据,说明已全部拉取
if (records.length < pageSize) break;
currentPage++;
}
applicationListAll.value = allRecords;
applicationList.value = allRecords.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
} catch (e) {
proxy.$message.error('获取检验项目列表失败');
applicationList.value = [];
} finally {
loading.value = false;
}
};
const transferValue = ref([]);
const form = reactive({

View File

@@ -86,6 +86,9 @@ import {getApplicationList, saveSurgery} from './api';
import {ElMessage} from 'element-plus';
const { proxy } = getCurrentInstance();
// 模块级缓存:避免每次打开弹窗都重新请求手术项目列表
let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
@@ -110,6 +113,12 @@ const getList = () => {
applicationList.value = [];
return;
}
// 命中缓存时直接使用,避免重复请求导致加载缓慢
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
return;
}
loading.value = true;
getApplicationList({
pageSize: 500,
@@ -132,6 +141,9 @@ const getList = () => {
key: item.adviceDefinitionId,
};
});
// 写入模块缓存,后续打开弹窗直接复用
surgeryRecordsCache = res.data.records;
surgeryMappedCache = applicationList.value;
} else {
console.warn('获取手术项目列表失败:', res.message);
applicationList.value = [];

View File

@@ -471,11 +471,13 @@ function handleExecute() {
console.log(list, 'list');
adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => {
if (res.code == 200) {
// 仅当选中医嘱中包含耗材类医嘱时,才调用耗材批号匹配(排除纯药品医嘱场景)
const hasDevice = list.some((item) =>
String(item.adviceTable || '').includes('device'),
// 仅当选中医嘱中包含诊疗类医嘱(可能绑定耗材)时,才调用耗材批号匹配
// adviceTable 取值为 med_medication_request药品或 wor_service_request诊疗/耗材)
// 原代码用 includes('device') 判断有误,两个表名均不含 "device" 字符串
const hasServiceRequest = list.some((item) =>
String(item.adviceTable || '') === 'wor_service_request',
);
if (hasDevice) {
if (hasServiceRequest) {
lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error);
});