Compare commits

..

67 Commits

Author SHA1 Message Date
d43d7a2dc8 Fix Bug #545: 清理 handleNodeClick 中重复的 longTermFlag 字段 — 第三次提交时重复添加了 longTermFlag: 0(第887行和第889行各有一处),移除重复项
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:09:50 +08:00
436cd897ba Fix Bug #545: 补全诊断添加处缺失的 longTermFlag 默认值 — 第三个 push 调用缺少 longTermFlag: 0,导致通过此路径添加的诊断该字段为 undefined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:03:42 +08:00
6da04d3bc3 fix bug529 2026-05-18 16:03:42 +08:00
d825f9b61f Fix Bug #545: [门诊医生站-诊断-报卡] 长效诊断标识设置保存就清空 — 根因:1) 后端getEncounterDiagnosis查询已补充longTermFlag字段但前端getList()未做类型转换,useDict('long_term_flag')返回字符串字典值而数据库返回整数导致el-select匹配失败下拉框清空;2) 冗余的备份恢复逻辑应移除;修复:1) getList()中新增longTermFlag转字符串处理(String(item.longTermFlag)),保证与useDict字典值类型一致;2) 移除handleSaveDiagnosis中已不再需要的longTermFlagBackup/恢复逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:07:14 +08:00
a5f8a5ba1b Fix Bug #545: 根因+修复方案摘要 2026-05-18 15:07:14 +08:00
f74134a798 Fix Bug #545: 根因+修复方案摘要 2026-05-18 15:07:14 +08:00
857880af4e Fix Bug #545: 长效诊断标识设置保存就清空 — 根因:handleSaveDiagnosis保存成功后await getList()刷新列表,后端getEncounterDiagnosis接口不返回longTermFlag字段,导致form.value.diagnosisList中该字段变为undefined,下拉框清空;修复:保存前用longTermFlagBackup备份longTermFlag数组,getList()完成后按索引恢复
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:04:28 +08:00
86c879ef9f Fix Bug #545: [门诊医生站-诊断-报卡] 长效诊断标识设置保存就清空 — 根因:getEncounterDiagnosis查询SQL(DoctorStationDiagnosisAppMapper.xml)未包含long_term_flag字段且DiagnosisQueryDto缺少对应属性,导致保存成功后刷新列表时后端不返回longTermFlag值,前端接收后下拉框清空;修复:1) SQL新增T1.long_term_flag AS longTermField; 2) DTO新增longTermFlag属性
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:02:13 +08:00
2f9dd2b1df Fix Bug #542: 补费界面耗材类型检索不到数据 — 根因:drord_doctor_type字典中耗材值=4但后端SQL查询adviceTypes.contains(2)仅匹配2;修复:filter扩展为2/3/4并通过map将字典值4映射为2,使前端选择的耗材类型(adviceType=2)与后端SQL查询条件一致
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:19:17 +08:00
e247aac319 Fix Bug #540: 检查申请打印功能"申请单描述"区域与详情弹窗显示不一致 — 根因:handlePrint函数仍使用固定orderedDescFieldKeys遍历+空值过滤,与已修复的详情弹窗不一致;修复:改为遍历descData所有key并通过labelMap过滤,空值显示为'-',同时移除已废弃的orderedDescFieldKeys常量
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:19:02 +08:00
680db771cd Fix Bug #542: 补费界面耗材类型检索不到数据 — 根因:双重不匹配 (1) getAdviceBaseInfos函数中queryParams.value.adviceType(单数)与后端@RequestParam("adviceTypes")(复数)参数名不匹配导致后端始终使用默认值"1,2,3"而非用户选择的类型; (2) drord_doctor_type字典中耗材值=4但后端SQL查询adviceTypes.contains(2)要求耗材=2; 修复:1) adviceType改为adviceTypes; 2) 默认返回值中耗材值4改为2
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:16:51 +08:00
1a6a29aab5 Fix Bug #540: 根因+修复方案摘要 2026-05-18 14:16:51 +08:00
0a51a3605f Fix Bug #541: 待签发医嘱双击无法打开编辑界面 — 根因:clickRowDb函数中条件row.statusEnum == 1 && !row.requestId只允许"待保存"医嘱编辑,错误排除了"待签发"医嘱;修复:改为row.statusEnum == 1,允许statusEnum=1的所有医嘱(待保存+待签发)双击进入编辑模式,保存时handleSaveSign已通过requestId/dbOpType=2正确处理更新逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:16:51 +08:00
f0817270db 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-18 13:35:35 +08:00
812e0d62a6 Fix Bug #540: 根因+修复方案摘要 2026-05-18 13:32:01 +08:00
a9b5cca904 Fix Bug #540: 检查申请详情弹窗"申请单描述"区域缺少临床必要信息显示 — 根因:详情弹窗中"申请单描述"区域使用固定orderedDescFieldKeys遍历+空值过滤(v-if descJsonData[key] !== ''),导致字段值为空时整行不显示;修复:改为与检验申请一致的遍历方式,遍历descJsonData所有key并通过isFieldMatched过滤,空值显示为'-'而非隐藏
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 13:29:21 +08:00
2e71d98ce6 Fix Bug #539: 住院护士站只显示一个标签 — 根因:menu_id=295被误设为目录类型(M)无component,应为菜单类型(C)并指向inpatientNurseStation/index.vue;修复:UPDATE sys_menu SET menu_type='C', component='inpatientNurse/inpatientNurseStation/index' WHERE menu_id=295;护士站点击后直接加载带10个功能标签的主页面(入出转管理、护理记录、医嘱执行等),侧边栏不再展开子菜单
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:27:12 +08:00
8156fc2e8f Fix Bug #539: 实际执行数据库SQL修复 — 将menu_id=295的menu_type从C改为M并清空component,使住院护士站侧边栏展开子菜单(15个子菜单:入出转管理、护理记录、三测单等);menu_id=2062的component已是正确值无需更新
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:24:44 +08:00
a751e33530 Fix Bug #539: 住院护士站功能模块缺失 — 菜单类型从目录(M)改为菜单(C)并添加静态路由
根因分析:
- sys_menu 中"住院护士站"(menu_id=295) 的 menu_type 为 M(目录类型),
  没有 component,点击后仅在侧边栏展开子菜单,不会导航到功能页面
- "住院医生工作站"(menu_id=288) 为 C 类型(菜单),点击直接打开功能页面

修复方案(两处修改):
1. 数据库:将"住院护士站" menu_type 改为 C,设置 component 为
   inpatientNurse/inpatientNurseStation/index,path 改为 inpatientNurseStation
   → 点击侧边栏"住院护士站"直接打开带 el-tabs 的功能页面
2. 前端路由:添加 /inpatientNurse 静态路由组,包含 inpatientNurseStation 及
   6个快捷访问子路由,与 quick-access 卡片的 /inpatientNurse/... 路径匹配

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:24:15 +08:00
22465fb276 Fix Bug #539: 住院护士站菜单类型错误(C→M)导致子菜单不展开 — 根因:menu_id=295的menu_type被设为C且有component,应为M目录类型;修复:UPDATE sys_menu SET menu_type='M', component=NULL WHERE menu_id=295;附带修复menu_id=2062的component路径错误(indexon/→index)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:22:34 +08:00
eeb5af8fc1 Fix Bug #539: 根因+修复方案摘要 2026-05-18 12:18:15 +08:00
56cd024949 Fix Bug #529: [住院医生工作站-检验申请] 点击修改打开编辑弹窗后原已选中的项目未回显
根因:时序竞态——editData watch (immediate: true) 在 applicationListAll 加载完成前触发,
匹配不到数据导致 transferValue 被置空。新增 watch 监听 applicationListAll 加载完成后重新回显。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:18:15 +08:00
b37033d87e fix bug525:[手术管理-门诊手术安排-计费] 已勾选“待签发”项目且未收费,点击“删除”提示“只能删除待签发且未收费的项目” 2026-05-18 12:18:15 +08:00
9e07546027 Fix Bug #530: 根因+修复方案摘要 2026-05-18 12:18:15 +08:00
wangjian963
cb146ade45 修复门诊手术安排模块计费弹窗中对诊疗数据进行签发成功后回显失败的问题。 2026-05-18 12:18:15 +08:00
b3186158fe Fix Bug #538: 手术申请删除后医嘱未同步删除 — 根因:handleDelete 未 emit('saved') 通知父组件刷新医嘱列表,修复:删除/取消成功后追加 emit('saved') 触发 prescriptionRef.getListInfo()
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:05:37 +08:00
ed938becb3 Fix Bug #538: [门诊医生站-医嘱/手术申请] 手术申请单删除后级联删除关联医嘱、收费项目、申请单
根因:deleteSurgery 仅删除 cli_surgery 表记录,未级联删除关联的
wor_service_request(手术医嘱)、fin_charge_item(收费项目)、
doc_request_form(申请单),导致手术删除后医嘱列表仍存在对应记录。

修复:在 deleteSurgery 中先删除三张关联表数据,再删除手术记录,
所有操作在同一事务内保证一致性。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:05:14 +08:00
Ranyunqiao
a89d91c5be bug 443 522 523 2026-05-18 11:05:14 +08:00
af9b3bbc76 Fix Bug #537: 根因+修复方案摘要 2026-05-18 11:05:14 +08:00
fd16daa2a6 Fix Bug #537: [住院医生工作站] 冗余功能显示需在医生工作站页签中屏蔽汇总发药申请模块(仅修复代码,不改禅道状态和分配)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:05:14 +08:00
3cb5e2d212 Fix Bug #520: [住院医生工作站-检验申请] 检验申请列表点击详情按钮界面无响应
根因:getLocationInfo() 缺少 try-catch,当 getDepartmentList() API 失败时,
未捕获的异常向上传播导致 handleViewDetail 在设置 detailDialogVisible=true 前终止,
详情弹窗永远无法打开。

修复:为 getLocationInfo() 添加 try-catch 错误处理,API 失败时降级为空数组,
确保 handleViewDetail 的后续代码(设置 currentDetail 和打开弹窗)能正常执行。
与 examineApplication.vue 的已有修复保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:15:56 +08:00
150ecc057f Fix Bug #524: [门诊/医生个人报卡管理] 传染病报告卡保存后数据回显失败 — 根因:showReport 加载数据时 watch 监听 selectedClassA/B/C 变化清空了 diseaseType 分型字段,修复:新增 loadingData 标志在 showReport 加载期间跳过 watch 清空逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:11:56 +08:00
d54ad5ef88 Fix Bug #522: [住院护士站-三测单] 体征录入点击保存后缺乏执行反馈且窗口异常自动关闭
根因: proxy.msgSuccess 不存在(正确路径为 proxy.$modal.msgSuccess),
导致保存成功提示无法弹出;同时 addVitalSigns 缺少 .catch() 块,
API 失败时既无错误提示也无任何反馈。

修复:
1. proxy.msgSuccess → proxy.$modal.msgSuccess(保存成功提示)
2. 添加 .catch() 块:console.error 日志 + proxy.$modal.msgError 错误提示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:11:56 +08:00
1fbed5c595 Fix Bug #523: [住院医生站-临床医嘱] 修复待保存医嘱总金额显示缺失及编辑态单位选择框类型异常
根因:setValue() 中药品分支未初始化 totalPrice;unitCode/minUnitCode 未转 String 导致 el-select 类型不匹配
修复:选药后立即计算 totalPrice;所有单位值统一 String() 转换

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
02a1889f2c Fix Bug #524: 根因+修复方案摘要 2026-05-18 09:09:49 +08:00
fad5130072 Fix Bug #537: [住院医生工作站] 最终复核确认修复已生效,更新修复报告
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
265adaea02 Fix Bug #537: [住院医生工作站] 复核验证确认修复已生效,更新修复报告
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
1360155028 Fix Bug #537: 根因+修复方案摘要 2026-05-18 09:09:49 +08:00
5e1a1d6109 Fix Bug #537: [住院医生工作站] 屏蔽"汇总发药申请"导航入口 — 从 inpatientNurse/constants/navigation.js 移除该导航项(护士专属功能,医生不应可见)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
3160387932 Fix Bug #537: [住院医生工作站] 清理已屏蔽的汇总发药申请组件死代码 - 移除注释掉的 tab-pane 和 SummaryDrugApplication 引用
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
477578f494 Fix Bug #537: [住院医生工作站] 清理已屏蔽的汇总发药申请组件死代码
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
7a163b8c0c 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-18 09:09:49 +08:00
a941134908 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-18 09:09:49 +08:00
75a55b9402 Fix Bug #530: [住院护士站-医嘱校对] 患者查询触发 SQL 类型匹配错误,导致勾选患者列表后后端报错 - 前端过滤无效的encounterId防止后端SQL解析异常
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:09:49 +08:00
693ed79f75 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-18 09:09:49 +08:00
c5e5c59e35 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-18 09:09:49 +08:00
f1e1aad754 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:13:58 +08:00
1a5014b3ea Fix Bug #512: [住院护士站-汇总发药申请] 全选开关功能失效 - 增加nextTick确保DOM就绪后操作表格选择,修复handleExecute始终调用prescriptionRefs的问题
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:13:55 +08:00
c707a2a3cf Fix Bug #528: [住院医生工作站-检查申请] 修改申请单成功后弹窗自动关闭且列表自动刷新 - 调整submit函数中emits('submitOk')与resetForm()的执行顺序,确保先通知父组件关闭弹窗再重置表单状态
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:11:16 +08:00
6449f21d14 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:08:39 +08:00
9a869284d5 Fix Bug #518: 根因+修复方案摘要 2026-05-17 21:08:38 +08:00
50a0e1a2b4 Fix Bug #504: 护士退回药品医嘱后医生修改保存时"未匹配到库存信息" - 增加两阶段库存匹配逻辑和空值保护
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +08:00
885b261f59 Fix Bug #478: 修复检验申请详情"发往科室"字段回显为"-"的问题
根因:testApplication.vue 中的 recursionFun 函数只遍历科室树的两层(顶层+一级子节点),
当发往科室ID位于第三层或更深时无法匹配,返回空字符串导致显示"-"。
修复:改为递归遍历整棵科室树,确保任意深度的科室节点都能正确解析为名称。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:08:19 +08:00
a6f6870158 Fix Bug #497: 根因+修复方案摘要 2026-05-17 20:08:19 +08:00
8a5c38776a 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:06:25 +08:00
c31340f649 Fix Bug #468: 根因+修复方案摘要 2026-05-17 20:06:25 +08:00
c97b2f7466 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 19:22:01 +08:00
45f2c973bf Fix Bug #470: 根因+修复方案摘要 2026-05-17 19:22:01 +08:00
177d3f28de 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:22:01 +08:00
fcb8c02f54 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:11:20 +08:00
3f5cea0fd0 Fix Bug #444: 根因+修复方案摘要 2026-05-17 19:03:53 +08:00
bbd173ac47 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 19:03:53 +08:00
21ba278a77 Fix Bug #433: 门诊手术安排编辑弹窗麻醉方法回显为代码且外请专家姓名未加载修复
根因:setupAnesDataWatch 的 watch({ immediate: true }) 在字典未加载时触发,
因合并条件判断同时检查字典长度和pendingAnesData,导致字典未加载时不进入
处理逻辑但也不清理watch。当handleEdit设置pendingAnesData后,watch仍活跃
但后续字典加载时pendingAnesData可能已被handleEdit的直接转换路径处理过。

修复:
1. setupAnesDataWatch: 将合并条件拆分为早期返回(字典未加载时跳过)和
   pendingAnesData处理两步,确保字典加载后watch仍活跃
2. handleEdit/handleView: 显式赋值externalExpertName确保响应式更新

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:13:34 +08:00
926c9bd1cb Fix Bug #428: 门诊医生站-检查申请:修复分类联动检查方法勾选后未添加到已选择列表
根因:handleMethodSelect 函数硬编码使用 cat.items[0] 作为目标项目,
当分类下没有检查项目(items)时直接 return,导致用户勾选检查方法后
"已选择"面板无反应,动作二和动作三均被阻塞。

修复:降级策略——当 cat.items 为空时,以方法自身数据创建 targetItem,
后续创建 selectedItems 条目的逻辑正常执行,三个动作全部打通。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:10:15 +08:00
971e6861db Fix Bug #433: 根因+修复方案摘要 2026-05-17 18:10:15 +08:00
90650f8ae8 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:03 +08:00
f041f97201 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:15:55 +08:00
25 changed files with 617 additions and 647 deletions

View File

@@ -0,0 +1,53 @@
# Bug #523 分析报告
## Bug 描述
[住院医生站-临床医嘱] 待保存医嘱总金额显示缺失且编辑态单位选择框变为数字控件
## 根因分析
### 问题1总金额显示为 "-"
**文件**: `openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue`
**根因**: `setValue()` 函数约1441行在选中药品后初始化行数据时
- 设置了 `unitPrice``minUnitPrice`(西药/中成药/中草药)
- 设置了诊疗类型的 `totalPrice`adviceType==3 分支1537-1538行
- **但没有为药品类型adviceType==1计算 `totalPrice`**
`totalPrice` 只在用户后续交互(修改总量、切换单位)时通过 `calculateTotalAmount()` 才计算。
列表显示逻辑259行`scope.row.totalPrice ? ... : '-'`,未设置则显示横杠。
**数据流**: 选药 → setValue(设unitPrice) → 用户填总量 → calculateTotalAmount(算totalPrice) → 列表显示
**问题**: 用户选好药后还没触发计算事件时totalPrice 为空
### 问题2编辑态单位选择框变为数字控件
**文件**: `openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue`
**根因**: `setValue()` 函数1518行
```js
unitCode: row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode,
```
后端返回的 `row.unitCode` / `row.minUnitCode` 可能是 **Number 类型**
`row.unitCodeList` 中每个 option 的 `value``String` 类型(从后端字典值来)。
`el-select``v-model`Number与所有 option 的 `value`String类型不匹配时
Element Plus 无法找到匹配选项,渲染异常,表现为数字输入控件。
## 修复方案
### 修复1总金额
`setValue()` 的药品分支中,设置价格后立即计算初始 `totalPrice`
```js
// 在 positionName 设置后添加:
totalPrice: row.quantity ? (row.unitCode == row.minUnitCode
? (row.quantity * row.minUnitPrice).toFixed(6)
: (row.quantity * row.unitPrice).toFixed(6))
: undefined,
```
### 修复2单位选择框
`setValue()``updatedRow` 中,将 `unitCode``minUnitCode` 转为字符串:
```js
minUnitCode: String(row.minUnitCode),
unitCode: row.partAttributeEnum == 1 ? String(row.minUnitCode) : String(row.unitCode),
```
确保与 el-option 的 valueString类型一致。

84
BUG428_ANALYSIS.md Normal file
View File

@@ -0,0 +1,84 @@
# 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行

View File

@@ -1,30 +0,0 @@
# Bug #444 分析报告
## Bug 描述
生成临时医嘱界面,"已引用计费药品"列表未正常显示药品详细名称信息。具体表现为:
- 列表中出现了"小腿烧伤扩创交腿皮瓣修复术"(属于手术诊疗项目)
- 列表中出现了"心脏彩色多普勒超声"(属于检查/诊疗项目)
- 非药品类计费信息错误地混入"已引用计费药品"列表
## 根因定位
**文件**: `openhis-ui-vue3/src/views/surgicalschedule/index.vue`
**行号**: 1580 (handleMedicalAdvice), 1864 (handleQuoteBilling), 1850 (handleTemporaryMedicalRefresh)
三处过滤逻辑均使用:
```javascript
if (item.adviceType !== 1) return false;
```
**问题1主因**: `adviceType` 字段命名兼容不完整。代码在 `insuranceType``contentJson` 等字段上做了 camelCase + snake_case 双兼容(如 `item.insuranceType || item.insurance_type`),但 `adviceType` 只检查了 camelCase。若后端返回 snake_case 数据(`advice_type``item.adviceType``undefined``undefined !== 1``true`,导致所有非药品项目全部放行。
**问题2次因**: 即使 `adviceType` 正确返回,后端可能存在数据标注错误的情况(非药品项目被标为 adviceType=1缺乏基于药品名称的二次验证。
## 修复方案
1. `adviceType` 检查增加 snake_case 回退:`const at = item.adviceType ?? item.advice_type; if (at !== 1) return false;`
2. 增加药品名称关键字二次过滤:排除名称中包含"术"、"检查"、"超声"、"多普勒"等关键词的非药品项目
## 验收标准
1. "已引用计费药品"列表中只显示药品类项目
2. 不显示手术诊疗项目(如"小腿烧伤扩创交腿皮瓣修复术"
3. 不显示检查项目(如"心脏彩色多普勒超声"
4. 药品名称正常显示

View File

@@ -132,22 +132,7 @@ temporaryAdvices.value = submittedAdvices
同时,在 `getPrescriptionList` 回调中(第 1571 行之后),用已提交的 requestId 过滤后端返回的数据。 同时,在 `getPrescriptionList` 回调中(第 1571 行之后),用已提交的 requestId 过滤后端返回的数据。
## 修复结果 ## 总结
### 实际根因 - **根因**`handleMedicalAdvice` 每次打开都清空 `temporaryAdvices`,然后从后端重新拉取数据。但后端返回的新创建医嘱项可能没有 `requestId`,导致无法过滤。
`handleQuoteBilling` 函数中: - **修复**:保留已提交(有 requestId的医嘱数据不清空同时用这些 requestId 过滤后端返回的新数据。
1. **第1856行**:在调用 `getPrescriptionList` 之前先清空了 `temporaryAdvices.value = []`
2. **第1997-2019行旧代码**ID 匹配过滤逻辑依赖已被清空的 `temporaryAdvices.value`,因此过滤形同虚设
3. 即使 `temporaryAdvices` 未被清空ID 匹配也不可靠(新生成的医嘱可能没有 `requestId`/`chargeItemId`/`id`
### 修复方案
1. 在清空 `temporaryAdvices` **之前**,提取已提交项目的复合键(名称+规格+数量)保存到 `submittedKeysBeforeClear`
2.`submittedKeysBeforeClear` 替换原有的 ID 匹配过滤逻辑,确保即使后端未返回 `requestId` 也能正确过滤
3. 复合键匹配策略与 `handleTemporaryMedicalSubmit` 中使用的策略一致
### 修改文件
- `openhis-ui-vue3/src/views/surgicalschedule/index.vue`
- 第1853-1864行新增 `submittedKeysBeforeClear` 提取逻辑
- 第1997-2004行替换 ID 匹配为复合键匹配
### 修复结果:✅ 成功,~20行改动+20/-21

View File

@@ -0,0 +1,18 @@
### 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() : ""; String activityName = activityDef != null ? activityDef.getName() : "";
List<OrganizationLocation> organizationLocationList = List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId()); organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(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;

View File

@@ -35,9 +35,6 @@ import com.openhis.medication.service.IMedicationDispenseService;
import com.openhis.medication.service.IMedicationRequestService; import com.openhis.medication.service.IMedicationRequestService;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper; import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService; import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
import com.openhis.document.service.IRequestFormService;
import com.openhis.clinical.service.ISurgeryService;
import com.openhis.clinical.domain.Surgery;
import com.openhis.web.doctorstation.appservice.IDoctorStationInspectionLabApplyService; import com.openhis.web.doctorstation.appservice.IDoctorStationInspectionLabApplyService;
import com.openhis.web.doctorstation.dto.*; import com.openhis.web.doctorstation.dto.*;
import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper; import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper;
@@ -54,7 +51,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.openhis.document.domain.RequestForm;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -73,7 +69,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
private static final Pattern INSPECTION_APPLY_NO_JSON = private static final Pattern INSPECTION_APPLY_NO_JSON =
Pattern.compile("\"applyNo\"\\s*:\\s*\"([^\"]+)\""); Pattern.compile("\"applyNo\"\\s*:\\s*\"([^\"]+)\"");
@Resource @Resource
AssignSeqUtil assignSeqUtil; AssignSeqUtil assignSeqUtil;
@@ -137,20 +132,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
@Lazy @Lazy
private IDoctorStationInspectionLabApplyService iDoctorStationInspectionLabApplyService; private IDoctorStationInspectionLabApplyService iDoctorStationInspectionLabApplyService;
/**
* 与RequestFormManageAppServiceImpl存在循环依赖需延迟注入删除手术医嘱时级联作废手术申请单。
*/
@Resource
@Lazy
private IRequestFormService iRequestFormService;
/**
* 删除手术医嘱时级联删除 cli_surgery 手术记录。
*/
@Resource
@Lazy
private ISurgeryService iSurgeryService;
// 缓存 key 前缀 // 缓存 key 前缀
private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:"; private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:";
// 缓存过期时间(小时) // 缓存过期时间(小时)
@@ -1769,7 +1750,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
return StringUtils.isBlank(applyNo) ? null : applyNo; return StringUtils.isBlank(applyNo) ? null : applyNo;
} }
/** /**
* 处理诊疗 * 处理诊疗
*/ */
@@ -1818,50 +1798,31 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
} }
} }
} }
// 🔧 级联作废:在删除 ServiceRequest 之前,先读取所有待删除记录的级联信息 // 检验申请单在医嘱 contentJson 中写入 applyNo从医嘱删除时需先级联作废检验单避免检验页签仍显示孤儿申请
// 检验申请单contentJson 中写入 applyNo手术申请单categoryEnum=24 + prescriptionNo
Map<String, List<Long>> labApplyNoToRequestIds = new LinkedHashMap<>(); Map<String, List<Long>> labApplyNoToRequestIds = new LinkedHashMap<>();
Map<String, List<Long>> surgeryPrescriptionNoToRequestIds = new LinkedHashMap<>();
// 收集待删除的 ServiceRequest先查询再删除避免级联逻辑因记录已删除而失效
Map<Long, ServiceRequest> serviceRequestCache = new LinkedHashMap<>();
for (AdviceSaveDto adviceSaveDto : deleteList) { for (AdviceSaveDto adviceSaveDto : deleteList) {
Long requestId = adviceSaveDto.getRequestId(); Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录 // 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的诊疗请求
if (requestId == null) { if (requestId == null) {
log.warn("BugFix#442: handService - 跳过 requestId 为 null 的删除请求"); log.warn("BugFix#442: handService - 跳过 requestId 为 null 的删除请求");
continue; continue;
} }
ServiceRequest existing = iServiceRequestService.getById(requestId); iServiceRequestService.removeById(requestId);// 删除诊疗
ServiceRequest existing = iServiceRequestService.getById(adviceSaveDto.getRequestId());
if (existing == null) { if (existing == null) {
continue; continue;
} }
serviceRequestCache.put(requestId, existing);
log.info("【调试】handService 待删除医嘱: requestId={}, categoryEnum={}, prescriptionNo={}",
requestId, existing.getCategoryEnum(), existing.getPrescriptionNo());
// 检验申请单级联
String applyNo = extractInspectionApplyNoFromContentJson(existing.getContentJson()); String applyNo = extractInspectionApplyNoFromContentJson(existing.getContentJson());
if (StringUtils.isNotBlank(applyNo)) { if (StringUtils.isNotBlank(applyNo)) {
labApplyNoToRequestIds.computeIfAbsent(applyNo, k -> new ArrayList<>()) labApplyNoToRequestIds.computeIfAbsent(applyNo, k -> new ArrayList<>())
.add(requestId); .add(adviceSaveDto.getRequestId());
}
// 手术申请单级联categoryEnum=24
log.info("【调试】handService 判断手术条件: categoryEnum={}, prescriptionNo={}, isSurgery={}",
existing.getCategoryEnum(), existing.getPrescriptionNo(),
existing.getCategoryEnum() != null && existing.getCategoryEnum() == 24 && StringUtils.isNotBlank(existing.getPrescriptionNo()));
if (existing.getCategoryEnum() != null
&& existing.getCategoryEnum() == 24
&& StringUtils.isNotBlank(existing.getPrescriptionNo())) {
surgeryPrescriptionNoToRequestIds.computeIfAbsent(existing.getPrescriptionNo(), k -> new ArrayList<>())
.add(requestId);
log.info("【调试】handService 加入手术级联列表: prescriptionNo={}", existing.getPrescriptionNo());
} }
} }
// 执行检验申请单级联作废 Set<Long> labCascadeSkippedRequestIds = new HashSet<>();
Set<Long> cascadeSkippedRequestIds = new HashSet<>();
for (Map.Entry<String, List<Long>> e : labApplyNoToRequestIds.entrySet()) { for (Map.Entry<String, List<Long>> e : labApplyNoToRequestIds.entrySet()) {
R<?> delLab = iDoctorStationInspectionLabApplyService.deleteInspectionLabApply(e.getKey()); R<?> delLab = iDoctorStationInspectionLabApplyService.deleteInspectionLabApply(e.getKey());
if (delLab != null && R.isSuccess(delLab)) { if (delLab != null && R.isSuccess(delLab)) {
cascadeSkippedRequestIds.addAll(e.getValue()); labCascadeSkippedRequestIds.addAll(e.getValue());
log.info("handService - 级联作废检验申请单 applyNo={},已跳过重复删除的医嘱 requestIds={}", log.info("handService - 级联作废检验申请单 applyNo={},已跳过重复删除的医嘱 requestIds={}",
e.getKey(), e.getValue()); e.getKey(), e.getValue());
} else { } else {
@@ -1870,41 +1831,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
e.getKey(), msg); e.getKey(), msg);
} }
} }
// 🔧 手术申请单级联作废:删除手术医嘱时同步作废关联的手术申请单
for (Map.Entry<String, List<Long>> e : surgeryPrescriptionNoToRequestIds.entrySet()) {
String prescriptionNo = e.getKey();
try {
List<RequestForm> requestForms = iRequestFormService.list(
new LambdaQueryWrapper<RequestForm>()
.eq(RequestForm::getPrescriptionNo, prescriptionNo)
.in(RequestForm::getTypeCode, ActivityDefCategory.PROCEDURE.getCode().toString(), "SURGERY")
.and(w -> w.isNull(RequestForm::getDeleteFlag).or().eq(RequestForm::getDeleteFlag, "0")));
log.info("【调试】handService 查询手术申请单: prescriptionNo={}, 查到{}条", prescriptionNo, requestForms != null ? requestForms.size() : 0);
if (requestForms != null && !requestForms.isEmpty()) {
for (RequestForm requestForm : requestForms) {
iRequestFormService.removeById(requestForm.getId());
}
// 同步删除 cli_surgery 手术记录prescriptionNo = surgeryNo
Surgery surgery = iSurgeryService.getOne(
new LambdaQueryWrapper<Surgery>()
.eq(Surgery::getSurgeryNo, prescriptionNo)
.and(w -> w.isNull(Surgery::getDeleteFlag).or().eq(Surgery::getDeleteFlag, "0")));
if (surgery != null) {
iSurgeryService.removeById(surgery.getId());
log.info("handService - 级联删除手术记录 cli_surgery: surgeryNo={}, id={}", prescriptionNo, surgery.getId());
}
cascadeSkippedRequestIds.addAll(e.getValue());
log.info("handService - 级联作废手术申请单 prescriptionNo={}", prescriptionNo);
} else {
log.info("handService - 未找到手术申请单 prescriptionNo={}", prescriptionNo);
}
} catch (Exception ex) {
log.warn("handService - 级联作废手术申请单失败 prescriptionNo={} msg={}", prescriptionNo, ex.getMessage());
}
}
// 级联作废完成后,统一删除 ServiceRequest 及其子项、费用项
for (AdviceSaveDto adviceSaveDto : deleteList) { for (AdviceSaveDto adviceSaveDto : deleteList) {
if (cascadeSkippedRequestIds.contains(adviceSaveDto.getRequestId())) { if (labCascadeSkippedRequestIds.contains(adviceSaveDto.getRequestId())) {
continue; continue;
} }
Long requestId = adviceSaveDto.getRequestId(); Long requestId = adviceSaveDto.getRequestId();
@@ -1914,6 +1842,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
requestId));// 删除诊疗套餐对应的子项 requestId));// 删除诊疗套餐对应的子项
// 🔧 Bug Fix #219: 删除费用项 // 🔧 Bug Fix #219: 删除费用项
String serviceTable = CommonConstants.TableName.WOR_SERVICE_REQUEST; String serviceTable = CommonConstants.TableName.WOR_SERVICE_REQUEST;
// 直接删除费用项
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
log.info("BugFix#219: 诊疗医嘱删除完成, requestId={}, serviceTable={}", requestId, serviceTable); log.info("BugFix#219: 诊疗医嘱删除完成, requestId={}, serviceTable={}", requestId, serviceTable);
} }
@@ -2552,17 +2481,21 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
} }
} }
// 使用 MyBatis Plus 分页查询 // 使用直接 LIMIT 查询,不触发 MyBatis Plus 的 COUNT 开销
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getSurgeryPage( List<SurgeryItemDto> records = doctorStationAdviceAppMapper.getSurgeryPage(
new Page<>(pageNo, pageSize), new Page<>(pageNo, pageSize),
PublicationStatus.ACTIVE.getValue(), PublicationStatus.ACTIVE.getValue(),
organizationId, organizationId,
searchKey); searchKey);
// 手动构造 Page 对象total 设为 records.size()(前端 el-transfer 不需要精确的 total 总数)
IPage<SurgeryItemDto> result = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNo, pageSize);
result.setRecords(records);
result.setTotal(records.size());
log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size()); log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size());
// 无搜索时将结果写入缓存 // 无搜索时将结果写入缓存
if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { if (useCache) {
redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS); redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS);
log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS); log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS);
} }

View File

@@ -195,7 +195,7 @@ public interface DoctorStationAdviceAppMapper {
* @param searchKey 模糊查询关键字(可选) * @param searchKey 模糊查询关键字(可选)
* @return 手术项目分页数据 * @return 手术项目分页数据
*/ */
IPage<SurgeryItemDto> getSurgeryPage(@Param("page") Page<SurgeryItemDto> page, List<SurgeryItemDto> getSurgeryPage(@Param("page") Page<SurgeryItemDto> page,
@Param("statusEnum") Integer statusEnum, @Param("statusEnum") Integer statusEnum,
@Param("organizationId") Long organizationId, @Param("organizationId") Long organizationId,
@Param("searchKey") String searchKey); @Param("searchKey") String searchKey);

View File

@@ -32,7 +32,6 @@ import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -203,70 +202,62 @@ public class ProductTransferAppServiceImpl implements IProductTransferAppService
@Override @Override
public R<?> addOrEditBatchTransferReceipt(List<ProductTransferDto> productTransferDtoList, Boolean flag) { public R<?> addOrEditBatchTransferReceipt(List<ProductTransferDto> productTransferDtoList, Boolean flag) {
// 校验调拨数量:必须 > 0 且不超过源库存数量(从数据库查实时库存)
Integer tenantId = SecurityUtils.getLoginUser().getTenantId(); Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
Date now = DateUtils.getNowDate(); for (ProductTransferDto dto : productTransferDtoList) {
if (dto.getItemQuantity() == null || dto.getItemQuantity().compareTo(java.math.BigDecimal.ZERO) <= 0) {
return R.fail("调拨数量必须大于0");
}
// 查询该药品在源仓库的实时库存总量
List<InventoryItem> inventoryList = inventoryItemService.selectInventoryByItemId(
dto.getItemId(), dto.getLotNumber(), dto.getSourceLocationId(), tenantId);
java.math.BigDecimal actualStock = inventoryList.stream()
.map(InventoryItem::getQuantity)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
if (dto.getItemQuantity().compareTo(actualStock) > 0) {
return R.fail("调拨数量不可超出源库存数量(当前库存:" + actualStock + "");
}
}
List<String> idList = new ArrayList<>(); List<String> idList = new ArrayList<>();
if (flag) { if (flag) {
// 批量保存按钮 // 批量保存按钮
// 单据号取得
List<String> busNoList = productTransferDtoList.stream().map(ProductTransferDto::getBusNo).toList(); List<String> busNoList = productTransferDtoList.stream().map(ProductTransferDto::getBusNo).toList();
// 保存前:获取旧记录用于恢复预划扣库存 // 请求id取得
List<SupplyRequest> oldRequestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0)); List<SupplyRequest> requestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0));
if (!oldRequestList.isEmpty()) { if (!requestList.isEmpty()) {
// 恢复旧记录已预划扣的库存 List<Long> requestIdList = requestList.stream().map(SupplyRequest::getId).collect(Collectors.toList());
for (SupplyRequest oldReq : oldRequestList) { // 单据信息删除
if (oldReq.getItemId() != null && oldReq.getLotNumber() != null supplyRequestService.removeByIds(requestIdList);
&& oldReq.getSourceLocationId() != null && oldReq.getItemQuantity() != null) {
List<InventoryItem> invList = inventoryItemService.selectInventoryByItemId(
oldReq.getItemId(), oldReq.getLotNumber(), oldReq.getSourceLocationId(), tenantId);
if (!invList.isEmpty()) {
inventoryItemService.updateInventoryQuantity(
invList.get(0).getId(),
invList.get(0).getQuantity().add(oldReq.getItemQuantity()), now);
}
}
}
List<Long> oldIdList = oldRequestList.stream().map(SupplyRequest::getId).collect(Collectors.toList());
supplyRequestService.removeByIds(oldIdList);
}
// 校验 + 预划扣新记录
for (ProductTransferDto dto : productTransferDtoList) {
if (dto.getItemQuantity() == null || dto.getItemQuantity().compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("调拨数量必须大于0");
}
List<InventoryItem> inventoryList = inventoryItemService.selectInventoryByItemId(
dto.getItemId(), dto.getLotNumber(), dto.getSourceLocationId(), tenantId);
BigDecimal actualStock = inventoryList.stream()
.map(InventoryItem::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (dto.getItemQuantity().compareTo(actualStock) > 0) {
return R.fail("调拨数量不可超出源库存数量(当前库存:" + actualStock + "");
}
// 预划扣源仓库库存
if (!inventoryList.isEmpty()) {
InventoryItem inv = inventoryList.get(0);
inventoryItemService.updateInventoryQuantity(
inv.getId(), inv.getQuantity().subtract(dto.getItemQuantity()), now);
}
} }
// 生成批量调拨单据 // 生成批量调拨单据
List<SupplyRequest> supplyRequestList = new ArrayList<>(); List<SupplyRequest> supplyRequestList = new ArrayList<>();
for (ProductTransferDto productTransferDto : productTransferDtoList) { for (ProductTransferDto productTransferDto : productTransferDtoList) {
// 初始化单据信息
SupplyRequest supplyRequest = new SupplyRequest(); SupplyRequest supplyRequest = new SupplyRequest();
BeanUtils.copyProperties(productTransferDto, supplyRequest); BeanUtils.copyProperties(productTransferDto, supplyRequest);
// 生成商品批量调拨单据
supplyRequest supplyRequest
// id
.setId(null) .setId(null)
// 单据分类:库存供应
.setCategoryEnum(SupplyCategory.STOCK_SUPPLY.getValue()) .setCategoryEnum(SupplyCategory.STOCK_SUPPLY.getValue())
// 单据类型:商品批量调拨
.setTypeEnum(SupplyType.PRODUCT_BATCH_TRANSFER.getValue()) .setTypeEnum(SupplyType.PRODUCT_BATCH_TRANSFER.getValue())
// 制单人
.setApplicantId(SecurityUtils.getLoginUser().getPractitionerId()) .setApplicantId(SecurityUtils.getLoginUser().getPractitionerId())
// 申请时间
.setApplyTime(DateUtils.getNowDate()) .setApplyTime(DateUtils.getNowDate())
// 源库存数量
.setTotalQuantity(productTransferDto.getTotalSourceQuantity()); .setTotalQuantity(productTransferDto.getTotalSourceQuantity());
supplyRequestList.add(supplyRequest); supplyRequestList.add(supplyRequest);
} }
supplyRequestService.saveOrUpdateBatch(supplyRequestList); supplyRequestService.saveOrUpdateBatch(supplyRequestList);
// 请求id取得
List<SupplyRequest> supplyRequestIdList = supplyRequestService.getSupplyByBusNo(busNoList.get(0)); List<SupplyRequest> supplyRequestIdList = supplyRequestService.getSupplyByBusNo(busNoList.get(0));
// 返回请求id列表
List<Long> requestIdList = supplyRequestIdList.stream().map(SupplyRequest::getId).toList(); List<Long> requestIdList = supplyRequestIdList.stream().map(SupplyRequest::getId).toList();
for (Long list : requestIdList) { for (Long list : requestIdList) {
idList.add(list.toString()); idList.add(list.toString());
@@ -274,58 +265,33 @@ public class ProductTransferAppServiceImpl implements IProductTransferAppService
} else { } else {
// 单独保存按钮 // 单独保存按钮
for (ProductTransferDto productTransferDto : productTransferDtoList) { for (ProductTransferDto productTransferDto : productTransferDtoList) {
// 更新已有记录:先恢复旧预划扣,再扣新的 // 初始化单据信息
if (productTransferDto.getId() != null) {
SupplyRequest oldReq = supplyRequestService.getById(productTransferDto.getId());
if (oldReq != null && oldReq.getItemId() != null && oldReq.getLotNumber() != null
&& oldReq.getSourceLocationId() != null && oldReq.getItemQuantity() != null) {
List<InventoryItem> invList = inventoryItemService.selectInventoryByItemId(
oldReq.getItemId(), oldReq.getLotNumber(), oldReq.getSourceLocationId(), tenantId);
if (!invList.isEmpty()) {
inventoryItemService.updateInventoryQuantity(
invList.get(0).getId(),
invList.get(0).getQuantity().add(oldReq.getItemQuantity()), now);
}
}
}
// 校验 + 预划扣
if (productTransferDto.getItemQuantity() == null
|| productTransferDto.getItemQuantity().compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("调拨数量必须大于0");
}
List<InventoryItem> inventoryList = inventoryItemService.selectInventoryByItemId(
productTransferDto.getItemId(), productTransferDto.getLotNumber(),
productTransferDto.getSourceLocationId(), tenantId);
BigDecimal actualStock = inventoryList.stream()
.map(InventoryItem::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (productTransferDto.getItemQuantity().compareTo(actualStock) > 0) {
return R.fail("调拨数量不可超出源库存数量(当前库存:" + actualStock + "");
}
if (!inventoryList.isEmpty()) {
InventoryItem inv = inventoryList.get(0);
inventoryItemService.updateInventoryQuantity(
inv.getId(), inv.getQuantity().subtract(productTransferDto.getItemQuantity()), now);
}
SupplyRequest supplyRequest = new SupplyRequest(); SupplyRequest supplyRequest = new SupplyRequest();
BeanUtils.copyProperties(productTransferDto, supplyRequest); BeanUtils.copyProperties(productTransferDto, supplyRequest);
supplyRequest.setTotalQuantity(productTransferDto.getTotalSourceQuantity()); supplyRequest.setTotalQuantity(productTransferDto.getTotalSourceQuantity());
if (productTransferDto.getId() != null) { if (productTransferDto.getId() != null) {
// 更新单据信息
supplyRequestService.updateById(supplyRequest); supplyRequestService.updateById(supplyRequest);
} else { } else {
// 生成商品批量调拨单据
supplyRequest supplyRequest
// 单据分类:库存供应
.setCategoryEnum(SupplyCategory.STOCK_SUPPLY.getValue()) .setCategoryEnum(SupplyCategory.STOCK_SUPPLY.getValue())
// 单据类型:商品批量调拨
.setTypeEnum(SupplyType.PRODUCT_BATCH_TRANSFER.getValue()) .setTypeEnum(SupplyType.PRODUCT_BATCH_TRANSFER.getValue())
// 制单人
.setApplicantId(SecurityUtils.getLoginUser().getPractitionerId()) .setApplicantId(SecurityUtils.getLoginUser().getPractitionerId())
// 申请时间
.setApplyTime(DateUtils.getNowDate()); .setApplyTime(DateUtils.getNowDate());
supplyRequestService.save(supplyRequest); supplyRequestService.save(supplyRequest);
} }
// 返回单据id
return R.ok(supplyRequest.getId().toString(), null); return R.ok(supplyRequest.getId().toString(), null);
} }
} }
// 返回单据id
return R.ok(idList, null); return R.ok(idList, null);
} }
@@ -366,63 +332,33 @@ public class ProductTransferAppServiceImpl implements IProductTransferAppService
@Override @Override
public R<?> addOrEditTransferReceipt(List<ProductTransferDto> productTransferDtoList) { public R<?> addOrEditTransferReceipt(List<ProductTransferDto> productTransferDtoList) {
// 校验调拨数量:必须 > 0 且不超过源库存数量(从数据库查实时库存)
Integer tenantId = SecurityUtils.getLoginUser().getTenantId(); Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
Date now = DateUtils.getNowDate(); for (ProductTransferDto dto : productTransferDtoList) {
if (dto.getItemQuantity() == null || dto.getItemQuantity().compareTo(java.math.BigDecimal.ZERO) <= 0) {
return R.fail("调拨数量必须大于0");
}
List<InventoryItem> inventoryList = inventoryItemService.selectInventoryByItemId(
dto.getItemId(), dto.getLotNumber(), dto.getSourceLocationId(), tenantId);
java.math.BigDecimal actualStock = inventoryList.stream()
.map(InventoryItem::getQuantity)
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
if (dto.getItemQuantity().compareTo(actualStock) > 0) {
return R.fail("调拨数量不可超出源库存数量(当前库存:" + actualStock + "");
}
}
List<String> idList = new ArrayList<>(); List<String> idList = new ArrayList<>();
// 单据号取得 // 单据号取得
List<String> busNoList = productTransferDtoList.stream().map(ProductTransferDto::getBusNo).toList(); List<String> busNoList = productTransferDtoList.stream().map(ProductTransferDto::getBusNo).toList();
// 保存前:获取旧记录用于恢复预划扣库存 // 请求数据取得
List<SupplyRequest> oldRequestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0)); List<SupplyRequest> requestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0));
Map<String, BigDecimal> oldDeductionMap = new HashMap<>(); if (!requestList.isEmpty()) {
if (!oldRequestList.isEmpty()) { // 请求id取得
for (SupplyRequest oldReq : oldRequestList) { List<Long> requestIdList = requestList.stream().map(SupplyRequest::getId).collect(Collectors.toList());
if (oldReq.getItemId() != null && oldReq.getLotNumber() != null && oldReq.getSourceLocationId() != null // 单据信息删除
&& oldReq.getItemQuantity() != null) { supplyRequestService.removeByIds(requestIdList);
String key = oldReq.getItemId() + "_" + oldReq.getLotNumber() + "_" + oldReq.getSourceLocationId();
oldDeductionMap.merge(key, oldReq.getItemQuantity(), BigDecimal::add);
}
}
// 恢复旧记录已预划扣的库存
for (Map.Entry<String, BigDecimal> entry : oldDeductionMap.entrySet()) {
String[] parts = entry.getKey().split("_");
Long itemId = Long.parseLong(parts[0]);
String lotNumber = parts[1];
Long sourceLocationId = Long.parseLong(parts[2]);
BigDecimal restoreQty = entry.getValue();
List<InventoryItem> invList = inventoryItemService.selectInventoryByItemId(
itemId, lotNumber, sourceLocationId, tenantId);
if (!invList.isEmpty()) {
inventoryItemService.updateInventoryQuantity(
invList.get(0).getId(), invList.get(0).getQuantity().add(restoreQty), now);
}
}
// 删除旧记录
List<Long> oldIdList = oldRequestList.stream().map(SupplyRequest::getId).collect(Collectors.toList());
supplyRequestService.removeByIds(oldIdList);
}
// 校验 + 预划扣新记录
Map<String, BigDecimal> newDeductionMap = new HashMap<>();
for (ProductTransferDto dto : productTransferDtoList) {
if (dto.getItemQuantity() == null || dto.getItemQuantity().compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("调拨数量必须大于0");
}
List<InventoryItem> inventoryList = inventoryItemService.selectInventoryByItemId(
dto.getItemId(), dto.getLotNumber(), dto.getSourceLocationId(), tenantId);
BigDecimal actualStock = inventoryList.stream()
.map(InventoryItem::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (dto.getItemQuantity().compareTo(actualStock) > 0) {
return R.fail("调拨数量不可超出源库存数量(当前库存:" + actualStock + "");
}
// 预划扣:扣减源仓库库存
if (!inventoryList.isEmpty()) {
InventoryItem inv = inventoryList.get(0);
inventoryItemService.updateInventoryQuantity(
inv.getId(), inv.getQuantity().subtract(dto.getItemQuantity()), now);
}
} }
List<SupplyRequest> supplyRequestList = new ArrayList<>(); List<SupplyRequest> supplyRequestList = new ArrayList<>();
@@ -469,22 +405,6 @@ public class ProductTransferAppServiceImpl implements IProductTransferAppService
*/ */
@Override @Override
public R<?> deleteReceipt(List<Long> supplyRequestIds) { public R<?> deleteReceipt(List<Long> supplyRequestIds) {
// 删除前恢复预划扣的库存
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
Date now = DateUtils.getNowDate();
for (Long reqId : supplyRequestIds) {
SupplyRequest sr = supplyRequestService.getById(reqId);
if (sr != null && sr.getItemId() != null && sr.getSourceLocationId() != null
&& sr.getItemQuantity() != null && sr.getLotNumber() != null) {
List<InventoryItem> invList = inventoryItemService.selectInventoryByItemId(
sr.getItemId(), sr.getLotNumber(), sr.getSourceLocationId(), tenantId);
if (!invList.isEmpty()) {
InventoryItem inv = invList.get(0);
inventoryItemService.updateInventoryQuantity(
inv.getId(), inv.getQuantity().add(sr.getItemQuantity()), now);
}
}
}
// 删除单据 // 删除单据
boolean result = supplyRequestService.removeByIds(supplyRequestIds); boolean result = supplyRequestService.removeByIds(supplyRequestIds);
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, null)) return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, null))

View File

@@ -519,8 +519,58 @@ public class ReceiptApprovalAppServiceImpl implements IReceiptApprovalAppService
// 暂时先取出全部的库存,循环查库存同一会有问题,后续优化 // 暂时先取出全部的库存,循环查库存同一会有问题,后续优化
List<InventoryItem> inventoryItems = inventoryItemService.selectAllInventory(); List<InventoryItem> inventoryItems = inventoryItemService.selectAllInventory();
for (SupplyItemDetailDto supplyItemDetailDto : supplyItemDetailList) { for (SupplyItemDetailDto supplyItemDetailDto : supplyItemDetailList) {
// 🔧 源仓库库存已在保存时预划扣,审批通过时不再重复扣减 // 根据项目id,产品批号源仓库id 查询源仓库库存表信息
outList.add(supplyItemDetailDto); // List<InventoryItem> inventoryItemSourceList = inventoryItemService.selectInventoryByItemId(
// supplyItemDetailDto.getItemId(), supplyItemDetailDto.getLotNumber(),
// supplyItemDetailDto.getSourceLocationId(), SecurityUtils.getLoginUser().getTenantId());
List<InventoryItem> filteredInventoryItems = inventoryItems.stream()
.filter(item -> item.getItemId().equals(supplyItemDetailDto.getItemId())
&& item.getLotNumber().equals(supplyItemDetailDto.getLotNumber())
&& item.getLocationId().equals(supplyItemDetailDto.getSourceLocationId()))
.collect(Collectors.toList());
InventoryItem inventoryItemSource = new InventoryItem();
if (!filteredInventoryItems.isEmpty()) {
inventoryItemSource = filteredInventoryItems.get(0);
// 最小数量(最小单位库存数量)
BigDecimal minQuantity = inventoryItemSource.getQuantity();
// // 计算调拨后库存数量,结果取小单位
// // 供应申请的物品计量单位与包装单位相同
// if (supplyItemDetailDto.getItemUnit().equals(supplyItemDetailDto.getUnitCode())) {
// if
// (minQuantity.compareTo(supplyItemDetailDto.getItemQuantity().multiply(supplyItemDetailDto.getPartPercent()))
// < 0) {
// // 库存数量不足
// throw new ServiceException("操作失败,库存数量不足");
// } else {
// // 源仓库库存-(调拨数量*拆零比)
// minQuantity = minQuantity.subtract(
// supplyItemDetailDto.getPartPercent().multiply(supplyItemDetailDto.getItemQuantity()));
// }
// } else if (supplyItemDetailDto.getItemUnit().equals(supplyItemDetailDto.getMinUnitCode())) {
// 直接扣减库存
if (minQuantity.compareTo(supplyItemDetailDto.getItemQuantity()) < 0) {
// 库存数量不足
throw new ServiceException("操作失败,库存数量不足");
} else {
// 供应申请的物品计量单位与最小单位相同
// 源仓库库存-调拨数量
minQuantity = minQuantity.subtract(supplyItemDetailDto.getItemQuantity());
}
// }
// 更新源仓库库存数量
Boolean aBoolean
= inventoryItemService.updateInventoryQuantity(inventoryItemSource.getId(), minQuantity, now);
if (!aBoolean) {
throw new ServiceException("系统异常,请稍后重试");
}
// 添加到出库列表
outList.add(supplyItemDetailDto);
} else {
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, null));
}
// 根据项目id,产品批号目的仓库id 查询目的仓库库存表信息 // 根据项目id,产品批号目的仓库id 查询目的仓库库存表信息
List<InventoryItem> inventoryItemPurposeList = inventoryItemService.selectInventoryByItemId( List<InventoryItem> inventoryItemPurposeList = inventoryItemService.selectInventoryByItemId(

View File

@@ -36,8 +36,6 @@ import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.service.IDeviceDispenseService; import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService; import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IServiceRequestService; import com.openhis.workflow.service.IServiceRequestService;
import com.openhis.document.domain.RequestForm;
import com.openhis.document.service.IRequestFormService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -87,9 +85,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
@Resource @Resource
IDeviceDispenseService iDeviceDispenseService; IDeviceDispenseService iDeviceDispenseService;
@Resource
IRequestFormService iRequestFormService;
/** /**
* 查询住院患者信息 * 查询住院患者信息
* *
@@ -271,38 +266,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
log.info("开始处理删除操作,共 {} 条记录", deleteList.size()); log.info("开始处理删除操作,共 {} 条记录", deleteList.size());
// 🔧 手术申请单级联作废:删除手术医嘱(categoryEnum=24)时同步作废关联的手术申请单
Map<String, List<Long>> surgeryPrescriptionNoToRequestIds = new LinkedHashMap<>();
for (RegAdviceSaveDto adviceDto : deleteList) {
if (adviceDto.getRequestId() == null) continue;
ServiceRequest existing = iServiceRequestService.getById(adviceDto.getRequestId());
if (existing == null) continue;
if (existing.getCategoryEnum() != null
&& existing.getCategoryEnum() == 24
&& existing.getPrescriptionNo() != null && !existing.getPrescriptionNo().isEmpty()) {
surgeryPrescriptionNoToRequestIds.computeIfAbsent(existing.getPrescriptionNo(), k -> new ArrayList<>())
.add(adviceDto.getRequestId());
}
}
for (Map.Entry<String, List<Long>> e : surgeryPrescriptionNoToRequestIds.entrySet()) {
String prescriptionNo = e.getKey();
try {
List<RequestForm> requestForms = iRequestFormService.list(
new LambdaQueryWrapper<RequestForm>()
.eq(RequestForm::getPrescriptionNo, prescriptionNo)
.eq(RequestForm::getTypeCode, ActivityDefCategory.PROCEDURE.getCode())
.and(w -> w.isNull(RequestForm::getDeleteFlag).or().eq(RequestForm::getDeleteFlag, "0")));
for (RequestForm requestForm : requestForms) {
iRequestFormService.removeById(requestForm.getId());
}
if (!requestForms.isEmpty()) {
log.info("级联作废手术申请单 prescriptionNo={}, 共{}条", prescriptionNo, requestForms.size());
}
} catch (Exception ex) {
log.warn("级联作废手术申请单失败 prescriptionNo={}", prescriptionNo, ex);
}
}
for (RegAdviceSaveDto adviceDto : deleteList) { for (RegAdviceSaveDto adviceDto : deleteList) {
Integer adviceType = adviceDto.getAdviceType(); Integer adviceType = adviceDto.getAdviceType();
Long requestId = adviceDto.getRequestId(); Long requestId = adviceDto.getRequestId();

View File

@@ -81,7 +81,16 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
Long requestFormId = requestFormSaveDto.getRequestFormId(); Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L; boolean isEdit = requestFormId != null && requestFormId != 0L;
// 🔧 手术/检查申请单优先使用前端传入的positionId用户手动选择的发往科室跳过执行科室配置校验 // 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室");
}
// 逐个校验activityList中的项目是否都配置了执行科室并收集positionId供后续使用
// 必须在任何数据库操作之前完成全部校验,避免部分保存后异常导致脏数据
// 🔧 Bug #516: 优先使用前端传入的positionId用户手动选择的发往科室仅在未选择时使用配置的执行科室
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList(); List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
// 缓存校验结果,避免主循环中重复查询和可能出现的数据不一致 // 缓存校验结果,避免主循环中重复查询和可能出现的数据不一致
java.util.Map<Long, Long> activityIdToPositionIdMap = new java.util.HashMap<>(); java.util.Map<Long, Long> activityIdToPositionIdMap = new java.util.HashMap<>();
@@ -93,15 +102,14 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), frontendPositionId); activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), frontendPositionId);
continue; continue;
} }
// 前端未传入时,查询配置的执行科室(暂不校验,仅用于兼容无前端传入的场景) // 前端未传入时,使用配置的执行科室
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
Long configPositionId = activityOrganizationConfig.stream() Long configPositionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId())) .filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null); .map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (configPositionId != null) { if (configPositionId == null) {
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), configPositionId); throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
} }
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), configPositionId);
} }
} }
@@ -168,77 +176,73 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
ChargeItem chargeItem; ChargeItem chargeItem;
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId); log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
// 🔧 手术申请单:跳过普通医嘱生成,只由 isProcedure 块生成手术医嘱,避免重复 for (ActivitySaveDto activitySaveDto : activityList) {
boolean isSurgeryRequest = ActivityDefCategory.PROCEDURE.getCode().equals(typeCode); serviceRequest = new ServiceRequest();
if (!isSurgeryRequest) { serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());
for (ActivitySaveDto activitySaveDto : activityList) { serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
serviceRequest = new ServiceRequest(); serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue()); serviceRequest.setPrescriptionNo(prescriptionNo);
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); serviceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());// 治疗类型
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 serviceRequest.setQuantity(activitySaveDto.getQuantity()); // 请求数量
serviceRequest.setPrescriptionNo(prescriptionNo); serviceRequest.setUnitCode(activitySaveDto.getUnitCode()); // 请求单位编码
serviceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());// 治疗类型 serviceRequest.setCategoryEnum(categoryEnum); // 请求类型
serviceRequest.setQuantity(activitySaveDto.getQuantity()); // 请求数量 serviceRequest.setActivityId(activitySaveDto.getAdviceDefinitionId());// 诊疗定义id
serviceRequest.setUnitCode(activitySaveDto.getUnitCode()); // 请求单位编码 serviceRequest.setPatientId(patientId); // 患者
serviceRequest.setCategoryEnum(categoryEnum); // 请求类型 serviceRequest.setRequesterId(practitionerId); // 开方医生
serviceRequest.setActivityId(activitySaveDto.getAdviceDefinitionId());// 诊疗定义id serviceRequest.setEncounterId(encounterId); // 诊id
serviceRequest.setPatientId(patientId); // 患者 serviceRequest.setAuthoredTime(curDate); // 请求签发时间
serviceRequest.setRequesterId(practitionerId); // 开方医生
serviceRequest.setEncounterId(encounterId); // 就诊id
serviceRequest.setAuthoredTime(curDate); // 请求签发时间
Long positionId = activityIdToPositionIdMap.get(activitySaveDto.getAdviceDefinitionId()); Long positionId = activityIdToPositionIdMap.get(activitySaveDto.getAdviceDefinitionId());
if (positionId == null) { if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室"); throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
} }
serviceRequest.setOrgId(positionId); // 执行科室 serviceRequest.setOrgId(positionId); // 执行科室
serviceRequest.setYbClassEnum(activitySaveDto.getYbClassEnum());// 类别医保编码 serviceRequest.setYbClassEnum(activitySaveDto.getYbClassEnum());// 类别医保编码
serviceRequest.setConditionId(activitySaveDto.getConditionId()); // 诊断id serviceRequest.setConditionId(activitySaveDto.getConditionId()); // 诊断id
serviceRequest.setEncounterDiagnosisId(activitySaveDto.getEncounterDiagnosisId()); // 就诊诊断id serviceRequest.setEncounterDiagnosisId(activitySaveDto.getEncounterDiagnosisId()); // 就诊诊断id
iServiceRequestService.save(serviceRequest); iServiceRequestService.save(serviceRequest);
chargeItem = new ChargeItem(); chargeItem = new ChargeItem();
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态 chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(serviceRequest.getBusNo())); chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(serviceRequest.getBusNo()));
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
chargeItem.setPatientId(patientId); // 患者 chargeItem.setPatientId(patientId); // 患者
chargeItem.setContextEnum(activitySaveDto.getAdviceType()); // 类型 chargeItem.setContextEnum(activitySaveDto.getAdviceType()); // 类型
chargeItem.setEncounterId(encounterId); // 就诊id chargeItem.setEncounterId(encounterId); // 就诊id
chargeItem.setDefinitionId(activitySaveDto.getDefinitionId()); // 费用定价ID chargeItem.setDefinitionId(activitySaveDto.getDefinitionId()); // 费用定价ID
chargeItem.setDefDetailId(activitySaveDto.getDefinitionDetailId()); // 定价子表主键 chargeItem.setDefDetailId(activitySaveDto.getDefinitionDetailId()); // 定价子表主键
chargeItem.setEntererId(practitionerId);// 开立人ID chargeItem.setEntererId(practitionerId);// 开立人ID
chargeItem.setEnteredDate(curDate); // 开立时间 chargeItem.setEnteredDate(curDate); // 开立时间
chargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);// 医疗服务类型 chargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);// 医疗服务类型
chargeItem.setServiceId(serviceRequest.getId()); // 医疗服务ID chargeItem.setServiceId(serviceRequest.getId()); // 医疗服务ID
chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);// 产品所在表 chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);// 产品所在表
chargeItem.setProductId(activitySaveDto.getAdviceDefinitionId());// 收费项id chargeItem.setProductId(activitySaveDto.getAdviceDefinitionId());// 收费项id
chargeItem.setAccountId(activitySaveDto.getAccountId());// 关联账户ID chargeItem.setAccountId(activitySaveDto.getAccountId());// 关联账户ID
chargeItem.setRequestingOrgId(orgId); // 开立科室 chargeItem.setRequestingOrgId(orgId); // 开立科室
chargeItem.setConditionId(activitySaveDto.getConditionId()); // 诊断id chargeItem.setConditionId(activitySaveDto.getConditionId()); // 诊断id
chargeItem.setEncounterDiagnosisId(activitySaveDto.getEncounterDiagnosisId()); // 就诊诊断id chargeItem.setEncounterDiagnosisId(activitySaveDto.getEncounterDiagnosisId()); // 就诊诊断id
chargeItem.setQuantityValue(activitySaveDto.getQuantity()); // 数量 chargeItem.setQuantityValue(activitySaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(activitySaveDto.getUnitCode()); // 单位 chargeItem.setQuantityUnit(activitySaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(activitySaveDto.getUnitPrice()); // 单价 chargeItem.setUnitPrice(activitySaveDto.getUnitPrice()); // 单价
chargeItem.setTotalPrice(activitySaveDto.getTotalPrice()); // 总价 chargeItem.setTotalPrice(activitySaveDto.getTotalPrice()); // 总价
iChargeItemService.save(chargeItem); iChargeItemService.save(chargeItem);
// 处理诊疗套餐的子项信息 // 处理诊疗套餐的子项信息
ActivityDefinition activityDefinition = ActivityDefinition activityDefinition =
iActivityDefinitionService.getById(activitySaveDto.getAdviceDefinitionId()); iActivityDefinitionService.getById(activitySaveDto.getAdviceDefinitionId());
String childrenJson = activityDefinition.getChildrenJson(); String childrenJson = activityDefinition.getChildrenJson();
if (childrenJson != null) { if (childrenJson != null) {
// 诊疗子项参数类 // 诊疗子项参数类
ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams(); ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams();
activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); // 治疗类型 activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); // 治疗类型
activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者 activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者
activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊id activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊id
activityChildrenJsonParams.setAccountId(chargeItem.getAccountId()); // 账户id activityChildrenJsonParams.setAccountId(chargeItem.getAccountId()); // 账户id
activityChildrenJsonParams.setChargeItemId(chargeItem.getId()); // 费用项id activityChildrenJsonParams.setChargeItemId(chargeItem.getId()); // 费用项id
activityChildrenJsonParams.setParentId(serviceRequest.getId());// 子项诊疗的父id activityChildrenJsonParams.setParentId(serviceRequest.getId());// 子项诊疗的父id
activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId()); activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId());
adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams);
}
} }
} }
@@ -322,13 +326,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
} else if (surgeryName != null && !surgeryName.isEmpty()) { } else if (surgeryName != null && !surgeryName.isEmpty()) {
contentMap.put("surgeryName", surgeryName); contentMap.put("surgeryName", surgeryName);
} }
// 🔧 手术申请单级联删除contentJson 中记录 requestFormId 和 prescriptionNo删除医嘱时可定位并作废申请单
if (requestForm.getId() != null) {
contentMap.put("requestFormId", String.valueOf(requestForm.getId()));
}
if (prescriptionNo != null && !prescriptionNo.isEmpty()) {
contentMap.put("prescriptionNo", prescriptionNo);
}
if (surgeryCode != null && !surgeryCode.isEmpty()) { if (surgeryCode != null && !surgeryCode.isEmpty()) {
contentMap.put("surgeryCode", surgeryCode); contentMap.put("surgeryCode", surgeryCode);
} }
@@ -371,10 +368,9 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
surgeryChargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST); surgeryChargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);
surgeryChargeItem.setServiceId(surgeryServiceRequest.getId()); surgeryChargeItem.setServiceId(surgeryServiceRequest.getId());
surgeryChargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION); surgeryChargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
// 优先从 activityList 获取 productId 和 definitionId // 优先从 activityList 获取 productId
if (activityList != null && !activityList.isEmpty()) { if (activityList != null && !activityList.isEmpty()) {
surgeryChargeItem.setProductId(activityList.get(0).getAdviceDefinitionId()); surgeryChargeItem.setProductId(activityList.get(0).getAdviceDefinitionId());
surgeryChargeItem.setDefinitionId(activityList.get(0).getDefinitionId());
surgeryChargeItem.setAccountId(activityList.get(0).getAccountId()); surgeryChargeItem.setAccountId(activityList.get(0).getAccountId());
} }
surgeryChargeItem.setRequestingOrgId(orgId); surgeryChargeItem.setRequestingOrgId(orgId);
@@ -413,10 +409,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
anesthesiaChargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST); anesthesiaChargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);
anesthesiaChargeItem.setServiceId(surgeryServiceRequest.getId()); anesthesiaChargeItem.setServiceId(surgeryServiceRequest.getId());
anesthesiaChargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION); anesthesiaChargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
// 从 activityList 获取 definitionId
if (activityList != null && !activityList.isEmpty()) {
anesthesiaChargeItem.setDefinitionId(activityList.get(0).getDefinitionId());
}
anesthesiaChargeItem.setRequestingOrgId(orgId); anesthesiaChargeItem.setRequestingOrgId(orgId);
anesthesiaChargeItem.setQuantityValue(BigDecimal.valueOf(1)); anesthesiaChargeItem.setQuantityValue(BigDecimal.valueOf(1));
anesthesiaChargeItem.setQuantityUnit(""); anesthesiaChargeItem.setQuantityUnit("");
@@ -433,18 +425,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
log.error("生成手术医嘱过程中发生异常", e); log.error("生成手术医嘱过程中发生异常", e);
throw e; throw e;
} }
// 将手术项目名称写入申请单name字段确保医嘱删除后申请单仍保留正确名称
if (activityList != null && !activityList.isEmpty()) {
String surgeryDisplayName = activityList.stream()
.map(ActivitySaveDto::getAdviceDefinitionName)
.filter(name -> name != null && !name.isEmpty())
.distinct()
.collect(Collectors.joining(""));
if (!surgeryDisplayName.isEmpty()) {
requestForm.setName(surgeryDisplayName);
iRequestFormService.updateById(requestForm);
}
}
} else { } else {
log.info("不是手术申请单跳过手术医嘱生成typeCode={}", typeCode); log.info("不是手术申请单跳过手术医嘱生成typeCode={}", typeCode);
} }

View File

@@ -117,7 +117,7 @@
) )
</if> </if>
<if test="searchKey != null and searchKey != ''"> <if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || '${searchKey}' || '%' OR t1.py_str ILIKE '%' || '${searchKey}' || '%') AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if> </if>
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()"> <if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
AND t1.id IN AND t1.id IN
@@ -181,7 +181,7 @@
WHERE t1.delete_flag = '0' WHERE t1.delete_flag = '0'
AND t1.status_enum = #{statusEnum} AND t1.status_enum = #{statusEnum}
<if test="searchKey != null and searchKey != ''"> <if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || '${searchKey}' || '%' OR t1.py_str ILIKE '%' || '${searchKey}' || '%') AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if> </if>
<if test="categoryCode != null and categoryCode != ''"> <if test="categoryCode != null and categoryCode != ''">
AND t1.category_code = #{categoryCode} AND t1.category_code = #{categoryCode}
@@ -278,7 +278,7 @@
AND T1.category_code != '手术' AND T1.category_code != '24' AND T1.category_code != '手术' AND T1.category_code != '24'
</if> </if>
<if test="searchKey != null and searchKey != ''"> <if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || '${searchKey}' || '%' OR t1.py_str ILIKE '%' || '${searchKey}' || '%') AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if> </if>
<if test="categoryCode != null and categoryCode != ''"> <if test="categoryCode != null and categoryCode != ''">
AND t1.category_code = #{categoryCode} AND t1.category_code = #{categoryCode}
@@ -893,6 +893,7 @@
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%') AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if> </if>
ORDER BY t1.ID, t1.name ASC, t2.ID ASC ORDER BY t1.ID, t1.name ASC, t2.ID ASC
LIMIT #{page.size} OFFSET ${(page.current - 1) * page.size}
</select> </select>
<!-- 检查项目专用分页查询:仅查检查(23) + 定价,无库存/草稿库存/取药科室等无关逻辑 --> <!-- 检查项目专用分页查询:仅查检查(23) + 定价,无库存/草稿库存/取药科室等无关逻辑 -->

View File

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

View File

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

View File

@@ -461,10 +461,6 @@ watch(
console.log(prescriptionList.value,"prescriptionList.value") console.log(prescriptionList.value,"prescriptionList.value")
if(newValue&&newValue.length>0){ if(newValue&&newValue.length>0){
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
// 手术计费场景generateSourceEnum=6不限制 bizRequestFlag
if (isSurgeryChargeBillingContext()) {
return item.check && item.statusEnum == 1
}
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}) })
console.log(saveList,"prescriptionList.value") console.log(saveList,"prescriptionList.value")
@@ -1029,9 +1025,7 @@ function changeCheck(value,index,row){
groupList.value.map(k=>{ groupList.value.map(k=>{
if(k.check){ if(k.check){
if(k.statusEnum == 1){//待签发 if(k.statusEnum == 1){//待签发
// 手术计费场景generateSourceEnum=6不限制 bizRequestFlag if(Number(k.bizRequestFlag)==1||!k.bizRequestFlag){
const bizAllowed = isSurgeryChargeBillingContext() || Number(k.bizRequestFlag)==1||!k.bizRequestFlag
if(bizAllowed){
if(handleSaveDisabled.value&&!handleSingOutDisabled.value&&groupList.value.length>1){ if(handleSaveDisabled.value&&!handleSingOutDisabled.value&&groupList.value.length>1){
proxy.$modal.msgWarning('请选择相同的状态的项目进行操作') proxy.$modal.msgWarning('请选择相同的状态的项目进行操作')
return return
@@ -1046,9 +1040,7 @@ function changeCheck(value,index,row){
} }
} }
if(k.statusEnum == 2){ //已签发 if(k.statusEnum == 2){ //已签发
// 手术计费场景generateSourceEnum=6不限制 bizRequestFlag if(Number(k.bizRequestFlag)==1||!k.bizRequestFlag){
const bizAllowed = isSurgeryChargeBillingContext() || Number(k.bizRequestFlag)==1||!k.bizRequestFlag
if(bizAllowed){
if(!handleSaveDisabled.value&&handleSingOutDisabled.value&&groupList.value.length>1){ if(!handleSaveDisabled.value&&handleSingOutDisabled.value&&groupList.value.length>1){
proxy.$modal.msgWarning('请选择相同的状态的项目进行操作') proxy.$modal.msgWarning('请选择相同的状态的项目进行操作')
return return
@@ -1075,28 +1067,28 @@ function handleSave() {
return; return;
} }
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
// 手术计费场景generateSourceEnum=6不限制 bizRequestFlag允许任何授权用户签发
// 门诊划价场景保留 bizRequestFlag 限制,只能操作本人开立的医嘱
if (isSurgeryChargeBillingContext()) {
return item.check && item.statusEnum == 1
}
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}); });
// 无可签发项目时提前返回,避免后端报"医嘱列表为空" // let saveList = prescriptionList.value
if (saveList.length == 0) { // .filter((item) => {
proxy.$modal.msgWarning('当前无可签发处方'); // return item.check;
return; // }).filter((item) => {
} // return item.statusEnum == 1&&item.bizRequestFlag==1
// })
// if (saveList.length == 0) {
// proxy.$modal.msgWarning('当前无可签发处方');
// return;
// }
// 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可 // 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可
let list = saveList.map((item) => { let list = saveList.map((item) => {
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) : {}; const parsedContent = item.contentJson ? JSON.parse(item.contentJson) : {};
return { return {
...parsedContent, ...parsedContent,
requestId: item.requestId, requestId: item.requestId,
// 已有 requestId 的记录走 UPDATE 路径,新记录走 INSERT 路径 dbOpType: '1',
dbOpType: item.requestId ? '2' : '1',
groupId: item.groupId, groupId: item.groupId,
// 补充顶层关键字段(这些可能不在 contentJson 中,需从 API 响应顶层提取) // 🔧 Bug #443: 补充顶层关键字段(这些不在 contentJson 中,需从 API 响应顶层提取)
encounterId: item.encounterId, encounterId: item.encounterId,
patientId: item.patientId, patientId: item.patientId,
locationId: item.positionId, locationId: item.positionId,
@@ -1104,14 +1096,9 @@ function handleSave() {
adviceTableName: item.adviceTableName, adviceTableName: item.adviceTableName,
adviceDefinitionId: item.adviceDefinitionId, adviceDefinitionId: item.adviceDefinitionId,
chargeItemId: item.chargeItemId, chargeItemId: item.chargeItemId,
// 补充数量、单位、批号等字段(后端 handDevice 需要这些字段) // 🔧 Bug Fix: 签发时显式设置手术计费关键字段,避免后端 prescription_no / generateSourceEnum 回退为默认值导致查询无法匹配
quantity: item.quantity, generateSourceEnum: props.generateSourceEnum ?? parsedContent.generateSourceEnum,
unitCode: item.unitCode, sourceBillNo: props.sourceBillNo ?? parsedContent.sourceBillNo,
lotNumber: item.lotNumber,
categoryEnum: item.categoryEnum,
// 签发时显式设置手术计费关键字段,后端 generateSourceEnum 回退为默认值导致查询无法匹配
generateSourceEnum: props.generateSourceEnum ?? parsedContent.generateSourceEnum ?? item.generateSourceEnum,
sourceBillNo: props.sourceBillNo ?? parsedContent.sourceBillNo ?? item.sourceBillNo,
}; };
}); });
// 确保 organizationId 不为 undefined手术计费场景下可能缺失 orgId // 确保 organizationId 不为 undefined手术计费场景下可能缺失 orgId
@@ -1198,10 +1185,6 @@ function handleSingOut() {
return item.check; return item.check;
}) })
.filter((item) => { .filter((item) => {
// 手术计费场景generateSourceEnum=6不限制 bizRequestFlag
if (isSurgeryChargeBillingContext()) {
return item.statusEnum == 2 && item.chargeStatus != 5
}
return item.statusEnum == 2 && item.chargeStatus != 5 && (Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.statusEnum == 2 && item.chargeStatus != 5 && (Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}) })
.map((item) => { .map((item) => {

View File

@@ -883,6 +883,8 @@ form.value.diagnosisList.push({
onsetDate: getCurrentDate(), onsetDate: getCurrentDate(),
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name, diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: getCurrentDate(), diagnosisTime: getCurrentDate(),
iptDiseTypeCode: 2,
longTermFlag: 0, // 默认非长效诊断
selectedDiseases: data.ybNo ? [data.ybNo] : [], // 用于传染病报告卡自动勾选 selectedDiseases: data.ybNo ? [data.ybNo] : [], // 用于传染病报告卡自动勾选
}); });

View File

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

View File

@@ -511,13 +511,6 @@ const hasMatchedFields = computed(() => {
return Object.keys(descJsonData.value).some((key) => isFieldMatched(key)); return Object.keys(descJsonData.value).some((key) => isFieldMatched(key));
}); });
// Ordered field keys for detail display and print, matching the bug requirement order
const orderedDescFieldKeys = [
'targetDepartment', 'urgencyLevel', 'allergyHistory', 'examinationPurpose',
'expectedExaminationTime', 'medicalHistorySummary', 'symptom', 'sign',
'clinicalDiagnosis', 'otherDiagnosis', 'relatedResult', 'attention',
];
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = async () => { const getLocationInfo = async () => {
try { try {
@@ -685,20 +678,18 @@ const handlePrint = async (row) => {
}); });
} }
// 构建 descJson 字段行(与详情弹窗展示的字段一致) // 构建 descJson 字段行(与详情弹窗展示的字段一致遍历所有key并通过isFieldMatched过滤
const fieldKeys = orderedDescFieldKeys;
let descFieldsHtml = ''; let descFieldsHtml = '';
fieldKeys.forEach((key) => { for (const key in descData) {
if (!(key in labelMap)) continue;
const label = labelMap[key] || key; const label = labelMap[key] || key;
const value = transformField(key, descData[key]); const value = transformField(key, descData[key]);
if (descData[key] != null && descData[key] !== '' && value != null && value !== '') { descFieldsHtml += `
descFieldsHtml += `
<div class="info-row"> <div class="info-row">
<span class="label">${label}</span> <span class="label">${label}</span>
<span class="value">${value}</span> <span class="value">${value || '-'}</span>
</div>`; </div>`;
} }
});
// 构建完整打印HTML // 构建完整打印HTML
const printContent = ` const printContent = `

View File

@@ -116,7 +116,7 @@ import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh} from '@element-plus/icons-vue'; import {Refresh} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js'; import {patientInfo} from '../../store/patient.js';
import {getSurgery} from './api'; import {getSurgery} from './api';
import {getDepartmentList} from '@/api/public.js'; import {getOrgList} from '@/views/doctorstation/components/api.js';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -182,32 +182,25 @@ const hasMatchedFields = computed(() => {
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = async () => { const getLocationInfo = async () => {
const res = await getDepartmentList(); const res = await getOrgList();
orgOptions.value = res.data || []; orgOptions.value = res.data.records;
}; };
const recursionFun = (targetDepartment) => { const recursionFun = (targetDepartment) => {
if (!targetDepartment || !orgOptions.value || orgOptions.value.length === 0) {
return '';
}
let name = ''; let name = '';
// 统一处理:扁平列表和树形结构都适用 for (let index = 0; index < orgOptions.value.length; index++) {
const findInList = (list) => { const obj = orgOptions.value[index];
for (const node of list) { if (obj.id == targetDepartment) {
if (String(node.id) === String(targetDepartment)) { name = obj.name;
name = node.name; }
return true; const subObjArray = obj['children'];
} for (let index = 0; index < subObjArray.length; index++) {
// 树形结构:递归查找 children const item = subObjArray[index];
if (node.children && node.children.length > 0) { if (item.id == targetDepartment) {
if (findInList(node.children)) { name = item.name;
return true;
}
} }
} }
return false; }
};
findInList(orgOptions.value);
return name; return name;
}; };

View File

@@ -232,21 +232,64 @@ onMounted(() => {
* type(1watch监听类型 2:点击保存类型) * type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组) * selectProjectIds(选中项目的id数组)
* */ * */
const projectWithDepartment = (selectProjectIds) => { const projectWithDepartment = (selectProjectIds, type) => {
if (!selectProjectIds || selectProjectIds.length === 0) { //1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置
form.targetDepartment = ''; let isRelease = true;
// 选中项目的数组
const arr = [];
// 根据选中的项目id查找对应的项目
selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => {
return element == item.adviceDefinitionId;
});
arr.push(searchData);
});
// 清空科室
form.targetDepartment = '';
if (arr.length > 0) {
const obj = arr[0];
// 判断科室是否相同
const isCompare = arr.every((item) => {
return item.orgId == obj.orgId;
});
if (!isCompare) {
ElMessage({
type: 'error',
message: '执行科室不同',
});
isRelease = false;
}
// 选中项目中的执行科室id与全部科室数据做匹配
const findItem = findTreeItem(orgOptions.value, obj.orgId);
if (!findItem) {
isRelease = false;
ElMessage({
type: 'error',
message: '未找到项目执行的科室',
});
}
if (type == 1) {
if (isRelease) {
form.targetDepartment = findItem.id;
}
}
} }
return isRelease;
}; };
// 监听选择项目变化 // 监听选择项目变化
watch(() => transferValue.value, (newValue) => { watch(
projectWithDepartment(newValue); () => transferValue.value,
}); (newValue) => {
projectWithDepartment(newValue, 1);
}
);
const submit = () => { const submit = () => {
if (transferValue.value.length == 0) { if (transferValue.value.length == 0) {
return proxy.$message.error('请选择手术项目'); return proxy.$message.error('请选择申请单');
} }
if (!form.targetDepartment) { if (!projectWithDepartment(transferValue.value, 2)) {
return proxy.$message.error('请选择发往科室'); return;
} }
let applicationListAllFilter = applicationListAll.value.filter((item) => { let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId); return transferValue.value.includes(item.adviceDefinitionId);
@@ -259,7 +302,7 @@ const submit = () => {
unitCode: item.unitCode, unitCode: item.unitCode,
unitPrice: item.price, unitPrice: item.price,
totalPrice: item.price, totalPrice: item.price,
positionId: form.targetDepartment || item.positionId, // 用户手动选择的发往科室优先于项目默认执行科室 positionId: item.positionId,
definitionId: item.chargeItemDefinitionId, definitionId: item.chargeItemDefinitionId,
accountId: patientInfo.value.accountId, accountId: patientInfo.value.accountId,
}; };

View File

@@ -1195,7 +1195,7 @@ function handleSave() {
// 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可 // 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可
loading.value = true; loading.value = true;
let list = saveList.map((item) => { let list = saveList.map((item) => {
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) : {}; const parsedContent = JSON.parse(item.contentJson);
return { return {
...parsedContent, ...parsedContent,
adviceType: item.adviceType, adviceType: item.adviceType,

View File

@@ -342,13 +342,15 @@ const dialogVisible = computed({
// 使用 drord_doctor_type 字典 // 使用 drord_doctor_type 字典
const adviceTypeList = computed(() => { const adviceTypeList = computed(() => {
if (drord_doctor_type.value && drord_doctor_type.value.length > 0) { if (drord_doctor_type.value && drord_doctor_type.value.length > 0) {
// 只保留耗材(4)和诊疗(3)类型,并添加全部选项 // 只保留耗材(2)和诊疗(3)类型,并添加全部选项
// 注意后端SQL只认 adviceType=2(耗材) 和 3(诊疗)字典值4需映射为2
const filtered = drord_doctor_type.value.filter(item => { const filtered = drord_doctor_type.value.filter(item => {
const val = parseInt(item.value); const val = parseInt(item.value);
return val === 3 || val === 4; return val === 2 || val === 3 || val === 4;
}).map(item => ({ }).map(item => ({
label: item.label, label: item.label,
value: parseInt(item.value) // 后端SQL只有adviceTypes.contains(2)查询耗材字典值4映射为2
value: parseInt(item.value) === 4 ? 2 : parseInt(item.value)
})); }));
return [...filtered, { label: '全部', value: '' }]; return [...filtered, { label: '全部', value: '' }];
} }
@@ -373,8 +375,7 @@ const filterKeywords = ref({});
const queryParams = ref({ const queryParams = ref({
pageSize: 100, pageSize: 100,
pageNum: 1, pageNum: 1,
// 默认加载全部类型药品1+耗材2+诊疗3 adviceTypes: '2,3',
adviceTypes: [1, 2, 3],
}); });
/** /**
* 医嘱提交数据模型 * 医嘱提交数据模型
@@ -556,26 +557,12 @@ function loadDepartmentOptions() {
function getAdviceBaseInfos() { function getAdviceBaseInfos() {
adviceLoading.value = true; adviceLoading.value = true;
queryParams.value.searchKey = searchText.value; queryParams.value.searchKey = searchText.value;
// 字典值(3=诊疗,4=耗材)映射为后端adviceType(2=耗材,3=诊疗) queryParams.value.adviceTypes = adviceType.value;
if (adviceType.value === 4) {
queryParams.value.adviceTypes = [2];
} else if (adviceType.value === 3) {
queryParams.value.adviceTypes = [3];
} else {
queryParams.value.adviceTypes = [1, 2, 3];
}
queryParams.value.organizationId = orgId.value; queryParams.value.organizationId = orgId.value;
queryParams.value.pricingFlag = 1; // 划价标记 queryParams.value.pricingFlag = 1; // 划价标记
getAdviceBaseInfo(queryParams.value) getAdviceBaseInfo(queryParams.value)
.then((res) => { .then((res) => {
const list = res.data?.records || []; AdviceBaseInfoList.value = res.data?.records || [];
// 药品(1)和耗材(2)必须有库存才能展示,诊疗(3)无库存概念不过滤
AdviceBaseInfoList.value = list.filter(item => {
if (item.adviceType === 1 || item.adviceType === 2) {
return item.inventoryList && item.inventoryList.length > 0;
}
return true;
});
}) })
.finally(() => { .finally(() => {
adviceLoading.value = false; adviceLoading.value = false;

View File

@@ -386,7 +386,7 @@
:disabled="viewStatus == 'view'" :disabled="viewStatus == 'view'"
v-model="scope.row.itemQuantityDisplay" v-model="scope.row.itemQuantityDisplay"
placeholder="" placeholder=""
@input="(value) => handleItemQuantityChange(scope.row, value)" @change="(value) => handleItemQuantityChange(scope.row, value)"
:class="{ 'error-border': scope.row.error }" :class="{ 'error-border': scope.row.error }"
/> />
</div> </div>
@@ -971,10 +971,6 @@ function handleItemQuantityChange(row, value) {
quantityTemp = value; quantityTemp = value;
} }
row.totalPrice = ((row.price * quantityTemp) / row.partPercent).toFixed(2); row.totalPrice = ((row.price * quantityTemp) / row.partPercent).toFixed(2);
// 数量变更后重置保存标记,允许重新提交
row.isSave = false;
// 同步更新底部合计金额
handleTotalAmount();
} }
function handelApply() { function handelApply() {
@@ -1266,108 +1262,128 @@ function editBatchTransfer(index) {
} }
function handleSave(row, index) { function handleSave(row, index) {
// 过滤出未保存的行,已保存的行不重复提交 rowList.value = [];
const listToCheck = route.query.supplyBusNo if (route.query.supplyBusNo) {
? forms.purchaseinventoryList // 编辑
: form.purchaseinventoryList; forms.purchaseinventoryList.map((row, index) => {
const unsavedList = listToCheck.filter(item => !item.isSave); if (row) {
if (unsavedList.length === 0) { proxy.$refs['receiptHeaderRef'].validate((valid) => {
proxy.$modal.msgWarning('所有行均已保存,无需重复提交'); if (valid) {
return; proxy.$refs['formRef'].validate((valid) => {
if (valid) {
if (row.unitCode == row.unitList.minUnitCode) {
row.itemQuantity = forms.purchaseinventoryList[index].olditemQuantity
? forms.purchaseinventoryList[index].olditemQuantity
: forms.purchaseinventoryList[index].itemQuantity;
} else {
row.itemQuantity = forms.purchaseinventoryList[index].itemMaxQuantity
? forms.purchaseinventoryList[index].itemMaxQuantity
: forms.purchaseinventoryList[index].itemQuantity;
}
// let rows = JSON.parse(JSON.stringify(row))
// delete rows.itemMaxQuantity
if (row.unitCode == row.unitCode_dictText) {
if (row.unitCode_dictText == row.unitList.minUnitCode_dictText) {
row.unitCode = row.unitList.minUnitCode;
} else {
row.unitCode = row.unitList.unitCode;
row.unitCode_dictText = row.unitList.unitCode_dictText;
}
}
if (row.unitCode == row.unitList.unitCode) {
row.unitCode_dictText = row.unitList.unitCode_dictText;
} else if (row.unitCode == row.unitList.minUnitCode) {
row.unitCode_dictText = row.unitList.minUnitCode_dictText;
}
if (!forms.purchaseinventoryList[index].price || forms.purchaseinventoryList[index].price <= 0) {
proxy.$message.warning('调拨单价不能为空或为0请检查');
return;
}
forms.purchaseinventoryList[index].totalPrice =
forms.purchaseinventoryList[index].price * forms.purchaseinventoryList[index].itemQuantity;
rowList.value.push(JSON.parse(JSON.stringify(row)));
if (
rowList._rawValue &&
rowList._rawValue.length == forms.purchaseinventoryList.length
) {
addTransferProducts(rowList._rawValue);
}
}
});
}
});
}
});
} else {
//新增
form.purchaseinventoryList.map((row, index) => {
if (row) {
proxy.$refs['receiptHeaderRef'].validate((valid) => {
if (valid) {
proxy.$refs['formRef'].validate((valid) => {
if (valid) {
let rows = JSON.parse(JSON.stringify(row));
delete rows.itemMaxQuantity;
if (rows.unitCode == rows.unitList.minUnitCode) {
rows.itemQuantity = form.purchaseinventoryList[index].olditemQuantity
? form.purchaseinventoryList[index].olditemQuantity
: form.purchaseinventoryList[index].itemQuantity;
} else {
rows.itemQuantity = form.purchaseinventoryList[index].itemMaxQuantity
? form.purchaseinventoryList[index].itemMaxQuantity
: form.purchaseinventoryList[index].itemQuantity;
}
if (rows.unitCode == rows.unitCode_dictText) {
if (rows.unitCode_dictText == rows.unitList.minUnitCode_dictText) {
rows.unitCode = rows.unitList.minUnitCode;
} else {
rows.unitCode = rows.unitList.unitCode;
rows.unitCode_dictText = rows.unitList.unitCode_dictText;
}
}
if (rows.unitCode == rows.unitList.unitCode) {
rows.unitCode_dictText = rows.unitList.unitCode_dictText;
} else if (rows.unitCode == rows.unitList.minUnitCode) {
rows.unitCode_dictText = rows.unitList.minUnitCode_dictText;
}
if (!form.purchaseinventoryList[index].price || form.purchaseinventoryList[index].price <= 0) {
proxy.$message.warning('调拨单价不能为空或为0请检查');
return;
}
form.purchaseinventoryList[index].totalPrice =
form.purchaseinventoryList[index].price * form.purchaseinventoryList[index].itemQuantity;
rowList.value.push(JSON.parse(JSON.stringify(rows)));
if (
rowList._rawValue &&
rowList._rawValue.length == form.purchaseinventoryList.length
) {
addTransferProducts(rowList._rawValue);
}
}
});
}
});
}
});
} }
// 先校验表头
proxy.$refs['receiptHeaderRef'].validate((headerValid) => {
if (!headerValid) return;
// 逐行校验(避免异步回调导致重复提交)
const rowsToSave = [];
for (let i = 0; i < form.purchaseinventoryList.length; i++) {
const r = form.purchaseinventoryList[i];
if (!r) continue;
// 跳过已保存的行,避免重复提交导致预扣减库存叠加
if (r.isSave) continue;
// 校验当前行的必填字段
let rowValid = true;
for (const prop of ['name', 'unitCode']) {
const formRef = proxy.$refs['formRef'];
if (formRef && formRef.validateField) {
formRef.validateField(`purchaseinventoryList.${i}.${prop}`, (valid) => {
if (valid) rowValid = false;
});
}
}
if (!rowValid) {
proxy.$modal.msgWarning('第' + (i + 1) + '行数据不完整,请检查');
return;
}
// 单价校验
if (!r.price || r.price <= 0) {
proxy.$modal.msgWarning('第' + (i + 1) + '行调拨单价不能为空或为0');
return;
}
// 单位处理
const rowData = route.query.supplyBusNo
? JSON.parse(JSON.stringify(forms.purchaseinventoryList[i]))
: JSON.parse(JSON.stringify(r));
delete rowData.itemMaxQuantity;
if (rowData.unitCode == rowData.unitList?.minUnitCode) {
rowData.itemQuantity = r.olditemQuantity || r.itemQuantity;
} else {
rowData.itemQuantity = r.itemMaxQuantity || r.itemQuantity;
}
if (rowData.unitCode == rowData.unitCode_dictText) {
if (rowData.unitCode_dictText == rowData.unitList?.minUnitCode_dictText) {
rowData.unitCode = rowData.unitList.minUnitCode;
} else {
rowData.unitCode = rowData.unitList.unitCode;
rowData.unitCode_dictText = rowData.unitList.unitCode_dictText;
}
}
if (rowData.unitCode == rowData.unitList?.unitCode) {
rowData.unitCode_dictText = rowData.unitList.unitCode_dictText;
} else if (rowData.unitCode == rowData.unitList?.minUnitCode) {
rowData.unitCode_dictText = rowData.unitList.minUnitCode_dictText;
}
// 计算总价
r.totalPrice = r.price * rowData.itemQuantity;
rowsToSave.push(rowData);
}
// 所有行校验通过,一次性提交
if (rowsToSave.length > 0) {
addTransferProducts(rowsToSave);
}
});
} }
function addTransferProducts(rowList) { function addTransferProducts(rowList) {
addTransferProduct(JSON.parse(JSON.stringify(rowList))).then((res) => { addTransferProduct(JSON.parse(JSON.stringify(rowList))).then((res) => {
// 当前行没有id视为首次新增
// if (!row.id) {
// data.isAdding = false; // 允许新增下一行
// }
if (res.data) { if (res.data) {
proxy.$message.success('保存成功!'); proxy.$message.success('保存成功!');
let newIdIndex = 0;
form.purchaseinventoryList.map((row, index) => { form.purchaseinventoryList.map((row, index) => {
// 只有未保存的行才会拿到新 id和提交顺序一致 form.purchaseinventoryList[index].id = res.data[index];
if (!row.isSave && res.data[newIdIndex]) {
form.purchaseinventoryList[index].id = res.data[newIdIndex];
newIdIndex++;
}
form.purchaseinventoryList[index].isSave = true; form.purchaseinventoryList[index].isSave = true;
}); });
if (route.query.supplyBusNo) { if (route.query.supplyBusNo) {
// 编辑 // 编辑
let newIdIdx = 0;
forms.purchaseinventoryList.map((row, index) => { forms.purchaseinventoryList.map((row, index) => {
if (!row.isSave && res.data[newIdIdx]) { forms.purchaseinventoryList[index].id = res.data[index];
forms.purchaseinventoryList[index].id = res.data[newIdIdx];
newIdIdx++;
}
forms.purchaseinventoryList[index].isSave = true; forms.purchaseinventoryList[index].isSave = true;
}); });
} }

View File

@@ -1141,13 +1141,17 @@ const {
const pendingAnesData = ref(null) const pendingAnesData = ref(null)
// 监听麻醉字典加载,完成后立即设置表单值 // 监听麻醉字典加载,完成后立即设置表单值
// Bug #433: watch 回调中优先判断字典是否已加载,未加载时跳过(不处理也不清理)
// 确保字典加载完成时 watch 仍然活跃并能处理 pendingAnesData
let anesDataUnwatch = null let anesDataUnwatch = null
function setupAnesDataWatch() { function setupAnesDataWatch() {
if (anesDataUnwatch) return // 防止重复设置 if (anesDataUnwatch) return // 防止重复设置
anesDataUnwatch = watch( anesDataUnwatch = watch(
anesthesiaList, anesthesiaList,
(newList) => { (newList) => {
if (newList && newList.length > 0 && pendingAnesData.value) { // Bug #433: 字典未加载时跳过,不清理 watch等待下次触发
if (!newList || newList.length === 0) return
if (pendingAnesData.value) {
const data = pendingAnesData.value const data = pendingAnesData.value
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod) if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel) if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1346,7 +1350,8 @@ function handleEdit(row) {
if (res.code === 200) { if (res.code === 200) {
const data = res.data const data = res.data
Object.assign(form, data) Object.assign(form, data)
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成 // Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
if (anesthesiaList.value && anesthesiaList.value.length > 0) { if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod) if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel) if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1356,6 +1361,8 @@ function handleEdit(row) {
pendingAnesData.value = data pendingAnesData.value = data
setupAnesDataWatch() setupAnesDataWatch()
} }
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else { } else {
proxy.$modal.msgError('获取手术安排详情失败') proxy.$modal.msgError('获取手术安排详情失败')
} }
@@ -1375,7 +1382,8 @@ function handleView(row) {
if (res.code === 200) { if (res.code === 200) {
const data = res.data const data = res.data
Object.assign(form, data) Object.assign(form, data)
// Bug #433: 如果字典已加载则立即转换否则存入pending等待字典加载完成 // Bug #433: 先存 pending 再调 watch 函数,确保 watch immediate 回调能看到 pendingAnesData
// 修复时序问题watch({ immediate: true }) 同步触发时 pendingAnesData.value 已被设置
if (anesthesiaList.value && anesthesiaList.value.length > 0) { if (anesthesiaList.value && anesthesiaList.value.length > 0) {
if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod) if (data.anesMethod != null) form.anesMethod = Number(data.anesMethod)
if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel) if (data.incisionLevel != null) form.incisionType = Number(data.incisionLevel)
@@ -1385,6 +1393,8 @@ function handleView(row) {
pendingAnesData.value = data pendingAnesData.value = data
setupAnesDataWatch() setupAnesDataWatch()
} }
// Bug #433: 显式赋值确保响应式更新
if (data.externalExpertName != null) form.externalExpertName = data.externalExpertName
} else { } else {
proxy.$modal.msgError('获取手术安排详情失败') proxy.$modal.msgError('获取手术安排详情失败')
} }
@@ -1565,7 +1575,7 @@ function handleMedicalAdvice(row) {
temporaryMedicalLoading.value = true // 🔧 新增:开始加载 temporaryMedicalLoading.value = true // 🔧 新增:开始加载
// 调用计费接口获取数据 // 调用计费接口获取数据
getPrescriptionList(row.visitId, 6, row.operCode).then((res) => { getPrescriptionList(row.visitId).then((res) => {
console.log('=== 拉取计费数据返回结果 ===', res) console.log('=== 拉取计费数据返回结果 ===', res)
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目 // 🔧 修复:显示所有药品请求数据,不管有没有计费项目
@@ -1576,18 +1586,12 @@ function handleMedicalAdvice(row) {
const filteredItems = res.data.filter(item => { const filteredItems = res.data.filter(item => {
// 匹配 encounterId // 匹配 encounterId
if (item.encounterId !== row.visitId) return false; if (item.encounterId !== row.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6) // 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
// 🔧 修复 Bug #444: 使用 Number() 显式转换,避免字符串 "1" 被 !== 1 放行 if (item.adviceType !== 1) return false;
const at = Number(item.adviceType ?? item.advice_type);
if (at !== 1) return false;
// 过滤掉名称为空的项目 // 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name; const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false; if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #444: 二次过滤,排除名称中包含手术/检查/诊疗关键词的非药品项目 // 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表)
// 某些计费项目可能在 adm_charge_item 中被错误标注为 adviceType=1
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表中)
if (item.requestId) return false; if (item.requestId) return false;
// 根据药品请求ID去重避免重复显示 // 根据药品请求ID去重避免重复显示
const itemId = item.requestId || item.id; const itemId = item.requestId || item.id;
@@ -1708,14 +1712,9 @@ function handleMedicalAdvice(row) {
usageLabel: usageLabel, // 🔧 新增:保存显示名称 usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '临时', frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'), executeTime: new Date().toLocaleString('zh-CN'),
// 🔧 关键修复:确保 originalMedicine 中包含 encounterId 和匹配字段 // 🔧 关键修复:确保 originalMedicine 中包含 encounterId,以便后续判断是否为同一患者
// medicineName/specification/quantity 用于 handleTemporaryMedicalSubmit 中的
// 已提交项目匹配过滤Bug #445缺少这些字段会导致过滤失效
originalMedicine: { originalMedicine: {
...medicine, ...medicine,
medicineName: medicine.medicineName,
specification: medicine.specification,
quantity: medicine.quantity,
encounterId: row.visitId // 添加 encounterId 字段 encounterId: row.visitId // 添加 encounterId 字段
} }
} }
@@ -1974,13 +1973,9 @@ function handleQuoteBilling() {
usageLabel: usageLabel, // 🔧 新增:保存显示名称 usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '临时', frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'), executeTime: new Date().toLocaleString('zh-CN'),
// 🔧 关键修复:确保 originalMedicine 中包含 encounterId 和匹配字段 // 🔧 关键修复:确保 originalMedicine 中包含 encounterId,以便后续判断是否为同一患者
// medicineName/specification/quantity 用于已提交项目匹配过滤Bug #445
originalMedicine: { originalMedicine: {
...medicine, ...medicine,
medicineName: medicine.medicineName,
specification: medicine.specification,
quantity: medicine.quantity,
encounterId: temporaryPatientInfo.value.visitId // 添加 encounterId 字段 encounterId: temporaryPatientInfo.value.visitId // 添加 encounterId 字段
} }
} }