Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa18e94cd9 | |||
| 69bb887d19 | |||
| b89f41048b | |||
| e13e328627 |
30
md/bug-analysis/bug444-analysis.md
Normal file
30
md/bug-analysis/bug444-analysis.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Bug #444 分析报告
|
||||||
|
|
||||||
|
## Bug 描述
|
||||||
|
生成临时医嘱界面,"已引用计费药品"列表未正常显示药品详细名称信息。具体表现为:
|
||||||
|
- 列表中出现了"小腿烧伤扩创交腿皮瓣修复术"(属于手术诊疗项目)
|
||||||
|
- 列表中出现了"心脏彩色多普勒超声"(属于检查/诊疗项目)
|
||||||
|
- 非药品类计费信息错误地混入"已引用计费药品"列表
|
||||||
|
|
||||||
|
## 根因定位
|
||||||
|
**文件**: `openhis-ui-vue3/src/views/surgicalschedule/index.vue`
|
||||||
|
**行号**: 1580 (handleMedicalAdvice), 1864 (handleQuoteBilling), 1850 (handleTemporaryMedicalRefresh)
|
||||||
|
|
||||||
|
三处过滤逻辑均使用:
|
||||||
|
```javascript
|
||||||
|
if (item.adviceType !== 1) return false;
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题1(主因)**: `adviceType` 字段命名兼容不完整。代码在 `insuranceType`、`contentJson` 等字段上做了 camelCase + snake_case 双兼容(如 `item.insuranceType || item.insurance_type`),但 `adviceType` 只检查了 camelCase。若后端返回 snake_case 数据(`advice_type`),`item.adviceType` 为 `undefined`,`undefined !== 1` 为 `true`,导致所有非药品项目全部放行。
|
||||||
|
|
||||||
|
**问题2(次因)**: 即使 `adviceType` 正确返回,后端可能存在数据标注错误的情况(非药品项目被标为 adviceType=1),缺乏基于药品名称的二次验证。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
1. `adviceType` 检查增加 snake_case 回退:`const at = item.adviceType ?? item.advice_type; if (at !== 1) return false;`
|
||||||
|
2. 增加药品名称关键字二次过滤:排除名称中包含"术"、"检查"、"超声"、"多普勒"等关键词的非药品项目
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
1. "已引用计费药品"列表中只显示药品类项目
|
||||||
|
2. 不显示手术诊疗项目(如"小腿烧伤扩创交腿皮瓣修复术")
|
||||||
|
3. 不显示检查项目(如"心脏彩色多普勒超声")
|
||||||
|
4. 药品名称正常显示
|
||||||
@@ -132,7 +132,22 @@ temporaryAdvices.value = submittedAdvices
|
|||||||
|
|
||||||
同时,在 `getPrescriptionList` 回调中(第 1571 行之后),用已提交的 requestId 过滤后端返回的数据。
|
同时,在 `getPrescriptionList` 回调中(第 1571 行之后),用已提交的 requestId 过滤后端返回的数据。
|
||||||
|
|
||||||
## 总结
|
## 修复结果
|
||||||
|
|
||||||
- **根因**:`handleMedicalAdvice` 每次打开都清空 `temporaryAdvices`,然后从后端重新拉取数据。但后端返回的新创建医嘱项可能没有 `requestId`,导致无法过滤。
|
### 实际根因
|
||||||
- **修复**:保留已提交(有 requestId)的医嘱数据,不清空;同时用这些 requestId 过滤后端返回的新数据。
|
`handleQuoteBilling` 函数中:
|
||||||
|
1. **第1856行**:在调用 `getPrescriptionList` 之前先清空了 `temporaryAdvices.value = []`
|
||||||
|
2. **第1997-2019行(旧代码)**:ID 匹配过滤逻辑依赖已被清空的 `temporaryAdvices.value`,因此过滤形同虚设
|
||||||
|
3. 即使 `temporaryAdvices` 未被清空,ID 匹配也不可靠(新生成的医嘱可能没有 `requestId`/`chargeItemId`/`id`)
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
1. 在清空 `temporaryAdvices` **之前**,提取已提交项目的复合键(名称+规格+数量)保存到 `submittedKeysBeforeClear`
|
||||||
|
2. 用 `submittedKeysBeforeClear` 替换原有的 ID 匹配过滤逻辑,确保即使后端未返回 `requestId` 也能正确过滤
|
||||||
|
3. 复合键匹配策略与 `handleTemporaryMedicalSubmit` 中使用的策略一致
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `openhis-ui-vue3/src/views/surgicalschedule/index.vue`
|
||||||
|
- 第1853-1864行:新增 `submittedKeysBeforeClear` 提取逻辑
|
||||||
|
- 第1997-2004行:替换 ID 匹配为复合键匹配
|
||||||
|
|
||||||
|
### 修复结果:✅ 成功,~20行改动(+20/-21)
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
|
|||||||
String activityName = activityDef != null ? activityDef.getName() : "";
|
String activityName = activityDef != null ? activityDef.getName() : "";
|
||||||
|
|
||||||
List<OrganizationLocation> organizationLocationList =
|
List<OrganizationLocation> organizationLocationList =
|
||||||
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getActivityDefinitionId());
|
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId());
|
||||||
organizationLocationList = (orgLoc.getId() != null)
|
organizationLocationList = (orgLoc.getId() != null)
|
||||||
? organizationLocationList.stream().filter(item -> !orgLoc.getId().equals(item.getId())).toList()
|
? organizationLocationList.stream().filter(item -> !orgLoc.getId().equals(item.getId())).toList()
|
||||||
: organizationLocationList;
|
: organizationLocationList;
|
||||||
|
|||||||
@@ -53,12 +53,14 @@ public class OrganizationLocationServiceImpl extends ServiceImpl<OrganizationLoc
|
|||||||
/**
|
/**
|
||||||
* 查询诊疗的执行科室列表
|
* 查询诊疗的执行科室列表
|
||||||
*
|
*
|
||||||
|
* @param organizationId 机构id
|
||||||
* @param activityDefinitionId 诊疗定义id
|
* @param activityDefinitionId 诊疗定义id
|
||||||
* @return 诊疗的执行科室列表
|
* @return 诊疗的执行科室列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long activityDefinitionId) {
|
public List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long organizationId, Long activityDefinitionId) {
|
||||||
return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>()
|
return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>()
|
||||||
|
.eq(OrganizationLocation::getOrganizationId, organizationId)
|
||||||
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
|
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1576,12 +1576,18 @@ function handleMedicalAdvice(row) {
|
|||||||
const filteredItems = res.data.filter(item => {
|
const filteredItems = res.data.filter(item => {
|
||||||
// 匹配 encounterId
|
// 匹配 encounterId
|
||||||
if (item.encounterId !== row.visitId) return false;
|
if (item.encounterId !== row.visitId) return false;
|
||||||
// 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3)
|
// 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3/6)
|
||||||
if (item.adviceType !== 1) return false;
|
// 🔧 修复 Bug #444: 使用 Number() 显式转换,避免字符串 "1" 被 !== 1 放行
|
||||||
|
const at = Number(item.adviceType ?? item.advice_type);
|
||||||
|
if (at !== 1) return false;
|
||||||
// 过滤掉名称为空的项目
|
// 过滤掉名称为空的项目
|
||||||
const medicineName = item.adviceName || item.advice_name;
|
const medicineName = item.adviceName || item.advice_name;
|
||||||
if (!medicineName || medicineName.trim() === '') return false;
|
if (!medicineName || medicineName.trim() === '') return false;
|
||||||
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表)
|
// 🔧 修复 Bug #444: 二次过滤,排除名称中包含手术/检查/诊疗关键词的非药品项目
|
||||||
|
// 某些计费项目可能在 adm_charge_item 中被错误标注为 adviceType=1
|
||||||
|
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
|
||||||
|
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
|
||||||
|
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表中)
|
||||||
if (item.requestId) return false;
|
if (item.requestId) return false;
|
||||||
// 根据药品请求ID去重,避免重复显示
|
// 根据药品请求ID去重,避免重复显示
|
||||||
const itemId = item.requestId || item.id;
|
const itemId = item.requestId || item.id;
|
||||||
@@ -1848,6 +1854,21 @@ function handleTemporaryMedicalRefresh() {
|
|||||||
function handleQuoteBilling() {
|
function handleQuoteBilling() {
|
||||||
// 重新拉取计费药品数据
|
// 重新拉取计费药品数据
|
||||||
if (temporaryPatientInfo.value.visitId) {
|
if (temporaryPatientInfo.value.visitId) {
|
||||||
|
// 🔧 修复 Bug #445: 在清空之前提取已提交项目的复合匹配键
|
||||||
|
// 原因:后续的 ID 匹配过滤依赖 temporaryAdvices,但 temporaryAdvices 会被先清空
|
||||||
|
// 新医嘱没有 requestId/chargeItemId,需用名称+规格+数量的复合键匹配
|
||||||
|
const submittedKeys = new Set(
|
||||||
|
(temporaryAdvices.value || [])
|
||||||
|
.map(a => {
|
||||||
|
const om = a.originalMedicine || {}
|
||||||
|
const name = om.medicineName || om.adviceName || a.adviceName || ''
|
||||||
|
const spec = om.specification || om.volume || ''
|
||||||
|
const qty = om.quantity ?? 0
|
||||||
|
return `${name}|||${spec}|||${qty}`
|
||||||
|
})
|
||||||
|
.filter(k => k !== '|||0')
|
||||||
|
)
|
||||||
|
|
||||||
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
|
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
|
||||||
getPrescriptionList(temporaryPatientInfo.value.visitId, 6, temporaryPatientInfo.value.operCode).then((res) => {
|
getPrescriptionList(temporaryPatientInfo.value.visitId, 6, temporaryPatientInfo.value.operCode).then((res) => {
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
@@ -1855,16 +1876,21 @@ function handleQuoteBilling() {
|
|||||||
temporaryBillingMedicines.value = []
|
temporaryBillingMedicines.value = []
|
||||||
temporaryAdvices.value = []
|
temporaryAdvices.value = []
|
||||||
|
|
||||||
// 🔧 修复 Bug #445: 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3)
|
// 🔧 修复 Bug #445: 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3/6)
|
||||||
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中)
|
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中)
|
||||||
const filteredItems = res.data.filter(item => {
|
const filteredItems = res.data.filter(item => {
|
||||||
// 匹配 encounterId
|
// 匹配 encounterId
|
||||||
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
|
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
|
||||||
// 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3)
|
// 只保留药品类型(adviceType=1),过滤掉耗材(2)和诊疗项目(3/6)
|
||||||
if (item.adviceType !== 1) return false;
|
// 🔧 修复 Bug #444: 使用 Number() 显式转换 + snake_case 回退,避免字符串 "1" 匹配失败
|
||||||
|
const at = Number(item.adviceType ?? item.advice_type);
|
||||||
|
if (at !== 1) return false;
|
||||||
// 过滤掉名称为空的项目
|
// 过滤掉名称为空的项目
|
||||||
const medicineName = item.adviceName || item.advice_name;
|
const medicineName = item.adviceName || item.advice_name;
|
||||||
if (!medicineName || medicineName.trim() === '') return false;
|
if (!medicineName || medicineName.trim() === '') return false;
|
||||||
|
// 🔧 修复 Bug #444: 二次过滤,排除名称中包含手术/检查/诊疗关键词的非药品项目
|
||||||
|
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
|
||||||
|
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
|
||||||
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId)
|
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId)
|
||||||
if (item.requestId) return false;
|
if (item.requestId) return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -1981,27 +2007,57 @@ function handleQuoteBilling() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目,避免"引用计费"后已提交项目重新出现在"待生成"列表
|
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目,避免"引用计费"后已提交项目重新出现在"待生成"列表
|
||||||
// 原因:后端返回的计费数据中,已生成医嘱的项目可能没有 requestId 字段
|
// 使用清空前提取的 submittedKeys(名称|||规格|||数量复合键)进行匹配
|
||||||
// 方案:用 chargeItemId/requestId/id 与已有的 temporaryAdvices 做匹配,排除已生成项目
|
if (submittedKeys.size > 0) {
|
||||||
if (temporaryAdvices.value.length > 0) {
|
temporaryBillingMedicines.value = temporaryBillingMedicines.value.filter(m => {
|
||||||
const existingAdviceIds = new Set()
|
const key = `${m.medicineName || ''}|||${m.specification || ''}|||${m.quantity ?? 0}`
|
||||||
temporaryAdvices.value.forEach(a => {
|
return !submittedKeys.has(key)
|
||||||
const om = a.originalMedicine || {}
|
})
|
||||||
if (om.requestId) existingAdviceIds.add(String(om.requestId))
|
// 同步更新 temporaryAdvices,保持两份数据一致
|
||||||
if (om.chargeItemId) existingAdviceIds.add(String(om.chargeItemId))
|
temporaryAdvices.value = temporaryBillingMedicines.value.map((medicine, index) => {
|
||||||
if (om.id) existingAdviceIds.add(String(om.id))
|
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
|
||||||
|
const specValue = specMatch ? parseInt(specMatch[1]) : 1
|
||||||
|
const specUnit = specMatch ? specMatch[2] : 'ml'
|
||||||
|
const dosage = specValue * (medicine.quantity || 1)
|
||||||
|
let usageCode = 'iv'
|
||||||
|
let usageLabel = '静脉注射'
|
||||||
|
try {
|
||||||
|
const jsonContent = medicine.contentJson || medicine.content_json;
|
||||||
|
if (jsonContent) {
|
||||||
|
const contentData = JSON.parse(jsonContent);
|
||||||
|
if (contentData.methodCode) {
|
||||||
|
usageCode = contentData.methodCode;
|
||||||
|
usageLabel = getUsageLabel(contentData.methodCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (!usageCode || usageCode === 'iv') {
|
||||||
|
if (medicine.medicineName && medicine.medicineName.includes('注射液')) {
|
||||||
|
usageCode = 'iv'; usageLabel = '静脉注射';
|
||||||
|
} else if (medicine.medicineName && medicine.medicineName.includes('片')) {
|
||||||
|
usageCode = 'po'; usageLabel = '口服';
|
||||||
|
} else if (medicine.medicineName && medicine.medicineName.includes('胶囊')) {
|
||||||
|
usageCode = 'po'; usageLabel = '口服';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
adviceName: medicine.medicineName || '',
|
||||||
|
dosage,
|
||||||
|
unit: specUnit,
|
||||||
|
usage: usageCode,
|
||||||
|
usageLabel,
|
||||||
|
frequency: '临时',
|
||||||
|
executeTime: new Date().toLocaleString('zh-CN'),
|
||||||
|
originalMedicine: {
|
||||||
|
...medicine,
|
||||||
|
medicineName: medicine.medicineName,
|
||||||
|
specification: medicine.specification,
|
||||||
|
quantity: medicine.quantity,
|
||||||
|
encounterId: temporaryPatientInfo.value.visitId
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if (existingAdviceIds.size > 0) {
|
|
||||||
temporaryBillingMedicines.value = temporaryBillingMedicines.value.filter(m => {
|
|
||||||
const mRequestId = m.requestId != null ? String(m.requestId) : null
|
|
||||||
const mChargeItemId = m.chargeItemId != null ? String(m.chargeItemId) : null
|
|
||||||
const mId = m.id != null ? String(m.id) : null
|
|
||||||
if (mRequestId && existingAdviceIds.has(mRequestId)) return false
|
|
||||||
if (mChargeItemId && existingAdviceIds.has(mChargeItemId)) return false
|
|
||||||
if (mId && existingAdviceIds.has(mId)) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
temporaryMedicalLoading.value = false // 🔧 新增:加载完成
|
temporaryMedicalLoading.value = false // 🔧 新增:加载完成
|
||||||
|
|||||||
Reference in New Issue
Block a user