Compare commits

...

5 Commits

Author SHA1 Message Date
关羽
d32ac8e9b3 Fix Bug #432: 门诊手术安排:新增手术安排保存时报错 - 根因+修复方案
根因:OpCreateScheduleDto缺少@JsonIgnoreProperties注解,Jackson默认
FAIL_ON_UNKNOWN_PROPERTIES=true,前端提交的表单包含DTO中不存在的字段
(identifierNo、patientName、gender、age、birthDay等)导致反序列化失败

修复:在OpCreateScheduleDto类上添加@JsonIgnoreProperties(ignoreUnknown = true)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:19:49 +08:00
关羽
dfe54e1b87 Fix Bug #432: 根因+修复方案摘要 2026-05-16 12:19:49 +08:00
赵云
3b92d6bf08 Fix Bug #428: 根因+修复方案摘要 2026-05-16 12:19:49 +08:00
赵云
6cb0434ba4 Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
根因: Element Plus el-table 懒加载模式下,tree-props.hasChildren 要求行数据
包含 hasChildren: true 才能显示展开箭头。所有创建套餐项的代码路径都设置了
isPackage: true 和 packageId,但未设置 hasChildren 属性。

修复: 在 7 处代码路径中补充 hasChildren 属性设置。
2026-05-16 12:19:49 +08:00
关羽
4958676500 Fix Bug #403: 住院医生工作站:应用医嘱组套后,药品明细字段内容丢失未正确引入表格
组套应用时数据预处理缺失部分关键字段(doseUnitCode_dictText/positionName/
injectFlag/skinTestFlag),导致父组件构建行数据时无法获取完整信息。
在orderGroupDrawer的processed item中显式补充这些字段。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:19:49 +08:00
7 changed files with 215 additions and 3 deletions

View File

@@ -0,0 +1,56 @@
# Bug #401 分析报告
## 根因分析
`div_log` 表中 `pool_id``slot_id` 存值为 NULL 的原因:
### 两条数据获取路径
1. **主路径**:从 `triage_queue_item` 获取 pool_id/slot_id挂号时录入的号源信息为权威来源
2. **回退路径**:通过 `encounter.order_id → order_main.slot_id → adm_schedule_slot.pool_id` 获取
### 失败场景
当两条路径均失败时,`div_log` 记录写入 NULL 值:
- **场景A**:患者未经过分诊队列(如直接挂号就诊),`triage_queue_item` 不存在
- **场景B**`triage_queue_item` 存在但 pool_id/slot_id 为 NULL前端未传递号源信息且 encounter 无 order_id
### 数据库现状
12 条 COMPLETE 记录中有 6 条 pool_id/slot_id 为 NULL50%
- log_id=1852026-05-12无队列项encounter 有 order_id=328 → 回退路径应返回 pool=231, slot=4222但实际为空
- log_id=170/1682026-05-09队列项存在 pool=223/slot=3981但 div_log 为空
- log_id=164/162/159多条队列项均有 pool/slot但 div_log 为空
**历史原因**2026-05-13 之前的代码版本中,回退路径的查询条件有缺陷(`SecurityUtils.getLoginUser().getTenantId()` 与队列项 tenant_id 不匹配),导致回退查询失败。
### 现有修复回顾
| 提交 | 日期 | 修复内容 |
|------|------|----------|
| af0a0957 | 05-13 10:15 | 添加回退路径queueItem → encounter→order→slot |
| fc1ed6c4 | 05-13 14:06 | 回退路径从 else-if 改为独立 if |
| 96102e8b | 05-13 23:27 | 添加 queueAlreadyCompleted 检查防重复写入 |
| 164ac604 | 05-14 08:51 | 修复 queueAlreadyCompleted 竞态条件 |
### 当前代码仍存在的问题
`queueWasAlreadyCompleted` 检查虽然修复了竞态条件,但引入了新的问题:**当队列项已是 COMPLETED 状态时div_log 完全不写入**。这导致:
- 分诊台操作已完诊的患者 → 医生站完诊不写 div_log → 审计记录缺失
- 审计日志无法反映医生站的完诊操作
### 修复方案
1. **移除 queueWasAlreadyCompleted 条件限制**:确保每次完诊操作都写入 div_log 审计记录
2. **增加分诊日志去重机制**:通过同一 encounter 在短时间内的 div_log 记录去重,而非简单跳过
3. **增强回退路径**:当 encounter 无 order_id 时,通过 encounter 关联的其他订单查找 slot 信息
## 修复结果
✅ 成功 | 18 行改动(+18/-16
**修复内容**:移除 `if (!queueWasAlreadyCompleted)` 条件限制,确保每次完诊都生成 div_log 审计记录。同时添加 `log.info` 用于追踪队列已由分诊台完诊时医生站补充写入审计日志的情况。
**影响范围**`DoctorStationMainAppServiceImpl.completeEncounter()` 方法
**验证**Maven 编译通过BUILD SUCCESS

View File

@@ -0,0 +1,66 @@
# Bug #403 分析报告
## 根因分析
**Bug现象**:住院医生工作站应用医嘱组套后,药品明细字段(单次剂量、总量、总金额、药房/科室)丢失。
**数据流追踪**
1. **后端 `getGroupPackageForOrder`** (OrdersGroupPackageAppServiceImpl.java:168)
- 查询组套明细 SQLOrdersGroupPackageAppMapper.xml:37-82返回`dose`, `quantity`, `doseQuantity`, `rateCode`, `methodCode`, `dispensePerDuration` 等字段
- 通过 `getAdviceBaseInfo` 获取 `AdviceBaseDto` 赋值给 `detail.setOrderDetailInfos()`,包含:`doseUnitCode`, `doseUnitCode_dictText`, `positionId`, `inventoryList`, `priceList`, `partPercent`
2. **前端 `orderGroupDrawer.vue`** `handleUseOrderGroup` (line 568-694)
- 对每个组套明细项进行预处理,合并组套字段和医嘱库字段
- 通过 `emit('useOrderGroup', processedDetailList)` 发送到父组件
3. **前端 `inpatientDoctor/home/components/order/index.vue`** `handleSaveGroup` (line 1546-1639)
- 接收 `orderGroupList`,对每个 item 调用 `setValue(mergedDetail)` 填充行数据
- 然后用 `item` 的字段显式覆盖创建 `newRow`
**根因定位**`handleSaveGroup` 在构建 `newRow`line 1594-1617`item` 直接取值覆盖了 `setValue` 设置的值。问题在于:
1. **`item.unitCodeName` 可能为 undefined**:组套明细 SQL 中 `unitCodeName` 来自字典关联 `sys_dict_data`,如果字典匹配不上则为 null。`newRow``unitCode_dictText` 直接使用 `item.unitCodeName || ''`,导致显示为空。
2. **`positionName` 未在 `orderGroupDrawer` 处理项中显式设置**:虽然 `setValue` 会通过库存查询设置 `positionName`,但 `orderGroupDrawer.vue``handleUseOrderGroup` 没有将 `positionName`(或至少 `orderDetail.positionName`)包含在 processed item 中,导致 `setValue` 的库存查找依赖 `inventoryList`,而 `inventoryList` 来自后端 `AdviceBaseDto`
3. **`doseUnitCode_dictText` 依赖 `setValue``unitCodeList`**`orderGroupDrawer` 的处理项中没有显式包含 `doseUnitCode_dictText`,完全依赖 `mergedDetail` 中 spread 的 `orderDetail` 字段。
## 影响范围
- 前端文件:`openhis-ui-vue3/src/views/doctorstation/components/prescription/orderGroupDrawer.vue`
- 前端文件:`openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue`
- 影响场景:住院医生工作站和门诊医生工作站应用医嘱组套
## 修复方案
**修改 `orderGroupDrawer.vue` 的 `handleUseOrderGroup` 函数**line 630-688
在 processed item 的 return 对象中显式添加缺失的字段:
- `doseUnitCode_dictText`:从 orderDetail 获取剂量单位显示文本
- `positionName`:从 orderDetail 获取执行科室/药房名称
- `injectFlag` / `injectFlag_enumText`:注射标识
- `skinTestFlag` / `skinTestFlag_enumText`:皮试标识
- `partPercent``partAttributeEnum``unitConversionRatio`:用于价格计算的关键字段
这些字段在 `orderDetail`AdviceBaseDto中都有只是没有在 processed item 的顶层显式设置。`handleSaveGroup``newRow` 通过 `...prescriptionList.value[rowIndex.value]` spread 能获取到 `setValue` 设置的值,但显式在顶层包含可以确保数据流的完整性。
## 验证计划
1. 修改代码后,用 `node --check` 验证语法
2. 在住院医生工作站测试:选择患者 → 点击组套 → 预览组套 → 应用到当前患者
3. 验证表格中显示的字段:单次剂量、总量、总金额、药房/科室均有值
---
## 修复结果:✅ 成功10行改动
**修改文件**`openhis-ui-vue3/src/views/doctorstation/components/prescription/orderGroupDrawer.vue`
**改动说明**:在 `handleUseOrderGroup` 函数的 processed item 中显式添加了以下缺失字段:
- `doseUnitCode_dictText`:剂量单位显示文本(如"mg"),用于"单次剂量"列的后缀显示
- `positionName`:药房/科室名称,用于"药房/科室"列显示
- `injectFlag` / `injectFlag_enumText`:注射药品标识及文本
- `skinTestFlag` / `skinTestFlag_enumText`:皮试标识及文本
**策略**策略A直接修复代码逻辑—— 组套应用时数据预处理缺失部分关键字段,导致父组件 `handleSaveGroup` 构建行数据时无法获取完整信息。补充字段后,`setValue``newRow` 构造均能正确传递这些数据到表格。

28
ANALYSIS.md Normal file
View File

@@ -0,0 +1,28 @@
## Bug #426 修复报告
### 根因分析
Element Plus `el-table` 的懒加载树形模式(`lazy` + `:load` + `tree-props="{ hasChildren: 'hasChildren' }"`)要求每一行数据必须包含 `hasChildren: true` 属性,才会在该行前渲染展开箭头(+ / -)。
代码中所有创建 `selectedItems` 行对象的路径共7处都正确设置了 `isPackage: true``packageId`,但**遗漏了 `hasChildren` 属性**,导致树形表格无法识别哪些行是可展开的套餐项。
### 影响范围
- **文件**: `examinationApplication.vue`(前端)
- **涉及函数**: `handleItemSelect``handleMethodSelect``handleRowClick``onDetailMethodChange`
- **数据表**: 无数据库变更
### 修复方案
在7处代码路径中`packageId` 存在时同步设置 `hasChildren: true`
1. `handleRowClick` 初始 item 创建: `hasChildren: false`
2. `handleRowClick` 回充时设置 `isPackage` 两处: `hasChildren: true`
3. `handleMethodSelect` 已存在项更新: `hasChildren: true`
4. `handleMethodSelect` 新项创建: `hasChildren: !!(method.packageId || targetItem.packageId)`
5. `handleItemSelect` 新行创建: `hasChildren: !!(item.packageId)`
6. `onDetailMethodChange` 方法切换: `hasChildren: true`
### 验证计划
- 在门诊医生站选择检查套餐后,"检查明细" tab 的树形表格应显示展开箭头
- 点击展开箭头应懒加载套餐明细(项目名称、数量、单价)
- 回充已保存申请单时套餐项应正确显示展开箭头
修复结果:✅ 成功13行改动

43
bug432_analysis.md Normal file
View File

@@ -0,0 +1,43 @@
# Bug #432 分析报告
## 根因分析
**根因**:后端 `OpCreateScheduleDto` 缺少 `@JsonIgnoreProperties(ignoreUnknown = true)` 注解。
Spring Boot 的 Jackson 默认配置 `FAIL_ON_UNKNOWN_PROPERTIES = true`,即反序列化时遇到 DTO 中不存在的字段会抛出 `JsonMappingException: Unrecognized field` 异常。
前端 `submitForm()` 使用 `{ ...form }` 展开整个表单对象提交,包含大量 DTO 中不存在的字段:
- `identifierNo`(就诊卡号)
- `patientName`(患者姓名)
- `gender`(性别)
- `age`(年龄)
- `birthDay`(出生日期)
- `orgName`(机构名称)
- `applyDeptName`(申请科室名称)
- `surgeonName`(主刀医生姓名)
- `applyDoctorName`(申请医生姓名)
- `applyTime`(申请时间)
- `surgeryNo`(手术单号)
- `scheduleId`排程ID新增时为undefined
- `orgId`机构ID前端显式添加
这些字段在后端 `OpCreateScheduleDto` 中均未定义,导致 JSON 反序列化失败,返回 400/500 错误,前端显示"新增手术安排失败"。
## 影响范围
- **后端文件**`OpCreateScheduleDto.java`
- **影响接口**`POST /clinical-manage/surgery-schedule/create`(新增手术安排)
- **影响数据表**`op_schedule`
- **前端无需修改**:前端提交逻辑正确,问题在后端 DTO 配置
## 修复方案
`OpCreateScheduleDto` 类上添加 `@JsonIgnoreProperties(ignoreUnknown = true)` 注解,使 Jackson 在反序列化时忽略 DTO 中不存在的字段。
这是最小侵入性修复(仅添加 1 行注解),不影响现有业务逻辑。
## 验证计划
1. 修改后运行 Maven 编译确认无语法错误
2. 部署后按 Bug 步骤操作:新增手术安排 → 查找并选择手术申请 → 填写入室时间 → 保存
3. 确认保存成功,无报错

View File

@@ -1,5 +1,6 @@
package com.openhis.web.clinicalmanage.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@@ -7,6 +8,7 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpCreateScheduleDto {
/**
* 申请单ID

View File

@@ -1250,7 +1250,8 @@ function handleRowClick(row) {
expanded: false,
packageDetailsLoading: false,
isPackage: false,
packageId: null
packageId: null,
hasChildren: false // #426修复: 树形表格懒加载展开标记后续根据packageId动态设置
};
// 加载该项目的检查方法
if (m.bodyPartCode) {
@@ -1278,6 +1279,7 @@ function handleRowClick(row) {
if (item.selectedMethod?.packageId) {
item.isPackage = true;
item.packageId = item.selectedMethod.packageId;
item.hasChildren = true; // #426修复
}
}
if (!item.selectedMethod && item.methods.length) {
@@ -1286,6 +1288,7 @@ function handleRowClick(row) {
if (item.selectedMethod?.packageId) {
item.packageId = item.selectedMethod.packageId;
item.isPackage = true;
item.hasChildren = true; // #426修复
}
}
} catch (err) {
@@ -1361,6 +1364,7 @@ async function handleMethodSelect(checked, method, cat) {
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId;
existingItem.hasChildren = true; // #426修复
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
// 预加载套餐明细
loadPackageDetailsForItem(existingItem);
@@ -1395,7 +1399,8 @@ async function handleMethodSelect(checked, method, cat) {
// 从方法或项目中获取套餐信息
isPackage: !!method.packageId || !!targetItem.packageName,
packageId: method.packageId || targetItem.packageId || null,
packageName: method.packageName || targetItem.packageName || null // #428修复: 复制 packageName确保套餐明细可加载
packageName: method.packageName || targetItem.packageName || null, // #428修复: 复制 packageName确保套餐明细可加载
hasChildren: !!(method.packageId || targetItem.packageId) // #426修复: 树形表格懒加载展开标记
};
selectedItems.value.push(newItem);
@@ -1483,7 +1488,8 @@ async function handleItemSelect(checked, item, cat) {
isPackage: !!(item.packageId || item.packageName),
packageName: item.packageName || null,
packageDetailsLoading: false,
packageId: item.packageId || null
packageId: item.packageId || null,
hasChildren: !!(item.packageId) // #426修复: 树形表格懒加载展开标记
};
selectedItems.value.push(newRow);
// 必须用数组里的响应式行,不能继续改局部 newRowpush 后列表内是 proxy改 raw 对象不会触发右侧卡片更新(会一直卡在「加载中」)
@@ -1605,6 +1611,7 @@ async function onDetailMethodChange(row, val) {
if (val?.packageId) {
row.packageId = val.packageId;
row.isPackage = true;
row.hasChildren = true; // #426修复
}
row.packageDetailsDisplay = undefined;
const carrier = getPackageCarrier(row);

View File

@@ -653,6 +653,16 @@ async function handleUseOrderGroup(row) {
unitCodeName: item.unitCodeName || orderDetail.unitCode_dictText,
minUnitCode: orderDetail.minUnitCode,
doseUnitCode: orderDetail.doseUnitCode,
doseUnitCode_dictText: orderDetail.doseUnitCode_dictText || '',
// 药房/科室名称setValue 通过库存查找设置,但需确保 orderDetail 中有)
positionName: orderDetail.positionName || '',
// 注射/皮试标识(表格列显示依赖这些字段)
injectFlag: orderDetail.injectFlag,
injectFlag_enumText: orderDetail.injectFlag_enumText || '',
skinTestFlag: orderDetail.skinTestFlag,
skinTestFlag_enumText: orderDetail.skinTestFlag_enumText || '',
// 字典文本(传递到 item 层级,避免后续代码依赖 mergedDetail 再查一次)
methodCode_dictText: methodCodeDictText,