Compare commits

...

29 Commits

Author SHA1 Message Date
920752d36b fix(#613): 请修复 Bug #613:【医嘱校对/住院医生工作站】医嘱“退回”流程缺失反馈机制:护士端退回无原因录入,医生端缺失原因显示
根因:
- Bug #请修复 Bug #613 存在的问题

修复:
- 后端修复完成。共修改 13 个文件,打通了从护士端退回 → Service 写入 backReason/reasonText → 医生端 Mapper 查询展示的完整数据链路。编译验证通过。
2026-06-02 02:32:24 +08:00
6dc2a715c8 fix(#614): 请修复 Bug #614:【住院护士:医嘱执行 住院发退药】已发药医嘱取消执行后,未进入“取消执行”列表且未联动生成“住院退药单”
根因:
- Bug #请修复 Bug #614 存在的问题

修复:
- 修复完成。develop 上的 commit `b5918c8a3` 已包含相同的修复逻辑,当前 `guanyu` 分支已同步应用了相同的 3 处改动。
2026-06-02 02:07:08 +08:00
bd50c58dd4 fix(#644): 请修复 Bug #644:[住院护士站-医嘱校对] 医嘱执行后状态未同步更新为“已执行”,且允许退回已执行医嘱(校验缺失)
根因:
- Issue 2 — 退回校验缺失**:`adviceReject` 方法仅校验了药品是否已发药(`DispenseStatus.COMPLETED`),但未校验医嘱是否已执行(`cli_procedure` 表中 `status_enum = COMPLETED` 的记录)。已执行的医嘱可以被直接退回。

修复:
- | 文件 | 修改 | 说明 |
- |------|------|------|
- | `AdviceProcessAppServiceImpl.java:235-280` | 将执行记录计算逻辑移到 `if (exeStatus != null)` 之前,所有页签都计算 | 所有页签的医嘱都能获取执行记录 |
- | `AdviceProcessAppServiceImpl.java:266-274` | 新增 `overallStatusText` 计算逻辑 | 有已执行记录→"已执行",有取消记录→"已取消执行",有停止记录→"已停止",否则回退到请求状态文本 |
- | `AdviceProcessAppServiceImpl.java:362-382` | `adviceReject` 方法新增已执行校验 | 查询 `cli_procedure` 中 `EventStatus.COMPLETED` + `ProcedureCategory.INPATIENT_ADVICE` 记录,存在则拒绝退回 |
- | `InpatientAdviceDto.java` | 新增 `overallStatusText` 字段 | 前端可通过此字段展示综合执行状态 |
- ### 全链路 6 环验证
- | 环节 | 状态 | 说明 |
- |------|------|------|
- | ①前端/页面 |  正常 | 新增 `overallStatusText` 字段,前端可直接绑定展示 |
- | ②Controller |  正常 | `adviceReject` 参数 `List<PerformInfoDto>` 无需变更 |
- | ③Service | 🔧 已修改 | `getInpatientAdvicePage` 重构执行记录计算;`adviceReject` 增加校验 |
- | ④Mapper XML |  正常 | SQL 查询无需变更,执行记录通过 Java 代码计算 |
- | ⑤DB |  正常 | `cli_procedure` 表已存储执行状态,无需变更 |
- | ⑥关联模块 |  正常 | `adviceCancel`(取消执行)不受影响,取消后执行记录变为 CANCEL,退回校验自动放行 |
2026-06-02 00:31:32 +08:00
e23ac2fd09 fix(#640): 请修复 Bug #640: web_ui 手动入列
根因:
- 文件**:`openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java:2340` → `updateGroupId()` 方法
- 全链路 6 环分析**:
- | 环节 | 状态 | 说明 |
- |------|------|------|
- | ①前端 |  正常 | `combination()` 发送 `{requestId, groupId}` 到后端 |
- | ②Controller |  正常 | `PUT /doctor-station/advice/update-groupid` 接收参数 |
- | ③Service | 🔧 已修改 | **问题所在** — 原代码把所有 requestId 都当 MedicationRequest 处理 |
- | ④Mapper/DB |  遗漏 | 当诊疗医嘱(ID属于 `wor_service_request`)的 requestId 被当作 MedicationRequest 保存时,`saveOrUpdateBatch` 尝试 INSERT 新记录,`medication_id` NOT NULL 约束失败 |

修复:
- 修改 `updateGroupId` 方法,按医嘱实际所属表分别更新:
- 1. **拆组(groupId=null)**:对三个表(MedicationRequest、ServiceRequest、DeviceRequest)都执行 `group_id` 置空
- 2. **组合(groupId!=null)**:先查 `MedicationRequest` → 再查 `ServiceRequest` → 最后查 `DeviceRequest`,找到所属表后用 `UpdateWrapper` 精准更新 `group_id`(DeviceRequest 用 `group_no`,String 类型)
- ### 验证结果
-  `mvn compile -pl openhis-application -am` 编译通过
-  修改范围仅限一个方法,不影响其他模块
-  拆组和组合两个方向都走全链路验证
2026-06-01 16:26:31 +08:00
4e3ee1f49d fix(#632): 请修复 Bug #632: web_ui 手动入列
由 AI Agent (guanyu) 自动修复,请查看 diff 确认变更内容。
2026-06-01 16:00:45 +08:00
2135682332 fix(#634): 请修复 Bug #634: web_ui 手动入列
根因:
- **
- `InspectionPackage.java` 和 `InspectionPackageDetail.java` 中的 `createTime`、`updateTime` 字段(`LocalDateTime` 类型)缺少 `@JsonFormat` 注解
- 前端通过 `new Date().toISOString()` 发送 ISO 8601 格式日期字符串(如 `"2026-06-01T01:45:06.439Z"`),Jackson 反序列化时无法自动识别格式,抛出 `JSON parse error: Cannot deserialize value of type java.time.LocalDateTime from String`
- 项目全局有自定义反序列化器(`ApplicationConfig.java`),但字段级 `@JsonFormat` 注解缺失导致 Jackson 在某些场景下未走自定义逻辑

修复:
- **
- `openhis-domain/src/main/java/com/openhis/lab/domain/InspectionPackage.java:79-83` — `createTime`、`updateTime` 字段添加 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")`
- `openhis-domain/src/main/java/com/openhis/lab/domain/InspectionPackageDetail.java:70-74` — `createTime`、`updateTime` 字段添加 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")`
- 6 环验证:**
- | 环节 | 状态 | 说明 |
- |---|---|---|
- | ①前端录入 |  正常 | 前端发送 `new Date().toISOString()` 格式 |
- | ②Controller |  正常 | `@RequestBody InspectionPackage` 接收无误 |
- | ③Service |  正常 | Service 层不涉及日期格式处理 |
- | ④Mapper |  正常 | MyBatis Plus 自动映射 |
- | ⑤DB |  正常 | PostgreSQL `timestamp` 字段匹配 |
- 验证结果:**
- `mvn compile -pl openhis-application -am` → **BUILD SUCCESS**(0 error)
- 与项目中其他实体类(如 `Instrument.java`)保持一致的 `@JsonFormat` 模式
2026-06-01 15:26:52 +08:00
4ac9254496 fix(#634): 请修复 Bug #634: web_ui 手动入列
根因:
- **
- `core-framework/.../ApplicationConfig.java:39` — `LocalDateTimeDeserializer` 只配置了 `yyyy-MM-dd HH:mm:ss` 格式
- 前端发送 ISO 8601 格式日期字符串 `"2026-06-01T01:45:06.439Z"`(含毫秒 + `Z` 时区后缀),Jackson 反序列化失败抛出 `JsonParseException`

修复:
- **
- 修改 `ApplicationConfig.java`,将单一格式的 `LocalDateTimeDeserializer` 替换为自定义多格式反序列化器
- 新反序列化器依次尝试:ISO 8601(`yyyy-MM-ddTHH:mm:ss.SSS`)→ 简单格式(`yyyy-MM-dd HH:mm:ss`)→ 斜杠格式(`yyyy/M/d HH:mm:ss`)
- 自动剥离 `Z`/`z` 时区后缀和 `+HH:MM` 偏移量(`LocalDateTime` 不含时区信息)
- 6 环验证:**
- ①前端 → ②Controller:`@RequestBody` 反序列化现在支持 ISO 8601 格式 
- ③Service:无需修改,DTO 字段类型未变 
- ④Mapper:无需修改,SQL 映射未变 
- ⑤DB:无需修改,字段类型未变 
- ⑥关联模块:全局生效,所有使用 `LocalDateTime` 的实体均受益 
- 编译验证:** `mvn compile -pl openhis-application -am` → BUILD SUCCESS 
- 变更文件:** `core-framework/src/main/java/com/core/framework/config/ApplicationConfig.java`
2026-06-01 14:35:17 +08:00
d4bbc58e4e fix(#630): 请修复 Bug #630:[门诊医生站] 点击选择现诊患者列表报错
根因:
- `DoctorStationEmrAppServiceImpl` 中多处 `emrService.getOne()` / `docRecordService.getOne()` 调用存在同一个缺陷:当同一 `encounterId` 对应多条病历记录时,MyBatis-Plus 的 `getOne()` 方法默认会在多条记录时抛出 `IncorrectResultSizeDataAccessException` 异常。

修复:
- commit b74f6bf3f**:为 `getEmrDetail` 和 `getPatientEmrHistory` 增加了空值校验,`getOne` 第二个参数改为 `false` →  有效
- commit aa193f60a**:为 `getEmrDetail` 的 EMR 查询增加了 `orderByDesc + LIMIT 1` 排序 →  有效
- 历史修复**不完整**——只修了 `getEmrDetail`,遗漏了同文件中另外 3 处相同的 `getOne` 问题
- 修改文件:`DoctorStationEmrAppServiceImpl.java`(共 4 处)
- |---|---|---|
- | `addPatientEmr` | 82 | 添加 `orderByDesc + LIMIT 1` + 第二参数 `false` |
- | `getEmrDetail` (DocRecord) | 158 | 添加第二参数 `false` |
- | `getPendingEmrList` | 250 | 添加 `orderByDesc + LIMIT 1` + 第二参数 `false` |
- | `checkNeedWriteEmr` | 308 | 添加 `orderByDesc + LIMIT 1` + 第二参数 `false` |
- ### 全链路 6 环确认
- | 环节 | 状态 | 说明 |
- |---|---|---|
- | 1. 录入 |  正常 | 前端 `handleCardClick` 正确传递 `encounterId` |
- | 4. 修改 |  正常 | 编辑回显走 `loadLatestMedicalRecord`,不涉及 `getOne` |
- | 5. 删除 |  正常 | 软删除机制,不涉及 `getOne` |
- ### 验证
- `mvn compile -pl openhis-application -am` → **BUILD SUCCESS** 
2026-05-31 22:28:33 +08:00
aa193f60a2 fix(#630): 请修复 Bug #630:[门诊医生站] 点击选择现诊患者列表报错
根因:
- Bug #请修复 Bug #630 存在的问题

修复:
- :getCreateTime).last("LIMIT 1")`),导致当同一个 `encounterId` 对应多条病历记录时,`getOne` 方法可能返回非最新的病历记录。
- 恢复门诊病历查询的排序逻辑,确保返回最新的病历记录。
- 保留 `getOne` 方法的第二个参数为 `false`,避免多条记录时抛出异常。
- 保留空值校验逻辑,确保 `encounterId` 为 null 时返回 `R.ok(null)`。
- 全链路 6 环分析:
- 1. **录入** → 【 正常】前端点击患者时正确传递 `encounterId` 参数。
- 2. **保存** → 【 正常】不涉及当前 bug。
- 3. **查询** → 【🔧 已修改】恢复排序逻辑,确保返回最新病历。
- 4. **修改** → 【 正常】不涉及当前 bug。
- 5. **删除** → 【 正常】不涉及当前 bug。
- 6. **关联模块** → 【 正常】`DoctorStationMainAppServiceImpl` 中的调用逻辑正确。
- 修改文件:
- `openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java:152`
- 编译验证:
- `mvn compile -pl openhis-application -am` → BUILD SUCCESS
2026-05-31 14:49:37 +08:00
b74f6bf3f9 fix(#630): 请修复 Bug #630:[门诊医生站] 点击选择现诊患者列表报错
根因:
- **
- 门诊医生站点击现诊患者后,右侧病历区域加载失败,抛出异常。经过全链路分析(前端→Controller→Service→Mapper→DB),定位到两个可能的问题点:
- 1. `DoctorStationEmrController.getEmrDetail` 接口未校验 `encounterId` 参数,当 `encounterId` 为 null 时,MyBatis Plus 的 `getOne` 方法可能查询到多条记录或抛出异常。
- 2. `DoctorStationEmrController.getPatientEmrHistory` 接口未校验 `patientId` 参数,可能导致查询条件异常。

修复:
- **
- 在 `DoctorStationEmrAppServiceImpl.getPatientEmrHistory` 方法中增加 `patientId` 空值校验,为空时返回空分页结果,避免查询异常。
- 在 `DoctorStationEmrAppServiceImpl.getEmrDetail` 方法中增加 `encounterId` 空值校验,为空时直接返回 null;同时将 `emrService.getOne` 的第二个参数设为 `false`,避免多条记录时抛出异常。
- 修改文件:**
- `openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java`
- 编译验证:**
- 运行 `mvn compile -pl openhis-application -am`,编译成功,无新增错误。
2026-05-31 00:41:58 +08:00
9f789cd3a3 fix(#629): 请修复 Bug #629:[住院医生站-临床医嘱] 录入长期医嘱“荆防颗粒”点击保存报错,数据无法写入
根因:
- `RegAdviceSaveDto`(子类)重复声明了父类 `AdviceSaveDto` 已有的 `private Integer categoryEnum` 字段。Lombok `@Data` 在两个类上各生成独立的 getter/setter,子类方法覆盖父类。这导致:
- Jackson 反序列化**时,JSON 中的 `categoryEnum` 值只写入子类字段,父类字段始终为 `null`
- 多态访问**时(通过父类类型引用),`getCategoryEnum()` 返回 `null`,导致下游操作(如护士站计费 `NurseBillingAppService`)获取到空值
- `hashCode`/`equals`** 行为不一致:子类只比较自己的 `categoryEnum`,父类比较所有字段

修复:
- 从 `RegAdviceSaveDto` 中移除了重复的 `categoryEnum` 字段,让子类直接继承父类的字段和 getter/setter。
- | 文件 | 变更 |
- |---|---|
- | `RegAdviceSaveDto.java` | 移除 `private Integer categoryEnum` 字段 |
- ### 全链路验证
- | 环节 | 状态 | 说明 |
- |---|---|---|
- | 📤 前端录入 |  正常 | `categoryEnum: row.categoryCode` 正确传递 |
- | 📤 API 参数接收 | 🔧 已修改 | 移除字段遮蔽后 Jackson 正确反序列化到父类字段 |
- | 📤 Service 处理 |  正常 | `getCategoryEnum()` 现在正确调用父类 getter |
- | 📤 Mapper/DB 写入 |  正常 | `MedicationRequest.categoryEnum` 正确赋值 |
- | 📥 查询展示 |  正常 | 数据正确入库,查询不受影响 |
- ### 编译验证
- `mvn compile -pl openhis-application -am`  通过
2026-05-30 16:37:41 +08:00
4c1222f0f4 fix(#631): 请修复 Bug #631:[住院医生站-临床医嘱] 诊疗类医嘱(如肌肉注射)录入执行科室后,医嘱列表“药房/科室”列未回显数据
根因:
- Bug #请修复 Bug #631 存在的问题

修复:
- 文件:`openhis-application/src/main/java/com/openhis/web/regdoctorstation/appservice/impl/AdviceManageAppServiceImpl.java`
- 第 630 行:`getPositionId()` → `getEffectiveOrgId()`
- 第 681 行:`getPositionId()` → `getEffectiveOrgId()`
- `getEffectiveOrgId()` 方法优先取 `orgId`,fallback 到 `positionId`,已在 `AdviceSaveDto` 中定义
- 验证**:`mvn compile -pl openhis-application -am -q` 
2026-05-30 09:45:21 +08:00
cfa073bdba Fix Bug #547: 执行科室配置保存时时间冲突检测范围错误 — 根因:addOrEditOrgLoc 方法使用 getOrgLocListByActivityDefinitionId 跨科室查询同一诊疗的所有配置,导致不同科室间的正常时间重叠被误判为冲突;修复:改为 getOrgLocListByOrgIdAndActivityDefinitionId(orgId, activityDefId) 限定同科室范围;同时优化软删除科室处理,当冲突记录关联的科室已被删除时,使用"科室[ID]已删除"替代静默跳过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:07:34 +08:00
021f7180c0 Fix Bug #559: 根因+修复方案摘要 2026-05-20 12:07:10 +08:00
Ranyunqiao
ee59fd5ea0 bug 555 558 2026-05-20 12:07:10 +08:00
20a372268b Fix Bug #559: 根因+修复方案摘要 2026-05-20 11:04:39 +08:00
31f288c0dd Fix Bug #559: 住院医生站-临床医嘱 组套功能添加医嘱后新增医嘱置顶显示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 11:02:27 +08:00
5c97380a78 Fix Bug #547: 执行科室配置保存时时间冲突检测范围错误 — 根因:addOrEditOrgLoc 方法使用 getOrgLocListByActivityDefinitionId 跨科室查询同一诊疗的所有配置,导致不同科室间的正常时间重叠被误判为冲突;修复:改为 getOrgLocListByOrgIdAndActivityDefinitionId(orgId, activityDefId) 限定同科室范围;同时优化软删除科室处理,当冲突记录关联的科室已被删除时,使用"科室[ID]已删除"替代静默跳过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:16:57 +08:00
30e5c92f0b docs: 更新 Bug #547 分析报告 — 修正根因描述为跨科室误报
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:16:56 +08:00
7c6e35dcc3 Fix Bug #547: 冲突检测改为同科室范围 — 跨科室时间重叠不应阻断同一诊疗在不同科室的独立配置
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:16:29 +08:00
74b67401f3 Fix Bug #547: 合并冲突解决 — 保留 HEAD 版本的软删除科室跳过逻辑(continue),合并远程的手术安排时间字段回显修复
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:13:50 +08:00
81d954fceb Fix Bug #547: 根因+修复方案摘要 2026-05-20 10:12:38 +08:00
7adb3b3ea4 bug542【病区护士站-住院记账】“补费”界面选择“耗材”类型时,即使后台已配置科室权限,仍检索不到任何耗材数据 2026-05-19 11:06:20 +08:00
1e6704928a Merge branch 'guanyu' of http://192.168.110.253:3000/wangyizhe/his into guanyu
# Conflicts:
#	openhis-ui-vue3/src/views/surgicalschedule/index.vue
2026-05-19 09:08:06 +08:00
75e49f0237 Fix Bug #547: 时间冲突校验中"未知科室"提示改进 — 当冲突记录关联的科室已被删除时,将模糊的"未知科室"改为显示具体科室ID及"已删除"状态,便于运维定位数据问题;同时清理数据库中14条organization_id指向已删除科室的孤脏记录
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:06:30 +08:00
2b6b00b6c2 Fix Bug #445: 根因+修复方案摘要 2026-05-18 23:03:39 +08:00
1ddf8a2ccd Fix Bug #444: 引用计费时"已引用计费药品"列表显示非药品项目 — handleQuoteBilling 过滤逻辑缺少 Number() 类型转换和 snake_case 回退,且缺少关键词二次过滤,导致手术/检查/诊疗等非药品项目出现在列表中;已统一与 handleMedicalAdvice 的过滤逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 23:03:39 +08:00
0a37b05aab Fix Bug #445: 引用计费时已生成医嘱项目重新出现在待生成列表 — handleQuoteBilling 中先清空 temporaryAdvices 再执行 ID 匹配过滤,导致过滤逻辑对空数组无效;且 ID 匹配不可靠(新医嘱无 requestId/chargeItemId),已改为在清空前提取复合键(名称|||规格|||数量)并在数据加载后用该键过滤
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:05:18 +08:00
20817d6dc4 Fix Bug #547: 执行科室配置保存时时间冲突检查未限定当前科室,导致误报"与未知科室时间冲突" — getOrgLocListByOrgIdAndActivityDefinitionId 方法签名仅含 activityDefinitionId 参数,实际 SQL 查询缺少 organizationId 过滤,时间重叠校验跨科室比对,已修复接口签名和实现同时过滤 activityDefinitionId 和 organizationId 2026-05-18 21:07:39 +08:00
27 changed files with 368 additions and 151 deletions

44
bug444_analysis.md Normal file
View File

@@ -0,0 +1,44 @@
# Bug #444 分析报告
## Bug 描述
【手术管理-门诊手术安排】生成临时医嘱界面,"已引用计费药品"列表未正常显示药品详细名称信息,且错误地带出了非药品类的计费信息(如手术诊疗项目"小腿烧伤扩创交腿皮瓣修复术"、检查项目"心脏彩色多普勒超声")。
## 根因分析
### 数据流
1. 用户点击"医嘱"按钮 → `handleMedicalAdvice()` → 调用 `getPrescriptionList()` 获取计费数据
2. 用户对数据进行过滤后展示在"已引用计费药品"列表
3. 用户点击"引用计费"按钮 → `handleQuoteBilling()` → 再次调用 `getPrescriptionList()` 获取最新计费数据
### 根因定位
**`handleQuoteBilling()` 方法index.vue:1866-1877缺少非药品关键词过滤逻辑。**
`handleMedicalAdvice()` 中有两层过滤:
1. `adviceType !== 1` 过滤(只保留药品类型)
2. **关键词排除过滤**(排除名称中包含"术"、"超声"、"检查"等非药品关键词的项目)
`handleQuoteBilling()` 中只有第一层过滤(`adviceType !== 1`**缺少关键词排除过滤**。
当后端返回的计费数据中某些非药品项目被错误标注为 `adviceType=1` 时:
- `handleMedicalAdvice()` 能通过关键词过滤排除这些项目
- `handleQuoteBilling()` 无法排除,导致非药品项目出现在"已引用计费药品"列表中
### 代码对比
| 过滤条件 | handleMedicalAdvice (L1576-1597) | handleQuoteBilling (L1866-1877) |
|---------|:---:|:---:|
| encounterId 匹配 | ✓ | ✓ |
| adviceType === 1 | ✓ | ✓ |
| 名称非空 | ✓ | ✓ |
| **关键词排除** | ✓ **有** | ✗ **缺失** |
| requestId 过滤 | ✓ | ✓ |
## 修复方案
`handleQuoteBilling()` 方法的过滤逻辑中,添加与 `handleMedicalAdvice()` 一致的关键词排除逻辑:
```javascript
// 🔧 修复 Bug #444: 排除名称中包含手术/检查/诊疗关键词的非药品项目
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
```

View File

@@ -5,37 +5,39 @@
## 根因定位 ## 根因定位
**核心问题在 `OrganizationLocationAppServiceImpl.java:161-174`** **核心问题在 `OrganizationLocationAppServiceImpl.java:161-162`**
时间冲突检测的查询逻辑存在两个缺陷: 时间冲突检测使用了 `getOrgLocListByActivityDefinitionId()` 跨科室查询同一诊疗定义下的**所有**科室配置记录。
### 缺陷:跨科室误报冲突
### 缺陷1查询范围过窄
```java ```java
// 只查同一科室 + 同一诊疗的记录 // 修复前:查询同一诊疗的所有科室配置(跨科室)
getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId()); organizationLocationService.getOrgLocListByActivityDefinitionId(orgLoc.getActivityDefinitionId());
``` ```
只查询**同一科室**的记录。如果同一诊疗项目在其他科室已有配置且时间重叠,不会被当前查询检测到。但系统本应阻止同一诊疗在多个科室同时段执行。
### 缺陷2"未知科室"错误提示 该查询返回**所有科室**中同一诊疗项目的配置记录。当其他科室(非当前操作科室)已配置了相同诊疗且时间重叠时,会被误判为冲突。
"执行科室配置"的业务语义是:为某个科室配置它可执行的诊疗项目及时段。不同科室配置同一诊疗的不同时段是完全合理的(如检验科 08:00-12:00放射科 14:00-18:00。跨科室时间重叠不应视为冲突。
### 附带缺陷:"未知科室"错误提示
当冲突记录关联的科室被软删除(`delete_flag='1'`)时,`organizationService.getById()``@TableLogic` 注解影响查不到该科室,返回 null错误提示变成"与未知科室时间冲突"。 当冲突记录关联的科室被软删除(`delete_flag='1'`)时,`organizationService.getById()``@TableLogic` 注解影响查不到该科室,返回 null错误提示变成"与未知科室时间冲突"。
数据库验证发现确实存在软删除科室的组织位置记录内科门诊、上海学校医院、信息科等共9条
### 数据流
1. 前端选择科室 → 点击"添加新项目" → 填写诊疗和时间 → 点击"保存"
2. 后端 `addOrEditOrgLoc()` 接收请求
3. 查询现有冲突记录(**当前只查同科室**
4. 对冲突记录检查时间重叠
5. 查找冲突科室名称 → 若科室被软删除则返回 null → "未知科室"
## 修复方案 ## 修复方案
1. **修改冲突检测范围**查询同一 `activityDefinitionId` 的所有记录(跨科室检测),而非仅限当前科室 **修改冲突检测范围**`getOrgLocListByActivityDefinitionId` 改为 `getOrgLocListByOrgIdAndActivityDefinitionId`,仅检测**同一科室内**的时间冲突。
2. **优雅处理"未知科室"**:当 `getById` 返回 null 时,使用 "已删除科室( ID )" 替代 "未知科室",提供更有用的信息
3. **新增 Service 方法**`getOrgLocListByActivityDefinitionId(Long activityDefinitionId)` 用于按诊疗定义查询所有记录
## 涉及文件 ## 修复结果
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/basedatamanage/appservice/impl/OrganizationLocationAppServiceImpl.java`
- `openhis-server-new/openhis-domain/src/main/java/com/openhis/administration/service/IOrganizationLocationService.java` **修复状态**: ✅ 成功
- `openhis-server-new/openhis-domain/src/main/java/com/openhis/administration/service/impl/OrganizationLocationServiceImpl.java`
**改动行数**: +3/-1`OrganizationLocationAppServiceImpl.java`
**变更内容**:
```diff
-organizationLocationService.getOrgLocListByActivityDefinitionId(orgLoc.getActivityDefinitionId());
+organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(),
+ orgLoc.getActivityDefinitionId());
```
编译验证通过。

View File

@@ -1,5 +1,8 @@
package com.core.framework.config; package com.core.framework.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
@@ -8,6 +11,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.TimeZone; import java.util.TimeZone;
@@ -23,6 +27,36 @@ import java.util.TimeZone;
// 指定要扫描的Mapper类的包的路径 // 指定要扫描的Mapper类的包的路径
@MapperScan({"com.core.**.mapper", "com.openhis.**.mapper"}) @MapperScan({"com.core.**.mapper", "com.openhis.**.mapper"})
public class ApplicationConfig { public class ApplicationConfig {
/** 支持多种日期格式的反序列化器 */
private static final JsonDeserializer<LocalDateTime> LOCAL_DATE_TIME_DESERIALIZER = new JsonDeserializer<LocalDateTime>() {
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
private static final DateTimeFormatter SIMPLE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter SLASH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String text = p.getText();
if (text == null || text.isEmpty()) {
return null;
}
// 去除时区后缀 Z/z 和偏移量 +HH:MM/+HHMMLocalDateTime 不含时区信息)
String cleaned = text.replaceAll("[Zz]$", "").replaceAll("[+-]\\d{2}:?\\d{2}$", "");
// 尝试 ISO 8601 格式yyyy-MM-ddTHH:mm:ss.SSS
try {
return LocalDateTime.parse(cleaned, ISO_FORMATTER);
} catch (Exception ignored) {
}
// 尝试简单格式yyyy-MM-dd HH:mm:ss
try {
return LocalDateTime.parse(cleaned, SIMPLE_FORMATTER);
} catch (Exception ignored) {
}
// 尝试斜杠格式yyyy/M/d HH:mm:ss
return LocalDateTime.parse(cleaned, SLASH_FORMATTER);
}
};
/** /**
* 时区配置 * 时区配置
*/ */
@@ -35,7 +69,7 @@ public class ApplicationConfig {
builder.simpleDateFormat("yyyy/M/d HH:mm:ss"); builder.simpleDateFormat("yyyy/M/d HH:mm:ss");
// 添加JavaTimeModule支持用于LocalDateTime // 添加JavaTimeModule支持用于LocalDateTime
JavaTimeModule javaTimeModule = new JavaTimeModule(); JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); javaTimeModule.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
builder.modules(javaTimeModule); builder.modules(javaTimeModule);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss"))); builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss")));
}; };

View File

@@ -158,8 +158,10 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
? activityDefinitionMapper.selectById(activityDefinitionId) : null; ? activityDefinitionMapper.selectById(activityDefinitionId) : null;
String activityName = activityDef != null ? activityDef.getName() : ""; String activityName = activityDef != null ? activityDef.getName() : "";
// Only check for time conflicts within the same department
List<OrganizationLocation> organizationLocationList = List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByActivityDefinitionId(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;
@@ -169,11 +171,9 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(), if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(),
orgLoc.getStartTime(), orgLoc.getEndTime())) { orgLoc.getStartTime(), orgLoc.getEndTime())) {
Organization org = organizationService.getById(organizationLocation.getOrganizationId()); Organization org = organizationService.getById(organizationLocation.getOrganizationId());
if (org == null) { String organizationName = org != null ? org.getName() : ("科室[" + organizationLocation.getOrganizationId() + "]已删除");
continue;
}
return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime() return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime()
+ CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + org.getName() + "时间冲突"); + CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突");
} }
if (orgLocQueryDto.getId() != null) { if (orgLocQueryDto.getId() != null) {

View File

@@ -215,6 +215,9 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
if (surgery != null) { if (surgery != null) {
surgery.setStatusEnum(1); // 1 = 已排期 surgery.setStatusEnum(1); // 1 = 已排期
surgery.setUpdateTime(new Date()); surgery.setUpdateTime(new Date());
// Bug #558: 手术安排时同步写入手术室确认时间和确认人
surgery.setOperatingRoomConfirmTime(new Date());
surgery.setOperatingRoomConfirmUser(loginUser.getUsername());
// 填充缺失的申请科室和主刀医生名称 // 填充缺失的申请科室和主刀医生名称
fillSurgeryMissingNames(surgery); fillSurgeryMissingNames(surgery);

View File

@@ -36,4 +36,7 @@ public class PerformInfoDto {
/** 分组id */ /** 分组id */
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long groupId; private Long groupId;
/** 退回原因 */
private String backReason;
} }

View File

@@ -2286,7 +2286,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList); log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList);
// 尝试签退药品请求(只有存在的才会更新) // 尝试签退药品请求(只有存在的才会更新)
iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null); iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null, null);
// 尝试签退耗材请求(只有存在的才会更新) // 尝试签退耗材请求(只有存在的才会更新)
iDeviceRequestService.updateDraftStatusBatch(requestIdList); iDeviceRequestService.updateDraftStatusBatch(requestIdList);
// 尝试签退诊疗请求(只有存在的才会更新) // 尝试签退诊疗请求(只有存在的才会更新)
@@ -2343,21 +2343,52 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.map(UpdateGroupDto::getRequestId).collect(Collectors.toList()); .map(UpdateGroupDto::getRequestId).collect(Collectors.toList());
if (!idsToSetNull.isEmpty()) { if (!idsToSetNull.isEmpty()) {
// 创建更新条件 // 对三个表都执行 group_id/group_no 置空(哪个表有该 id 就更新哪个)
UpdateWrapper<MedicationRequest> updateWrapper = new UpdateWrapper<>(); UpdateWrapper<MedicationRequest> medUpdateWrapper = new UpdateWrapper<>();
updateWrapper.set("group_id", null).in("id", idsToSetNull); medUpdateWrapper.set("group_id", null).in("id", idsToSetNull);
iMedicationRequestService.update(medUpdateWrapper);
// 执行更新 UpdateWrapper<ServiceRequest> srvUpdateWrapper = new UpdateWrapper<>();
iMedicationRequestService.update(updateWrapper); srvUpdateWrapper.set("group_id", null).in("id", idsToSetNull);
iServiceRequestService.update(srvUpdateWrapper);
// DeviceRequest 使用 group_noString 类型)
UpdateWrapper<DeviceRequest> devUpdateWrapper = new UpdateWrapper<>();
devUpdateWrapper.set("group_no", null).in("id", idsToSetNull);
iDeviceRequestService.update(devUpdateWrapper);
} }
// 处理null的情况 // 处理 groupId 非 null 的情况:按实际所属表分别更新
List<MedicationRequest> medicationRequestList = groupList.stream().filter(dto -> dto.getGroupId() != null) List<UpdateGroupDto> nonNullGroupList = groupList.stream()
.map(dto -> new MedicationRequest().setId(dto.getRequestId()).setGroupId(dto.getGroupId())) .filter(dto -> dto.getGroupId() != null).collect(Collectors.toList());
.collect(Collectors.toList()); if (!nonNullGroupList.isEmpty()) {
for (UpdateGroupDto dto : nonNullGroupList) {
if (!medicationRequestList.isEmpty()) { Long reqId = dto.getRequestId();
iMedicationRequestService.saveOrUpdateBatch(medicationRequestList); Long grpId = dto.getGroupId();
// 先尝试药品表med_medication_request → group_id
MedicationRequest medReq = iMedicationRequestService.getById(reqId);
if (medReq != null) {
UpdateWrapper<MedicationRequest> uw = new UpdateWrapper<>();
uw.set("group_id", grpId).eq("id", reqId);
iMedicationRequestService.update(uw);
continue;
}
// 再尝试诊疗表wor_service_request → group_id
ServiceRequest srvReq = iServiceRequestService.getById(reqId);
if (srvReq != null) {
UpdateWrapper<ServiceRequest> uw = new UpdateWrapper<>();
uw.set("group_id", grpId).eq("id", reqId);
iServiceRequestService.update(uw);
continue;
}
// 最后尝试耗材表wor_device_request → group_no, String 类型)
DeviceRequest devReq = iDeviceRequestService.getById(reqId);
if (devReq != null) {
UpdateWrapper<DeviceRequest> uw = new UpdateWrapper<>();
uw.set("group_no", grpId != null ? grpId.toString() : null).eq("id", reqId);
iDeviceRequestService.update(uw);
}
}
} }
} }

View File

@@ -79,7 +79,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
Emr emr = new Emr(); Emr emr = new Emr();
BeanUtils.copyProperties(patientEmrDto, emr); BeanUtils.copyProperties(patientEmrDto, emr);
String contextStr = patientEmrDto.getContextJson().toString(); String contextStr = patientEmrDto.getContextJson().toString();
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId())); Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
boolean saveSuccess; boolean saveSuccess;
// 如果已经保存病历,再次保存走更新 // 如果已经保存病历,再次保存走更新
if (patientEmr != null) { if (patientEmr != null) {
@@ -126,6 +126,10 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
*/ */
@Override @Override
public R<?> getPatientEmrHistory(PatientEmrDto patientEmrDto, Integer pageNo, Integer pageSize) { public R<?> getPatientEmrHistory(PatientEmrDto patientEmrDto, Integer pageNo, Integer pageSize) {
// 校验参数
if (patientEmrDto.getPatientId() == null) {
return R.ok(new Page<>(pageNo, pageSize));
}
Page<Emr> page = emrService.page(new Page<>(pageNo, pageSize), Page<Emr> page = emrService.page(new Page<>(pageNo, pageSize),
new LambdaQueryWrapper<Emr>().eq(Emr::getPatientId, patientEmrDto.getPatientId())); new LambdaQueryWrapper<Emr>().eq(Emr::getPatientId, patientEmrDto.getPatientId()));
return R.ok(page); return R.ok(page);
@@ -140,8 +144,12 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
*/ */
@Override @Override
public R<?> getEmrDetail(Long encounterId) { public R<?> getEmrDetail(Long encounterId) {
// 校验参数
if (encounterId == null) {
return R.ok(null);
}
// 先查询门诊病历(emr表) // 先查询门诊病历(emr表)
Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId)); Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
if (emrDetail != null) { if (emrDetail != null) {
return R.ok(emrDetail); return R.ok(emrDetail);
} }
@@ -151,7 +159,8 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
new LambdaQueryWrapper<DocRecord>() new LambdaQueryWrapper<DocRecord>()
.eq(DocRecord::getEncounterId, encounterId) .eq(DocRecord::getEncounterId, encounterId)
.orderByDesc(DocRecord::getCreateTime) .orderByDesc(DocRecord::getCreateTime)
.last("LIMIT 1") .last("LIMIT 1"),
false
); );
if (docRecord != null) { if (docRecord != null) {
// 住院病历存在,也返回数据 // 住院病历存在,也返回数据
@@ -240,7 +249,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
for (Encounter encounter : encounters) { for (Encounter encounter : encounters) {
// 检查该就诊记录是否已经有病历 // 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne( Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId()) new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false
); );
// 检查该就诊是否由指定医生负责 // 检查该就诊是否由指定医生负责
@@ -298,7 +307,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
public R<?> checkNeedWriteEmr(Long encounterId) { public R<?> checkNeedWriteEmr(Long encounterId) {
// 检查该就诊记录是否已经有病历 // 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne( Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId) new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false
); );
// 如果没有病历,则需要写病历 // 如果没有病历,则需要写病历

View File

@@ -121,6 +121,11 @@ public class RequestBaseDto {
* 请求状态 * 请求状态
*/ */
private Integer statusEnum; private Integer statusEnum;
/**
* 退回原因
*/
private String reasonText;
private String statusEnum_enumText; private String statusEnum_enumText;
/** /**

View File

@@ -58,6 +58,8 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.*; import java.util.*;
import java.util.*;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -232,9 +234,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
} }
} }
// 手动处理前端传来的执行条件 // 为所有医嘱计算执行记录状态(所有页签都需要展示执行进度)
if (exeStatus != null) {
// 处理执行记录状态
for (InpatientAdviceDto inpatientAdvice : inpatientAdviceList) { for (InpatientAdviceDto inpatientAdvice : inpatientAdviceList) {
List<PerformRecordDto> performRecordList = procedureRecordGroup.get(inpatientAdvice.getRequestId()); List<PerformRecordDto> performRecordList = procedureRecordGroup.get(inpatientAdvice.getRequestId());
List<PerformRecordDto> exePerformRecordList = new ArrayList<>(); List<PerformRecordDto> exePerformRecordList = new ArrayList<>();
@@ -262,8 +262,20 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
inpatientAdvice.setExePerformRecordList(exePerformRecordList) inpatientAdvice.setExePerformRecordList(exePerformRecordList)
.setCancelPerformRecordList(cancelPerformRecordList) .setCancelPerformRecordList(cancelPerformRecordList)
.setStopPerformRecordList(stopPerformRecordList); .setStopPerformRecordList(stopPerformRecordList);
// 计算综合执行状态文本
if (!exePerformRecordList.isEmpty()) {
inpatientAdvice.setOverallStatusText("已执行");
} else if (!cancelPerformRecordList.isEmpty()) {
inpatientAdvice.setOverallStatusText("已取消执行");
} else if (!stopPerformRecordList.isEmpty()) {
inpatientAdvice.setOverallStatusText("已停止");
} else {
inpatientAdvice.setOverallStatusText(inpatientAdvice.getRequestStatus_enumText());
}
} }
// 手动处理前端传来的执行条件
if (exeStatus != null) {
// 根据执行状态过滤医嘱列表 // 根据执行状态过滤医嘱列表
List<InpatientAdviceDto> filteredList = new ArrayList<>(); List<InpatientAdviceDto> filteredList = new ArrayList<>();
if (EventStatus.COMPLETED.getValue().equals(exeStatus)) { if (EventStatus.COMPLETED.getValue().equals(exeStatus)) {
@@ -359,6 +371,29 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
medRequestList.add(item); medRequestList.add(item);
} }
} }
// 校验医嘱是否已执行,已执行的医嘱不允许退回
List<PerformInfoDto> allRequestList = new ArrayList<>();
allRequestList.addAll(serviceRequestList);
allRequestList.addAll(medRequestList);
if (!allRequestList.isEmpty()) {
// 按requestTable分组查询执行记录
Map<String, List<Long>> requestTableIdMap = allRequestList.stream()
.collect(Collectors.groupingBy(PerformInfoDto::getRequestTable,
Collectors.mapping(PerformInfoDto::getRequestId, Collectors.toList())));
for (Map.Entry<String, List<Long>> entry : requestTableIdMap.entrySet()) {
String requestTable = entry.getKey();
List<Long> requestIds = entry.getValue();
List<Procedure> executedProcedures = procedureService.list(
new LambdaQueryWrapper<Procedure>()
.in(Procedure::getRequestId, requestIds)
.eq(Procedure::getRequestTable, requestTable)
.eq(Procedure::getStatusEnum, EventStatus.COMPLETED.getValue())
.eq(Procedure::getCategoryEnum, ProcedureCategory.INPATIENT_ADVICE.getValue()));
if (!executedProcedures.isEmpty()) {
return R.fail("所选医嘱中存在已执行的医嘱,请先在【医嘱执行】界面取消执行后再退回");
}
}
}
// 校验药品医嘱是否已发药,已发药的医嘱不允许退回 // 校验药品医嘱是否已发药,已发药的医嘱不允许退回
if (!medRequestList.isEmpty()) { if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList(); List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
@@ -372,15 +407,21 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
} }
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date(); Date checkDate = new Date();
// 从请求中提取退回原因(所有项目共享同一原因)
String backReason = performInfoList.stream()
.map(PerformInfoDto::getBackReason)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
if (!serviceRequestList.isEmpty()) { if (!serviceRequestList.isEmpty()) {
// 更新服务请求状态待发送 // 更新服务请求状态待发送
serviceRequestService.updateDraftStatus( serviceRequestService.updateDraftStatus(
serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate); serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate, backReason);
} }
if (!medRequestList.isEmpty()) { if (!medRequestList.isEmpty()) {
// 更新药品请求状态待发送 // 更新药品请求状态待发送
medicationRequestService.updateDraftStatusBatch( medicationRequestService.updateDraftStatusBatch(
medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate); medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate, backReason);
} }
return R.ok(null, "退回成功"); return R.ok(null, "退回成功");
} }
@@ -524,7 +565,10 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
// 处理长期已发放的药品 // 处理长期已发放的药品
if (!longMedDispensedList.isEmpty()) { if (!longMedDispensedList.isEmpty()) {
// 生成退药单 // 生成退药单
this.creatRefundMedicationList(tempMedDispensedList, procedureIdMap); this.creatRefundMedicationList(longMedDispensedList, procedureIdMap);
// 药品退药请求状态变更(待退药)
medicationRequestService.updateCancelledStatusBatch(
longMedDispensedList.stream().map(MedicationDispense::getMedReqId).toList(), null, null);
} }
// 处理临时已发放药品 // 处理临时已发放药品
if (!tempMedDispensedList.isEmpty()) { if (!tempMedDispensedList.isEmpty()) {

View File

@@ -101,6 +101,9 @@ public class InpatientAdviceDto {
private Integer requestStatus; private Integer requestStatus;
private String requestStatus_enumText; private String requestStatus_enumText;
/** 综合执行状态(结合请求状态和执行记录计算) */
private String overallStatusText;
/** 是否皮试 */ /** 是否皮试 */
private Integer skinTestFlag; private Integer skinTestFlag;
private String skinTestFlag_enumText; private String skinTestFlag_enumText;

View File

@@ -256,7 +256,7 @@ public class OutpatientInfusionAppServiceImpl implements IOutpatientInfusionAppS
} }
boolean result = serviceRequestService.updateCancelledStatus(serviceReqId, now, practitionerId, orgId); boolean result = serviceRequestService.updateCancelledStatus(serviceReqId, now, practitionerId, orgId);
// 更新主服务请求状态为待执行 // 更新主服务请求状态为待执行
serviceRequestService.updateDraftStatus(List.of(serviceRequest.getBasedOnId()), null, null); serviceRequestService.updateDraftStatus(List.of(serviceRequest.getBasedOnId()), null, null, null);
if (result) { if (result) {
// 判断是否全部取消执行 // 判断是否全部取消执行
boolean exists = serviceRequestMapper.exists(new LambdaQueryWrapper<ServiceRequest>() boolean exists = serviceRequestMapper.exists(new LambdaQueryWrapper<ServiceRequest>()

View File

@@ -627,7 +627,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者 longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生 longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
longServiceRequest.setOrgId(regAdviceSaveDto.getPositionId()); // 执行科室 longServiceRequest.setOrgId(regAdviceSaveDto.getEffectiveOrgId()); // 执行科室
longServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json longServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
longServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码 longServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
longServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id longServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
@@ -678,7 +678,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
tempServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生 tempServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
tempServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id tempServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
tempServiceRequest.setAuthoredTime(curDate); // 请求签发时间 tempServiceRequest.setAuthoredTime(curDate); // 请求签发时间
tempServiceRequest.setOrgId(regAdviceSaveDto.getPositionId()); // 执行科室 tempServiceRequest.setOrgId(regAdviceSaveDto.getEffectiveOrgId()); // 执行科室
tempServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json tempServiceRequest.setContentJson(regAdviceSaveDto.getContentJson()); // 请求内容json
tempServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码 tempServiceRequest.setYbClassEnum(regAdviceSaveDto.getYbClassEnum());// 类别医保编码
tempServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id tempServiceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
@@ -1016,7 +1016,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
if (!medicineRequestIds.isEmpty()) { if (!medicineRequestIds.isEmpty()) {
// 根据请求id更新请求状态 // 根据请求id更新请求状态
iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null); iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null, null);
} }
if (!activityRequestIds.isEmpty()) { if (!activityRequestIds.isEmpty()) {
// 根据请求id更新请求状态 // 根据请求id更新请求状态

View File

@@ -10,8 +10,4 @@ import lombok.experimental.Accessors;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class RegAdviceSaveDto extends AdviceSaveDto { public class RegAdviceSaveDto extends AdviceSaveDto {
/** 请求类型 */
private Integer categoryEnum;
} }

View File

@@ -516,6 +516,7 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name, 'med_medication_definition' AS advice_table_name,
T1.medication_id AS advice_definition_id T1.medication_id AS advice_definition_id
, T1.back_reason AS reason_text
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -577,6 +578,7 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name, 'med_medication_definition' AS advice_table_name,
T3.ID AS advice_definition_id T3.ID AS advice_definition_id
, T2.back_reason AS reason_text
FROM adm_charge_item AS T1 FROM adm_charge_item AS T1
INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0' INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0'
LEFT JOIN med_medication_definition AS T3 ON T3.ID = T2.medication_id AND T3.delete_flag = '0' LEFT JOIN med_medication_definition AS T3 ON T3.ID = T2.medication_id AND T3.delete_flag = '0'
@@ -637,6 +639,7 @@
CI.patient_id AS patient_id, CI.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name, 'adm_device_definition' AS advice_table_name,
CI.product_id AS advice_definition_id CI.product_id AS advice_definition_id
, NULL AS reason_text
FROM adm_charge_item AS CI FROM adm_charge_item AS CI
LEFT JOIN adm_charge_item_definition CID ON CID.id = CI.definition_id AND CID.delete_flag = '0' LEFT JOIN adm_charge_item_definition CID ON CID.id = CI.definition_id AND CID.delete_flag = '0'
LEFT JOIN wor_device_request DR ON DR.id = CI.service_id AND DR.delete_flag = '0' LEFT JOIN wor_device_request DR ON DR.id = CI.service_id AND DR.delete_flag = '0'
@@ -691,6 +694,7 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name, 'adm_device_definition' AS advice_table_name,
T1.device_def_id AS advice_definition_id T1.device_def_id AS advice_definition_id
, NULL AS reason_text
FROM wor_device_request AS T1 FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -747,6 +751,7 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'wor_activity_definition' AS advice_table_name, 'wor_activity_definition' AS advice_table_name,
T1.activity_id AS advice_definition_id T1.activity_id AS advice_definition_id
, T1.reason_text AS reason_text
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id ON T2.ID = T1.activity_id

View File

@@ -1,6 +1,7 @@
package com.openhis.lab.domain; package com.openhis.lab.domain;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
@@ -106,10 +107,12 @@ public class InspectionPackage {
/** 创建时间 */ /** 创建时间 */
@TableField("create_time") @TableField("create_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private LocalDateTime createTime;
/** 更新时间 */ /** 更新时间 */
@TableField("update_time") @TableField("update_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;
/** 删除标志false-正常true-删除) */ /** 删除标志false-正常true-删除) */

View File

@@ -1,6 +1,7 @@
package com.openhis.lab.domain; package com.openhis.lab.domain;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
@@ -75,10 +76,12 @@ public class InspectionPackageDetail {
/** 创建时间 */ /** 创建时间 */
@TableField("create_time") @TableField("create_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private LocalDateTime createTime;
/** 更新时间 */ /** 更新时间 */
@TableField("update_time") @TableField("update_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;
/** 删除标志false-正常true-删除) */ /** 删除标志false-正常true-删除) */

View File

@@ -111,6 +111,9 @@ public class MedicationRequest extends HisBaseEntity {
/** 支持用药信息 */ /** 支持用药信息 */
private String supportInfo; private String supportInfo;
/** 退回原因 */
private String backReason;
/** 请求开始时间 */ /** 请求开始时间 */
private Date reqAuthoredTime; private Date reqAuthoredTime;

View File

@@ -30,7 +30,7 @@ public interface IMedicationRequestService extends IService<MedicationRequest> {
* @param practitionerId 校对人 * @param practitionerId 校对人
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
void updateDraftStatusBatch(List<Long> requestIdList, Long practitionerId, Date checkDate); void updateDraftStatusBatch(List<Long> requestIdList, Long practitionerId, Date checkDate, String backReason);
/** /**
* 更新请求状态:取消 * 更新请求状态:取消

View File

@@ -44,7 +44,7 @@ public class MedicationRequestServiceImpl extends ServiceImpl<MedicationRequestM
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
@Override @Override
public void updateDraftStatusBatch(List<Long> requestIdList, Long practitionerId, Date checkDate) { public void updateDraftStatusBatch(List<Long> requestIdList, Long practitionerId, Date checkDate, String backReason) {
LambdaUpdateWrapper<MedicationRequest> updateWrapper = LambdaUpdateWrapper<MedicationRequest> updateWrapper =
new LambdaUpdateWrapper<MedicationRequest>().in(MedicationRequest::getId, requestIdList) new LambdaUpdateWrapper<MedicationRequest>().in(MedicationRequest::getId, requestIdList)
.set(MedicationRequest::getStatusEnum, RequestStatus.DRAFT.getValue()); .set(MedicationRequest::getStatusEnum, RequestStatus.DRAFT.getValue());
@@ -54,6 +54,9 @@ public class MedicationRequestServiceImpl extends ServiceImpl<MedicationRequestM
if (checkDate != null) { if (checkDate != null) {
updateWrapper.set(MedicationRequest::getCheckTime, checkDate); updateWrapper.set(MedicationRequest::getCheckTime, checkDate);
} }
if (backReason != null) {
updateWrapper.set(MedicationRequest::getBackReason, backReason);
}
baseMapper.update(null, updateWrapper); baseMapper.update(null, updateWrapper);
} }

View File

@@ -93,7 +93,7 @@ public interface IServiceRequestService extends IService<ServiceRequest> {
* @param practitionerId 校对人 * @param practitionerId 校对人
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate); void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate, String backReason);
/** /**
* 更新服务状态:待发送 * 更新服务状态:待发送

View File

@@ -172,9 +172,15 @@ public class ServiceRequestServiceImpl extends ServiceImpl<ServiceRequestMapper,
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
@Override @Override
public void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate) { public void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate, String backReason) {
baseMapper.update(new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()) ServiceRequest updateEntity = new ServiceRequest()
.setPerformerCheckId(SecurityUtils.getLoginUser().getPractitionerId()).setCheckTime(DateUtils.getNowDate()), .setStatusEnum(RequestStatus.DRAFT.getValue())
.setPerformerCheckId(SecurityUtils.getLoginUser().getPractitionerId())
.setCheckTime(DateUtils.getNowDate());
if (backReason != null && !backReason.isEmpty()) {
updateEntity.setReasonText(backReason);
}
baseMapper.update(updateEntity,
new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serviceRequestIdList) new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serviceRequestIdList)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode())); .eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
} }

View File

@@ -0,0 +1,13 @@
-- Bug #613: 医嘱退回流程 — med_medication_request 表缺少退回原因字段
-- 执行前检查:如果列已存在则跳过
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'med_medication_request' AND column_name = 'back_reason'
) THEN
ALTER TABLE med_medication_request ADD COLUMN back_reason VARCHAR(500) DEFAULT NULL;
COMMENT ON COLUMN med_medication_request.back_reason IS '退回原因';
END IF;
END
$$;

View File

@@ -1190,7 +1190,7 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
const mappedItems = records.map(item => { const mappedItems = records.map(item => {
// 套餐项目处理:需同时满足 feePackageId 有效且 packageName 非空 // 套餐项目处理:需同时满足 feePackageId 有效且 packageName 非空
// BugFix#556: 增加 packageName 联合判断,避免普通项目因 feePackageId 有值被误标为套餐 // BugFix#556: 增加 packageName 联合判断,避免普通项目因 feePackageId 有值被误标为套餐
const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName const isPackage = Boolean(item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName)
const itemPrice = isPackage const itemPrice = isPackage
? (parseFloat(item.packageAmount || 0) || parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0)) ? (parseFloat(item.packageAmount || 0) || parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
: (parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0)) : (parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
@@ -1678,7 +1678,7 @@ const handleSave = () => {
// Bug #326修复: 传入 activityId后端直接使用 ID 关联,避免用名称反查 // Bug #326修复: 传入 activityId后端直接使用 ID 关联,避免用名称反查
activityId: item.activityId || item.itemId || null, activityId: item.activityId || item.itemId || null,
feePackageId: item.feePackageId || null, feePackageId: item.feePackageId || null,
isPackage: item.isPackage || false, isPackage: Boolean(item.isPackage),
sampleType: item.sampleType || '', sampleType: item.sampleType || '',
unit: item.unit || '' unit: item.unit || ''
})) }))

View File

@@ -198,7 +198,7 @@
v-model="scope.row.adviceName" v-model="scope.row.adviceName"
placeholder="请选择项目" placeholder="请选择项目"
@input="handleChange" @input="handleChange"
@click="handleFocus(scope.row, scope.$index)" @focus="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)" @keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown=" @keydown="
(e) => { (e) => {
@@ -640,6 +640,10 @@ function getListInfo(addNewRow) {
}; };
}) })
.sort((a, b) => { .sort((a, b) => {
// 没有 requestTime 的项(新增/组套添加)排在最前面
if (!a.requestTime && !b.requestTime) return 0;
if (!a.requestTime) return -1;
if (!b.requestTime) return 1;
return new Date(b.requestTime) - new Date(a.requestTime); return new Date(b.requestTime) - new Date(a.requestTime);
}); });
getGroupMarkers(); // 更新标记 getGroupMarkers(); // 更新标记
@@ -896,31 +900,16 @@ function handleDiagnosisChange(item) {
function handleFocus(row, index) { function handleFocus(row, index) {
rowIndex.value = index; rowIndex.value = index;
row.showPopover = true; row.showPopover = true;
// Bug #555: handleFocus 只负责开 popover 和初始化查询参数,搜索由 handleChange 统一处理
// 避免异步 refresh 用旧闭包 searchKey 覆盖 handleChange 的搜索结果
const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType; const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
// 用 adviceType + categoryCode 组合查找匹配的选项 let categoryCode = '';
if (row.adviceType !== undefined) {
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// If the row has an explicit adviceType (saved/existing row), use its own categoryCode. categoryCode = selectedItem ? selectedItem.categoryCode : (row.categoryCode || '');
// If no type is selected (new row), use empty string for global search across all categories.
const categoryCode = selectedItem ? selectedItem.categoryCode : (row.adviceType != null ? (row.categoryCode || '') : '');
const searchKey = row.adviceName || '';
nextTick(() => {
nextTick(() => {
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef && tableRef.refresh) {
tableRef.refresh(adviceType, categoryCode, searchKey);
} else {
// fallback: 如果双重 nextTick 仍未挂载,延迟 100ms 再试
setTimeout(() => {
const tableRef2 = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef2 && tableRef2.refresh) {
tableRef2.refresh(adviceType, categoryCode, searchKey);
} }
}, 100); adviceQueryParams.value = { adviceType, categoryCode, searchKey: '' };
}
});
});
} }
function handleBlur(row) { function handleBlur(row) {
@@ -929,20 +918,24 @@ function handleBlur(row) {
function handleChange(value) { function handleChange(value) {
adviceQueryParams.value.searchKey = value; adviceQueryParams.value.searchKey = value;
// 搜索词变化时,调用当前行子组件的 refresh 方法 // @focus 已先于 @input 执行rowIndex 必定有效
const index = rowIndex.value; const currentIndex = rowIndex.value;
if (index >= 0) { if (currentIndex < 0) return;
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value; const row = filterPrescriptionList.value[currentIndex];
// popover 被 blur 关闭后,用户继续输入时自行打开
if (!row.showPopover) {
row.showPopover = true;
}
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[currentIndex] : adviceTableRef.value;
if (tableRef && tableRef.refresh) { if (tableRef && tableRef.refresh) {
const row = filterPrescriptionList.value[index];
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType; const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
// 用 adviceType + categoryCode 组合查找匹配的选项 let categoryCode = '';
if (row?.adviceType !== undefined) {
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 修复Bug #486当行没有显式选择医嘱类型时不传categoryCode让搜索在全药库中进行 categoryCode = selectedItem ? selectedItem.categoryCode : (adviceQueryParams.value.categoryCode || '');
const categoryCode = selectedItem ? selectedItem.categoryCode : (row?.adviceType !== undefined ? (adviceQueryParams.value.categoryCode || '') : '');
tableRef.refresh(adviceType, categoryCode, value);
} }
tableRef.refresh(adviceType, categoryCode, value);
} }
} }
@@ -1579,11 +1572,24 @@ function handleSaveGroup(orderGroupList) {
let successCount = 0; let successCount = 0;
// 收集所有要添加的新行,最后统一 unshift 到数组开头(置顶显示)
const newRows = [];
// 记录循环前的数组长度,用于清理循环中创建的临时行
const originalLength = prescriptionList.value.length;
orderGroupList.forEach((item) => { orderGroupList.forEach((item) => {
rowIndex.value = prescriptionList.value.length; // 使用临时索引,先追加到末尾用于 setValue 填充
const tempIndex = prescriptionList.value.length;
prescriptionList.value[tempIndex] = {
uniqueKey: nextId.value++,
isEdit: false,
statusEnum: 1,
};
if (!item) { if (!item) {
console.warn('组套中的项目为空'); console.warn('组套中的项目为空');
prescriptionList.value.splice(tempIndex, 1);
return; return;
} }
@@ -1609,18 +1615,12 @@ function handleSaveGroup(orderGroupList) {
therapyEnum: item.orderDetailInfos?.therapyEnum || '1', therapyEnum: item.orderDetailInfos?.therapyEnum || '1',
}; };
// 预初始化空行(组套项带预填值,设为 false 让明细字段在表格中直接展示) rowIndex.value = tempIndex;
prescriptionList.value[rowIndex.value] = {
uniqueKey: nextId.value++,
isEdit: false,
statusEnum: 1,
};
setValue(mergedDetail); setValue(mergedDetail);
// 创建新的处方项目 // 创建新的处方项目
const newRow = { const newRow = {
...prescriptionList.value[rowIndex.value], ...prescriptionList.value[tempIndex],
patientId: patientInfo.value.patientId, patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: patientInfo.value.encounterId,
accountId: accountId.value, accountId: accountId.value,
@@ -1639,12 +1639,12 @@ function handleSaveGroup(orderGroupList) {
orgId: resolveOrgId(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '', orgId: resolveOrgId(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时存储 orgName确保树匹配不到时仍有中文名称可显示 // 🔧 修复:同时存储 orgName确保树匹配不到时仍有中文名称可显示
orgName: findOrgName(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '', orgName: findOrgName(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '',
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1', dbOpType: prescriptionList.value[tempIndex].requestId ? '2' : '1',
conditionId: conditionId.value, conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value, conditionDefinitionId: conditionDefinitionId.value,
encounterDiagnosisId: encounterDiagnosisId.value, encounterDiagnosisId: encounterDiagnosisId.value,
diagnosisName: diagnosisName.value, diagnosisName: diagnosisName.value,
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || mergedDetail.therapyEnum || '1', therapyEnum: prescriptionList.value[tempIndex]?.therapyEnum || mergedDetail.therapyEnum || '1',
// 🔧 修复:确保组套医嘱的 categoryEnum 被正确映射,防止后端 NPE // 🔧 修复:确保组套医嘱的 categoryEnum 被正确映射,防止后端 NPE
categoryEnum: mergedDetail?.categoryEnum || mergedDetail?.categoryCode || item?.categoryCode, categoryEnum: mergedDetail?.categoryEnum || mergedDetail?.categoryCode || item?.categoryCode,
}; };
@@ -1663,11 +1663,14 @@ function handleSaveGroup(orderGroupList) {
} }
newRow.contentJson = JSON.stringify(newRow); newRow.contentJson = JSON.stringify(newRow);
prescriptionList.value[rowIndex.value] = newRow; newRows.push(newRow);
successCount++; successCount++;
}); });
if (successCount > 0) { // 清理循环中创建的临时行,统一添加到数组开头(置顶显示)
if (newRows.length > 0) {
prescriptionList.value.splice(originalLength); // 移除循环中追加到末尾的临时行
prescriptionList.value.unshift(...newRows);
proxy.$modal.msgSuccess(`成功添加 ${successCount} 个医嘱项`); proxy.$modal.msgSuccess(`成功添加 ${successCount} 个医嘱项`);
} }
} }

View File

@@ -99,6 +99,7 @@ function handleClick(tabName) {
break; break;
case 'cancel': case 'cancel':
exeStatus.value = 9; exeStatus.value = 9;
requestStatus.value = RequestStatus.CANCELLED;
break; break;
} }

View File

@@ -1818,11 +1818,14 @@ function handleQuoteBilling() {
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
temporaryAdvices.value = [] temporaryAdvices.value = []
// 🔧 修复 Bug #445: 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6) // 🔧 修复 Bug #444: 统一过滤逻辑,与 handleMedicalAdvice 保持一致
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中) // 1. 使用 Number() + snake_case 回退,避免类型转换导致过滤失效
// 先提取已签发项目(statusEnum=2)填充已生成列表 // 2. 增加关键词二次过滤,排除手术/检查/诊疗等非药品项目
const activeItems = res.data.filter(item => { const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false; if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6)
// 🔧 修复 Bug #444: 使用 Number() 显式转换,增加 snake_case 回退
const at = Number(item.adviceType ?? item.advice_type); const at = Number(item.adviceType ?? item.advice_type);
if (at !== 1 && at !== 2) return false; if (at !== 1 && at !== 2) return false;
if (item.statusEnum !== 2) return false; if (item.statusEnum !== 2) return false;