Compare commits

..

30 Commits

Author SHA1 Message Date
ea2abfd1eb Fix Bug #537: [住院医生工作站] 清理已屏蔽的汇总发药申请组件死代码
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:08:32 +08:00
2259fa609c Fix Bug #532: 【手术管理】点击"查看"或"编辑"按钮弹出 SQL 语法报错
根因:getSurgeryScheduleDetail SQL 查询中 cs.incision_level AS "incisionLevel"
使用了双引号包裹列别名,在 PostgreSQL 中双引号使标识符大小写敏感,
导致 MyBatis 无法正确映射到 OpScheduleDto 的 incisionLevel 字段。
修复:移除双引号,改为 cs.incision_level AS incisionLevel。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:15:41 +08:00
ba5ed2bfb0 Fix Bug #533: 【门诊手术安排-计费】generateSourceEnum硬编码为1导致保存后列表查询过滤不匹配
根因:手术计费弹窗中prescriptionlist组件的:generateSourceEnum硬编码为"1",
但handleChargeCharge设置chargePatientInfo.generateSourceEnum=6(手术计费),
handleSaveSign保存时也设置cleanRow.generateSourceEnum=6。
保存成功后getListInfo(false)刷新列表时用prop值1查询,后端按generateSourceEnum=1过滤,
但已保存项目的generateSourceEnum=6,被过滤掉导致列表不显示。

修复:将:generateSourceEnum="1"改为:generateSourceEnum="chargePatientInfo.generateSourceEnum",
使查询参数与保存值一致(均为6)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:14:27 +08:00
42ce79f68c Fix Bug #530: [住院护士站-医嘱校对] 患者查询触发 SQL 类型匹配错误,导致勾选患者列表后后端报错 - 前端过滤无效的encounterId防止后端SQL解析异常
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:13:13 +08:00
58f7e64045 Fix Bug #533: 根因+修复方案摘要 2026-05-17 23:13:13 +08:00
fd34fe0c72 Fix Bug #517: [库房管理-领用管理] 业务逻辑校验缺失:允许保存并提交领用数量大于库存数量(零库存领用)的单据
根因分析:
- 前端 handleSubmitApproval(提交审批)未做库存校验,直接调用后端 API
- 后端 submitApproval 也未做库存校验,仅在保存时(addOrEditIssueReceipt)有 validateRequisitionStock
- 用户可绕过前端保存校验(如编辑已有草稿后直接提交审批),将超库存单据提交审批流

修复方案:
1. 后端:在 submitApproval 方法中增加 validateRequisitionStockByBusNo,通过单据详情查询已保存明细,逐行校验领用数量是否超过源仓库库存
2. 前端:在 handleSubmitApproval 提交前逐行调用 validateRequisitionQtyVsStock 校验库存,超库存时拦截并提示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:31:28 +08:00
14dc9964d5 Fix Bug #536: [门诊手术安排]手术申请查询弹窗底部,分页组件与界面底部元素重叠
根因:弹窗底部存在多层冗余间距叠加(分页容器inline样式+48px spacer div+
footer margin-top+CSS padding),导致弹窗尺寸变化时分页与footer重叠。

修复:移除冗余spacer div和分页容器inline样式,统一用CSS管理分页与footer
间距,避免固定高度堆叠导致的布局溢出问题。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:31:28 +08:00
33424d0a72 Fix Bug #528: [住院医生工作站-检查申请] 修改申请单成功后弹窗自动关闭且列表自动刷新 - 调整submit函数中emits('submitOk')与resetForm()的执行顺序,确保先通知父组件关闭弹窗再重置表单状态
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:31:28 +08:00
10a80587f1 Fix Bug #514: [库房管理-调拨管理-调拨] 调拨单保存与提交校验缺失 - 前端增加数量>0和库存校验,后端批量保存接口补充@Validated注解
根因:批量调拨页面handleSave仅校验单价未校验数量,submitApproval未校验数据完整性即提交审批;后端批量保存接口缺少@Validated导致DTO层@Min(1)未生效
修复:
1. batchTransfer/index.vue handleSave() 增加调拨数量>0和不超过源库存的前端校验
2. batchTransfer/index.vue handleSubmitApproval() 增加数量>0校验后再提交审批
3. ProductTransferController.java 批量保存接口添加@Validated注解启用DTO校验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:18:55 +08:00
71739cf271 Fix Bug #514: 根因+修复方案摘要 2026-05-17 21:18:55 +08:00
7b55c76e4c Fix Bug #524: 报卡详情日期字段回显为空 - 添加@JsonFormat注解确保Jackson正确序列化日期
根因:InfectiousCardDto和DoctorCardListDto中的LocalDate/LocalDateTime字段缺少@JsonFormat注解,
Jackson默认将日期序列化为数组格式[2026,5,15],前端normalizeDate函数无法解析导致字段显示为空。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:18:54 +08:00
5d258b0ced Fix Bug #518: 根因+修复方案摘要 2026-05-17 21:18:54 +08:00
a4c1af8086 Fix Bug #512: [住院护士站-汇总发药申请] 全选开关功能失效 - 增加nextTick确保DOM就绪后操作表格选择,修复handleExecute始终调用prescriptionRefs的问题
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:09:49 +08:00
6c077df932 Fix Bug #504: 护士退回药品医嘱后医生修改保存时"未匹配到库存信息" - 增加两阶段库存匹配逻辑和空值保护
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:24:28 +08:00
3a016100e7 Fix Bug #478: 修复检验申请详情"发往科室"字段回显为"-"的问题
根因:testApplication.vue 中的 recursionFun 函数只遍历科室树的两层(顶层+一级子节点),
当发往科室ID位于第三层或更深时无法匹配,返回空字符串导致显示"-"。
修复:改为递归遍历整棵科室树,确保任意深度的科室节点都能正确解析为名称。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:24:28 +08:00
bbe047e645 Fix Bug #476: 紧急程度移入el-form作为正式表单项,修正字段排列顺序
根因:紧急程度渲染在el-form外的独立urgency-bar中,不是正式表单项,
不随表单校验和数据流走;第一行字段布局只有发往科室和期望检查时间,
紧急程度未放在发往科室之后。

修复:将紧急程度从独立div移入el-form第一行,位于发往科室和期望检查时间之间;
同步移除urgency-bar废弃CSS;修正date picker函数名disabledFutureDate为disabledPastDate。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:24:28 +08:00
17d005051d Fix Bug #497: 根因+修复方案摘要 2026-05-17 20:24:28 +08:00
f9f76b74da Fix Bug #470: 手术项目查询去除MyBatis Plus COUNT开销,改用直接LIMIT查询
根因:MyBatis Plus分页拦截器在执行手术项目查询时,先做COUNT全表扫描
(10,102条记录,~4ms)再查数据(~0.3ms)。前端el-transfer不需要精确total,
COUNT查询纯属多余开销。

修复:Mapper返回值改为List,XML添加LIMIT/OFFSET,Service手动构造Page。
数据库层面从~5ms降至~0.3ms。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:24:28 +08:00
3ca0522b66 Fix Bug #491: 补充修复结果摘要到分析报告
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:22:44 +08:00
0a777ee700 Fix Bug #491: 【执行科室配置】保存配置时系统报错
根因:时间冲突检查中 organizationService.getById() 返回 null 时直接调用 getName() 导致 NPE;
同时 getOrgLocListByOrgIdAndActivityDefinitionId 方法只按 activityDefinitionId 查询,未按 organizationId 过滤,
导致跨科室误判冲突且可能查询到已删除机构的脏数据。

修复:
1. 增加 org.getName() 前的双重判空(org != null && org.getName() != null)
2. getOrgLocListByOrgIdAndActivityDefinitionId 增加 organizationId 参数并加入查询条件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:22:27 +08:00
b551872a1f Fix Bug #491: 根因+修复方案摘要 2026-05-17 19:22:27 +08:00
36c84633cf Fix Bug #468: 根因+修复方案摘要 2026-05-17 19:17:32 +08:00
4d26e26134 Fix Bug #469: [住院医生工作站-检验申请] 操作列"详情"按钮未包裹在条件分支中导致始终显示
根因:操作列模板中"详情"按钮位于 v-if/v-else-if 条件块之外,对所有状态始终渲染。
导致待签发状态显示"修改 删除 详情"三个按钮、已签发显示"撤回 详情"两个按钮,
违背了按状态严格区分操作权限的业务要求。

修复:将"详情"按钮包裹在 <template v-else> 中,确保仅在非待签发/非已签发状态显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:17:32 +08:00
79d67b1f07 Fix Bug #461: [系统管理-执行科室配置] 保存项目配置后,项目名称回显为ID码,未显示正确名称
根因:DictAspect 的 @Around 后置处理中,SQL查询失败返回空字符串,覆盖了控制器方法中手动设置的 activityDefinitionId_dictText 有效值。前端 el-select 因 _dictText 为空而回显 ID 码。

修复:
1. DictAspect 在执行 SQL 查询前,先检查 _dictText 字段是否已被手动填充(非空),若已有值则跳过查询,避免覆盖
2. 增加空字符串防护:dictLabel 为空字符串时不设置 _dictText

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:11:25 +08:00
79b04bdb4e Fix Bug #444: 根因+修复方案摘要 2026-05-17 19:11:25 +08:00
8e3bd5aeb3 Fix Bug #401: 门诊完诊审计日志 div_log pool_id/slot_id 优先级修复
根因:完诊时获取 pool_id/slot_id 的逻辑优先使用 triage_queue_item,
回退使用 order_main → adm_schedule_slot。但 order_main.slot_id 才是
挂号时实际锁定的号源(权威来源),queueItem 值可能不准确或缺失。

修复:反转优先级,优先通过 encounter.orderId → order_main → adm_schedule_slot
获取 pool_id/slot_id;订单链路无数据时回退使用 queueItem。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:12:30 +08:00
090c99d409 Fix Bug #444: 根因+修复方案摘要 2026-05-17 18:12:30 +08:00
f3855c9d30 Fix Bug #439: 领用出库选择领用药品后"总库存数量"列数据未显示
根因:handleLocationClick 中 pickBestOrgQuantityRow 返回的 d 有数据但 orgQuantity <= 0 时,
applyFromDto 不被调用,导致 totalQuantity 保持空字符串 '',界面显示为空白。
修复:将条件从 "d && Number(d.orgQuantity ?? 0) > 0" 改为 "d",
确保只要后端返回库存记录就调用 applyFromDto 填充 totalQuantity(无论数量是否为 0)。
同时在批号回退分支(lotTrimmed 路径)中做同样处理。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:10:36 +08:00
1136a479d1 Fix Bug #403: 住院医生工作站-应用医嘱组套后药品明细字段丢失未正确引入表格
根因:handleSaveGroup 中组套项预初始化行设置 isEdit: true,但表格明细列
(单次剂量/总量/总金额/药房/频次/用法等)均使用 v-if="!scope.row.isEdit" 条
件渲染。isEdit 为 true 时所有明细字段被隐藏,仅显示医嘱名称。正常药品选择流
程中 isEdit: true 后紧跟 expandOrder 展开 OrderForm 表单供编辑,但组套应用流
程未展开行,导致预填的组套明细值完全不可见。

修复:组套项带预填完整明细值,isEdit 设为 false,让表格列直接展示明细字段。
用户仍可双击行进入编辑模式修改。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:36:23 +08:00
f519d83ed1 Fix Bug #426: handleMethodSelect/onDetailMethodChange 补充 packageName 套餐解析支持
根因:check_method 表只有 package_name 字段无 package_id,handleMethodSelect
等路径只检查 packageId 导致套餐的 hasChildren、右侧卡片展开、套餐明细加载
全部不生效。补充 6 处 packageId→packageName 兜底检查,使所有选择路径
一致支持 packageName→packageId 解析。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:16:26 +08:00
20 changed files with 433 additions and 170 deletions

View File

@@ -1,84 +0,0 @@
# Bug #428 分析报告与修复记录
**标题**: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
**类型**: codeerror | **严重度**: 3 | **优先级**: 3
**提出人**: 陈显精(chenxj)
## 需求描述
医生站在为患者新增检查申请时,需实现三个联动功能:
1. **动作一**:展开右侧项目分类(如:彩超)后,下方自动加载后台维护的"检查方法"列表
2. **动作二**:勾选某个检查方法后,该项目自动填充到右侧顶部"已选择"列表
3. **动作三**:在"已选择"列表中点击展开图标,展示该套餐包含的收费明细
## 根因分析
### 动作一(分类联动加载检查方法):✅ 已实现
- `handleCollapseChange`第949行`handleCategoryExpand`第913行`searchCheckMethod({ checkType: cat.typeName })`
- 代码路径完整数据解析正确Vue 响应式绑定正确
### 动作二(勾选方法后填充到"已选择"列表):❌ 存在根因缺陷
**根因位置**`handleMethodSelect` 函数第1373行
```javascript
const targetItem = cat.items[0]; // ← 根因:硬编码假设分类下必有 items
if (!targetItem) {
console.warn('分类下没有检查项目,无法关联方法');
return; // ← 当分类下没有 items 时直接返回,不执行任何操作
}
```
**问题链**
1. 用户展开分类 → 检查方法列表加载成功(动作一 OK
2. 用户勾选检查方法 → `handleMethodSelect(checked, method, cat)` 被调用
3. 代码使用 `cat.items[0]` 作为目标项目,但很多分类**没有 items检查部位**,只有 methods检查方法
4.`cat.items` 为空数组时,`targetItem``undefined`函数在第1377行直接 `return`
5. 结果:用户勾选了方法,但"已选择"面板没有任何反应
### 动作三(套餐明细展示):❌ 被动作二阻塞
- `loadPackageDetailsForItem` 和套餐明细渲染逻辑本身是完整的
- 但由于动作二无法将项目添加到 `selectedItems`,套餐明细的触发条件永远不满足
## 数据流(修复前)
```
用户勾选方法 → handleMethodSelect(checked=true, method, cat)
→ targetItem = cat.items[0] ← 根因:可能为 undefined
→ if (!targetItem) return; ← 直接退出,什么都不做
→ ❌ selectedItems 不变
→ ❌ 右侧"已选择"面板无反应
```
## 数据流(修复后)
```
用户勾选方法 → handleMethodSelect(checked=true, method, cat)
→ targetItem = cat.items[0]
→ if (!targetItem) {
targetItem = { ← 修复:以方法自身作为项目
id: method.id, name: method.name,
price: method.packagePrice || method.price || 0,
packageId: method.packageId, packageName: method.packageName
}
}
→ ✅ 正常创建 selectedItems 条目
→ ✅ 右侧"已选择"面板正确显示
→ ✅ 如有套餐 → loadPackageDetailsForItem → 动作三正常触发
```
## 修复方案
**文件**`openhis-ui-vue3/src/views/doctorstation/components/examination/examinationApplication.vue`
**改动**`handleMethodSelect` 函数第1370-1378行
将硬编码的 `cat.items[0]` + 直接 return 改为降级策略:
- 当分类下有 items 时,使用 `cat.items[0]`(原有行为不变)
- 当分类下无 items 时,以方法自身数据创建 `targetItem`,后续逻辑正常执行
## Gate 验证
- Gate A: ✅ 根因已定位到第1373行 `cat.items[0]` + 第1377行 `return`
- Gate B: ✅ 已读取所有相关文件(前端 Vue + 后端 Controller + API + 实体)
- Gate C: ✅ 修复方案与验收标准一致
- Gate D: N/A不涉及数据库修改
## 修复结果:✅ 成功10行改动新增7行修改3行

36
BUG_401_ANALYSIS.md Normal file
View File

@@ -0,0 +1,36 @@
# Bug #401 分析报告
## 问题描述
门诊完诊审计日志错误div_log 表中 pool_id 与 slot_id 存值与设计规范不符。
## 数据验证
```sql
-- div_log COMPLETE 统计
total=12, null_pool=6, null_slot=6, has_pool=6, has_slot=6
```
- 有值的 6 条记录pool_id/slot_id 与 adm_schedule_pool/adm_schedule_slot 完全一致 ✅
- 空的 6 条记录:对应 encounter 的 order_id 全部为 NULLwalk-in 患者)
## 根因定位
`DoctorStationMainAppServiceImpl.completeEncounter()` (第 303-325 行) 获取 pool_id/slot_id 的逻辑:
```java
// 优先使用 triage_queue_item
if (queueItem != null && queueItem.getPoolId() != null && queueItem.getSlotId() != null) {
divPoolId = queueItem.getPoolId();
divSlotId = queueItem.getSlotId();
}
// fallback: 仅当 queueItem 不存在或字段缺失时
if ((divPoolId == null || divSlotId == null) && encounter.getOrderId() != null) {
...
}
```
**问题**fallback 条件 `(divPoolId == null || divSlotId == null)` 在 queueItem 存在时不会执行(因为 queueItem 的 poolId/slotId 可能为 NULL但 queueItem != null 时不进入 fallback。实际上对于有 encounter.orderId 的患者(挂号患者),应该始终通过 order → schedule_slot 获取权威的 pool_id/slot_id。
## 修复方案
调整 fallback 逻辑:只要有 encounter.orderId就通过 order → schedule_slot 获取 pool_id/slot_id不再依赖 queueItem 的字段值。queueItem 仅用于确定是否需要写审计日志的时机判断。
## 影响范围
- 修改文件:`DoctorStationMainAppServiceImpl.java`(约 10 行调整)
- 不涉及数据库 DDL 变更

40
BUG_512_ANALYSIS.md Normal file
View File

@@ -0,0 +1,40 @@
# Bug #512 分析报告
## Bug 描述
[住院护士站-汇总发药申请] "全选"开关功能失效,点击后下方医嘱明细未能联动勾选
## 根因分析
### 问题定位
`index.vue``handelSwicthChange` 函数第176-186行`handleExecute` 函数第200-202行
### 问题1`handelSwicthChange` 只操作 `prescriptionRefs`(明细组件),未覆盖汇总组件
虽然 `:disabled="isDetails != '1'"` 限制了开关仅在明细模式可用,但一旦在明细模式下切换后,数据刷新或模式切换后 ref 可能出现空值情况,缺少 `nextTick` 确保 DOM 更新完成后再操作表格选择。
### 问题2`handleExecute` 永远调用 `prescriptionRefs`
```js
function handleExecute() {
proxy.$refs['prescriptionRefs'].handleMedicineSummary();
}
```
无论当前是"明细"还是"汇总"模式,都调用 `prescriptionRefs`,没有根据 `isDetails` 判断调用正确的子组件。
### 问题3`summaryMedicineList.vue` 缺少 `selectAllRows` 和 `clearSelection` 方法
汇总组件没有暴露这些方法,如果后续需要支持汇总模式的全选功能,需要先补充。
## 修复方案
1.`handelSwicthChange` 中添加 `nextTick` 确保 DOM 更新后再操作表格
2. 修复 `handleExecute` 根据 `isDetails` 判断调用正确的子组件
3.`summaryMedicineList.vue` 添加 `selectAllRows``clearSelection` 方法
## 修复结果:✅ 成功33行改动
### 修改内容
1. `index.vue` - `handelSwicthChange` 改为 async 函数,添加 `nextTick` 确保 DOM 更新后再调用表格选择方法
2. `index.vue` - `handelSwicthChange` 增加 `isDetails` 判断分支,覆盖明细和汇总两种模式
3. `index.vue` - `handleExecute` 修复:根据 `isDetails` 判断调用正确的子组件方法(之前始终调用 `prescriptionRefs`
4. `index.vue` - `provide('handleGetPrescription')` 修复:根据 `isDetails` 判断调用正确的子组件刷新方法
5. `index.vue` - 导入 `nextTick` from vue
### 构建验证
`vite build --mode dev` 通过,无编译错误。

85
bug468_analysis.md Normal file
View File

@@ -0,0 +1,85 @@
# Bug #468 分析报告
## Bug 描述
[住院医生工作站-检验申请] 列表页缺失【单据状态】列,无法闭环管理检验医嘱执行进度
## 阶段1深度分析
### 数据流追踪
1. **前端查询**: `getInspection(params)` → GET `/reg-doctorstation/request-form/get-inspection`
2. **后端控制器**: `RequestFormManageController.getInspectionRequestForm()` → 调用 `iRequestFormManageAppService.getRequestForm()`
3. **后端服务**: `RequestFormManageAppServiceImpl.getRequestForm()` → 调用 `requestFormManageAppMapper.getRequestForm()`
4. **SQL查询**: `RequestFormManageAppMapper.xml` 中的 `getRequestForm` 语句
5. **状态计算**: SQL 使用 CASE WHEN 根据 `wor_service_request.status_enum` 计算 `computed_status`
6. **前端渲染**: `parseBillStatus(scope.row.billStatus ?? scope.row.status)` 显示状态文本
### 状态映射关系
**后端 ServiceRequest.status_enum 原始值:**
| status_enum | 含义 |
|-------------|------|
| 1 | 待发送 (DRAFT) |
| 2 | 已发送 (ACTIVE) |
| 3 | 已完成 (COMPLETED) |
| 5 | 取消/待退 (CANCELLED) |
| 8 | 已出报告 (COMPLETED_REPORT) |
**SQL CASE 计算映射computed_status**
| status_enum | → computed_status | 前端显示 |
|-------------|-------------------|----------|
| 8 | 6 | 已出报告 |
| 3 | 5 | 已收样 |
| 2 | 1 | 已签发 |
| 5 | 7 | 已作废 |
| 其他 | 0 | 待签发 |
**前端 parseBillStatus 映射:**
| computed_status | 显示文本 |
|-----------------|----------|
| 0 | 待签发 |
| 1 | 已签发 |
| 2 | 已校对 |
| 3 | 待接收 |
| 4 | 已收样 |
| 6 | 已出报告 |
| 7 | 已作废 |
**前端筛选下拉选项:**
| 选项label | 值 |
|-----------|-----|
| 全部 | "" |
| 待签发 | "0" |
| 已签发 | "1" |
| 已出报告 | "6" |
| 已作废 | "7" |
### 根因定位
**原始问题**:列表页完全没有【单据状态】列。
**已有修复**(已在 develop 分支合并):
1. 新增 `el-table-column` 单据状态列(位于申请单号之后)
2. 新增 `parseBillStatus()` 函数用于状态码→文本转换
3. 新增筛选表单中的单据状态下拉选择
4. 后端 SQL 新增 `computed_status` 动态计算逻辑
5. 前端使用 `scope.row.billStatus ?? scope.row.status` 兼容字段名
## 修复结果
✅ 成功Bug #468 已在 develop 分支修复并合并。
当前 guanyu 分支与 develop 分支代码完全一致diff 为空),无需额外代码改动。
已有提交记录:
- a95c9c9f - 列表页新增单据状态列
- ae50a704 - 列表页新增【单据状态】列
- 02b9dc87 / e694b758 / a99ecaee - 修复前后端状态码映射不一致
验证通过:
- ✅ 表格列存在line 92-96
- ✅ 列位置正确(申请单号之后)
- ✅ parseBillStatus 覆盖所有后端状态
- ✅ 筛选表单支持状态过滤
- ✅ 操作列按状态动态显示按钮
- ✅ 后端 SQL computed_status 计算正确

56
bug491_analysis.md Normal file
View File

@@ -0,0 +1,56 @@
# Bug #491 分析报告
## Bug 信息
- **标题**: 【执行科室配置】保存配置时系统报错
- **报错信息**: `Cannot invoke "com.openhis.administration.domain.Organization.getName()" because the return value of "com.openhis.administration.service..." is null`
- **严重程度**: 3 | **优先级**: 3 | **类型**: codeerror
## 复现步骤
1. 登录 HIS 系统 → 【系统管理】→【业务规则配置】→【执行科室配置】
2. 左侧选择科室(如"超声诊断科"
3. 新增或修改某行的时间区间
4. 点击【保存】按钮
5. 顶部弹出红色错误提示NPE
## 根因分析
### 文件定位
- `openhis-server-new/.../appservice/impl/OrganizationLocationAppServiceImpl.java`第161-175行
### 根本原因
`addOrEditOrgLoc` 方法中,保存时会检查时间冲突。当发现冲突时,代码需要获取冲突记录的科室名称用于错误提示:
```java
// 第171-172行
Organization org = organizationService.getById(organizationLocation.getOrganizationId());
String organizationName = org.getName(); // NPE 这里!
```
**问题**`organizationService.getById()` 可能返回 `null`(当冲突记录的 `organizationId` 指向已被删除的机构时),直接调用 `.getName()` 导致 NPE。
### 附加问题
`getOrgLocListByOrgIdAndActivityDefinitionId` 方法(`OrganizationLocationServiceImpl.java:60-62`)只按 `activityDefinitionId` 查询,**没有按 `organizationId` 过滤**,导致:
- 方法名含 "OrgId" 但实际不查 organizationId
- 时间冲突检测范围过广(跨科室误判冲突)
- 可能查到已被删除机构的脏数据
### 数据流
```
前端保存 → POST /base-data-manage/org-loc/org-loc
→ addOrEditOrgLoc(OrgLocQueryDto)
→ 查询同 activityDefinitionId 的所有机构位置记录(含脏数据)
→ 检查时间是否重叠
→ 若重叠getById(organizationId) → null → getName() → NPE
```
## 修复方案
1. `OrganizationLocationAppServiceImpl.java` 第172行增加 `org != null` 判空,回退为 `"未知科室"`
2. `IOrganizationLocationService.java`:修改 `getOrgLocListByOrgIdAndActivityDefinitionId` 签名,增加 `organizationId` 参数
3. `OrganizationLocationServiceImpl.java`:查询条件增加 `.eq(OrganizationLocation::getOrganizationId, organizationId)`
4. `OrganizationLocationAppServiceImpl.java` 第162行调用时传入 `orgLoc.getOrganizationId()`
## 修复结果:✅ 成功4行改动
- 编译验证BUILD SUCCESS
- 改动文件:`OrganizationLocationAppServiceImpl.java``IOrganizationLocationService.java``OrganizationLocationServiceImpl.java`
- 已提交并推送到远程分支 guanyu

View File

@@ -0,0 +1,53 @@
## Bug #530 分析报告
**标题**: [住院护士站-医嘱校对] 患者查询触发 SQL 类型匹配错误,导致勾选患者列表后后端报错
### 数据流追踪
1. `patientList.vue` → 树节点勾选触发 `handleCheckChange`
2. `handleCheckChange``updatePatientInfoList(checkedPatients)` 存储选中的患者节点
3. `handleGetPrescription()` 被触发 → `prescriptionList.vue``handleGetPrescription`
4. 前端构造 encounterIds: `patientInfoList.value.map((i) => i.encounterId).join(',')`
5. 后端解析: `Arrays.stream(encounterIds.split(",")).map(Long::parseLong).toList()`
6. SQL 执行: `SELECT ... FROM ... WHERE ii.encounter_id IN (?, ?, ...)`
### 根因定位
**`patientList.vue` 第122行**:患者数据格式化时
```javascript
const patients = records.map((item) => ({
id: item.id || item.encounterId, // 问题行
name: item.patientName || '',
leaf: true,
...item,
}));
```
而后端 `AdmissionPatientPageDto` 中**没有 `id` 字段**(只有 `encounterId``patientId` 等),所以 `item.id``undefined`,此时 `item.id || item.encounterId` 回退到 `item.encounterId`
但在 `prescriptionList.vue` 第186行提取 encounterId 时:
```javascript
let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(',');
```
如果 `patientInfoList` 中的某个对象其 `encounterId` 因任何原因为 `undefined``null` 或空字符串,`join(',')` 会产生类似 `"123,,456"` 的字符串。
后端解析时:
```java
List<Long> encounterIdList = Arrays.stream(encounterIds.split(",")).map(Long::parseLong).toList();
```
`Long.parseLong("")` 会抛出 `NumberFormatException`,导致后端报错。
### 修复方案
`prescriptionList.vue``handleGetPrescription` 函数中,过滤掉无效的 encounterId 值:
- 过滤 `undefined``null`、空字符串
- 如果过滤后无有效 encounterId提示用户并阻止请求
### 验证门禁
- [x] Gate A根因已定位到 `prescriptionList.vue` 第186行未过滤无效 encounterId
- [x] Gate B已读取前后端所有相关文件理解完整数据流
- [x] Gate C修复方案与验收标准一致前端防御性处理 + 正常查询)
- [x] Gate D不涉及数据库字段变更

View File

@@ -1,18 +0,0 @@
### Bug #469 分析报告
**标题**: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
**根因**: 操作列(`testApplication.vue` 第 108-122 行)模板中,"详情"按钮 `<el-button>` 位于 `v-if`/`v-else-if` 条件块之外,作为独立元素始终渲染。导致:
- 待签发状态status=0/null显示 "修改 删除 **详情**" 三个按钮(应仅显示"修改 删除"
- 已签发状态status=1显示 "撤回 **详情**" 两个按钮(应仅显示"撤回"
- 其他状态2/3/4/6/7仅显示"详情"(正确)
**数据流**:
- 前端: `testApplication.vue` → 操作列 template → 条件判断 `scope.row.status`
- 后端 SQL: `RequestFormManageAppMapper.xml``computed_status` CASE 表达式将 `status_enum` 映射为前端显示码0=待签发, 1=已签发, 6=已出报告, 7=已作废)
- 后端删除: `RequestFormManageAppServiceImpl.deleteRequestForm` 校验 `RequestStatus.DRAFT` (status_enum=1)
- 后端撤回: `RequestFormManageAppServiceImpl.withdrawRequestForm` 校验 `RequestStatus.ACTIVE` (status_enum=2)
**修复方案**: 将"详情"按钮包裹在 `<template v-else>` 中,形成完整的 `v-if` / `v-else-if` / `v-else` 三分支结构,确保每个状态仅显示对应的操作按钮。
**修复结果:✅ 成功4行改动**1行删除3行新增`<template v-else>` + 按钮 + `</template>`

View File

@@ -159,7 +159,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
String activityName = activityDef != null ? activityDef.getName() : "";
List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getActivityDefinitionId());
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId());
organizationLocationList = (orgLoc.getId() != null)
? organizationLocationList.stream().filter(item -> !orgLoc.getId().equals(item.getId())).toList()
: organizationLocationList;
@@ -169,7 +169,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(),
orgLoc.getStartTime(), orgLoc.getEndTime())) {
Organization org = organizationService.getById(organizationLocation.getOrganizationId());
String organizationName = org != null ? org.getName() : "未知科室";
String organizationName = org != null && org.getName() != null ? org.getName() : "未知科室";
return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime()
+ CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突");
}

View File

@@ -300,16 +300,12 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
}
}
// 3. 获取 pool_id 和 slot_id优先使用 triage_queue_item挂号时录入的号源信息为权威来源
// 队列项不存在或值缺失时,回退使用 encounter → order_main → adm_schedule_slot 链路
// 3. 获取 pool_id 和 slot_id优先使用 encounter.orderId → order_main → adm_schedule_slot 链路
// order_main.slot_id 为挂号时实际锁定的号源,是最权威的数据来源)
// 当无 orderId 或订单无 slot_id 时,回退使用 triage_queue_item 的 poolId/slotId
Long divPoolId = null;
Long divSlotId = null;
if (queueItem != null && queueItem.getPoolId() != null && queueItem.getSlotId() != null) {
divPoolId = queueItem.getPoolId();
divSlotId = queueItem.getSlotId();
}
// 队列项 poolId/slotId 缺失时,通过 encounter.orderId → order_main.slot_id → adm_schedule_slot.pool_id 回退获取
if ((divPoolId == null || divSlotId == null) && encounter.getOrderId() != null) {
if (encounter.getOrderId() != null) {
try {
Order order = iOrderService.getById(encounter.getOrderId());
if (order != null && order.getSlotId() != null) {
@@ -320,7 +316,16 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
}
}
} catch (Exception e) {
log.warn("回退获取完诊div_log的pool_id/slot_id失败encounterId={}", encounterId, e);
log.warn("完诊获取div_log的pool_id/slot_id失败(order链路)encounterId={}", encounterId, e);
}
}
// 订单链路无数据时,回退使用 triage_queue_item 的 poolId/slotId
if ((divPoolId == null || divSlotId == null) && queueItem != null) {
if (queueItem.getPoolId() != null) {
divPoolId = queueItem.getPoolId();
}
if (queueItem.getSlotId() != null) {
divSlotId = queueItem.getSlotId();
}
}

View File

@@ -256,6 +256,9 @@ public class RequisitionIssueAppServiceImpl implements IRequisitionIssueAppServi
*/
@Override
public R<?> submitApproval(String busNo) {
// 提交审批前校验库存,防止超库存单据进入审批流
this.validateRequisitionStockByBusNo(busNo);
// 单据提交审核
boolean result = supplyRequestService.submitApproval(busNo);
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, null))
@@ -419,4 +422,80 @@ public class RequisitionIssueAppServiceImpl implements IRequisitionIssueAppServi
}
}
}
/**
* 根据单据号校验领用数量是否超过源仓库实际库存(提交审批前调用)
*
* @param busNo 单据号
*/
private void validateRequisitionStockByBusNo(String busNo) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 通过单据详情查询已保存的单据明细
R<List<IssueDetailDto>> detailResult = this.getDetail(busNo);
if (detailResult.getCode() != 200 || detailResult.getData() == null) {
return;
}
List<IssueDetailDto> detailList = detailResult.getData();
for (IssueDetailDto detail : detailList) {
Long itemId = detail.getItemId();
String lotNumber = detail.getLotNumber();
Long sourceLocationId = detail.getSourceLocationId();
BigDecimal reqQuantity = detail.getItemQuantity();
String itemTable = CommonConstants.TableName.MED_MEDICATION_DEFINITION;
// 根据药品类型判断表名
if (ItemType.DEVICE.getValue().equals(detail.getItemType())) {
itemTable = CommonConstants.TableName.ADM_DEVICE_DEFINITION;
}
// 查询定义信息(拆零比、单位)
BigDecimal partPercent = BigDecimal.ONE;
String unitCode = detail.getUnitCode();
String minUnitCode = detail.getMinUnitCode();
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(itemTable)) {
MedicationDefinition medDef = medicationDefinitionService.getById(itemId);
if (medDef != null) {
unitCode = medDef.getUnitCode();
minUnitCode = medDef.getMinUnitCode();
if (medDef.getPartPercent() != null) {
partPercent = medDef.getPartPercent();
}
}
} else if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(itemTable)) {
DeviceDefinition devDef = deviceDefinitionService.getById(itemId);
if (devDef != null) {
unitCode = devDef.getUnitCode();
minUnitCode = devDef.getMinUnitCode();
if (devDef.getPartPercent() != null) {
partPercent = devDef.getPartPercent();
}
}
}
// 计算领用数量折合最小单位的值
BigDecimal reqQuantityInMinUnit;
if (unitCode != null && detail.getUnitCode().equals(unitCode)) {
reqQuantityInMinUnit = reqQuantity.multiply(partPercent);
} else {
reqQuantityInMinUnit = reqQuantity;
}
// 查询源仓库实际库存
List<InventoryItem> inventoryItemList = inventoryItemService.selectInventoryByItemId(
itemId, lotNumber, sourceLocationId, tenantId);
// 累加总库存
BigDecimal totalStock = BigDecimal.ZERO;
for (InventoryItem inventoryItem : inventoryItemList) {
if (inventoryItem.getLocationId().equals(sourceLocationId)) {
totalStock = totalStock.add(inventoryItem.getQuantity());
}
}
// 比较领用数量与库存
if (reqQuantityInMinUnit.compareTo(totalStock) > 0) {
throw new ServiceException("提交失败,单据中存在领用数量超过库存的明细,请核对后重新保存");
}
}
}
}

View File

@@ -93,7 +93,7 @@ public class ProductTransferController {
* @return 操作结果
*/
@PutMapping("/product-transfer-batch")
public R<?> addOrEditBatchTransferReceipt(@RequestBody List<ProductTransferDto> productTransferDtoList) {
public R<?> addOrEditBatchTransferReceipt(@Validated @RequestBody List<ProductTransferDto> productTransferDtoList) {
// 批量保存按钮
Boolean flag = true;
return productTransferAppService.addOrEditBatchTransferReceipt(productTransferDtoList, flag);

View File

@@ -89,7 +89,7 @@
cs.apply_doctor_name AS apply_doctor_name,
drf.create_time AS apply_time,
os.surgery_nature AS surgeryType,
cs.incision_level AS "incisionLevel",
cs.incision_level AS incisionLevel,
os.fee_type AS feeType,
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os

View File

@@ -36,6 +36,6 @@ public interface IOrganizationLocationService extends IService<OrganizationLocat
* @param activityDefinitionId 诊疗定义id
* @return 诊疗的执行科室列表
*/
List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long activityDefinitionId);
List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long organizationId, Long activityDefinitionId);
}

View File

@@ -53,12 +53,14 @@ public class OrganizationLocationServiceImpl extends ServiceImpl<OrganizationLoc
/**
* 查询诊疗的执行科室列表
*
* @param organizationId 机构id
* @param activityDefinitionId 诊疗定义id
* @return 诊疗的执行科室列表
*/
@Override
public List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long activityDefinitionId) {
public List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long organizationId, Long activityDefinitionId) {
return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>()
.eq(OrganizationLocation::getOrganizationId, organizationId)
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
}

View File

@@ -1369,17 +1369,12 @@ function isMethodSelected(method, cat) {
// Bug #428修复: 勾选检查方法
async function handleMethodSelect(checked, method, cat) {
if (checked) {
// 优先使用分类下的检查项目,若无则以方法自身作为已选项核心
let targetItem = cat.items[0];
// 找到该方法所属的第一个检查项目
const targetItem = cat.items[0];
if (!targetItem) {
targetItem = {
id: method.id, name: method.name,
price: method.packagePrice || method.price || 0,
serviceFee: method.serviceFee || 0, unit: '次',
checkType: method.checkType || cat.typeName || '',
nationalCode: '', packageName: method.packageName || null,
packageId: method.packageId || null
};
// 如果分类下没有项目,尝试从其他分类找同名项目或创建
console.warn('分类下没有检查项目,无法关联方法');
return;
}
// 如果该项目已存在,只更新 selectedMethod

View File

@@ -1,20 +0,0 @@
<!--
* 住院医生站 汇总发药申请复用药房发药汇总单能力
-->
<template>
<div class="summary-drug-application">
<MedicationSummary />
</div>
</template>
<script setup>
import MedicationSummary from '@/views/drug/inpatientMedicationDispensing/components/MedicationSummary.vue';
</script>
<style scoped>
.summary-drug-application {
height: 100%;
min-height: 400px;
overflow: auto;
}
</style>

View File

@@ -182,8 +182,16 @@ function handleRadioChange() {
function handleGetPrescription() {
if (patientInfoList.value.length > 0) {
// 过滤掉 encounterId 无效的患者节点,防止后端 SQL 解析异常
const validEncounterIds = patientInfoList.value
.map((i) => i.encounterId)
.filter((id) => id !== undefined && id !== null && id !== '');
if (validEncounterIds.length === 0) {
proxy.$message.warning('请选择有效的患者');
return;
}
loading.value = true;
let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(',');
let encounterIds = validEncounterIds.join(',');
getPrescriptionList({
encounterIds: encounterIds,
requestStatus: props.requestStatus,

View File

@@ -982,6 +982,16 @@ function handleSubmitApproval() {
} else if (!form.purchaseinventoryList[length - 1].isSave) {
proxy.$modal.msgWarning('第' + length + '行单据未保存,请先保存');
} else {
// 提交审批前逐行校验库存,防止超库存单据进入审批
for (let i = 0; i < form.purchaseinventoryList.length; i++) {
const line = form.purchaseinventoryList[i];
if (!line) continue;
const err = validateRequisitionQtyVsStock(line, i + 1);
if (err) {
proxy.$modal.msgWarning(err);
return;
}
}
submitApproval(receiptHeaderForm.busNo).then((response) => {
if (response.code == 200) {
proxy.$modal.msgSuccess('提交审批成功');

View File

@@ -739,15 +739,25 @@ function handleSubmitApproval() {
let length = totalIncentoryInfoList.value.length;
if (length < 1) {
proxy.$modal.msgWarning('请先添加单据');
} else if (data.isEdit) {
proxy.$modal.msgWarning('单据未保存');
} else {
submitApproval(receiptHeaderForm.busNo).then((response) => {
proxy.$modal.msgSuccess('提交审批成功');
tagsViewStore.delView(router.currentRoute.value);
router.replace({ path: 'transferManagentList' });
});
return;
}
// 校验调拨数量:必须 > 0
const invalidQtyRow = totalIncentoryInfoList.value.find(
(row) => !row.itemQuantity || row.itemQuantity <= 0
);
if (invalidQtyRow) {
proxy.$modal.msgWarning('存在调拨数量为0或无效的明细请检查后提交');
return;
}
if (data.isEdit) {
proxy.$modal.msgWarning('单据未保存');
return;
}
submitApproval(receiptHeaderForm.busNo).then((response) => {
proxy.$modal.msgSuccess('提交审批成功');
tagsViewStore.delView(router.currentRoute.value);
router.replace({ path: 'transferManagentList' });
});
}
// 切换仓库类型获取药房/药库列表 目的仓库切换
@@ -907,6 +917,22 @@ function remakeBlur(row, index) {
editBatchTransfer(index);
}
function handleSave() {
// 校验调拨数量:必须 > 0
const invalidQtyRow = totalIncentoryInfoList.value.find(
(row) => !row.itemQuantity || row.itemQuantity <= 0
);
if (invalidQtyRow) {
proxy.$modal.msgError('调拨数量必须大于0请检查');
return;
}
// 校验调拨数量不能超过源仓库库存
const exceedStockRow = totalIncentoryInfoList.value.find(
(row) => row.itemQuantity > row.totalSourceQuantity
);
if (exceedStockRow) {
proxy.$modal.msgError('调拨数量不可超出源库存数量,请检查!');
return;
}
// 校验单价
const invalidPriceRow = totalIncentoryInfoList.value.find(
(row) => !row.price || row.price <= 0

View File

@@ -829,7 +829,7 @@
</div>
<div style="padding: 10px">
<prescriptionlist :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1"
:generateSourceEnum="chargePatientInfo.generateSourceEnum"
:sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div>
</div>
@@ -1141,17 +1141,13 @@ const {
const pendingAnesData = ref(null)
// 监听麻醉字典加载,完成后立即设置表单值
// Bug #433: watch 回调中优先判断字典是否已加载,未加载时跳过(不处理也不清理)
// 确保字典加载完成时 watch 仍然活跃并能处理 pendingAnesData
let anesDataUnwatch = null
function setupAnesDataWatch() {
if (anesDataUnwatch) return // 防止重复设置
anesDataUnwatch = watch(
anesthesiaList,
(newList) => {
// Bug #433: 字典未加载时跳过,不清理 watch等待下次触发
if (!newList || newList.length === 0) return
if (pendingAnesData.value) {
if (newList && newList.length > 0 && pendingAnesData.value) {
const data = pendingAnesData.value
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1350,8 +1346,7 @@ function handleEdit(row) {
if (res.code === 200) {
const data = res.data
Object.assign(form, data)
// Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成
if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1361,8 +1356,6 @@ function handleEdit(row) {
pendingAnesData.value = data
setupAnesDataWatch()
}
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else {
proxy.$modal.msgError('获取手术安排详情失败')
}
@@ -1382,8 +1375,7 @@ function handleView(row) {
if (res.code === 200) {
const data = res.data
Object.assign(form, data)
// Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成
if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1393,8 +1385,6 @@ function handleView(row) {
pendingAnesData.value = data
setupAnesDataWatch()
}
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else {
proxy.$modal.msgError('获取手术安排详情失败')
}