Compare commits
7 Commits
7cc8bfe139
...
4ec422fc0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec422fc0e | ||
|
|
fdedad618a | ||
|
|
2594e372b8 | ||
|
|
0197f5509c | ||
|
|
a75063430c | ||
|
|
300d53bdc6 | ||
|
|
284818bd8f |
89
BUG461_ANALYSIS.md
Normal file
89
BUG461_ANALYSIS.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Bug #461 分析报告
|
||||
|
||||
## Bug描述
|
||||
[系统管理-执行科室配置] 保存项目配置后,项目名称回显为ID码,未显示正确名称
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 数据流
|
||||
1. 前端调用 `getDiagnosisTreatmentList()` → GET `/base-data-manage/org-loc/org-loc`
|
||||
2. 后端 `OrganizationLocationController.getOrgLocPage()` 返回 `Page<OrgLocQueryDto>`
|
||||
3. `OrgLocQueryDto.activityDefinitionId` 标注了 `@Dict(dictTable="wor_activity_definition", dictCode="id", dictText="name")`
|
||||
4. `DictAspect` AOP 拦截 GET/POST 请求,对带 `@Dict` 注解的字段执行 SQL 翻译:`SELECT name FROM wor_activity_definition WHERE id::varchar = ? LIMIT 1`
|
||||
5. 翻译结果写入 `activityDefinitionId_dictText` 字段
|
||||
6. 前端使用 `record.activityDefinitionId_dictText` 作为项目名称显示
|
||||
|
||||
### 根本原因
|
||||
**`DictAspect.queryDictLabel` 方法在 SQL 查询失败时返回空字符串 `""`,导致 `_dictText` 字段被设置为空值。**
|
||||
|
||||
具体代码 (`DictAspect.java:123-149`):
|
||||
```java
|
||||
private String queryDictLabel(String dictTable, String dictCode, String dictText, String deleteFlag, String dictValue) {
|
||||
if (!StringUtils.hasText(dictTable)) {
|
||||
return DictUtils.getDictLabel(dictCode, dictValue);
|
||||
} else {
|
||||
if (!StringUtils.hasText(dictText)) {
|
||||
return DictUtils.getDictLabel(dictCode, dictValue);
|
||||
}
|
||||
String sql = String.format("SELECT %s FROM %s WHERE %s::varchar = ?", dictText, dictTable, dictCode);
|
||||
// ...
|
||||
try {
|
||||
return jdbcTemplate.queryForObject(sql, String.class, dictValue);
|
||||
} catch (DataAccessException e) {
|
||||
return ""; // ← 关键问题:查询失败返回空字符串
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当 `jdbcTemplate.queryForObject` 查询失败(无结果或异常)时,返回空字符串 `""`。而在 `processDict` 中:
|
||||
```java
|
||||
String dictLabel = queryDictLabel(...);
|
||||
if (dictLabel != null) { // ← 空字符串 "" 不等于 null,条件为 true
|
||||
textField.set(dto, dictLabel); // ← 设置为空字符串
|
||||
}
|
||||
```
|
||||
|
||||
空字符串 `""` 不是 `null`,所以 `_dictText` 被设为空字符串,而不是保持 `null`(未翻译)。前端收到 `activityDefinitionId_dictText: ""`,当作有效值处理,显示为空或回退到 ID。
|
||||
|
||||
### 前端 fallback 的不足
|
||||
前端代码在 `getList()` 中尝试用 `activityDefinitionId_dictText` 补充选项,但:
|
||||
```javascript
|
||||
if (record.activityDefinitionId && !filteredOptions.some(o => o.value === record.activityDefinitionId)) {
|
||||
filteredOptions.push({
|
||||
value: record.activityDefinitionId,
|
||||
label: record.activityDefinitionId_dictText || record.activityDefinitionId // ← 空字符串时显示ID
|
||||
});
|
||||
}
|
||||
```
|
||||
空字符串是 falsy 值,所以 `"" || record.activityDefinitionId` 回退到 ID,仍然显示 ID。
|
||||
|
||||
## 影响范围
|
||||
- **前端**: `openhis-ui-vue3/src/views/basicmanage/implementDepartment/index.vue`
|
||||
- **后端**: `openhis-server-new/.../basedatamanage/appservice/impl/OrganizationLocationAppServiceImpl.java`
|
||||
- **AOP**: `openhis-server-new/.../common/aspectj/DictAspect.java`
|
||||
- **DTO**: `openhis-server-new/.../basedatamanage/dto/OrgLocQueryDto.java`
|
||||
- **数据库表**: `adm_organization_location`, `wor_activity_definition`
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 方案:在 service 层直接 JOIN 查询项目名称
|
||||
|
||||
修改 `OrganizationLocationAppServiceImpl.getOrgLocPage()` 方法,在返回结果前手动填充 `activityDefinitionId_dictText`,不依赖 DictAspect 的行为。这样更可靠,避免了 AOP 执行顺序、SQL异常处理等不确定因素。
|
||||
|
||||
具体改动:在 `OrganizationLocationAppServiceImpl` 中,利用已有的 `activityDefinitionMapper` 对每条记录补充 `activityDefinitionId_dictText`。
|
||||
|
||||
## 验证计划
|
||||
1. 修改代码后编译通过
|
||||
2. 验证 `activityDefinitionId_dictText` 在 API 响应中正确填充
|
||||
3. 前端 el-select 能正确显示项目名称而非 ID
|
||||
|
||||
---
|
||||
|
||||
## 修复结果:✅ 成功,12行改动
|
||||
|
||||
**修改文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/basedatamanage/appservice/impl/OrganizationLocationAppServiceImpl.java`
|
||||
|
||||
**改动内容**: 在 `getOrgLocPage()` 方法中,对分页返回的每条记录,使用已注入的 `activityDefinitionMapper` 查询对应的 `ActivityDefinition` 实体,手动填充 `activityDefinitionId_dictText` 字段。
|
||||
|
||||
**策略**: 不依赖 DictAspect 的 AOP 翻译机制,在 service 层直接填充字典翻译值,确保前端能接收到正确的项目名称。
|
||||
44
docs/bug462_analysis.md
Normal file
44
docs/bug462_analysis.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Bug #462 分析报告
|
||||
|
||||
## Bug 描述
|
||||
[目录管理-诊疗目录] 编辑弹窗中"所需标本"下拉框数据加载失败,显示为"无数据"
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 数据流追踪
|
||||
1. 前端组件 `diagnosisTreatmentDialog.vue` 第168-178行渲染"所需标本"下拉框
|
||||
2. 下拉框选项来自 `specimen_code` 变量(第172行 `v-for="category in specimen_code"`)
|
||||
3. `specimen_code` 通过 `proxy.useDict('specimen_code', ...)` 加载(第378-386行)
|
||||
4. `useDict` 调用 API `/system/dict/data/type/specimen_code`(`src/utils/dict.js` 第16行)
|
||||
5. 后端 `SysDictDataController.dictType()` 处理请求(第65-73行,**无权限校验**)
|
||||
6. 最终查询 `sys_dict_data` 表,条件:`status = '0' AND dict_type = 'specimen_code'`
|
||||
|
||||
### 根因
|
||||
`sys_dict_type` 表中 **缺少 `specimen_code` 字典类型**,导致 `sys_dict_data` 表中也无对应的标本数据记录。
|
||||
|
||||
前端 `useDict('specimen_code')` 调用 API 后返回空数组 `[]`,下拉框 `v-for` 遍历空数组,没有任何 `<el-option>` 渲染,Element Plus 显示默认空状态文案"无数据"。
|
||||
|
||||
**与 Bug #433 对比**:Bug #433 是"麻醉方法回显为代码"和"外请专家姓名数据未加载",根因也是字典数据缺失。本次 Bug #462 属于同类问题——新增字典类型时只在前端引用了 `useDict('specimen_code')`,但后端数据库中未创建对应的字典类型和数据。
|
||||
|
||||
## 影响范围
|
||||
- **前端文件**:`openhis-ui-vue3/src/views/catalog/diagnosistreatment/components/diagnosisTreatmentDialog.vue`(仅一处引用)
|
||||
- **后端文件**:无代码变更,纯数据问题
|
||||
- **数据库表**:`sys_dict_type`(插入字典类型)、`sys_dict_data`(插入7条标本数据)
|
||||
- **影响接口**:`GET /system/dict/data/type/specimen_code`
|
||||
|
||||
## 修复方案
|
||||
执行 DDL 脚本 `sql/bug_462_add_specimen_code_dict.sql`:
|
||||
|
||||
1. 在 `sys_dict_type` 表插入 `specimen_code` 字典类型(dict_name='所需标本')
|
||||
2. 在 `sys_dict_data` 表插入7条标本记录:
|
||||
- 血液(1)、尿液(2)、粪便(3)、呼吸道(4)、无菌体液(5)、生殖道(6)、其他(99)
|
||||
|
||||
**注意**:数据库中已存在该字典数据(由测试验证),需检查 DDL 是否已执行。
|
||||
若数据已存在但前端仍显示"无数据",则需重启后端服务刷新字典缓存(`SysDictTypeServiceImpl.loadingDictCache()`)。
|
||||
|
||||
## 验证计划
|
||||
1. 确认数据库中 `sys_dict_type` 存在 `specimen_code` 记录
|
||||
2. 确认数据库中 `sys_dict_data` 存在7条 `specimen_code` 数据(status='0')
|
||||
3. 重启后端服务(刷新字典缓存)
|
||||
4. 前端进入诊疗目录编辑弹窗,点击"所需标本"下拉框,应显示7条标本选项
|
||||
5. 选择任意标本后保存,再次编辑应正确回显已选标本
|
||||
@@ -121,6 +121,18 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
|
||||
// 查询机构位置分页列表
|
||||
Page<OrgLocQueryDto> orgLocQueryDtoPage =
|
||||
HisPageUtils.selectPage(organizationLocationMapper, queryWrapper, pageNo, pageSize, OrgLocQueryDto.class);
|
||||
// 手动填充项目名称字典翻译,确保前端能正确回显项目名称
|
||||
if (orgLocQueryDtoPage != null && !orgLocQueryDtoPage.getRecords().isEmpty()) {
|
||||
for (OrgLocQueryDto dto : orgLocQueryDtoPage.getRecords()) {
|
||||
if (dto.getActivityDefinitionId() != null) {
|
||||
ActivityDefinition activityDef =
|
||||
activityDefinitionMapper.selectById(dto.getActivityDefinitionId());
|
||||
if (activityDef != null && activityDef.getName() != null) {
|
||||
dto.setActivityDefinitionId_dictText(activityDef.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return R.ok(orgLocQueryDtoPage);
|
||||
}
|
||||
|
||||
|
||||
@@ -926,23 +926,36 @@ function handleDelete() {
|
||||
}
|
||||
|
||||
if (hasSavedItem) {
|
||||
// 有已保存的行,调用后端API删除
|
||||
savePrescription({ adviceSaveList: deleteList }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
getListInfo(false);
|
||||
}
|
||||
// 🔧 Bug #454: 删除前弹出确认提示,告知用户将级联删除关联检验申请单
|
||||
const hasLabItem = deleteList.some(item => item.adviceType === 3);
|
||||
const confirmMsg = hasLabItem
|
||||
? '删除此医嘱将同时删除关联的检验申请单,是否确认删除?'
|
||||
: '确认删除选中的医嘱项目吗?';
|
||||
|
||||
proxy.$modal.confirm(confirmMsg).then(() => {
|
||||
savePrescription({ adviceSaveList: deleteList }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
getListInfo(false);
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
} else {
|
||||
// 只有新增行,已经在前端删除完成
|
||||
proxy.$modal.msgSuccess('操作成功');
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
|
||||
expandOrder.value = [];
|
||||
groupIndexList.value = [];
|
||||
groupList.value = [];
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceType = undefined;
|
||||
}
|
||||
|
||||
function handleNumberClick(item, index) {
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待签发" value="0" />
|
||||
<el-option label="已签发" value="1" />
|
||||
<el-option label="报告已出" value="4" />
|
||||
<el-option label="已作废" value="5" />
|
||||
<el-option label="已出报告" value="6" />
|
||||
<el-option label="已作废" value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字">
|
||||
@@ -105,15 +105,18 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
|
||||
<el-table-column label="操作" align="center" fixed="right" width="160">
|
||||
<el-table-column label="操作" align="center" fixed="right" width="180">
|
||||
<template #default="scope">
|
||||
<!-- 待签发:可修改、删除 -->
|
||||
<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.status == 1">
|
||||
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
|
||||
</template>
|
||||
<!-- 已校对(2)、待接收(3)、已接收(4)、已检查(6)、已作废(7):仅查看详情 -->
|
||||
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -328,8 +331,11 @@ const parseBillStatus = (status) => {
|
||||
const statusMap = {
|
||||
'0': '待签发',
|
||||
'1': '已签发',
|
||||
'4': '报告已出',
|
||||
'5': '已作废',
|
||||
'2': '已校对',
|
||||
'3': '待接收',
|
||||
'4': '已收样',
|
||||
'6': '已出报告',
|
||||
'7': '已作废',
|
||||
};
|
||||
return statusMap[String(status)] || '-';
|
||||
};
|
||||
|
||||
@@ -1944,6 +1944,30 @@ function handleQuoteBilling() {
|
||||
}
|
||||
})
|
||||
|
||||
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目,避免"引用计费"后已提交项目重新出现在"待生成"列表
|
||||
// 原因:后端返回的计费数据中,已生成医嘱的项目可能没有 requestId 字段
|
||||
// 方案:用 chargeItemId/requestId/id 与已有的 temporaryAdvices 做匹配,排除已生成项目
|
||||
if (temporaryAdvices.value.length > 0) {
|
||||
const existingAdviceIds = new Set()
|
||||
temporaryAdvices.value.forEach(a => {
|
||||
const om = a.originalMedicine || {}
|
||||
if (om.requestId) existingAdviceIds.add(String(om.requestId))
|
||||
if (om.chargeItemId) existingAdviceIds.add(String(om.chargeItemId))
|
||||
if (om.id) existingAdviceIds.add(String(om.id))
|
||||
})
|
||||
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 // 🔧 新增:加载完成
|
||||
ElMessage.success('已成功引用最新计费药品信息!')
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user