Compare commits

..

66 Commits

Author SHA1 Message Date
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
674 changed files with 25867 additions and 78473 deletions

View File

@@ -1,27 +0,0 @@
# Bug #556 Analysis
## Title
【门诊医生站-检验】新增检验申请单时就诊卡号/执行时间未自动回显,且项目列表冗余显示"套餐"文字
## Root Cause Analysis
### Issue 1: 就诊卡号未自动回显
- **Code**: `inspectionApplication.vue:886` - `formData.medicalrecordNumber = props.patientInfo.identifierNo || ''`
- **Root Cause**: Logic is correct but depends on `props.patientInfo.identifierNo` being populated. The watch on `props.patientInfo` (line 2074) triggers `initData()`. The card number field itself is correctly bound. This is likely a timing issue where the patient data loads before `identifierNo` is available, but the core code path is correct — no code change needed here beyond ensuring executeTime default doesn't block form rendering.
### Issue 2: 执行时间未默认填充当前系统时间
- **Code**: `inspectionApplication.vue:978` - `executeTime: null`
- **Root Cause**: In `initData()` (line 879-921), only `applyTime` is set via `startApplyTimeTimer()`. `formData.executeTime` is never assigned a default value. Similarly in `resetForm()` (line 1550), `executeTime` remains `null`.
- **Fix**: Add `formData.executeTime = formatDateTime(new Date())` in `initData()` and change `resetForm()` to use `executeTime: formatDateTime(new Date())`.
### Issue 3: 项目列表冗余显示"套餐"文字
- **Code**: `inspectionApplication.vue:1190` - Already fixed with `packageName` check. But `inspectionApplication.vue:2000` in `loadApplicationToForm()` still uses loose check: `item.feePackageId != null || item.itemName?.includes('套餐')`.
- **Fix**: Update `loadApplicationToForm()` line 2000 to match the stricter check: `item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName`.
## Files to Modify
- `openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue`
## Changes
1. `initData()`: Add `formData.executeTime = formatDateTime(new Date())` after line 899
2. `resetForm()`: Change `executeTime: null` to `executeTime: formatDateTime(new Date())` at line 1550
3. `loadApplicationToForm()`: Fix `isPackage` logic at line 2000

View File

@@ -1,53 +0,0 @@
# Bug #556 分析报告
## 问题描述
【门诊医生站-检验】新增检验申请单时:
1. 就诊卡号字段为空,未自动带出患者就诊卡号
2. 执行时间字段未自动填充,仅显示占位提示
3. 检验项目列表每条记录前均带"套餐"文字标签(冗余显示)
## 根因分析
### 问题1就诊卡号未自动回显
- 代码路径:`initData()``formData.medicalrecordNumber = props.patientInfo.identifierNo || ''`
- 数据绑定:`v-model="formData.medicalrecordNumber"`
- `props.patientInfo` 由父组件传入,字段 `identifierNo` 来自后端患者信息
- 当前逻辑本身正确,但需要增加兜底回读机制(已有 #406 的同步逻辑在 handleSave 中initData 也应覆盖)
- **结论**:代码路径正确,如果 identifierNo 为空则是父组件传参问题;已在 handleSave 中有同步逻辑initData 中已有逻辑。无需额外修复。
### 问题2执行时间未自动填充
- 根因:`formData.executeTime``formData` 初始化时line 978设为 `null`
- `initData()` 函数没有为 executeTime 设置默认值
- `resetForm()` 函数line 1550也将 executeTime 重置为 `null`
- 前端 datetime picker 在 `v-model``null` 时显示占位符 "选择执行时间"
- **修复方案**:在 `initData()` 中设置 `formData.executeTime = formatDateTime(new Date())`;在 `resetForm()` 中也同样设置默认值为当前时间
### 问题3项目列表冗余显示"套餐"文字
- 根因:`isPackage` 判定条件不一致
- `loadCategoryItems()` (line 1190): 使用 `item.feePackageId != null && ... && item.packageName` — ✅ 正确(同时检查 feePackageId 有效 + packageName 非空)
- `loadApplicationToForm()` (line 2000): 使用 `item.feePackageId != null || item.itemName?.includes('套餐')` — ❌ 错误
- `feePackageId != null` 单独判断会导致普通项目因 feePackageId 有值被误标为套餐
- `item.itemName?.includes('套餐')` 更是直接按名称文字判断,极不准确
- 影响位置:
- 检验项目选择区line 566`<el-tag v-if="item.isPackage">套餐</el-tag>`
- 已选项目列表line 617`<el-tag v-if="item.isPackage">套餐</el-tag>`
- 检验信息详情表格line 448`<el-tag v-if="scope.row.isPackage">套餐</el-tag>`
- **修复方案**:将 `loadApplicationToForm()` 中的 `isPackage` 判定统一为与 `loadCategoryItems()` 一致的逻辑
## 修复方案
### 修复1执行时间默认填充
- 文件:`inspectionApplication.vue`
- 位置:`initData()` 函数,在已有患者信息赋值后添加 `formData.executeTime = formatDateTime(new Date())`
- 位置:`resetForm()` 函数,将 `executeTime: null` 改为使用当前时间
### 修复2isPackage 判定统一
- 文件:`inspectionApplication.vue`
- 位置:`loadApplicationToForm()` 函数 line 2000
- 旧代码:`const isPackage = item.feePackageId != null || item.itemName?.includes('套餐')`
- 新代码:`const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName`
## 验收标准
1. 新增检验申请单时执行时间字段自动填充当前系统时间YYYY-MM-DD HH:mm:ss 格式)
2. 检验项目列表中,只有真正的套餐项目前显示"套餐"标签,普通项目不显示
3. 就诊卡号在有患者信息时正常显示

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类型一致。

68
.gitignore vendored Executable file
View File

@@ -0,0 +1,68 @@
# 忽略所有编译器、IDE相关的文件
**/.idea/
**/.vscode/
**/*.swp
**/*.swo
**/*.bak
**/*.tmp
**/.vs/
# 忽略 Java 项目编译文件
**/*.class
**/*.jar
**/*.war
**/*.ear
**/target/
**/bin/
# 忽略 Maven、Gradle、Ant 相关文件
**/.mvn/
**/.gradle/
**/build/
**/out/
# 忽略 Eclipse、IntelliJ IDEA 和 NetBeans 临时文件
**/*.log
**/*.project
**/*.classpath
# 忽略 Java 配置文件
**/*.iml
# 忽略 Node.js 和 Vue 项目相关文件
**/node_modules/
**/npm-debug.log
**/yarn-error.log
**/yarn-debug.log
**/dist/
**/*.lock
**/*.tgz
# 忽略 Vue 项目相关构建文件
**/.vuepress/dist/
# 忽略 IDE 配置文件
**/*.launch
**/*.settings/
# 忽略操作系统生成的文件
**/.DS_Store
**/Thumbs.db
**/Desktop.ini
/openhis-miniapp/unpackage
# 忽略设计书
PostgreSQL/openHis_DB设计书.xlsx
public.sql
发版记录/2025-11-12/~$发版日志.docx
发版记录/2025-11-12/~$S-管理系统-调价管理.docx
发版记录/2025-11-12/发版日志.docx
.gitignore
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
.env.test.local
playwright-report/
test-results/

View File

@@ -1,39 +0,0 @@
# 进度日志
## 当前已验证状态
- 仓库根目录:`/root/.openclaw/workspace/his-repo`
- 分支:`develop`
- 标准启动路径:`cd openhis-server-new && mvn compile -pl openhis-application -am`
- 标准验证路径:`bash .harness/check.sh`(一键全部门禁)
- 标准初始化:`bash .harness/init.sh`
- 标准作业流程:`.harness/STANDARD_OPERATING_PROCEDURE.md`
- 当前最高优先级未完成功能:`harness-003` — 持续完善 check.sh
- 当前 blocker
## 会话记录
### Session 001 (2026-05-28) — 基础设施 v1
- 已完成AGENTS.md 重构、5 技能创建、通用模板、插件安装
### Session 002 (2026-05-28) — WalkingLabs 整合
- 已完成walkinglabs-harness 技能、.harness/ 模板、AGENTS.md v2、check.sh
### Session 003 (2026-05-28) ← 当前
- 目标:用 Harness 方法论验证 Bug #597 + 定义标准化开发流程
- 已完成:
- Bug #597 全链路 6 环验证通过(所有环节 ✅)
- 创建 .harness/STANDARD_OPERATING_PROCEDURE.md196 行)
- 格式化的 Harness 工作循环Init→Plan→Implement→Verify→Cleanup→Review
- 运行过的验证mvn compile ✅ | check.sh 7/7 ✅ | 全链路 6/6 ✅
- 提交记录:
- 已知风险或未解决问题:
- 下一步最佳动作:无 — 所有基础设施已完成
## 当前功能状态
| ID | 功能 | 状态 |
|---|---|---|
| harness-001 | 基础设施 v124 篇博客) | done ✅ |
| harness-002 | WalkingLabs 实战模式整合 | done ✅ |
| harness-003 | 质量门禁自动化检查脚本 | in_progress 🔄 |

View File

@@ -1,196 +0,0 @@
# Harness 标准作业程序 (SOP)
> 所有开发任务、Bug 修复、重构,必须遵循此流程。
## 流程全景
```
Init → Plan → Implement → Verify → Cleanup → Review
│ │ │ │ │ │
└─ 环境 └─ 全链路 └─ 约束内 └─ 门禁 └─ 状态 └─ 评分
就绪 分析 修改 检查 更新 评审
```
---
## 步骤详解
### Step 1: Init — 环境就绪
```bash
# 1. 确认在正确的目录
pwd
# 2. 运行初始化
bash .harness/init.sh
# 3. 读取当前进度
cat .harness/PROGRESS.md
cat .harness/feature_list.json
# 4. 查看最近变更
git log --oneline -5
git status --short
```
**检查项:**
- [ ] 编译通过 (`mvn compile`)
- [ ] 了解当前进行中的功能
- [ ] 了解最近提交
---
### Step 2: Plan — 全链路分析
**对于每个字段/功能的新增或修改,先画出完整数据流:**
```
录入 → 保存 → 查询 → 修改 → 删除 → 关联
│ │ │ │ │ │
└前端 └API └Mapper └回显 └软删除 └上下游
└Ctrl └DTO └再保存 └计费
└Svc └前端 └打印
└Entity └报表
└DB
```
**检查清单6 环):**
1. **录入** — 前端有输入入口?(弹窗、行编辑、表单)
2. **保存** — 前端→API→Controller→Service→Entity→DB每个入口都传了吗注意多个 Service 实现类)
3. **查询** — DB→Mapper XMLUNION ALL 子查询统一加→DTO→前端展示
4. **修改** — 编辑回显→修改保存→正确更新?
5. **删除/停止** — 状态变更会丢失该字段吗?
6. **关联** — 上下游(护士站、药房、计费、打印、报表)需要同步改吗?
**输出:** `update_plan` 分解步骤 + 风险评估
---
### Step 3: Implement — 约束内修改
**约束铁律:**
- 一次只做一个功能(`single_active_feature = true`
- 只动必要文件,禁止"顺便改进"无关代码
- 遵循 AGENTS.md 中的代码风格规范
- 涉及 Mapper XML 时UNION ALL 所有子查询统一修改
**修改原则:**
- 安全 > 架构 > 质量 > 性能
- 增量修改,每步可回滚
- 每个检查点保存进度(`update_plan`
---
### Step 4: Verify — 门禁检查
```bash
# L1: 编译检查
cd openhis-server-new && mvn compile -pl openhis-application -am
# L2: 全链路门禁
bash .harness/check.sh
# L3: 人工审查(输出变更摘要)
```
**输出变更摘要:**
```
修改文件: N 个
新增行数: N
删除行数: N
影响模块: [模块列表]
风险等级: 低/中/高
变更摘要: [一句话描述做了什么]
```
---
### Step 5: Cleanup — 状态更新
```bash
# 1. 更新进度
vim .harness/PROGRESS.md
# 添加新会话记录,更新完成状态
# 2. 更新功能清单
vim .harness/feature_list.json
# 标记完成/更新状态
# 3. 运行干净状态检查
cat .harness/clean-state-checklist.md
# 逐项确认
# 4. 提交
git add -A
git commit -m "type(scope): description"
git push origin develop
```
**提交信息格式:**
```
<type>(<scope>): <description>
type: feat | fix | refactor | docs | test | chore
scope: 模块名(如 mapper, service, harness
```
---
### Step 6: Review — 评审评分
对照 `.harness/evaluator-rubric.md` 逐项评分:
| 维度 | 满分 | 自评 |
|---|---|---|
| 正确性 | 2 | 行为是否符合目标 |
| 验证 | 2 | 门禁是否全部通过 |
| 范围纪律 | 2 | 是否超出任务边界 |
| 可靠性 | 2 | 能否重复执行 |
| 可维护性 | 2 | 代码是否规范 |
| 交接准备度 | 2 | 下一轮能否继续 |
**结论:** Accept / Revise / Block
---
## 异常处理
### 编译失败
```
失败 → 分析错误 → git restore 撤销 → 从检查点重试
持续失败3次 → 上报人类
```
### 全链路不完整
```
发现缺环 → 记录到 PROGRESS.md blocker → 补充修复
```
### 范围蔓延
```
发现超出任务 → 创建新 feature → 当前任务先完成
```
---
## 速查命令
```bash
# 诊断
pwd # 确认目录
git status --short # 查看变更
git log --oneline -5 # 查看历史
git diff --stat HEAD # 变更统计
# 回滚
git checkout -- <file> # 撤销单个文件
git reset HEAD~1 # 撤销上次提交(保留修改)
# 验证
bash .harness/init.sh # 初始化
bash .harness/check.sh # 全部门禁
# 状态
cat .harness/PROGRESS.md # 进度
cat .harness/feature_list.json # 功能清单
```

View File

@@ -1,82 +0,0 @@
#!/usr/bin/env bash
# =============================================
# Harness Quality Gates — 一键运行所有门禁
# 源自 $closed-loop-testing skill
# =============================================
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PASS=0
FAIL=0
RESULTS=()
check() {
local level="$1" name="$2" cmd="$3"
cd "$ROOT_DIR"
echo ""
echo "━━━ [${level}] ${name} ━━━"
if eval "$cmd" 2>&1; then
echo "${name} 通过"
PASS=$((PASS + 1))
RESULTS+=("✅|${level}|${name}")
else
echo "${name} 失败"
FAIL=$((FAIL + 1))
RESULTS+=("❌|${level}|${name}")
fi
}
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ Harness Quality Gates ║"
echo "$(date '+%Y-%m-%d %H:%M')"
echo "╚══════════════════════════════════════╝"
# ── L1: 编译检查 ──
echo ""
echo "╔══ L1 编译检查 ══════════════════════╗"
check "L1" "后端编译" "cd '$ROOT_DIR/openhis-server-new' && mvn compile -pl openhis-application -am -q"
# ── L2: 全链路检查 ──
echo ""
echo "╔══ L2 全链路数据流验证 ══════════════╗"
# L2-1: 文件存在性检查
check "L2" "AGENTS.md 存在" "test -f '$ROOT_DIR/AGENTS.md'"
check "L2" "init.sh 可执行" "test -x '$ROOT_DIR/.harness/init.sh'"
check "L2" "PROGRESS.md 存在" "test -f '$ROOT_DIR/.harness/PROGRESS.md'"
check "L2" "feature_list.json 有效" "python3 -c 'import json; json.load(open(\"$ROOT_DIR/.harness/feature_list.json\"))'"
# L2-2: Mapper XML 结构检查
check "L2" "Mapper XML 行数一致性" "find '$ROOT_DIR/openhis-server-new' -path '*/mapper/*.xml' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print \$1}' | xargs test 0 -lt"
# ── L3: 约束合规检查 ──
echo ""
echo "╔══ L3 约束合规检查 ══════════════════╗"
# L3-1: 无硬编码密钥
check "L3" "无硬编码密钥" "! grep -r 'password=.*[a-zA-Z0-9]\{8,\}' --include='*.java' --include='*.yml' --include='*.xml' --include='*.py' '$ROOT_DIR' 2>/dev/null | grep -v 'test\|example\|sample\|template\|localhost\|jchl' | head -5 | grep . && false || true"
# ── 汇总 ──
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ 质量门禁结果汇总 ║"
echo "╚══════════════════════════════════════╝"
echo ""
for r in "${RESULTS[@]}"; do
IFS='|' read -r status level name <<< "$r"
echo " $status [$level] $name"
done
echo ""
echo " 总计: $((PASS + FAIL)) | ✅ $PASS 通过 | ❌ $FAIL 失败"
echo ""
if [ "$FAIL" -gt 0 ]; then
echo " ⚠️ 有 $FAIL 项未通过"
echo " 提示:新增/修改文件后记得 git add 后再检查"
exit 1
else
echo " 🎉 所有门禁通过!"
fi

View File

@@ -1,13 +0,0 @@
# 干净状态检查清单
会话结束前逐项检查:
- [ ] 标准启动路径仍然可用mvn compile 通过)
- [ ] 标准验证路径仍然可运行
- [ ] 当前进度已记录到 PROGRESS.md
- [ ] 功能状态真实反映 passing 和未验证的边界
- [ ] feature_list.json 已更新
- [ ] 没有任何半成品步骤处于未记录状态
- [ ] 临时文件和调试代码已清理
- [ ] 提交信息清晰描述了变更内容
- [ ] 下一轮会话无需人工修复即可继续

View File

@@ -1,22 +0,0 @@
# 评审评分表
| 维度 | 问题 | 0-2分 | 备注 |
|---|---|---|---|
| 正确性 | 实现的行为是否符合目标功能? | | |
| 验证 | 编译检查是否通过?数据流是否完整? | | |
| 范围纪律 | 是否保持在选定功能范围内? | | |
| 可靠性 | 结果能否在重启后继续工作? | | |
| 可维护性 | 代码是否遵循项目规范? | | |
| 交接准备度 | 下一轮能否只靠仓库内文件继续推进? | | |
## 结论
- [ ] Accept
- [ ] Revise
- [ ] Block
## 后续动作
- 缺失的证据:
- 必须补的修复:
- 下次复审触发条件:

View File

@@ -1,72 +0,0 @@
{
"project": "OpenHIS",
"last_updated": "2026-05-28",
"rules": {
"single_active_feature": true,
"passing_requires_evidence": true,
"do_not_skip_verification": true
},
"status_legend": {
"not_started": "功能还没开始做",
"in_progress": "当前唯一正在进行的任务",
"blocked": "有已记录的阻塞问题",
"passing": "验证已通过,证据已记录",
"done": "已完成并合入主干"
},
"features": [
{
"id": "harness-001",
"priority": 1,
"area": "infrastructure",
"title": "Harness Engineering 基础设施搭建",
"user_visible_behavior": "Codex 具备完整的约束/反馈/控制/持久执行能力",
"status": "done",
"verification": [
"AGENTS.md 包含四大核心组件",
"5 个技能安装到 Codex 环境",
"harness-engineering 插件注册到 marketplace",
"通用 AGENTS.md 模板可用"
],
"evidence": ["AGENTS.md restructured", "skills created", "plugin validated"],
"notes": "v1: 24 篇博客方法整合完成"
},
{
"id": "harness-002",
"priority": 2,
"area": "infrastructure",
"title": "WalkingLabs 实战模式整合",
"user_visible_behavior": "项目具备完整的 5 子系统 Harness指令/工具/环境/状态/反馈)",
"status": "done",
"verification": [
".harness/ 目录包含所有模板文件",
"init.sh 可正常运行",
"PROGRESS.md 记录当前状态",
"feature_list.json 跟踪所有功能",
"walkinglabs-harness 技能已安装"
],
"evidence": [
"init.sh verified (compile OK)",
"6 templates installed in .harness/",
"AGENTS.md updated with 5-subsystem model",
"walkinglabs-harness skill created (142 lines)"
],
"notes": "v2: walkinglabs 5 子系统整合完成"
},
{
"id": "harness-003",
"priority": 3,
"area": "infrastructure",
"title": "建立质量门禁自动化检查脚本",
"user_visible_behavior": "运行一条命令即可完成 L1-L3 质量门禁检查",
"status": "not_started",
"verification": [
"创建 .harness/check.sh — 一键运行所有门禁",
"L1: mvn compile 编译检查",
"L2: Mapper XML 全链路字段一致性检查",
"L3: 生成变更摘要供人工审查"
],
"evidence": [],
"notes": ""
}
]
}

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env bash
# Harness Init — 统一启动与验证入口
# 每次新会话开始前运行
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
echo "==> 当前目录: $PWD"
echo "==> Git 状态"
git status --short 2>/dev/null || true
git log --oneline -3 2>/dev/null || true
echo ""
echo "==> 编译检查"
cd openhis-server-new
mvn compile -pl openhis-application -am -q 2>/dev/null && echo " ✅ 编译通过" || echo " ❌ 编译失败"
echo ""
echo "==> 读取进度"
if [ -f .harness/PROGRESS.md ]; then
head -20 .harness/PROGRESS.md
else
echo " (无进度文件)"
fi
echo ""
echo "==> 读取功能清单"
if [ -f .harness/feature_list.json ]; then
python3 -c "
import json
with open('.harness/feature_list.json') as f:
data = json.load(f)
features = [f for f in data.get('features', []) if f.get('status') == 'in_progress']
if features:
print(f\" 当前进行中: {features[0].get('title', 'unknown')}\")
else:
print(' 当前无进行中的功能')
" 2>/dev/null || echo " (无法解析)"
fi
echo ""
echo "==> 环境就绪 ✅"

View File

@@ -1,29 +0,0 @@
# 会话交接
## 当前已验证
- 现在明确可用的部分:
- 本轮实际跑过的验证:
## 本轮改动
- 新增了哪些代码或行为:
- Harness 发生了哪些变化:
## 仍损坏或未验证
- 已知缺陷:
- 未验证路径:
- 下一轮需要注意的风险:
## 下一步最佳动作
- 最高优先级未完成功能:
- 为什么它是下一步:
- 什么结果才算 passing
## 命令速查
- 编译:`cd openhis-server-new && mvn compile -pl openhis-application -am`
- 打包:`mvn clean package -DskipTests`
- 启动:`mvn spring-boot:run`

361
AGENTS.md
View File

@@ -1,237 +1,188 @@
# OpenHIS — Harness Engineering 开发指南 # OpenHIS - AI Agent Development Guide
> **模型决定上限Harness 决定底线。** ## 项目概览
> 本文件是 OpenHIS 项目的 Harness Engineering 落地。整合了 OpenAI/Anthropic Harness Engineering 方法论与 walkinglabs 实战模式 OpenHIS 是一个医院管理系统,采用 Java 17 + Spring Boot 后端和 Vue 3 + Vite 前端架构
--- ## 构建和运行命令
## 📋 项目信息
OpenHIS 医院管理系统 | Java 17 + Spring Boot + MyBatis Plus | Vue 3 + Element Plus | PostgreSQL
### 构建和运行
### 后端Java/Spring Boot
```bash ```bash
cd /root/.openclaw/workspace/his-repo # 构建整个项目
cd openhis-server-new
# 初始化(每次新会话先运行)
bash .harness/init.sh
# 后端编译
cd openhis-server-new && mvn compile -pl openhis-application -am
# 后端打包
mvn clean package -DskipTests mvn clean package -DskipTests
# 后端运行 # 运行后端(开发模式)
cd openhis-application && mvn spring-boot:run cd openhis-server-new/openhis-application
mvn spring-boot:run
# 前端 # 运行特定模块
cd openhis-ui-vue3 && npm install && npm run dev cd openhis-server-new/[module-name]
mvn spring-boot:run
``` ```
### 关键路径 ### 前端Vue 3 + Vite
```bash
# 安装依赖
cd openhis-ui-vue3
npm install
``` # 开发服务器
后端代码: openhis-server-new/openhis-application/src/main/java/com/ npm run dev
后端配置: openhis-server-new/openhis-application/src/main/resources/
Mapper XML: .../mapper/ (regdoctorstation/, doctorstation/, ...) # 生产构建
前端代码: openhis-ui-vue3/src/ npm run build:prod
Harness: .harness/ (init.sh, PROGRESS.md, feature_list.json, ...)
# 测试环境构建
npm run build:test
# 预览构建结果
npm run preview
``` ```
--- ### 测试
项目当前没有配置正式的测试框架。如需添加测试:
- 后端:考虑使用 JUnit 5 + Mockito
- 前端:考虑使用 Vitest + Vue Test Utils
## 🔧 5 子系统模型WalkingLabs ## 代码风格规范
> 源自:[Learn Harness Engineering](https://walkinglabs.github.io/learn-harness-engineering/zh/) ### Java 后端规范
- **Java 版本**: 17
- **框架**: Spring Boot 2.5.15
- **ORM**: MyBatis Plus 3.5.5
- **数据库**: PostgreSQL
- **包结构**:
- `com.openhis` - 业务逻辑
- `com.core` - 核心框架
- **命名约定**:
- 类名PascalCase`UserController`
- 方法名camelCase`getUserList`
- 常量SCREAMING_SNAKE_CASE
- 配置文件kebab-case
- **注解使用**:
- 使用 `@Slf4j` 替代手动声明 logger
- 使用 `@Data` 在实体类中
- 使用 `@Service/@Controller/@Repository` 等 Spring 注解
- **异常处理**:
- 使用统一的异常处理机制
- 自定义业务异常继承 `RuntimeException`
### 1. 指令子系统Instruction ### Vue 前端规范
- **框架**: Vue 3 + Composition API
| 文件 | 用途 | - **UI 库**: Element Plus
|---|---| - **状态管理**: Pinia
| **AGENTS.md**(本文件) | 项目规则、约束、工作流程 | - **路由**: Vue Router 4
| `.harness/feature_list.json` | 机器可读的功能状态追踪 | - **构建工具**: Vite 5
| `.harness/PROGRESS.md` | 会话进度和已验证状态 | - **组件命名**: PascalCase
| `.harness/session-handoff.md` | 跨会话交接摘要 | - **文件命名**: kebab-case
- **变量命名**: camelCase
### 2. 工具子系统Tools - **常量命名**: SCREAMING_SNAKE_CASE
- **函数命名**:
| 工具 | 用途 | - 事件处理:`handle` 前缀
|---|---| - 数据获取:`get`/`load` 前缀
| `mvn compile` | 编译验证 | - 提交操作:`submit` 前缀
| `git` | 版本控制 + 回滚 |
| `pwd` | 确认当前目录 |
| shell | 文件操作、命令执行 |
### 3. 环境子系统Environment
| 组件 | 状态 |
|---|---|
| Java 17 | ✅ `pom.xml` 锁定 |
| Maven | ✅ `mvn-wrapper` |
| PostgreSQL | ✅ 192.168.110.252:15432 |
| Node.js | ✅ `package.json` 锁定 |
### 4. 状态子系统State
| 机制 | 用途 |
|---|---|
| `update_plan` | 当前步骤检查点 |
| `.harness/PROGRESS.md` | 跨会话进度记录 |
| `.harness/feature_list.json` | 功能状态跟踪 |
| `git log` | 变更历史追溯 |
### 5. 反馈子系统Feedback
| 层级 | 命令 | 时间 |
|---|---|---|
| L1 编译 | `mvn compile -pl openhis-application -am` | <30 |
| L2 全链路 | 六环检查清单见下文 | <5 分钟 |
| L3 审查 | 你人工审查 diff | 10-30 分钟 |
---
## 📋 标准工作循环
```
开始会话
├→ 1. Init
│ ├── bash .harness/init.sh
│ ├── 读取 PROGRESS.md / feature_list.json
│ ├── git log --oneline -5
│ └── 确认编译通过
├→ 2. Plan
│ ├── update_plan / checklist_write 分解步骤
│ ├── 评估复杂度/风险
│ └── 设定检查点
├→ 3. Implement
│ ├── 一次只做一个功能
│ ├── 全链路检查清单核对
│ └── 增量修改,只动必要文件
├→ 4. Verify
│ ├── L1: mvn compile
│ ├── L2: 全链路数据流验证
│ └── 生成变更摘要
└→ 5. Cleanup
├── 运行 clean-state-checklist.md
├── 更新 PROGRESS.md + feature_list.json
├── git add + commit + push
└── init.sh 确认干净状态
```
---
## 🔗 全链路修复原则
Bug 不得"就事论事"必须走通完整的**数据流全链路**
### 六环检查清单
```
1. 录入 → 前端有无输入入口?(弹窗、行编辑、表单...
2. 保存 → 前端 → API → Controller → Service → Entity → DB
每个保存入口都传了该字段吗?
3. 查询 → DB → Mapper XMLUNION ALL 子查询统一加)→ DTO → 前端展示
4. 修改 → 编辑回显 → 修改保存 → 正确更新?
5. 删除 → 状态变更会丢失该字段吗?
6. 关联 → 上下游(护士站、计费、打印、报表)需要同步改吗?
```
### 常见陷阱
| 陷阱 | 解决 |
|---|---|
| 只修主入口批量保存/签发保存漏了 | 检查所有 Service 实现类 |
| 前端加了后端没传 | 逐个入口确认 |
| UNION ALL 只改一半 | 所有子查询统一加 |
| DTO 继承链没检查 | 检查父类/子类字段一致性 |
| 只测新增没测编辑 | 新增和编辑都要测 |
---
## 📐 代码风格规范
### Java 后端
| 项目 | 规范 |
|---|---|
| 包结构 | `com.openhis`业务)、`com.core`核心 |
| 命名 | PascalCase方法 camelCase常量 SCREAMING_SNAKE_CASE |
| 注解 | `@Slf4j``@Data``@Service/@Controller/@Repository` |
| 异常 | 统一异常处理业务异常继承 `RuntimeException` |
| 缩进 | 4 空格 120 字符 |
### Vue 前端
| 项目 | 规范 |
|---|---|
| 框架 | Vue 3 + Composition API + Element Plus + Pinia |
| 命名 | 组件 PascalCase文件 kebab-case变量 camelCase |
| 缩进 | 2 空格单引号 100 字符 |
### 导入顺序 ### 导入顺序
#### Java
1. `java.*`
2. `javax.*`
3. 第三方库
4. `com.core.*`
5. `com.openhis.*`
6. `*.*`(其他包)
**Java** `java.*` `javax.*` 第三方 `com.core.*` `com.openhis.*` #### JavaScript/Vue
**Vue** `vue` 相关 第三方 `@/` 别名 相对路径 1. `vue` 相关
2. 第三方库
3. `@/` 别名导入
4. 相对路径导入
--- ### 代码格式
#### Java
- 缩进4个空格
- 行长度120字符
- 左大括号不换行
## 🏗️ 开发约定 #### Vue/JavaScript
- 缩进2个空格
- 字符串:优先使用单引号
- 行长度100字符
| 领域 | 约定 | ## 关键配置文件
|---|---|
| API | RESTful统一响应格式Swagger 文档 |
| 数据库 | snake_case 命名主键 `id`软删除 `valid_flag` |
| 安全 | 所有 API 需权限验证SQL 注入/XSS 防护 |
| 性能 | Druid 连接池路由懒加载虚拟滚动 |
--- ### 后端配置
- 主配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`
- 环境配置:`application-{profile}.yml`
- Maven 父 POM`openhis-server-new/pom.xml`
## ⚙️ 关键配置 ### 前端配置
- Vite 配置:`openhis-ui-vue3/vite.config.js`
- 环境变量:`.env.*` 文件
- 路由配置:`openhis-ui-vue3/src/router/index.js`
| 项目 | | ## 开发约定
|---|---|
| 后端端口 | 18080 |
| 前端端口 | 81 |
| API 前缀 | `/openhis` |
| Swagger | `/openhis/swagger-ui/index.html` |
| 后端配置 | `application.yml` / `application-{profile}.yml` |
| 前端配置 | `vite.config.js` / `.env.*` |
--- ### API 设计
- RESTful API 风格
- 统一响应格式
- 使用 Swagger 文档
- 错误码统一管理
## 📈 成熟度追踪 ### 数据库
- 表名snake_case
- 字段名snake_case
- 主键:使用 `id`
- 软删除:使用 `valid_flag` 字段
| 等级 | 特征 | 本项目 | ### 前端组件
|---|---|---| - 单一职责原则
| **L1 初始** | 零星使用 AI 工具 | 已超越 | - Props 使用 camelCase
| **L2 管理** | 基础约束 + 反馈 + 控制 | **当前** | - Events 使用 kebab-case
| **L3 定义** | 标准化可复用 | 🔄 walkinglabs 5 子系统整合 | - 使用 Composition API
| **L4 量化** | 数据驱动优化 | | - 组件文档使用 JSDoc
| **L5 优化** | AI 自主优化 Harness | |
--- ### 状态管理
- 模块化设计
- 异步操作使用 actions
- 避免在组件中直接修改状态
## 📚 技能索引Codex 内置) ## 环境变量
| 技能 | 用途 | ### 前端
|---|---| - `VITE_APP_BASE_API`: API 基础路径
| `$harness-engineering` | 主方法论 约束 + 反馈 + 控制 + 持久 | - `VITE_APP_ENV`: 环境标识
| `$walkinglabs-harness` | 实战模式 5 子系统 + 模板 + 会话持续 |
| `$durable-execution` | 检查点幂等性事件溯源 |
| `$closed-loop-testing` | 质量门禁测试策略反馈循环 |
| `$constraint-design` | DSL 设计策略模式约束编排 |
| `$review-audit` | 审查工作流审计追踪合规检查 |
| `$full-chain-fix` | 全链路数据流修复 |
| `$karpathy-guidelines` | 减少 LLM 编码常见错误 |
--- ### 后端
- `spring.profiles.active`: 激活的配置文件
- `core.name`: 应用名称
- `core.version`: 应用版本
> **总纲:** 你负责"做什么"和"为什么"Agent 负责"怎么做"和"做多好" ## 安全规范
> **工作循环:** Init → Plan → Implement → Verify → Cleanup - 所有 API 接口需要权限验证
- 敏感信息使用环境变量
- SQL 注入防护
- XSS 攻击防护
## 性能优化
- 后端使用连接池Druid
- 前端使用路由懒加载
- 图片使用 WebP 格式
- 大列表使用虚拟滚动
## 常用工具类
- 后端:`com.core.common.utils.*`
- 前端:`@/utils/*`
## 注意事项
1. 修改数据库结构需要同步 SQL 脚本
2. 新增功能需要添加权限配置
3. 前端路由需要在权限系统中注册
4. 接口变更需要更新 Swagger 文档
5. 遵循现有代码风格,避免不必要的变化
## 故障排除
- 后端端口18080
- 前端端口81
- API 前缀:`/openhis`
- Swagger UI`/openhis/swagger-ui/index.html`
- Druid 监控:`/openhis/druid/login.html`

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,42 +0,0 @@
# 分析报告 — Bug #469
## 问题描述
检验申请列表的【操作】列仅显示固定的"打印"和"删除"按钮,未根据申请单状态动态切换操作权限。
## 根因分析
文件 `openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue` 第97-104行
- 操作列模板中固定渲染"打印"和"删除"按钮,没有任何状态判断逻辑
- 缺少"修改"和"撤回"按钮
## 状态机设计
| 状态 | 条件 | 允许的操作 |
|------|------|-----------|
| 待开立 | applyStatus == 0 | 修改、删除 |
| 已开立 | applyStatus == 1 && needExecute != true | 撤回 |
| 已执行 | applyStatus == 1 && needExecute == true | 无(仅打印) |
## 修复方案
1. **前端 Vue**: 操作列改为 `v-if` 条件渲染按钮(修改/删除/撤回/打印)
2. **前端 API**: 新增撤回接口 `withdrawInspectionApplication(applyNo)`
3. **后端 Controller**: 新增 `POST /withdraw/{applyNo}` 端点
4. **后端 Service**: 新增 `withdrawInspectionLabApply` 方法,将 applyStatus 置回 0needRefund/needExecute 置回 false
## 修复结果
✅ 成功共14行改动2个commit完成
### 修复详情
1. **commit c643a78b** - 初始修复:将操作列从静态"打印/删除"改为基于状态的动态按钮(修改/删除/撤回/详情10行改动
2. **commit f369ea41** - 跟进修复:将"详情"按钮包裹在 `<template v-else>`避免对所有状态始终渲染4行改动
### 状态机实现
| 状态 | 条件 | 显示按钮 |
|------|------|---------|
| 待签发 | billStatus == '0' | 修改 + 删除 |
| 已签发 | billStatus == '1' | 撤回 |
| 其他状态 | 已采证/已送检/报告已出/已作废 | 详情 |
### 涉及文件
- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/testApplication.vue` - 前端操作列动态按钮
- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/api.js` - 前端APIdeleteRequestForm, withdrawRequestForm
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/regdoctorstation/controller/RequestFormManageController.java` - 后端Controller/delete, /withdraw 端点)
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/regdoctorstation/appservice/impl/RequestFormManageAppServiceImpl.java` - 后端Service实现

View File

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

Submodule his-repo updated: 515ed84118...414c204578

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

@@ -1,7 +1,6 @@
package com.core.framework.config; package com.core.framework.config;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
@@ -35,9 +34,7 @@ public class ApplicationConfig {
// 设置日期格式为 yyyy/M/d HH:mm:ss支持多种格式反序列化 // 设置日期格式为 yyyy/M/d HH:mm:ss支持多种格式反序列化
builder.simpleDateFormat("yyyy/M/d HH:mm:ss"); builder.simpleDateFormat("yyyy/M/d HH:mm:ss");
// 添加JavaTimeModule支持用于LocalDateTime // 添加JavaTimeModule支持用于LocalDateTime
JavaTimeModule javaTimeModule = new JavaTimeModule(); builder.modules(new JavaTimeModule());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
builder.modules(javaTimeModule);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss"))); builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss")));
}; };
} }

View File

@@ -1,7 +1,7 @@
package com.openhis.web.Inspection.dto; package com.openhis.web.Inspection.dto;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.experimental.Accessors;
import java.util.List; import java.util.List;
@@ -11,17 +11,30 @@ import java.util.List;
* @author * @author
* @date * @date
*/ */
@Getter @Data
@Setter @Accessors(chain = true)
public class InstrumentManageInitDto { public class InstrumentManageInitDto {
private List<statusEnumOption> statusFlagOptions; private List<statusEnumOption> statusFlagOptions;
private List<InstrumentType> instrumentTypeList; private List<InstrumentType> InstrumentTypeList;
private List<InstrumentStatusEnumOption> instrumentStatusEnumList; private List<InstrumentStatusEnumOption> InstrumentStatusEnumList;
// 手动添加 setter 方法
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
this.statusFlagOptions = statusFlagOptions;
}
public void setInstrumentTypeList(List<InstrumentType> InstrumentTypeList) {
this.InstrumentTypeList = InstrumentTypeList;
}
public void setInstrumentStatusEnumList(List<InstrumentStatusEnumOption> InstrumentStatusEnumList) {
this.InstrumentStatusEnumList = InstrumentStatusEnumList;
}
/** /**
* 状态 * 状态
*/ */
@Getter @Data
public static class statusEnumOption { public static class statusEnumOption {
private Integer value; private Integer value;
private String info; private String info;
@@ -31,7 +44,7 @@ public class InstrumentManageInitDto {
} }
} }
@Getter @Data
public static class InstrumentStatusEnumOption { public static class InstrumentStatusEnumOption {
private Integer value; private Integer value;
private String info; private String info;
@@ -41,7 +54,7 @@ public class InstrumentManageInitDto {
} }
} }
@Getter @Data
public static class InstrumentType { public static class InstrumentType {
private Integer value; private Integer value;
private String info; private String info;
@@ -50,4 +63,6 @@ public class InstrumentManageInitDto {
this.info = info; this.info = info;
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils; import com.core.common.utils.SecurityUtils;
import com.openhis.common.enums.SlotStatus; import com.openhis.common.constant.CommonConstants;
import com.openhis.appointmentmanage.domain.DoctorSchedule; import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto; import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
import com.openhis.appointmentmanage.domain.SchedulePool; import com.openhis.appointmentmanage.domain.SchedulePool;
@@ -502,8 +502,8 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
// 该排班下存在有效患者预约(号源槽:已预约/已锁定/已取号)则禁止删除;已退号、仅可用/已取消槽位不计入 // 该排班下存在有效患者预约(号源槽:已预约/已锁定/已取号)则禁止删除;已退号、仅可用/已取消槽位不计入
long appointmentCount = scheduleSlotService.count(new QueryWrapper<ScheduleSlot>() long appointmentCount = scheduleSlotService.count(new QueryWrapper<ScheduleSlot>()
.in("pool_id", poolIds) .in("pool_id", poolIds)
.in("status", SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue(), .in("status", CommonConstants.SlotStatus.BOOKED, CommonConstants.SlotStatus.LOCKED,
SlotStatus.CHECKED_IN.getValue())); CommonConstants.SlotStatus.CHECKED_IN));
if (appointmentCount > 0) { if (appointmentCount > 0) {
return R.fail("该排班已有患者预约,禁止删除!如需取消请先处理患者退预约或使用'停诊'功能。"); return R.fail("该排班已有患者预约,禁止删除!如需取消请先处理患者退预约或使用'停诊'功能。");
} }

View File

@@ -9,7 +9,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.service.ITicketService; import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService; import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto; import com.openhis.web.appointmentmanage.dto.TicketDto;
import com.openhis.common.enums.SlotStatus; import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.OrderStatus; import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -193,24 +193,25 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) { if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊"); dto.setStatus("已停诊");
} else { } else {
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus()); Integer slotStatus = raw.getSlotStatus();
if (status != null) { if (slotStatus != null) {
if (status == SlotStatus.LOCKED) { if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) { if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else { } else {
dto.setStatus("已预约"); dto.setStatus("已预约");
} }
} else if (status == SlotStatus.BOOKED) { } else if (SlotStatus.RETURNED.equals(slotStatus)) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else { } else if (SlotStatus.CANCELLED.equals(slotStatus)) {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊"); dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) { } else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("退号"); dto.setStatus("锁定");
} else { } else {
dto.setStatus("未预约"); dto.setStatus("未预约");
} }
@@ -236,10 +237,6 @@ public class TicketAppServiceImpl implements ITicketAppService {
/** /**
* 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据 * 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据
*/ */
/**
* 规范前端传入的状态查询参数,映射到 SQL 的 slotStatusNormExpr 值。
* 数值映射: 0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
*/
private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) { private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
String rawStatus = query.getStatus(); String rawStatus = query.getStatus();
if (rawStatus == null) { if (rawStatus == null) {
@@ -266,31 +263,28 @@ public class TicketAppServiceImpl implements ITicketAppService {
case "已预约": case "已预约":
query.setStatus("booked"); query.setStatus("booked");
break; break;
case "locked":
case "2":
case "已锁定":
query.setStatus("locked");
break;
case "checked": case "checked":
case "checkin": case "checkin":
case "checkedin": case "checkedin":
case "3": case "2":
case "已取号": case "已取号":
query.setStatus("checked"); query.setStatus("checked");
break; break;
case "cancelled": case "cancelled":
case "canceled": case "canceled":
case "4": case "3":
case "已停诊": case "已停诊":
case "已取消": case "已取消":
query.setStatus("cancelled"); query.setStatus("cancelled");
break; break;
case "returned": case "returned":
case "4":
case "5": case "5":
case "已退号": case "已退号":
query.setStatus("returned"); query.setStatus("returned");
break; break;
default: default:
// 设置为 impossible 值,配合 mapper 的 otherwise 分支直接返回空
query.setStatus("__invalid__"); query.setStatus("__invalid__");
break; break;
} }
@@ -373,25 +367,26 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) { if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊"); dto.setStatus("已停诊");
} else { } else {
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已锁定...) // 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...)
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus()); Integer slotStatus = raw.getSlotStatus();
if (status != null) { if (slotStatus != null) {
if (status == SlotStatus.LOCKED) { if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) { if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else { } else {
dto.setStatus("已预约"); dto.setStatus("已预约");
} }
} else if (status == SlotStatus.BOOKED) { } else if (SlotStatus.RETURNED.equals(slotStatus)) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else { } else if (SlotStatus.CANCELLED.equals(slotStatus)) {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊"); dto.setStatus("已停诊");
} else if (status == SlotStatus.RETURNED) { } else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("退号"); dto.setStatus("锁定");
} else { } else {
dto.setStatus("未预约"); dto.setStatus("未预约");
} }

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.getOrgLocListByActivityDefinitionId(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;
@@ -169,7 +169,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(), if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(),
orgLoc.getStartTime(), orgLoc.getEndTime())) { orgLoc.getStartTime(), orgLoc.getEndTime())) {
Organization org = organizationService.getById(organizationLocation.getOrganizationId()); Organization org = organizationService.getById(organizationLocation.getOrganizationId());
String organizationName = org != null ? org.getName() : ("科室[" + organizationLocation.getOrganizationId() + "]已删除"); String organizationName = org != null ? org.getName() : "未知科室";
return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime() return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime()
+ CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突"); + CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突");
} }

View File

@@ -31,9 +31,4 @@ public class OrgLocQueryParam implements Serializable {
/** 发放类别 */ /** 发放类别 */
private String distributionCategoryCode; private String distributionCategoryCode;
/**
* 项目编码 | 药品:1 耗材:2
*/
private String itemCode;
} }

View File

@@ -18,7 +18,6 @@ import com.openhis.administration.mapper.PatientMapper;
import com.openhis.administration.service.*; import com.openhis.administration.service.*;
import com.openhis.common.constant.CommonConstants; import com.openhis.common.constant.CommonConstants;
import com.openhis.common.constant.PromptMsgConstant; import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.*; import com.openhis.common.enums.*;
import com.openhis.common.enums.ybenums.YbPayment; import com.openhis.common.enums.ybenums.YbPayment;
import com.openhis.common.utils.EnumUtils; import com.openhis.common.utils.EnumUtils;
@@ -644,7 +643,8 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
.set(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue()) .set(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue())
.set(Order::getPayStatus, PaymentStatus.REFUND_ALL.getValue()) .set(Order::getPayStatus, PaymentStatus.REFUND_ALL.getValue())
.set(Order::getCancelTime, new Date()) .set(Order::getCancelTime, new Date())
.set(Order::getCancelReason, "诊前退号") .set(Order::getCancelReason,
StringUtils.isNotEmpty(reason) ? reason : "诊前退号")
.set(Order::getUpdateTime, new Date()) .set(Order::getUpdateTime, new Date())
.setSql("version = version + 1") .setSql("version = version + 1")
.eq(Order::getId, appointmentOrder.getId()) .eq(Order::getId, appointmentOrder.getId())
@@ -660,28 +660,18 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
return appointmentOrder.getId(); return appointmentOrder.getId();
} }
// 只有已预约(1)的号源才能退号,对应签到后的 BOOKED 状态 int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.AVAILABLE);
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId); if (slotRows > 0) {
if (slot == null || !SlotStatus.BOOKED.getValue().equals(slot.getStatus())) {
log.warn("退号跳过:槽位非已预约状态, slotId={}, status={}", slotId,
slot != null ? slot.getStatus() : null);
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue());
if (slotRows == 0) {
log.warn("退号时更新槽位状态未影响任何行, slotId={}", slotId);
return appointmentOrder.getId();
}
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId); Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) { if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.update(null, schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>() new LambdaUpdateWrapper<SchedulePool>()
.setSql("booked_num = booked_num - 1, version = version + 1") .setSql("version = version + 1")
.set(SchedulePool::getUpdateTime, new Date()) .set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId)); .eq(SchedulePool::getId, poolId));
} }
}
return appointmentOrder.getId(); return appointmentOrder.getId();
} catch (Exception e) { } catch (Exception e) {
log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e); log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e);

View File

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

View File

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

View File

@@ -147,6 +147,6 @@ public interface IDoctorStationAdviceAppService {
*/ */
IPage<SurgeryItemDto> getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey); IPage<SurgeryItemDto> getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey);
IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey, String categoryCode); IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey);
} }

View File

@@ -63,21 +63,17 @@ public interface IDoctorStationEmrAppService {
* 获取待写病历列表 * 获取待写病历列表
* *
* @param doctorId 医生ID * @param doctorId 医生ID
* @param pageNo 当前页码 * @return 待写病历列表
* @param pageSize 每页条数
* @param patientName 患者姓名(可选)
* @return 待写病历分页数据
*/ */
R<?> getPendingEmrList(Long doctorId, Integer pageNo, Integer pageSize, String patientName); R<?> getPendingEmrList(Long doctorId);
/** /**
* 获取待写病历数量 * 获取待写病历数量
* *
* @param doctorId 医生ID * @param doctorId 医生ID
* @param patientName 患者姓名(可选)
* @return 待写病历数量 * @return 待写病历数量
*/ */
R<?> getPendingEmrCount(Long doctorId, String patientName); R<?> getPendingEmrCount(Long doctorId);
/** /**
* 检查患者是否需要写病历 * 检查患者是否需要写病历

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;
@@ -48,16 +45,12 @@ import com.openhis.web.personalization.dto.ActivityDeviceDto;
import com.openhis.workflow.domain.ActivityDefinition; import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.domain.DeviceRequest; import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.InventoryItem; import com.openhis.workflow.domain.InventoryItem;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.openhis.workflow.domain.ServiceRequest; import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.*; import com.openhis.workflow.service.*;
import lombok.extern.slf4j.Slf4j; 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;
@@ -76,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;
@@ -140,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:";
// 缓存过期时间(小时) // 缓存过期时间(小时)
@@ -949,27 +927,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
/** /**
* 处理药品 * 处理药品
*/ */
/**
* 将 remark 合并到 contentJson 中,确保 Mapper 能从 content_json 提取 remark
*/
private String injectRemarkIntoContentJson(String contentJson, String remark) {
if (remark == null || remark.isEmpty() || contentJson == null || contentJson.isEmpty()) {
return contentJson;
}
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(contentJson);
if (node instanceof ObjectNode) {
((ObjectNode) node).put("remark", remark);
return mapper.writeValueAsString(node);
}
} catch (Exception e) {
log.warn("Failed to inject remark into contentJson: {}", e.getMessage());
}
return contentJson;
}
private List<String> handMedication(List<AdviceSaveDto> medicineList, Date curDate, String adviceOpType, private List<String> handMedication(List<AdviceSaveDto> medicineList, Date curDate, String adviceOpType,
Long organizationId, String signCode) { Long organizationId, String signCode) {
// 当前登录账号的科室id // 当前登录账号的科室id
@@ -1186,10 +1143,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
if (medicationRequest.getId() == null) { if (medicationRequest.getId() == null) {
firstTimeSave = true; firstTimeSave = true;
} }
// 确保 contentJson 包含 remark
if (adviceSaveDto.getRemark() != null && !adviceSaveDto.getRemark().isEmpty()) {
medicationRequest.setContentJson(injectRemarkIntoContentJson(medicationRequest.getContentJson(), adviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(medicationRequest); iMedicationRequestService.saveOrUpdate(medicationRequest);
if (firstTimeSave) { if (firstTimeSave) {
medRequestIdList.add(medicationRequest.getId().toString()); medRequestIdList.add(medicationRequest.getId().toString());
@@ -1650,10 +1603,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id deviceRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id deviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
// 确保 contentJson 包含 remark
if (adviceSaveDto.getRemark() != null && !adviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), adviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest); iDeviceRequestService.saveOrUpdate(deviceRequest);
if (is_save) { if (is_save) {
// 处理耗材发放 // 处理耗材发放
@@ -1801,7 +1750,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
return StringUtils.isBlank(applyNo) ? null : applyNo; return StringUtils.isBlank(applyNo) ? null : applyNo;
} }
/** /**
* 处理诊疗 * 处理诊疗
*/ */
@@ -1850,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 {
@@ -1902,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")).last("LIMIT 1"));
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();
@@ -1946,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);
} }
@@ -2065,9 +1962,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST); serviceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
} }
// 备注
serviceRequest.setRemark(adviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(serviceRequest); iServiceRequestService.saveOrUpdate(serviceRequest);
// 保存时保存诊疗费用项 // 保存时保存诊疗费用项
@@ -2186,7 +2080,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.eq(ChargeItem::getServiceId, adviceSaveDto.getRequestId()) .eq(ChargeItem::getServiceId, adviceSaveDto.getRequestId())
.eq(ChargeItem::getServiceTable, CommonConstants.TableName.WOR_SERVICE_REQUEST) .eq(ChargeItem::getServiceTable, CommonConstants.TableName.WOR_SERVICE_REQUEST)
.eq(ChargeItem::getDeleteFlag, DelFlag.NO.getCode()) .eq(ChargeItem::getDeleteFlag, DelFlag.NO.getCode())
.last("LIMIT 1")); );
log.info("BugFix#328: 通过requestId查询费用项requestId={}, chargeItem={}", log.info("BugFix#328: 通过requestId查询费用项requestId={}, chargeItem={}",
adviceSaveDto.getRequestId(), existingChargeItem != null ? existingChargeItem.getId() : "null"); adviceSaveDto.getRequestId(), existingChargeItem != null ? existingChargeItem.getId() : "null");
} }
@@ -2227,6 +2121,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(), CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
sourceEnum, sourceBillNo); sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时过滤掉药品1保留耗材2和诊疗3/6
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& dto.getAdviceType() == 1);
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) { for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态 // 请求状态
requestBaseDto requestBaseDto
@@ -2240,10 +2139,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 收费状态 // 收费状态
requestBaseDto.setChargeStatus_enumText( requestBaseDto.setChargeStatus_enumText(
EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus())); EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus()));
// 单位字典翻译失败时回退使用原始值(如手术申请硬编码了中文单位名)
if (StringUtils.isNotBlank(requestBaseDto.getUnitCode()) && StringUtils.isBlank(requestBaseDto.getUnitCode_dictText())) {
requestBaseDto.setUnitCode_dictText(requestBaseDto.getUnitCode());
}
} }
return R.ok(requestBaseInfo); return R.ok(requestBaseInfo);
} }
@@ -2295,7 +2190,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
new LambdaQueryWrapper<InventoryItem>() new LambdaQueryWrapper<InventoryItem>()
.eq(InventoryItem::getItemId, dispense.getMedicationId()) .eq(InventoryItem::getItemId, dispense.getMedicationId())
.eq(InventoryItem::getLotNumber, dispense.getLotNumber()) .eq(InventoryItem::getLotNumber, dispense.getLotNumber())
.last("LIMIT 1")); );
if (inventoryItem != null) { if (inventoryItem != null) {
// 计算回滚后的数量(加上已发放的数量) // 计算回滚后的数量(加上已发放的数量)
@@ -2325,7 +2220,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList); log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList);
// 尝试签退药品请求(只有存在的才会更新) // 尝试签退药品请求(只有存在的才会更新)
iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null, null); iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null);
// 尝试签退耗材请求(只有存在的才会更新) // 尝试签退耗材请求(只有存在的才会更新)
iDeviceRequestService.updateDraftStatusBatch(requestIdList); iDeviceRequestService.updateDraftStatusBatch(requestIdList);
// 尝试签退诊疗请求(只有存在的才会更新) // 尝试签退诊疗请求(只有存在的才会更新)
@@ -2586,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);
} }
@@ -2605,13 +2504,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
} }
@Override @Override
public IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey, String categoryCode) { public IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey) {
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getExaminationPage( IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getExaminationPage(
new Page<>(pageNo, pageSize), new Page<>(pageNo, pageSize),
PublicationStatus.ACTIVE.getValue(), PublicationStatus.ACTIVE.getValue(),
organizationId, organizationId,
searchKey, searchKey);
categoryCode);
return result; return result;
} }

View File

@@ -29,7 +29,6 @@ import com.openhis.document.service.IEmrTemplateService;
import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService; import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService;
import com.openhis.web.doctorstation.dto.EmrTemplateDto; import com.openhis.web.doctorstation.dto.EmrTemplateDto;
import com.openhis.web.doctorstation.dto.PatientEmrDto; import com.openhis.web.doctorstation.dto.PatientEmrDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -42,7 +41,6 @@ import java.util.stream.Collectors;
/** /**
* 医生站-电子病历 应用实现类 * 医生站-电子病历 应用实现类
*/ */
@Slf4j
@Service @Service
public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppService { public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppService {
@@ -62,7 +60,13 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
IDocRecordService docRecordService; IDocRecordService docRecordService;
@Resource @Resource
private com.openhis.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper; private EncounterMapper encounterMapper;
@Resource
private PatientMapper patientMapper;
@Resource
private com.openhis.administration.mapper.EncounterParticipantMapper encounterParticipantMapper;
/** /**
* 添加病人病历信息 * 添加病人病历信息
@@ -75,7 +79,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
Emr emr = new Emr(); Emr emr = new Emr();
BeanUtils.copyProperties(patientEmrDto, emr); BeanUtils.copyProperties(patientEmrDto, emr);
String contextStr = patientEmrDto.getContextJson().toString(); String contextStr = patientEmrDto.getContextJson().toString();
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).last("LIMIT 1")); Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()));
boolean saveSuccess; boolean saveSuccess;
// 如果已经保存病历,再次保存走更新 // 如果已经保存病历,再次保存走更新
if (patientEmr != null) { if (patientEmr != null) {
@@ -137,7 +141,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
@Override @Override
public R<?> getEmrDetail(Long encounterId) { public R<?> getEmrDetail(Long encounterId) {
// 先查询门诊病历(emr表) // 先查询门诊病历(emr表)
Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).orderByDesc(Emr::getCreateTime).last("LIMIT 1")); Emr emrDetail = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId));
if (emrDetail != null) { if (emrDetail != null) {
return R.ok(emrDetail); return R.ok(emrDetail);
} }
@@ -219,29 +223,52 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
* @return 待写病历列表 * @return 待写病历列表
*/ */
@Override @Override
public R<?> getPendingEmrList(Long doctorId, Integer pageNo, Integer pageSize, String patientName) { public R<?> getPendingEmrList(Long doctorId) {
// 先查询总数 // 由于Encounter实体中没有jzPractitionerUserId字段我们需要通过关联查询来获取相关信息
Long total = doctorStationEmrAppMapper.getPendingEmrCount(doctorId, patientName); // 使用医生工作站的mapper来查询相关数据
// 这里我们直接使用医生工作站的查询逻辑
// 计算分页偏移量,再查询分页数据 // 查询当前医生负责的、状态为"就诊中"但还没有写病历的患者
int offset = (pageNo - 1) * pageSize; // 需要通过EncounterParticipant表来关联医生信息
List<Map<String, Object>> pageRows = doctorStationEmrAppMapper.getPendingEmrList(doctorId, patientName, pageSize, offset); List<Encounter> encounters = encounterMapper.selectList(
new LambdaQueryWrapper<Encounter>()
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
);
// 计算年龄列 // 过滤出由指定医生负责且还没有写病历的就诊记录
for (Map<String, Object> row : pageRows) { List<Map<String, Object>> pendingEmrs = new ArrayList<>();
Object birthDate = row.get("birthDate"); for (Encounter encounter : encounters) {
if (birthDate instanceof Date) { // 检查该就诊记录是否已经有病历
row.put("age", calculateAge((Date) birthDate)); Emr existingEmr = emrService.getOne(
} else { new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId())
row.put("age", null); );
// 检查该就诊是否由指定医生负责
boolean isAssignedToDoctor = isEncounterAssignedToDoctor(encounter.getId(), doctorId);
if (existingEmr == null && isAssignedToDoctor) {
// 如果没有病历且由该医生负责,则添加到待写病历列表
Map<String, Object> pendingEmr = new java.util.HashMap<>();
// 获取患者信息
Patient patient = patientMapper.selectById(encounter.getPatientId());
pendingEmr.put("encounterId", encounter.getId());
pendingEmr.put("patientId", encounter.getPatientId());
pendingEmr.put("patientName", patient != null ? patient.getName() : "未知");
pendingEmr.put("gender", patient != null ? patient.getGenderEnum() : null);
// 使用出生日期计算年龄
pendingEmr.put("age", patient != null && patient.getBirthDate() != null ?
calculateAge(patient.getBirthDate()) : null);
// 使用创建时间作为挂号时间
pendingEmr.put("registerTime", encounter.getCreateTime());
pendingEmr.put("busNo", encounter.getBusNo()); // 病历号
pendingEmrs.add(pendingEmr);
} }
row.remove("birthDate");
} }
Map<String, Object> result = new java.util.HashMap<>(); return R.ok(pendingEmrs);
result.put("rows", pageRows);
result.put("total", total != null ? total : 0L);
return R.ok(result);
} }
/** /**
@@ -251,9 +278,14 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
* @return 待写病历数量 * @return 待写病历数量
*/ */
@Override @Override
public R<?> getPendingEmrCount(Long doctorId, String patientName) { public R<?> getPendingEmrCount(Long doctorId) {
Long count = doctorStationEmrAppMapper.getPendingEmrCount(doctorId, patientName); // 获取待写病历列表,然后返回数量
return R.ok(count != null ? count.intValue() : 0); R<?> result = getPendingEmrList(doctorId);
if (result.getCode() == 200) {
List<?> pendingEmrs = (List<?>) result.getData();
return R.ok(pendingEmrs.size());
}
return R.ok(0);
} }
/** /**
@@ -266,7 +298,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
public R<?> checkNeedWriteEmr(Long encounterId) { public R<?> checkNeedWriteEmr(Long encounterId) {
// 检查该就诊记录是否已经有病历 // 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne( Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId).last("LIMIT 1") new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId)
); );
// 如果没有病历,则需要写病历 // 如果没有病历,则需要写病历
@@ -274,6 +306,24 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
return R.ok(needWrite); return R.ok(needWrite);
} }
/**
* 检查就诊是否分配给指定医生
*
* @param encounterId 就诊ID
* @param doctorId 医生ID
* @return 是否分配给指定医生
*/
private boolean isEncounterAssignedToDoctor(Long encounterId, Long doctorId) {
// 查询就诊参与者表,检查是否有指定医生的接诊记录
com.openhis.administration.domain.EncounterParticipant participant =
encounterParticipantMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.openhis.administration.domain.EncounterParticipant>()
.eq(com.openhis.administration.domain.EncounterParticipant::getEncounterId, encounterId)
.eq(com.openhis.administration.domain.EncounterParticipant::getPractitionerId, doctorId)
);
return participant != null;
}
/** /**
* 根据出生日期计算年龄 * 根据出生日期计算年龄

View File

@@ -274,7 +274,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
new QueryWrapper<Organization>() new QueryWrapper<Organization>()
.eq("bus_no", performDeptCode) .eq("bus_no", performDeptCode)
.eq("delete_flag", "0") .eq("delete_flag", "0")
.last("LIMIT 1")); );
if (organization != null) { if (organization != null) {
positionId = organization.getId(); positionId = organization.getId();
} else { } else {
@@ -410,7 +410,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
new QueryWrapper<InspectionLabApply>() new QueryWrapper<InspectionLabApply>()
.eq("apply_no", applyNo) .eq("apply_no", applyNo)
.eq("delete_flag", DelFlag.NO.getCode()) .eq("delete_flag", DelFlag.NO.getCode())
.last("LIMIT 1")); );
if (mainEntity == null) { if (mainEntity == null) {
return null; return null;
@@ -532,7 +532,7 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
// 1. 根据申请单号查询检验申请单信息 // 1. 根据申请单号查询检验申请单信息
InspectionLabApply inspectionLabApply = inspectionLabApplyService.getOne( InspectionLabApply inspectionLabApply = inspectionLabApplyService.getOne(
new QueryWrapper<InspectionLabApply>().eq("apply_no", applyNo) new QueryWrapper<InspectionLabApply>().eq("apply_no", applyNo)
.last("LIMIT 1")); );
if (inspectionLabApply == null) { if (inspectionLabApply == null) {
log.warn("未找到申请单号为 [{}] 的检验申请单", applyNo); log.warn("未找到申请单号为 [{}] 的检验申请单", applyNo);

View File

@@ -215,7 +215,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
// 限定当天日期,避免复诊患者匹配到历史队列记录 // 限定当天日期,避免复诊患者匹配到历史队列记录
.eq(TriageQueueItem::getQueueDate, LocalDate.now()) .eq(TriageQueueItem::getQueueDate, LocalDate.now())
.eq(TriageQueueItem::getDeleteFlag, "0") .eq(TriageQueueItem::getDeleteFlag, "0")
.last("LIMIT 1")); );
if (queueItem != null) { if (queueItem != null) {
// 使用 TriageQueueStatus 枚举替代原有硬编码数字 20保证状态值一致性 // 使用 TriageQueueStatus 枚举替代原有硬编码数字 20保证状态值一致性
queueItem.setStatus(TriageQueueStatus.IN_CLINIC.getValue()); queueItem.setStatus(TriageQueueStatus.IN_CLINIC.getValue());
@@ -282,7 +282,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
.eq(TriageQueueItem::getEncounterId, encounterId) .eq(TriageQueueItem::getEncounterId, encounterId)
.eq(TriageQueueItem::getQueueDate, LocalDate.now()) .eq(TriageQueueItem::getQueueDate, LocalDate.now())
.eq(TriageQueueItem::getDeleteFlag, "0") .eq(TriageQueueItem::getDeleteFlag, "0")
.last("LIMIT 1")); );
// 当天未找到时回退:不限日期查最近一条(防止跨日就诊队列项遗漏更新) // 当天未找到时回退:不限日期查最近一条(防止跨日就诊队列项遗漏更新)
if (queueItem == null) { if (queueItem == null) {
@@ -292,8 +292,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
.eq(TriageQueueItem::getEncounterId, encounterId) .eq(TriageQueueItem::getEncounterId, encounterId)
.eq(TriageQueueItem::getDeleteFlag, "0") .eq(TriageQueueItem::getDeleteFlag, "0")
.orderByDesc(TriageQueueItem::getQueueDate) .orderByDesc(TriageQueueItem::getQueueDate)
.last("LIMIT 1")); .last("LIMIT 1")
);
if (queueItem != null) { if (queueItem != null) {
log.warn("完诊:当天队列项未找到,回退使用最近队列记录 queueDate={}, id={}", log.warn("完诊:当天队列项未找到,回退使用最近队列记录 queueDate={}, id={}",
queueItem.getQueueDate(), queueItem.getId()); queueItem.getQueueDate(), queueItem.getId());

View File

@@ -77,10 +77,8 @@ public class DoctorStationAdviceController {
*/ */
@PostMapping(value = "/save-advice") @PostMapping(value = "/save-advice")
@RepeatSubmit(interval = 5000, message = "请勿重复提交医嘱,请稍候再试") @RepeatSubmit(interval = 5000, message = "请勿重复提交医嘱,请稍候再试")
public R<?> saveAdvice(@RequestBody AdviceSaveParam adviceSaveParam, public R<?> saveAdvice(@RequestBody AdviceSaveParam adviceSaveParam) {
@RequestParam(required = false, defaultValue = "1") String adviceOpType) { return iDoctorStationAdviceAppService.saveAdvice(adviceSaveParam, AdviceOpType.SAVE_ADVICE.getCode());
// 🔧 Bug #445 修复:使用前端传入的 adviceOpType 参数1=保存草稿2=签发),而非硬编码
return iDoctorStationAdviceAppService.saveAdvice(adviceSaveParam, adviceOpType);
} }
/** /**
@@ -228,9 +226,8 @@ public class DoctorStationAdviceController {
@RequestParam(value = "organizationId", required = false) Long organizationId, @RequestParam(value = "organizationId", required = false) Long organizationId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "500") Integer pageSize, @RequestParam(value = "pageSize", defaultValue = "500") Integer pageSize,
@RequestParam(value = "searchKey", defaultValue = "") String searchKey, @RequestParam(value = "searchKey", defaultValue = "") String searchKey) {
@RequestParam(value = "categoryCode", defaultValue = "23") String categoryCode) { return R.ok(iDoctorStationAdviceAppService.getExaminationPage(organizationId, pageNo, pageSize, searchKey));
return R.ok(iDoctorStationAdviceAppService.getExaminationPage(organizationId, pageNo, pageSize, searchKey, categoryCode));
} }
} }

View File

@@ -26,36 +26,34 @@ public class PendingEmrController {
* 获取待写病历列表 * 获取待写病历列表
* *
* @param doctorId 医生ID * @param doctorId 医生ID
* @param pageNo 当前页码 * @return 待写病历列表
* @param pageSize 每页条数
* @param patientName 患者姓名(可选)
* @return 待写病历分页数据
*/ */
@GetMapping("/pending-list") @GetMapping("/pending-list")
public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId, public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId) {
@RequestParam(defaultValue = "1") Integer pageNum, // 如果没有传递医生ID则使用当前登录用户ID
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String patientName) {
if (doctorId == null) { if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getPractitionerId(); doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
} }
return iDoctorStationEmrAppService.getPendingEmrList(doctorId, pageNum, pageSize, patientName);
// 调用服务获取待写病历列表
return iDoctorStationEmrAppService.getPendingEmrList(doctorId);
} }
/** /**
* 获取待写病历数量 * 获取待写病历数量
* *
* @param doctorId 医生ID * @param doctorId 医生ID
* @param patientName 患者姓名(可选)
* @return 待写病历数量 * @return 待写病历数量
*/ */
@GetMapping("/pending-count") @GetMapping("/pending-count")
public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId, public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId) {
@RequestParam(required = false) String patientName) { // 如果没有传递医生ID则使用当前登录用户ID
if (doctorId == null) { if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getPractitionerId(); doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
} }
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId, patientName);
// 调用服务获取待写病历数量
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId);
} }
/** /**

View File

@@ -198,10 +198,8 @@ public class AdviceBaseDto {
/** /**
* 所属科室 * 所属科室
*/ */
@Dict(dictTable = "adm_organization", dictCode = "id", dictText = "name")
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long orgId; private Long orgId;
private String orgId_dictText;
/** /**
* 所在位置 * 所在位置
@@ -250,9 +248,4 @@ public class AdviceBaseDto {
* 是否缺少取药科室配置(仅药品类型使用) * 是否缺少取药科室配置(仅药品类型使用)
*/ */
private Boolean pharmacyConfigMissing; private Boolean pharmacyConfigMissing;
/**
* 备注最长50字
*/
private String remark;
} }

View File

@@ -8,10 +8,6 @@ import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.annotation.Nulls;
@@ -30,14 +26,6 @@ public class AdviceSaveDto {
/** 医嘱类型 */ /** 医嘱类型 */
private Integer adviceType; // 1:药品 , 2: 耗材 , 3:项目 private Integer adviceType; // 1:药品 , 2: 耗材 , 3:项目
/**
* 医嘱开始时间
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** /**
* 请求id * 请求id
*/ */
@@ -282,11 +270,6 @@ public class AdviceSaveDto {
*/ */
private String sourceBillNo; private String sourceBillNo;
/**
* 备注最长50字
*/
private String remark;
/** /**
* 设置默认值 * 设置默认值
*/ */

View File

@@ -63,18 +63,6 @@ public class PatientDetailsDto {
*/ */
private String address; private String address;
/** 地址省 */
private String addressProvince;
/** 地址市 */
private String addressCity;
/** 地址区 */
private String addressDistrict;
/** 地址街道 */
private String addressStreet;
/** /**
* 工作单位 * 工作单位
*/ */

View File

@@ -22,12 +22,6 @@ public class RequestBaseDto {
*/ */
private Integer adviceType; // 1:药品 , 2: 耗材 , 3:项目 private Integer adviceType; // 1:药品 , 2: 耗材 , 3:项目
/**
* 医嘱开始时间
*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** /**
* 唯一标识 * 唯一标识
*/ */
@@ -127,11 +121,6 @@ public class RequestBaseDto {
* 请求状态 * 请求状态
*/ */
private Integer statusEnum; private Integer statusEnum;
/**
* 退回原因
*/
private String reasonText;
private String statusEnum_enumText; private String statusEnum_enumText;
/** /**
@@ -249,15 +238,4 @@ public class RequestBaseDto {
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long patientId; private Long patientId;
/**
* 停嘱医生
*/
private String stopUserName;
/**
* 停嘱时间
*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date stopTime;
} }

View File

@@ -23,9 +23,6 @@ public class SurgeryItemDto {
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long orgId; private Long orgId;
/** 所属科室名称 */
private String orgName;
/** 执行科室ID */ /** 执行科室ID */
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long positionId; private Long positionId;

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);
@@ -203,7 +203,6 @@ public interface DoctorStationAdviceAppMapper {
IPage<SurgeryItemDto> getExaminationPage(@Param("page") Page<SurgeryItemDto> page, IPage<SurgeryItemDto> getExaminationPage(@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);
@Param("categoryCode") String categoryCode);
} }

View File

@@ -1,22 +1,11 @@
package com.openhis.web.doctorstation.mapper; package com.openhis.web.doctorstation.mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
/** /**
* 医生站-电子病历 应用Mapper * 医生站-电子病历 应用Mapper
*/ */
@Repository @Repository
public interface DoctorStationEmrAppMapper { public interface DoctorStationEmrAppMapper {
List<Map<String, Object>> getPendingEmrList(@Param("doctorId") Long doctorId,
@Param("patientName") String patientName,
@Param("pageSize") Integer pageSize,
@Param("offset") Integer offset);
Long getPendingEmrCount(@Param("doctorId") Long doctorId,
@Param("patientName") String patientName);
} }

View File

@@ -58,7 +58,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.*; import java.util.*;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -179,25 +178,11 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
inpatientAdviceParam.setEncounterIds(null); inpatientAdviceParam.setEncounterIds(null);
Integer exeStatus = inpatientAdviceParam.getExeStatus(); Integer exeStatus = inpatientAdviceParam.getExeStatus();
inpatientAdviceParam.setExeStatus(null); inpatientAdviceParam.setExeStatus(null);
// 提取requestStatus手动处理支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询 // requestStatus由前端tab传入通过QueryWrapper自动添加到SQL外层WHERE过滤
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
inpatientAdviceParam.setRequestStatus(null);
// 构建查询条件 // 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
// 手动拼接requestStatus条件COMPLETED(3)时同时包含CHECK_VERIFIED(10)和PENDING_RECEIVE(11)
// UNION查询外层列名为request_statusT1.status_enum AS request_status不是status_enum
if (requestStatus != null) {
if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
queryWrapper.in("request_status",
RequestStatus.COMPLETED.getValue(), RequestStatus.CHECK_VERIFIED.getValue(),
RequestStatus.PENDING_RECEIVE.getValue());
} else {
queryWrapper.eq("request_status", requestStatus);
}
}
// 手动拼接住院患者id条件 // 手动拼接住院患者id条件
if (encounterIds != null && !encounterIds.isEmpty()) { if (encounterIds != null && !encounterIds.isEmpty()) {
List<Long> encounterIdList List<Long> encounterIdList
@@ -330,29 +315,19 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date(); Date checkDate = new Date();
if (!serviceRequestList.isEmpty()) { if (!serviceRequestList.isEmpty()) {
List<Long> serviceReqIds = serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(); // 更新服务请求状态已完成
// 先查询服务请求,按 categoryEnum 分流:检查类(23)走 CHECK_VERIFIED其余走 COMPLETED serviceRequestService.updateCompleteRequestStatus(
List<ServiceRequest> allServiceRequests = serviceRequestService.listByIds(serviceReqIds); serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate);
List<Long> checkReqIds = allServiceRequests.stream() List<ServiceRequest> serviceRequests = serviceRequestService
.filter(sr -> ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum())) .listByIds(serviceRequestList.stream().map(PerformInfoDto::getRequestId).collect(Collectors.toList()));
.map(ServiceRequest::getId).toList(); for (ServiceRequest serviceRequest : serviceRequests) {
List<Long> otherReqIds = allServiceRequests.stream() // 判断医嘱类型
.filter(sr -> !ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.map(ServiceRequest::getId).toList();
// 检查类 → 已校对CHECK_VERIFIED=10
if (!checkReqIds.isEmpty()) {
serviceRequestService.updateCheckVerifiedStatus(checkReqIds, practitionerId, checkDate);
}
// 其他类 → 已完成COMPLETED=3
if (!otherReqIds.isEmpty()) {
serviceRequestService.updateCompleteRequestStatus(otherReqIds, practitionerId, checkDate);
}
// 处理转科/出院等特殊医嘱
for (ServiceRequest serviceRequest : allServiceRequests) {
if (ActivityDefCategory.TRANSFER.getValue().equals(serviceRequest.getCategoryEnum())) { if (ActivityDefCategory.TRANSFER.getValue().equals(serviceRequest.getCategoryEnum())) {
// 更新患者状态 待转科
encounterService.updateEncounterStatus(serviceRequest.getEncounterId(), encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
EncounterZyStatus.PENDING_TRANSFER.getValue()); EncounterZyStatus.PENDING_TRANSFER.getValue());
} else if (ActivityDefCategory.DISCHARGE.getValue().equals(serviceRequest.getCategoryEnum())) { } else if (ActivityDefCategory.DISCHARGE.getValue().equals(serviceRequest.getCategoryEnum())) {
// 更新患者状态 待出院
encounterService.updateEncounterStatus(serviceRequest.getEncounterId(), encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
EncounterZyStatus.AWAITING_DISCHARGE.getValue()); EncounterZyStatus.AWAITING_DISCHARGE.getValue());
} }
@@ -384,24 +359,6 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
medRequestList.add(item); medRequestList.add(item);
} }
} }
// 校验医嘱是否已执行,已执行的医嘱需要先取消执行后才能退回
List<Long> allRequestIds = performInfoList.stream().map(PerformInfoDto::getRequestId).toList();
List<Procedure> allProcedures = procedureService.list(
new LambdaQueryWrapper<Procedure>()
.in(Procedure::getRequestId, allRequestIds)
.eq(Procedure::getDeleteFlag, "0"));
Set<Long> executedIds = allProcedures.stream()
.filter(p -> EventStatus.COMPLETED.getValue().equals(p.getStatusEnum()))
.map(Procedure::getId)
.collect(Collectors.toSet());
Set<Long> cancelledRefundIds = allProcedures.stream()
.filter(p -> EventStatus.CANCEL.getValue().equals(p.getStatusEnum()) && p.getRefundId() != null)
.map(Procedure::getRefundId)
.collect(Collectors.toSet());
executedIds.removeAll(cancelledRefundIds);
if (!executedIds.isEmpty()) {
return R.fail("该医嘱已执行,请先取消执行后再退回");
}
// 校验药品医嘱是否已发药,已发药的医嘱不允许退回 // 校验药品医嘱是否已发药,已发药的医嘱不允许退回
if (!medRequestList.isEmpty()) { if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList(); List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
@@ -415,21 +372,15 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
} }
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date(); Date checkDate = new Date();
// 从请求中提取退回原因(所有项目共享同一原因)
String backReason = performInfoList.stream()
.map(PerformInfoDto::getBackReason)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
if (!serviceRequestList.isEmpty()) { if (!serviceRequestList.isEmpty()) {
// 更新服务请求状态待发送 // 更新服务请求状态待发送
serviceRequestService.updateDraftStatus( serviceRequestService.updateDraftStatus(
serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate, backReason); serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate);
} }
if (!medRequestList.isEmpty()) { if (!medRequestList.isEmpty()) {
// 更新药品请求状态待发送 // 更新药品请求状态待发送
medicationRequestService.updateDraftStatusBatch( medicationRequestService.updateDraftStatusBatch(
medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate, backReason); medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate);
} }
return R.ok(null, "退回成功"); return R.ok(null, "退回成功");
} }
@@ -473,15 +424,6 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
List<ServiceRequestUseExe> actUseExeList = this.assemblyActivity(activityList); List<ServiceRequestUseExe> actUseExeList = this.assemblyActivity(activityList);
// 处理诊疗执行 // 处理诊疗执行
this.exeActivity(actUseExeList, exeDate); this.exeActivity(actUseExeList, exeDate);
// 检查类医嘱执行后,状态改为"待接收"PENDING_RECEIVE=11
List<Long> actReqIds = activityList.stream().map(AdviceExecuteDetailParam::getRequestId).toList();
List<ServiceRequest> executedReqs = serviceRequestService.listByIds(actReqIds);
List<Long> checkReqIds = executedReqs.stream()
.filter(sr -> ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.map(ServiceRequest::getId).toList();
if (!checkReqIds.isEmpty()) {
serviceRequestService.updatePendingReceiveStatus(checkReqIds);
}
} }
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱执行"})); return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱执行"}));

View File

@@ -78,10 +78,12 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
.map(notPerformedReason -> new DispenseInitDto.NotPerformedReasonOption(notPerformedReason.getValue(), .map(notPerformedReason -> new DispenseInitDto.NotPerformedReasonOption(notPerformedReason.getValue(),
notPerformedReason.getInfo())) notPerformedReason.getInfo()))
.collect(Collectors.toList()); .collect(Collectors.toList());
// 发药状态(汇总单:待配药→已提交,已发放→已发药) // 发药状态
List<DispenseStatusOption> dispenseStatusOptions = new ArrayList<>(); List<DispenseStatusOption> dispenseStatusOptions = new ArrayList<>();
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(), "已提交")); dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(),
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(), "已发药")); DispenseStatus.PREPARATION.getInfo()));
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(),
DispenseStatus.COMPLETED.getInfo()));
initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions); initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions);
return R.ok(initDto); return R.ok(initDto);
@@ -159,8 +161,8 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
new Page<>(pageNo, pageSize), queryWrapper, DispenseStatus.COMPLETED.getValue(), new Page<>(pageNo, pageSize), queryWrapper, DispenseStatus.COMPLETED.getValue(),
DispenseStatus.PREPARATION.getValue(), SupplyType.SUMMARY_DISPENSE.getValue()); DispenseStatus.PREPARATION.getValue(), SupplyType.SUMMARY_DISPENSE.getValue());
medicineSummaryFormPage.getRecords().forEach(e -> { medicineSummaryFormPage.getRecords().forEach(e -> {
// 发药状态(汇总单展示文案) // 发药状态
e.setStatusEnum_enumText(getSummaryFormStatusText(e.getStatusEnum())); e.setStatusEnum_enumText(EnumUtils.getInfoByValue(DispenseStatus.class, e.getStatusEnum()));
}); });
return R.ok(medicineSummaryFormPage); return R.ok(medicineSummaryFormPage);
} }
@@ -290,17 +292,4 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
} }
return R.ok(MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"取消"})); return R.ok(MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"取消"}));
} }
/**
* 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→已提交,发药→已发药)
*/
private String getSummaryFormStatusText(Integer statusEnum) {
if (DispenseStatus.PREPARATION.getValue().equals(statusEnum)) {
return "已提交";
}
if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) {
return "已发药";
}
return EnumUtils.getInfoByValue(DispenseStatus.class, statusEnum);
}
} }

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 查询源仓库库存表信息
// 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); 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

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

View File

@@ -86,13 +86,8 @@ public class OutpatientRecordServiceImpl implements IOutpatientRecordService {
// 处理就诊对象状态筛选 // 处理就诊对象状态筛选
if (outpatientRecordSearchParam.getSubjectStatusEnum() != null) { if (outpatientRecordSearchParam.getSubjectStatusEnum() != null) {
if (outpatientRecordSearchParam.getSubjectStatusEnum() == 0) {
// 前端选择"无状态"(0)时,过滤 status_enum IS NULL 的记录
queryWrapper.isNull("enc.status_enum");
} else {
queryWrapper.eq("enc.status_enum", outpatientRecordSearchParam.getSubjectStatusEnum()); queryWrapper.eq("enc.status_enum", outpatientRecordSearchParam.getSubjectStatusEnum());
} }
}
// 处理医生姓名查询(支持模糊查询) // 处理医生姓名查询(支持模糊查询)
if (outpatientRecordSearchParam.getDoctorName() != null && !outpatientRecordSearchParam.getDoctorName().isEmpty()) { if (outpatientRecordSearchParam.getDoctorName() != null && !outpatientRecordSearchParam.getDoctorName().isEmpty()) {

View File

@@ -133,13 +133,47 @@ public class PatientInformationServiceImpl implements IPatientInformationService
@Override @Override
public IPage<PatientBaseInfoDto> getPatientInfo(PatientBaseInfoDto patientBaseInfoDto, String searchKey, public IPage<PatientBaseInfoDto> getPatientInfo(PatientBaseInfoDto patientBaseInfoDto, String searchKey,
Integer pageNo, Integer pageSize, HttpServletRequest request) { Integer pageNo, Integer pageSize, HttpServletRequest request) {
// 构建基础查询条件 // 获取登录者信息
LoginUser loginUser = SecurityUtils.getLoginUser(); LoginUser loginUser = SecurityUtils.getLoginUser();
Long userId = loginUser.getUserId();
Integer tenantId = loginUser.getTenantId().intValue();
// 先构建基础查询条件
QueryWrapper<PatientBaseInfoDto> queryWrapper = HisQueryUtils.buildQueryWrapper( QueryWrapper<PatientBaseInfoDto> queryWrapper = HisQueryUtils.buildQueryWrapper(
patientBaseInfoDto, searchKey, new HashSet<>(Arrays.asList(CommonConstants.FieldName.Name, patientBaseInfoDto, searchKey, new HashSet<>(Arrays.asList(CommonConstants.FieldName.Name,
CommonConstants.FieldName.BusNo, CommonConstants.FieldName.PyStr, CommonConstants.FieldName.WbStr)), CommonConstants.FieldName.BusNo, CommonConstants.FieldName.PyStr, CommonConstants.FieldName.WbStr)),
request); request);
// 检查是否是精确ID查询从门诊挂号页面跳转时使用
boolean hasExactIdQuery = (patientBaseInfoDto.getId() != null);
// 只有非精确ID查询时才添加医生患者过滤条件
if (!hasExactIdQuery) {
// 查询当前用户对应的医生信息
LambdaQueryWrapper<com.openhis.administration.domain.Practitioner> practitionerQuery = new LambdaQueryWrapper<>();
practitionerQuery.eq(com.openhis.administration.domain.Practitioner::getUserId, userId);
// 使用list()避免TooManyResultsException异常然后取第一个记录
List<com.openhis.administration.domain.Practitioner> practitionerList = practitionerService.list(practitionerQuery);
com.openhis.administration.domain.Practitioner practitioner = practitionerList != null && !practitionerList.isEmpty() ? practitionerList.get(0) : null;
// 如果当前用户是医生,添加医生患者过滤条件
if (practitioner != null) {
// 查询该医生作为接诊医生ADMITTER, code="1"和挂号医生REGISTRATION_DOCTOR, code="12"的所有就诊记录的患者ID
List<Long> doctorPatientIds = patientManageMapper.getPatientIdsByPractitionerId(
practitioner.getId(),
Arrays.asList(ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode()),
tenantId);
if (doctorPatientIds != null && !doctorPatientIds.isEmpty()) {
// 添加患者ID过滤条件 - 注意:这里使用列名而不是表别名
queryWrapper.in("id", doctorPatientIds);
} else {
// 如果没有相关患者,返回空结果
queryWrapper.eq("id", -1); // 设置一个不存在的ID
}
}
// 如果不是医生,查询所有患者
}
IPage<PatientBaseInfoDto> patientInformationPage IPage<PatientBaseInfoDto> patientInformationPage
= patientManageMapper.getPatientPage(new Page<>(pageNo, pageSize), queryWrapper); = patientManageMapper.getPatientPage(new Page<>(pageNo, pageSize), queryWrapper);
@@ -235,7 +269,7 @@ public class PatientInformationServiceImpl implements IPatientInformationService
// log.debug("添加病人信息,patientInfoDto:{}", patientBaseInfoDto); // log.debug("添加病人信息,patientInfoDto:{}", patientBaseInfoDto);
// 如果患者没有输入身份证号则根据年龄自动生成 // 如果患者没有输入身份证号则根据年龄自动生成
String idCard = patientBaseInfoDto.getIdCard(); String idCard = patientBaseInfoDto.getIdCard();
if (idCard == null || idCard.length() < 6 || CommonConstants.Common.AREA_CODE.equals(idCard.substring(0, 6))) { if (idCard == null || CommonConstants.Common.AREA_CODE.equals(idCard.substring(0, 6))) {
if (patientBaseInfoDto.getAge() != null) { if (patientBaseInfoDto.getAge() != null) {
idCard = IdCardUtil.generateIdByAge(patientBaseInfoDto.getAge()); idCard = IdCardUtil.generateIdByAge(patientBaseInfoDto.getAge());
patientBaseInfoDto.setIdCard(idCard); patientBaseInfoDto.setIdCard(idCard);

View File

@@ -52,8 +52,7 @@ public class PendingMedicationDetailsAppServiceImpl implements IPendingMedicatio
Page<PendingMedicationPageDto> pendingMedicationPage = pendingMedicationDetailsMapper Page<PendingMedicationPageDto> pendingMedicationPage = pendingMedicationDetailsMapper
.selectPendingMedicationDetailsPage(new Page<>(pageNo, pageSize), queryWrapper, .selectPendingMedicationDetailsPage(new Page<>(pageNo, pageSize), queryWrapper,
DispenseStatus.IN_PROGRESS.getValue(), DispenseStatus.PREPARATION.getValue(), DispenseStatus.IN_PROGRESS.getValue(), DispenseStatus.PREPARATION.getValue(),
DispenseStatus.PREPARED.getValue(), DispenseStatus.SUMMARIZED.getValue(), DispenseStatus.PREPARED.getValue(), EncounterClass.AMB.getValue(), EncounterClass.IMP.getValue());
EncounterClass.AMB.getValue(), EncounterClass.IMP.getValue());
pendingMedicationPage.getRecords().forEach(e -> { pendingMedicationPage.getRecords().forEach(e -> {
// 发药类型 // 发药类型

View File

@@ -22,7 +22,6 @@ public interface PendingMedicationDetailsMapper {
* @param inProgress 发药类型:待发药 * @param inProgress 发药类型:待发药
* @param preparation 发药类型:待配药 * @param preparation 发药类型:待配药
* @param prepared 发药类型:已配药 * @param prepared 发药类型:已配药
* @param summarized 发药类型:已汇总
* @param amb 门诊类型 * @param amb 门诊类型
* @param imp 住院类型 * @param imp 住院类型
* @return 待发药明细 * @return 待发药明细
@@ -33,7 +32,6 @@ public interface PendingMedicationDetailsMapper {
@Param("inProgress") Integer inProgress, @Param("inProgress") Integer inProgress,
@Param("preparation") Integer preparation, @Param("preparation") Integer preparation,
@Param("prepared") Integer prepared, @Param("prepared") Integer prepared,
@Param("summarized") Integer summarized,
@Param("amb") Integer amb, @Param("amb") Integer amb,
@Param("imp") Integer imp); @Param("imp") Integer imp);

View File

@@ -69,12 +69,4 @@ public interface IAdviceManageAppService {
*/ */
R<?> stopRegAdvice(List<AdviceBatchOpParam> paramList); R<?> stopRegAdvice(List<AdviceBatchOpParam> paramList);
/**
* 住院医嘱取消停嘱(恢复)
*
* @param paramList 恢复参数
* @return 结果
*/
R<?> cancelStopRegAdvice(List<AdviceBatchOpParam> paramList);
} }

View File

@@ -18,7 +18,6 @@ import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.*; import com.openhis.common.enums.*;
import com.openhis.common.utils.EnumUtils; import com.openhis.common.utils.EnumUtils;
import com.openhis.common.utils.HisQueryUtils; import com.openhis.common.utils.HisQueryUtils;
import com.openhis.medication.domain.MedicationDispense;
import com.openhis.medication.domain.MedicationRequest; import com.openhis.medication.domain.MedicationRequest;
import com.openhis.medication.service.IMedicationDispenseService; import com.openhis.medication.service.IMedicationDispenseService;
import com.openhis.medication.service.IMedicationRequestService; import com.openhis.medication.service.IMedicationRequestService;
@@ -31,17 +30,12 @@ import com.openhis.web.regdoctorstation.dto.*;
import com.openhis.web.regdoctorstation.mapper.AdviceManageAppMapper; import com.openhis.web.regdoctorstation.mapper.AdviceManageAppMapper;
import com.openhis.web.regdoctorstation.utils.RegPrescriptionUtils; import com.openhis.web.regdoctorstation.utils.RegPrescriptionUtils;
import com.openhis.workflow.domain.DeviceRequest; import com.openhis.workflow.domain.DeviceRequest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.openhis.workflow.domain.ServiceRequest; import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IActivityDefinitionService; import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.domain.ActivityDefinition; 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;
@@ -91,9 +85,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
@Resource @Resource
IDeviceDispenseService iDeviceDispenseService; IDeviceDispenseService iDeviceDispenseService;
@Resource
IRequestFormService iRequestFormService;
/** /**
* 查询住院患者信息 * 查询住院患者信息
* *
@@ -275,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();
@@ -355,27 +314,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
/** /**
* 处理药品 * 处理药品
*/ */
/**
* 将 remark 合并到 contentJson 中,确保 Mapper 能从 content_json 提取 remark
*/
private String injectRemarkIntoContentJson(String contentJson, String remark) {
if (remark == null || remark.isEmpty() || contentJson == null || contentJson.isEmpty()) {
return contentJson;
}
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(contentJson);
if (node instanceof ObjectNode) {
((ObjectNode) node).put("remark", remark);
return mapper.writeValueAsString(node);
}
} catch (Exception e) {
log.warn("Failed to inject remark into contentJson: {}", e.getMessage());
}
return contentJson;
}
private List<String> handMedication(List<RegAdviceSaveDto> medicineList, Date startTime, Date authoredTime, private List<String> handMedication(List<RegAdviceSaveDto> medicineList, Date startTime, Date authoredTime,
Date curDate, String adviceOpType, Long organizationId, String signCode) { Date curDate, String adviceOpType, Long organizationId, String signCode) {
// 当前登录账号的科室id // 当前登录账号的科室id
@@ -440,7 +378,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
longMedicationRequest.setEffectiveDoseStart(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 longMedicationRequest.setEffectiveDoseStart(startTime); // 医嘱开始时间
longMedicationRequest longMedicationRequest
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.MEDICATION_RES_NO.getPrefix(), 4)); .setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.MEDICATION_RES_NO.getPrefix(), 4));
longMedicationRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 longMedicationRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
@@ -474,10 +412,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (longMedicationRequest.getId() == null) { if (longMedicationRequest.getId() == null) {
firstTimeSave = true; firstTimeSave = true;
} }
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
longMedicationRequest.setContentJson(injectRemarkIntoContentJson(longMedicationRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(longMedicationRequest); iMedicationRequestService.saveOrUpdate(longMedicationRequest);
if (firstTimeSave) { if (firstTimeSave) {
medRequestIdList.add(longMedicationRequest.getId().toString()); medRequestIdList.add(longMedicationRequest.getId().toString());
@@ -532,7 +466,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
tempMedicationRequest.setEffectiveDoseStart(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 tempMedicationRequest.setEffectiveDoseStart(startTime); // 医嘱开始时间
tempMedicationRequest tempMedicationRequest
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.MEDICATION_RES_NO.getPrefix(), 4)); .setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.MEDICATION_RES_NO.getPrefix(), 4));
tempMedicationRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 tempMedicationRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
@@ -565,10 +499,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (tempMedicationRequest.getId() == null) { if (tempMedicationRequest.getId() == null) {
firstTimeSave = true; firstTimeSave = true;
} }
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
tempMedicationRequest.setContentJson(injectRemarkIntoContentJson(tempMedicationRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(tempMedicationRequest); iMedicationRequestService.saveOrUpdate(tempMedicationRequest);
if (firstTimeSave) { if (firstTimeSave) {
medRequestIdList.add(tempMedicationRequest.getId().toString()); medRequestIdList.add(tempMedicationRequest.getId().toString());
@@ -648,7 +578,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
longServiceRequest.setOccurrenceStartTime(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 longServiceRequest.setOccurrenceStartTime(startTime); // 医嘱开始时间
longServiceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); longServiceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
longServiceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 longServiceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
longServiceRequest.setQuantity(new BigDecimal("1")); // 请求数量 | 诊疗的长期医嘱数量都是1 longServiceRequest.setQuantity(new BigDecimal("1")); // 请求数量 | 诊疗的长期医嘱数量都是1
@@ -672,7 +602,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
longServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST); longServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
} }
} }
longServiceRequest.setRemark(regAdviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(longServiceRequest); iServiceRequestService.saveOrUpdate(longServiceRequest);
if (longServiceRequest.getId() != null) { if (longServiceRequest.getId() != null) {
processedRequestIds.add(longServiceRequest.getId()); processedRequestIds.add(longServiceRequest.getId());
@@ -700,7 +629,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
tempServiceRequest.setOccurrenceStartTime(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 tempServiceRequest.setOccurrenceStartTime(startTime); // 医嘱开始时间
tempServiceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); tempServiceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
tempServiceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 tempServiceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
tempServiceRequest.setQuantity(regAdviceSaveDto.getQuantity()); // 请求数量 tempServiceRequest.setQuantity(regAdviceSaveDto.getQuantity()); // 请求数量
@@ -724,7 +653,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
tempServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST); tempServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
} }
} }
tempServiceRequest.setRemark(regAdviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(tempServiceRequest); iServiceRequestService.saveOrUpdate(tempServiceRequest);
if (tempServiceRequest.getId() != null) { if (tempServiceRequest.getId() != null) {
processedRequestIds.add(tempServiceRequest.getId()); processedRequestIds.add(tempServiceRequest.getId());
@@ -847,7 +775,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者 deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生 deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室 deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室
deviceRequest.setReqAuthoredTime(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 deviceRequest.setReqAuthoredTime(startTime); // 医嘱开始时间
deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室 deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室
deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id
@@ -856,10 +784,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
} }
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest); iDeviceRequestService.saveOrUpdate(deviceRequest);
} }
@@ -890,7 +814,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者 deviceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生 deviceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室 deviceRequest.setOrgId(regAdviceSaveDto.getFounderOrgId()); // 开方人科室
deviceRequest.setReqAuthoredTime(regAdviceSaveDto.getStartTime() != null ? regAdviceSaveDto.getStartTime() : startTime); // 医嘱开始时间 deviceRequest.setReqAuthoredTime(startTime); // 医嘱开始时间
deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室 deviceRequest.setPerformLocation(regAdviceSaveDto.getLocationId()); // 发放科室
deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id deviceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id deviceRequest.setPackageId(regAdviceSaveDto.getPackageId()); // 组套id
@@ -899,10 +823,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
} }
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest); iDeviceRequestService.saveOrUpdate(deviceRequest);
// 保存时,保存耗材费用项 // 保存时,保存耗材费用项
@@ -1059,7 +979,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
if (!medicineRequestIds.isEmpty()) { if (!medicineRequestIds.isEmpty()) {
// 根据请求id更新请求状态 // 根据请求id更新请求状态
iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null, null); iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null);
} }
if (!activityRequestIds.isEmpty()) { if (!activityRequestIds.isEmpty()) {
// 根据请求id更新请求状态 // 根据请求id更新请求状态
@@ -1086,14 +1006,8 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
*/ */
@Override @Override
public R<?> stopRegAdvice(List<AdviceBatchOpParam> paramList) { public R<?> stopRegAdvice(List<AdviceBatchOpParam> paramList) {
// 获取停嘱时间:优先从前端传入的 stopTime否则用当前时间 // 当前时间
Date stopTime = paramList.stream() Date date = new Date();
.map(AdviceBatchOpParam::getStopTime)
.filter(Objects::nonNull)
.findFirst()
.orElse(new Date());
// 获取当前操作用户昵称作为停嘱医生
String stopUserName = SecurityUtils.getNickName();
// 药品 // 药品
List<AdviceBatchOpParam> medicineList = paramList.stream() List<AdviceBatchOpParam> medicineList = paramList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList()); .filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
@@ -1108,112 +1022,15 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); = activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
if (!medicineRequestIds.isEmpty()) { if (!medicineRequestIds.isEmpty()) {
iMedicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>() iMedicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
.in(MedicationRequest::getId, medicineRequestIds) .in(MedicationRequest::getId, medicineRequestIds).set(MedicationRequest::getEffectiveDoseEnd, date)
.set(MedicationRequest::getEffectiveDoseEnd, stopTime) .set(MedicationRequest::getStatusEnum, RequestStatus.STOPPED.getValue()));
.set(MedicationRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
.set(MedicationRequest::getUpdateBy, stopUserName));
} }
if (!activityRequestIds.isEmpty()) { if (!activityRequestIds.isEmpty()) {
iServiceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>() iServiceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, activityRequestIds) .in(ServiceRequest::getId, activityRequestIds).set(ServiceRequest::getOccurrenceEndTime, date)
.set(ServiceRequest::getOccurrenceEndTime, stopTime) .set(ServiceRequest::getStatusEnum, RequestStatus.STOPPED.getValue()));
.set(ServiceRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
.set(ServiceRequest::getUpdateBy, stopUserName));
} }
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱停止"})); return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱停止"}));
}
/**
* 住院医嘱取消停嘱(恢复)
*
* 核心业务逻辑:
* 1. 护士站校验:护士站尚未对该医嘱的停止进行"停止核对/确认"(即 dispense 状态未进入已发药/完成状态)
* 2. 药房端校验:药房尚未对该停嘱单进行退药接收/退费入库确认
* 3. 若校验通过,将医嘱状态复原为"已签发";清空停嘱时间与停嘱医生字段;
* 同时自动作废已生成的待发药退回/退药申请
*
* @param paramList 恢复参数
* @return 结果
*/
@Override
public R<?> cancelStopRegAdvice(List<AdviceBatchOpParam> paramList) {
// 药品
List<AdviceBatchOpParam> medicineList = paramList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
List<Long> medicineRequestIds
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
// 诊疗包含护理adviceType=26
List<AdviceBatchOpParam> activityList = paramList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
.collect(Collectors.toList());
List<Long> activityRequestIds
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
// ============ 前置校验 ============
// 1. 护士站校验:查询药品发放记录,确认护士站是否已执行停止核对(发药)
if (!medicineRequestIds.isEmpty()) {
List<MedicationDispense> dispenseList = iMedicationDispenseService.selectByRequestIdList(medicineRequestIds);
for (MedicationDispense dispense : dispenseList) {
// 如果发放状态 >= COMPLETED(4),说明护士站已发药/已确认停止
if (dispense.getStatusEnum() != null && dispense.getStatusEnum() >= DispenseStatus.COMPLETED.getValue()
&& !DispenseStatus.ON_HOLD.getValue().equals(dispense.getStatusEnum())
&& !DispenseStatus.STOPPED.getValue().equals(dispense.getStatusEnum())
&& !DispenseStatus.CANCELLED.getValue().equals(dispense.getStatusEnum())) {
throw new ServiceException("护士站已确认停止该医嘱,无法取消停嘱!");
}
// 2. 药房端校验:如果已有退药/退费记录,说明药房已处理
if (DispenseStatus.RETURNED.getValue().equals(dispense.getStatusEnum())
|| DispenseStatus.REFUNDED.getValue().equals(dispense.getStatusEnum())
|| DispenseStatus.PART_REFUND.getValue().equals(dispense.getStatusEnum())) {
throw new ServiceException("药房已完成退药处理,无法取消停嘱!");
}
}
}
// ============ 执行恢复 ============
if (!medicineRequestIds.isEmpty()) {
// 恢复药品请求状态为"已发送"(ACTIVE=2),清空停嘱时间和更新人
iMedicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
.in(MedicationRequest::getId, medicineRequestIds)
.set(MedicationRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
.set(MedicationRequest::getEffectiveDoseEnd, null)
.set(MedicationRequest::getUpdateBy, null));
// 作废/删除与这些药品请求相关的待退药发放记录
List<MedicationDispense> relatedDispenseList = iMedicationDispenseService.selectByRequestIdList(medicineRequestIds);
for (MedicationDispense dispense : relatedDispenseList) {
if (DispenseStatus.PENDING_REFUND.getValue().equals(dispense.getStatusEnum())
|| DispenseStatus.CANCELLED.getValue().equals(dispense.getStatusEnum())
|| DispenseStatus.ON_HOLD.getValue().equals(dispense.getStatusEnum())) {
// 将待退药/暂停/撤回的记录标记为草稿,或删除
iMedicationDispenseService.update(new LambdaUpdateWrapper<MedicationDispense>()
.eq(MedicationDispense::getId, dispense.getId())
.set(MedicationDispense::getStatusEnum, DispenseStatus.DRAFT.getValue())
.set(MedicationDispense::getStatusChangedTime, new Date()));
}
// 如果 dispense 已处于 STOPPED(6) 状态,也恢复为草稿以重新触发配药流程
if (DispenseStatus.STOPPED.getValue().equals(dispense.getStatusEnum())) {
iMedicationDispenseService.update(new LambdaUpdateWrapper<MedicationDispense>()
.eq(MedicationDispense::getId, dispense.getId())
.set(MedicationDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue())
.set(MedicationDispense::getStatusChangedTime, new Date()));
}
}
}
if (!activityRequestIds.isEmpty()) {
// 恢复诊疗请求状态为"已发送"(ACTIVE=2),清空停嘱时间和更新人
iServiceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, activityRequestIds)
.set(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
.set(ServiceRequest::getOccurrenceEndTime, null)
.set(ServiceRequest::getUpdateBy, null));
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱恢复"}));
} }
} }

View File

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.core.common.enums.DelFlag;
import com.core.common.exception.ServiceException; import com.core.common.exception.ServiceException;
import com.core.common.utils.AssignSeqUtil; import com.core.common.utils.AssignSeqUtil;
import com.core.common.utils.MessageUtils; import com.core.common.utils.MessageUtils;
@@ -18,8 +17,6 @@ import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.*; import com.openhis.common.enums.*;
import com.openhis.document.domain.RequestForm; import com.openhis.document.domain.RequestForm;
import com.openhis.document.service.IRequestFormService; import com.openhis.document.service.IRequestFormService;
import com.openhis.lab.domain.Specimen;
import com.openhis.lab.service.ISpecimenService;
import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams; import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams;
import com.openhis.web.doctorstation.utils.AdviceUtils; import com.openhis.web.doctorstation.utils.AdviceUtils;
import com.openhis.web.regdoctorstation.appservice.IRequestFormManageAppService; import com.openhis.web.regdoctorstation.appservice.IRequestFormManageAppService;
@@ -70,39 +67,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Resource @Resource
IActivityDefinitionService iActivityDefinitionService; IActivityDefinitionService iActivityDefinitionService;
@Resource
ISpecimenService iSpecimenService;
/**
* 校验当前用户是否有权操作该申请单(申请者本人或管理员)
*/
private R<?> validateRequestFormPermission(RequestForm requestForm) {
if (SecurityUtils.isAdmin(SecurityUtils.getUserId())) {
return null;
}
Long currentPractitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Long requesterId = requestForm.getRequesterId();
if (currentPractitionerId == null || requesterId == null
|| !currentPractitionerId.equals(requesterId)) {
return R.fail("无操作权限,仅申请开立者或管理员可操作");
}
return null;
}
/**
* 校验关联医嘱是否已采证(存在已采集/已接收标本则不可撤回)
*/
private boolean hasCollectedSpecimen(List<Long> serviceRequestIds) {
if (serviceRequestIds == null || serviceRequestIds.isEmpty()) {
return false;
}
long count = iSpecimenService.count(
new LambdaQueryWrapper<Specimen>()
.in(Specimen::getServiceId, serviceRequestIds)
.ge(Specimen::getCollectionStatusEnum, SpecCollectStatus.COLLECTED.getValue()));
return count > 0;
}
/** /**
* 保存申请单 * 保存申请单
* *
@@ -117,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<>();
@@ -129,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);
} }
} }
@@ -155,13 +127,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.fail("无待签发的医嘱,该申请单不可编辑"); return R.fail("无待签发的医嘱,该申请单不可编辑");
} }
} else { } else {
// 根据申请单类型生成不同前缀的单 // 检查申请单JC检查+ Z住院标识+ yyMMdd日期+ 5位顺序
String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date()); String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
AssignSeqEnum seqEnum = ActivityDefCategory.PROCEDURE.getCode().equals(typeCode) int seq = assignSeqUtil.getSeqNoByDay(AssignSeqEnum.CHECK_APPLY_NO.getPrefix());
? AssignSeqEnum.SURGERY_APPLY_NO prescriptionNo = "JCZ" + dateStr + String.format("%05d", seq);
: AssignSeqEnum.CHECK_APPLY_NO;
int seq = assignSeqUtil.getSeqNoByDay(seqEnum.getPrefix());
prescriptionNo = seqEnum.getPrefix() + dateStr + String.format("%05d", seq);
} }
// 当前时间 // 当前时间
@@ -207,9 +176,6 @@ 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 块生成手术医嘱,避免重复
boolean isSurgeryRequest = ActivityDefCategory.PROCEDURE.getCode().equals(typeCode);
if (!isSurgeryRequest) {
for (ActivitySaveDto activitySaveDto : activityList) { for (ActivitySaveDto activitySaveDto : activityList) {
serviceRequest = new ServiceRequest(); serviceRequest = new ServiceRequest();
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue()); serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());
@@ -279,7 +245,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams);
} }
} }
}
// 如果是手术申请单,需要额外生成手术医嘱 // 如果是手术申请单,需要额外生成手术医嘱
log.info("【调试】判断手术医嘱生成条件: typeCode={}, PROCEDURE.code={}, typeCode类型={}, PROCEDURE.code类型={}", log.info("【调试】判断手术医嘱生成条件: typeCode={}, PROCEDURE.code={}, typeCode类型={}, PROCEDURE.code类型={}",
@@ -361,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);
} }
@@ -410,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);
@@ -452,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("");
@@ -472,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);
} }
@@ -567,17 +508,12 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
if (requestForm == null) { if (requestForm == null) {
return R.fail("申请单不存在"); return R.fail("申请单不存在");
} }
R<?> permissionResult = validateRequestFormPermission(requestForm);
if (permissionResult != null) {
return permissionResult;
}
String prescriptionNo = requestForm.getPrescriptionNo(); String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest含子项 // 查询该申请单下所有 ServiceRequest含子项
List<ServiceRequest> serviceRequests = iServiceRequestService.list( List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>() new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo) .eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (serviceRequests == null || serviceRequests.isEmpty()) { if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱"); return R.fail("未找到关联的诊疗医嘱");
} }
@@ -607,7 +543,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
// 4. 删除申请单 // 4. 删除申请单
iRequestFormService.removeById(requestFormId); iRequestFormService.removeById(requestFormId);
log.info("申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo); log.info("申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("删除成功"); return R.ok("删除成功");
} }
@@ -620,47 +556,32 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
if (requestForm == null) { if (requestForm == null) {
return R.fail("申请单不存在"); return R.fail("申请单不存在");
} }
R<?> permissionResult = validateRequestFormPermission(requestForm);
if (permissionResult != null) {
return permissionResult;
}
String prescriptionNo = requestForm.getPrescriptionNo(); String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest // 查询该申请单下所有 ServiceRequest
List<ServiceRequest> serviceRequests = iServiceRequestService.list( List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>() new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo) .eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (serviceRequests == null || serviceRequests.isEmpty()) { if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱"); return R.fail("未找到关联的诊疗医嘱");
} }
// 校验:只有已签发(status=2)的申请单可撤回
boolean allActive = serviceRequests.stream()
.allMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!allActive) {
return R.fail("只有已签发状态的申请单可撤回");
}
// 将所有 ServiceRequest 状态改回待签发(DRAFT=0)
List<Long> serviceRequestIds = serviceRequests.stream() List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList()); .map(ServiceRequest::getId).collect(Collectors.toList());
iServiceRequestService.update(
// 校验:标本已采集则不可撤回
if (hasCollectedSpecimen(serviceRequestIds)) {
return R.fail("标本已采集,无法撤回");
}
// 校验任一ServiceRequest为ACTIVE(status=2)即可撤回与SQL的EXISTS逻辑一致
boolean hasActive = serviceRequests.stream()
.anyMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!hasActive) {
return R.fail("只有已签发且未采证的申请单可撤回");
}
// 将所有已签发的 ServiceRequest 状态改回待签发,与申请单展示状态同步
boolean updated = iServiceRequestService.update(
new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()), new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()),
new LambdaUpdateWrapper<ServiceRequest>() new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds) .in(ServiceRequest::getId, serviceRequestIds));
.eq(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue()));
if (!updated) {
return R.fail("撤回失败,医嘱状态已变更,请刷新后重试");
}
log.info("申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo); log.info("申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("撤回成功"); return R.ok("撤回成功");
} }

View File

@@ -28,7 +28,6 @@ import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.util.CollectionUtils;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -162,7 +161,7 @@ public class SpecialAdviceAppServiceImpl implements ISpecialAdviceAppService {
adviceBaseDto.setAdviceDefinitionId(definitionId); // 医嘱定义id adviceBaseDto.setAdviceDefinitionId(definitionId); // 医嘱定义id
// 对应的诊疗医嘱信息 // 对应的诊疗医嘱信息
activityAdviceBaseDto = iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, null, null, activityAdviceBaseDto = iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, null, null,
null, null, 1, 1, null, List.of(3), null, null).getRecords().get(0); null, null, 1, 1, Whether.NO.getValue(), List.of(3), null, null).getRecords().get(0);
// 逻辑1---------------------直接新增 // 逻辑1---------------------直接新增
longServiceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态 longServiceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态
longServiceRequest.setOccurrenceStartTime(startTime); // 医嘱开始时间 longServiceRequest.setOccurrenceStartTime(startTime); // 医嘱开始时间
@@ -209,7 +208,7 @@ public class SpecialAdviceAppServiceImpl implements ISpecialAdviceAppService {
adviceBaseDto.setAdviceDefinitionId(definitionId); // 医嘱定义id adviceBaseDto.setAdviceDefinitionId(definitionId); // 医嘱定义id
// 对应的诊疗医嘱信息 // 对应的诊疗医嘱信息
activityAdviceBaseDto = iDoctorStationAdviceAppService activityAdviceBaseDto = iDoctorStationAdviceAppService
.getAdviceBaseInfo(adviceBaseDto, null, null, null, null, 1, 1, null, List.of(3), null, null) .getAdviceBaseInfo(adviceBaseDto, null, null, null, null, 1, 1, Whether.NO.getValue(), List.of(3), null, null)
.getRecords().get(0); .getRecords().get(0);
longServiceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态 longServiceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态
@@ -349,7 +348,7 @@ public class SpecialAdviceAppServiceImpl implements ISpecialAdviceAppService {
adviceBaseDto.setAdviceDefinitionId(transferOrganizationDefinitionId); // 医嘱定义id adviceBaseDto.setAdviceDefinitionId(transferOrganizationDefinitionId); // 医嘱定义id
// 转科的医嘱信息 // 转科的医嘱信息
AdviceBaseDto activityAdviceBaseDto = iDoctorStationAdviceAppService AdviceBaseDto activityAdviceBaseDto = iDoctorStationAdviceAppService
.getAdviceBaseInfo(adviceBaseDto, null, null, null, null, 1, 1, null, List.of(3), null, null) .getAdviceBaseInfo(adviceBaseDto, null, null, null, null, 1, 1, Whether.NO.getValue(), List.of(3), null, null)
.getRecords().get(0); .getRecords().get(0);
// 保存转科医嘱请求 // 保存转科医嘱请求
ServiceRequest serviceRequest = new ServiceRequest(); ServiceRequest serviceRequest = new ServiceRequest();
@@ -401,7 +400,7 @@ public class SpecialAdviceAppServiceImpl implements ISpecialAdviceAppService {
// 计划出院时间 // 计划出院时间
Date endTime = leaveHospitalParam.getEndTime(); Date endTime = leaveHospitalParam.getEndTime();
if (endTime == null) { if (endTime == null) {
endTime = new Date(); endTime = endTime;
} }
// 就诊id // 就诊id
Long encounterId = leaveHospitalParam.getEncounterId(); Long encounterId = leaveHospitalParam.getEncounterId();
@@ -430,12 +429,9 @@ public class SpecialAdviceAppServiceImpl implements ISpecialAdviceAppService {
} }
// 出院的医嘱信息 // 出院的医嘱信息
List<AdviceBaseDto> adviceList = iDoctorStationAdviceAppService.getAdviceBaseInfo(null, null, null, AdviceBaseDto activityAdviceBaseDto = iDoctorStationAdviceAppService.getAdviceBaseInfo(null, null, null,
List.of(transferOrganizationDefinitionId), null, 1, 1, null, List.of(3), null, null).getRecords(); List.of(transferOrganizationDefinitionId), null, 1, 1, Whether.NO.getValue(), List.of(3), null, null).getRecords()
if (CollectionUtils.isEmpty(adviceList)) { .get(0);
return R.fail("未找到出院医嘱定义数据,请确认诊疗目录中已配置出院医嘱");
}
AdviceBaseDto activityAdviceBaseDto = adviceList.get(0);
// 保存出院医嘱请求 // 保存出院医嘱请求
ServiceRequest serviceRequest = new ServiceRequest(); ServiceRequest serviceRequest = new ServiceRequest();
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态 serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());// 请求状态

View File

@@ -143,15 +143,4 @@ public class AdviceManageController {
return iAdviceManageAppService.stopRegAdvice(paramList); return iAdviceManageAppService.stopRegAdvice(paramList);
} }
/**
* 住院医嘱取消停嘱(恢复)
*
* @param paramList 恢复参数
* @return 结果
*/
@PostMapping(value = "/cancel-stop-reg-advice")
public R<?> cancelStopRegAdvice(@RequestBody List<AdviceBatchOpParam> paramList) {
return iAdviceManageAppService.cancelStopRegAdvice(paramList);
}
} }

View File

@@ -143,23 +143,14 @@ public class RequestFormManageController {
* 查询手术申请单 * 查询手术申请单
* *
* @param encounterId 就诊id * @param encounterId 就诊id
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/手术项目名称模糊匹配)
* @return 手术申请单 * @return 手术申请单
*/ */
@GetMapping(value = "/get-surgery") @GetMapping(value = "/get-surgery")
public R<?> getSurgeryRequestForm( public R<?> getSurgeryRequestForm(@RequestParam(required = false) Long encounterId) {
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) { if (encounterId == null) {
return R.fail("就诊ID不能为空"); return R.fail("就诊ID不能为空");
} }
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROCEDURE.getCode(), startDate, endDate, status, keyword)); return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROCEDURE.getCode()));
} }
/** /**
* 分页查询手术申请单全局不需要encounterId用于门诊手术安排查找弹窗 * 分页查询手术申请单全局不需要encounterId用于门诊手术安排查找弹窗
@@ -203,8 +194,8 @@ public class RequestFormManageController {
* @return 结果 * @return 结果
*/ */
@PostMapping(value = "/delete") @PostMapping(value = "/delete")
public R<?> deleteRequestForm(@RequestBody Map<String, Object> data) { public R<?> deleteRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.deleteRequestForm(parseLong(data.get("requestFormId"))); return iRequestFormManageAppService.deleteRequestForm(data.get("requestFormId"));
} }
/** /**
@@ -214,24 +205,7 @@ public class RequestFormManageController {
* @return 结果 * @return 结果
*/ */
@PostMapping(value = "/withdraw") @PostMapping(value = "/withdraw")
public R<?> withdrawRequestForm(@RequestBody Map<String, Object> data) { public R<?> withdrawRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.withdrawRequestForm(parseLong(data.get("requestFormId"))); return iRequestFormManageAppService.withdrawRequestForm(data.get("requestFormId"));
}
private Long parseLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
return null;
}
} }
} }

View File

@@ -1,13 +1,10 @@
package com.openhis.web.regdoctorstation.dto; package com.openhis.web.regdoctorstation.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.Date;
/** /**
* 医嘱批量操作参数类 * 医嘱批量操作参数类
*/ */
@@ -24,10 +21,4 @@ public class AdviceBatchOpParam {
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long requestId; private Long requestId;
/**
* 停嘱时间
*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date stopTime;
} }

View File

@@ -50,9 +50,4 @@ public class RegRequestBaseDto extends RequestBaseDto {
private String doseUnitCode; private String doseUnitCode;
private String doseUnitCode_dictText; private String doseUnitCode_dictText;
/**
* 备注最长50字
*/
private String remark;
} }

View File

@@ -31,8 +31,8 @@ public class HomeController {
HomeStatisticsDto statisticsDto = homeStatisticsService.getHomeStatistics(); HomeStatisticsDto statisticsDto = homeStatisticsService.getHomeStatistics();
// 获取待写病历数量 // 获取待写病历数量
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long userId = SecurityUtils.getLoginUser().getUserId();
R<?> pendingEmrCount = doctorStationEmrAppService.getPendingEmrCount(practitionerId, null); R<?> pendingEmrCount = doctorStationEmrAppService.getPendingEmrCount(userId);
// 将待写病历数量添加到统计数据中 // 将待写病历数量添加到统计数据中
statisticsDto.setPendingEmr((Integer) pendingEmrCount.getData()); statisticsDto.setPendingEmr((Integer) pendingEmrCount.getData());

View File

@@ -74,6 +74,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getTenantId, tenantId) .eq(TriageQueueItem::getTenantId, tenantId)
.eq(TriageQueueItem::getQueueDate, qd) .eq(TriageQueueItem::getQueueDate, qd)
.eq(TriageQueueItem::getDeleteFlag, "0") .eq(TriageQueueItem::getDeleteFlag, "0")
.ne(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue())
.orderByAsc(TriageQueueItem::getQueueOrder); .orderByAsc(TriageQueueItem::getQueueOrder);
// 如果指定了科室,按科室过滤;否则查询所有科室(全科模式) // 如果指定了科室,按科室过滤;否则查询所有科室(全科模式)
@@ -91,6 +92,14 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
} }
}); });
} }
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
if (list != null && !list.isEmpty()) {
int beforeSize = list.size();
list = list.stream()
.filter(item -> !TriageQueueStatus.COMPLETED.getValue().equals(item.getStatus()))
.collect(java.util.stream.Collectors.toList());
}
return R.ok(list); return R.ok(list);
} }

View File

@@ -262,7 +262,7 @@
AND T1.inventory_status_enum != 3 AND T1.inventory_status_enum != 3
AND T1.delete_flag = '0' AND T1.delete_flag = '0'
<choose> <choose>
<when test="lotNumber != null and lotNumber != ''"> <when test="lotNumber != null">
AND T1.lot_number = #{lotNumber} AND T1.lot_number = #{lotNumber}
</when> </when>
</choose> </choose>

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}
@@ -516,8 +516,6 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name, 'med_medication_definition' AS advice_table_name,
T1.medication_id AS advice_definition_id T1.medication_id AS advice_definition_id
, T1.content_json::jsonb ->> 'remark' AS remark
, T1.back_reason AS reason_text
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -579,8 +577,6 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name, 'med_medication_definition' AS advice_table_name,
T3.ID AS advice_definition_id T3.ID AS advice_definition_id
, T2.content_json::jsonb ->> 'remark' AS remark
, T2.back_reason AS reason_text
FROM adm_charge_item AS T1 FROM adm_charge_item AS T1
INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0' INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0'
LEFT JOIN med_medication_definition AS T3 ON T3.ID = T2.medication_id AND T3.delete_flag = '0' LEFT JOIN med_medication_definition AS T3 ON T3.ID = T2.medication_id AND T3.delete_flag = '0'
@@ -588,9 +584,6 @@
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.service_table = #{MED_MEDICATION_REQUEST} AND T1.service_table = #{MED_MEDICATION_REQUEST}
<if test="historyFlag == '0'.toString()"> <if test="historyFlag == '0'.toString()">
<if test="generateSourceEnum != null">
AND (T2.generate_source_enum IS NULL OR T2.generate_source_enum = #{generateSourceEnum})
</if>
AND T1.encounter_id = #{encounterId} AND T1.encounter_id = #{encounterId}
</if> </if>
<if test="historyFlag == '1'.toString()"> <if test="historyFlag == '1'.toString()">
@@ -644,8 +637,6 @@
CI.patient_id AS patient_id, CI.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name, 'adm_device_definition' AS advice_table_name,
CI.product_id AS advice_definition_id CI.product_id AS advice_definition_id
, NULL AS remark
, NULL AS reason_text
FROM adm_charge_item AS CI FROM adm_charge_item AS CI
LEFT JOIN adm_charge_item_definition CID ON CID.id = CI.definition_id AND CID.delete_flag = '0' LEFT JOIN adm_charge_item_definition CID ON CID.id = CI.definition_id AND CID.delete_flag = '0'
LEFT JOIN wor_device_request DR ON DR.id = CI.service_id AND DR.delete_flag = '0' LEFT JOIN wor_device_request DR ON DR.id = CI.service_id AND DR.delete_flag = '0'
@@ -700,8 +691,6 @@
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name, 'adm_device_definition' AS advice_table_name,
T1.device_def_id AS advice_definition_id T1.device_def_id AS advice_definition_id
, T1.content_json::jsonb ->> 'remark' AS remark
, NULL AS reason_text
FROM wor_device_request AS T1 FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -757,9 +746,7 @@
T1.encounter_id AS encounter_id, T1.encounter_id AS encounter_id,
T1.patient_id AS patient_id, T1.patient_id AS patient_id,
'wor_activity_definition' AS advice_table_name, 'wor_activity_definition' AS advice_table_name,
T1.activity_id AS advice_definition_id, T1.activity_id AS advice_definition_id
T1.remark AS remark
, T1.reason_text AS reason_text
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id ON T2.ID = T1.activity_id
@@ -884,7 +871,6 @@
</select> </select>
<!-- 手术项目专用分页查询:仅查手术 + 定价,无库存/草稿库存/取药科室等无关逻辑 --> <!-- 手术项目专用分页查询:仅查手术 + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
<!-- 使用 LIMIT/OFFSET 直接查询,避免 MyBatis Plus 分页插件的 COUNT 开销 -->
<select id="getSurgeryPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto"> <select id="getSurgeryPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
SELECT DISTINCT ON (t1.ID) SELECT DISTINCT ON (t1.ID)
t1.ID AS advice_definition_id, t1.ID AS advice_definition_id,
@@ -907,15 +893,15 @@
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>
<!-- 检查/检验项目专用分页查询:仅查指定 category_code + 定价,无库存/草稿库存/取药科室等无关逻辑 --> <!-- 检查项目专用分页查询:仅查检查(23) + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
<select id="getExaminationPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto"> <select id="getExaminationPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
SELECT DISTINCT ON (t1.ID) SELECT DISTINCT ON (t1.ID)
t1.ID AS advice_definition_id, t1.ID AS advice_definition_id,
t1.NAME AS advice_name, t1.NAME AS advice_name,
t1.org_id AS org_id, t1.org_id AS org_id,
t3.name AS org_name,
t1.org_id AS position_id, t1.org_id AS position_id,
t2.ID AS charge_item_definition_id, t2.ID AS charge_item_definition_id,
t2.price AS price, t2.price AS price,
@@ -927,11 +913,8 @@
AND t2.delete_flag = '0' AND t2.delete_flag = '0'
AND t2.status_enum = #{statusEnum} AND t2.status_enum = #{statusEnum}
AND t2.instance_table = 'wor_activity_definition' AND t2.instance_table = 'wor_activity_definition'
LEFT JOIN adm_organization t3
ON t3.id = t1.org_id
AND t3.delete_flag = '0'
WHERE t1.delete_flag = '0' WHERE t1.delete_flag = '0'
AND t1.category_code = #{categoryCode} AND t1.category_code = '23'
<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>

View File

@@ -4,38 +4,4 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationEmrAppMapper"> <mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationEmrAppMapper">
<select id="getPendingEmrList" resultType="java.util.HashMap">
SELECT e.id AS "encounterId",
e.patient_id AS "patientId",
p.name AS "patientName",
p.gender_enum AS "gender",
p.birth_date AS "birthDate",
e.create_time AS "registerTime",
e.bus_no AS "busNo"
FROM adm_encounter e
INNER JOIN adm_encounter_participant ep ON e.id = ep.encounter_id AND ep.practitioner_id = #{doctorId}
LEFT JOIN adm_patient p ON e.patient_id = p.id
LEFT JOIN doc_emr emr ON e.id = emr.encounter_id
WHERE e.status_enum = 2
AND emr.id IS NULL
<if test="patientName != null and patientName != ''">
AND p.name LIKE CONCAT('%', #{patientName}, '%')
</if>
ORDER BY e.create_time DESC
LIMIT #{pageSize} OFFSET #{offset}
</select>
<select id="getPendingEmrCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM adm_encounter e
INNER JOIN adm_encounter_participant ep ON e.id = ep.encounter_id AND ep.practitioner_id = #{doctorId}
LEFT JOIN adm_patient p ON e.patient_id = p.id
LEFT JOIN doc_emr emr ON e.id = emr.encounter_id
WHERE e.status_enum = 2
AND emr.id IS NULL
<if test="patientName != null and patientName != ''">
AND p.name LIKE CONCAT('%', #{patientName}, '%')
</if>
</select>
</mapper> </mapper>

View File

@@ -11,10 +11,6 @@
p.birth_date, p.birth_date,
p.phone, p.phone,
p.address, p.address,
p.address_province,
p.address_city,
p.address_district,
p.address_street,
p.work_company, p.work_company,
p.nationality_code, p.nationality_code,
p.marital_status_enum, p.marital_status_enum,

View File

@@ -51,7 +51,7 @@
ON T5.medication_def_id = T6.id ON T5.medication_def_id = T6.id
AND T5.delete_flag = '0' AND T5.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.status_enum IN (#{inProgress}, #{preparation}, #{prepared}, #{summarized}) AND T1.status_enum IN (#{inProgress}, #{preparation}, #{prepared})
ORDER BY T1.create_time DESC ORDER BY T1.create_time DESC
) AS T7 ) AS T7
${ew.customSqlSegment} ${ew.customSqlSegment}

View File

@@ -216,11 +216,8 @@
ccd.name AS condition_definition_name, ccd.name AS condition_definition_name,
T1.therapy_enum AS therapyEnum, T1.therapy_enum AS therapyEnum,
T1.sort_number AS sort_number, T1.sort_number AS sort_number,
T1.effective_dose_start AS start_time,
T1.based_on_id AS based_on_id, T1.based_on_id AS based_on_id,
T1.medication_id AS advice_definition_id, T1.medication_id AS advice_definition_id
T1.effective_dose_end AS stop_time,
T1.update_by AS stop_user_name
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -272,11 +269,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
2 AS therapyEnum, 2 AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.req_authored_time AS start_time,
T1.based_on_id AS based_on_id, T1.based_on_id AS based_on_id,
T1.device_def_id AS advice_definition_id, T1.device_def_id AS advice_definition_id
NULL::timestamp AS stop_time,
'' AS stop_user_name
FROM wor_device_request AS T1 FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -325,11 +319,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
COALESCE(T1.therapy_enum, 2) AS therapyEnum, COALESCE(T1.therapy_enum, 2) AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.occurrence_start_time AS start_time,
T1.based_on_id AS based_on_id, T1.based_on_id AS based_on_id,
T1.activity_id AS advice_definition_id, T1.activity_id AS advice_definition_id
T1.occurrence_end_time AS stop_time,
T1.update_by AS stop_user_name
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id ON T2.ID = T1.activity_id

View File

@@ -35,36 +35,21 @@
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0' WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 8 AND ws.status_enum = 8
) THEN 6 ) THEN 6
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 5
) THEN 7
WHEN EXISTS ( WHEN EXISTS (
SELECT 1 FROM wor_service_request ws SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0' WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 3 AND ws.status_enum = 3
) THEN 5 ) THEN 5
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 12
) THEN 4
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 11
) THEN 3
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 10
) THEN 2
WHEN EXISTS ( WHEN EXISTS (
SELECT 1 FROM wor_service_request ws SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0' WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 2 AND ws.status_enum = 2
) THEN 1 ) THEN 1
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 5
) THEN 7
ELSE 0 ELSE 0
END AS computed_status END AS computed_status
FROM doc_request_form AS drf FROM doc_request_form AS drf
@@ -72,6 +57,8 @@
AND ae.delete_flag = '0' AND ae.delete_flag = '0'
LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id
AND ap.delete_flag = '0' AND ap.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.prescription_no = drf.prescription_no
AND wsr.delete_flag = '0'
WHERE drf.delete_flag = '0' WHERE drf.delete_flag = '0'
AND drf.encounter_id = #{encounterId} AND drf.encounter_id = #{encounterId}
AND drf.type_code = #{typeCode} AND drf.type_code = #{typeCode}

View File

@@ -768,4 +768,36 @@ public class CommonConstants {
Integer ACCOUNT_DEVICE_TYPE = 6; Integer ACCOUNT_DEVICE_TYPE = 6;
} }
/**
* 号源槽位状态 (adm_schedule_slot.status)
*/
public interface SlotStatus {
/** 可用 / 待预约 */
Integer AVAILABLE = 0;
/** 已预约 */
Integer BOOKED = 1;
/** 已取消 / 已停诊 */
Integer CANCELLED = 2;
/** 已签到 / 已取号 */
Integer CHECKED_IN = 3;
/** 已锁定 */
Integer LOCKED = 4;
/** 已退号 */
Integer RETURNED = 5;
}
/**
* 预约订单状态 (order_main.status)
*/
public interface AppointmentOrderStatus {
/** 已预约 (待就诊) */
Integer BOOKED = 1;
/** 已取号 (已就诊) */
Integer CHECKED_IN = 2;
/** 已取消 */
Integer CANCELLED = 3;
/** 已退号 */
Integer RETURNED = 4;
}
} }

View File

@@ -274,10 +274,6 @@ public enum AssignSeqEnum {
* 检查申请单号(住院) * 检查申请单号(住院)
*/ */
CHECK_APPLY_NO("72", "检查申请单号", "JCZ"), CHECK_APPLY_NO("72", "检查申请单号", "JCZ"),
/**
* 手术申请单号(住院)
*/
SURGERY_APPLY_NO("73", "手术申请单号", "SSZ"),
/** /**
* b 病历文书 * b 病历文书
*/ */

View File

@@ -57,22 +57,7 @@ public enum RequestStatus implements HisEnumInterface {
/** /**
* 未知 * 未知
*/ */
UNKNOWN(9, "unknown", "未知"), UNKNOWN(9, "unknown", "未知");
/**
* 已校对(检查申请:护士校对通过)
*/
CHECK_VERIFIED(10, "check_verified", "已校对"),
/**
* 待接收(检查申请:等待医技科室接单)
*/
PENDING_RECEIVE(11, "pending_receive", "待接收"),
/**
* 已接收(检查申请:医技科室已接单)
*/
CHECK_RECEIVED(12, "check_received", "已接收");
@EnumValue @EnumValue
private final Integer value; private final Integer value;

View File

@@ -1,57 +0,0 @@
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 号源槽位状态 (adm_schedule_slot.status)
*
* <pre>
* 状态流转:
* 预约 → 0→2 (锁定), locked_num+1
* 取消预约 → 2→0 (释放), locked_num-1
* 签到 → 2→1 (已约), locked_num-1, booked_num+1
* 退号 → 1→0 (释放), booked_num-1
* 停诊 → 任意→4 (已取消)
* </pre>
*
* @author system
*/
@Getter
@AllArgsConstructor
public enum SlotStatus implements HisEnumInterface {
/** 可用 / 待预约 */
AVAILABLE(0, "available", "可用"),
/** 已预约 */
BOOKED(1, "booked", "已预约"),
/** 已锁定 (约而不付:预约后锁定号源) */
LOCKED(2, "locked", "已锁定"),
/** 已签到 / 已取号 */
CHECKED_IN(3, "checked_in", "已签到"),
/** 已取消 / 已停诊 */
CANCELLED(4, "cancelled", "已取消"),
/** 已退号 */
RETURNED(5, "returned", "已退号");
private final Integer value;
private final String code;
private final String info;
public static SlotStatus getByValue(Integer value) {
if (value == null) {
return null;
}
for (SlotStatus val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -36,14 +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);
/**
* 根据诊疗定义id查询所有执行科室列表跨科室
*
* @param activityDefinitionId 诊疗定义id
* @return 执行科室列表
*/
List<OrganizationLocation> getOrgLocListByActivityDefinitionId(Long activityDefinitionId);
} }

View File

@@ -53,25 +53,11 @@ 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>()
.eq(OrganizationLocation::getOrganizationId, organizationId)
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
}
/**
* 根据诊疗定义id查询所有执行科室列表跨科室
*
* @param activityDefinitionId 诊疗定义id
* @return 执行科室列表
*/
@Override
public List<OrganizationLocation> getOrgLocListByActivityDefinitionId(Long activityDefinitionId) {
return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>() return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>()
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId)); .eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
} }

View File

@@ -10,11 +10,10 @@ import org.springframework.stereotype.Repository;
public interface SchedulePoolMapper extends BaseMapper<SchedulePool> { public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
/** /**
* 按号源池实时重算统计值。 * 按号源池实时重算统计值,避免并发场景下计数漂移
* *
* @param poolId 号源池ID * 说明available_num 在当前项目中可能为数据库生成列,因此这里仅维护
* @param bookedStatus 已约状态值,由 SlotStatus.BOOKED.getValue() 传入 * booked_num / locked_num剩余号由数据库或查询逻辑计算。
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/ */
@Update(""" @Update("""
UPDATE adm_schedule_pool p UPDATE adm_schedule_pool p
@@ -24,22 +23,20 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
FROM adm_schedule_slot s FROM adm_schedule_slot s
WHERE s.pool_id = p.id WHERE s.pool_id = p.id
AND s.delete_flag = '0' AND s.delete_flag = '0'
AND s.status = #{bookedStatus} AND s.status = 1
), 0), ), 0),
locked_num = COALESCE(( locked_num = COALESCE((
SELECT COUNT(1) SELECT COUNT(1)
FROM adm_schedule_slot s FROM adm_schedule_slot s
WHERE s.pool_id = p.id WHERE s.pool_id = p.id
AND s.delete_flag = '0' AND s.delete_flag = '0'
AND s.status = #{lockedStatus} AND s.status = 3
), 0), ), 0),
update_time = now() update_time = now()
WHERE p.id = #{poolId} WHERE p.id = #{poolId}
AND p.delete_flag = '0' AND p.delete_flag = '0'
""") """)
int refreshPoolStats(@Param("poolId") Long poolId, int refreshPoolStats(@Param("poolId") Long poolId);
@Param("bookedStatus") Integer bookedStatus,
@Param("lockedStatus") Integer lockedStatus);
/** /**
* 签到时更新号源池统计:锁定数-1已预约数+1 * 签到时更新号源池统计:锁定数-1已预约数+1

View File

@@ -22,12 +22,9 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
TicketSlotDTO selectTicketSlotById(@Param("id") Long id); TicketSlotDTO selectTicketSlotById(@Param("id") Long id);
/** /**
* 原子抢占槽位:仅当当前状态=0(待约)时,更新为目标锁定状态 * 原子抢占槽位:仅当当前状态=0(可用)时,更新为1(已预约)
*
* @param slotId 槽位ID
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/ */
int lockSlotForBooking(@Param("slotId") Long slotId, @Param("lockedStatus") Integer lockedStatus); int lockSlotForBooking(@Param("slotId") Long slotId);
/** /**
* 按主键更新槽位状态。 * 按主键更新槽位状态。
@@ -38,15 +35,11 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
* 更新槽位状态并记录签到时间 * 更新槽位状态并记录签到时间
* *
* @param slotId 槽位ID * @param slotId 槽位ID
* @param status 目标状态,由 SlotStatus.BOOKED.getValue() 传入 * @param status 状态
* @param checkInTime 签到时间 * @param checkInTime 签到时间
* @param requiredStatus 前置状态,由 SlotStatus.LOCKED.getValue() 传入
* @return 结果 * @return 结果
*/ */
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, @Param("status") Integer status, @Param("checkInTime") Date checkInTime);
@Param("status") Integer status,
@Param("checkInTime") Date checkInTime,
@Param("requiredStatus") Integer requiredStatus);
/** /**
* 根据槽位ID查询所属号源池ID。 * 根据槽位ID查询所属号源池ID。

View File

@@ -1,12 +1,10 @@
package com.openhis.clinical.service.impl; package com.openhis.clinical.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.appointmentmanage.domain.AppointmentConfig; import com.openhis.appointmentmanage.domain.AppointmentConfig;
import com.openhis.appointmentmanage.service.IAppointmentConfigService; import com.openhis.appointmentmanage.service.IAppointmentConfigService;
import com.openhis.appointmentmanage.domain.TicketSlotDTO; import com.openhis.appointmentmanage.domain.TicketSlotDTO;
import com.openhis.appointmentmanage.domain.SchedulePool;
import com.openhis.appointmentmanage.domain.ScheduleSlot; import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper; import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper; import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
@@ -15,7 +13,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.TicketMapper; import com.openhis.clinical.mapper.TicketMapper;
import com.openhis.clinical.service.IOrderService; import com.openhis.clinical.service.IOrderService;
import com.openhis.clinical.service.ITicketService; import com.openhis.clinical.service.ITicketService;
import com.openhis.common.enums.SlotStatus; import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.OrderStatus; import com.openhis.common.enums.OrderStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -179,7 +177,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
logger.error("安全拦截号源底库核对失败slotId: {}", slotId); logger.error("安全拦截号源底库核对失败slotId: {}", slotId);
throw new RuntimeException("号源数据不存在"); throw new RuntimeException("号源数据不存在");
} }
if (slot.getSlotStatus() != null && SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.AVAILABLE) { if (slot.getSlotStatus() != null && !SlotStatus.AVAILABLE.equals(slot.getSlotStatus())) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占"); throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
} }
if (Boolean.TRUE.equals(slot.getIsStopped())) { if (Boolean.TRUE.equals(slot.getIsStopped())) {
@@ -207,7 +205,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
} }
// 原子抢占:避免并发下同一槽位被重复预约 // 原子抢占:避免并发下同一槽位被重复预约
int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId, SlotStatus.LOCKED.getValue()); int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId);
if (lockRows <= 0) { if (lockRows <= 0) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占"); throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
} }
@@ -262,15 +260,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
throw new RuntimeException("预约成功但号源回填订单失败,请重试"); throw new RuntimeException("预约成功但号源回填订单失败,请重试");
} }
// 6. 预约成功后 locked_num+1原子递增替代全量 recount避免并发计数漂移 refreshPoolStatsBySlotId(slotId);
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("locked_num = locked_num + 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return 1; return 1;
} }
@@ -287,8 +277,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
if (slot == null) { if (slot == null) {
throw new RuntimeException("号源槽位不存在"); throw new RuntimeException("号源槽位不存在");
} }
// 只有锁定态(2)的号源可以取消预约 if (slot.getSlotStatus() == null || !SlotStatus.BOOKED.equals(slot.getSlotStatus())) {
if (slot.getSlotStatus() == null || SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.LOCKED) {
throw new RuntimeException("号源不可取消预约"); throw new RuntimeException("号源不可取消预约");
} }
@@ -303,7 +292,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "患者取消预约"); orderService.cancelAppointmentOrder(order.getId(), "患者取消预约");
} }
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue()); int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE);
if (updated > 0) { if (updated > 0) {
refreshPoolStatsBySlotId(slotId); refreshPoolStatsBySlotId(slotId);
} }
@@ -329,14 +318,11 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue()); orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date()); orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 只有锁定态(2)的号源才能签到,签到时 2→1(LOCKED→BOOKED) // 2. 查询号源槽位信息
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId); ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
throw new RuntimeException("号源状态异常,无法签到");
}
// 3. 更新号源槽位状态 2→1LOCKED→BOOKED已预约=已签到) // 3. 更新号源槽位状态为已签到,记录签到时间
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue()); scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN, new Date());
// 4. 更新号源池统计:锁定数-1已预约数+1 // 4. 更新号源池统计:锁定数-1已预约数+1
if (slot != null && slot.getPoolId() != null) { if (slot != null && slot.getPoolId() != null) {
@@ -365,7 +351,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "医生停诊"); orderService.cancelAppointmentOrder(order.getId(), "医生停诊");
} }
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED.getValue()); int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED);
if (updated > 0) { if (updated > 0) {
refreshPoolStatsBySlotId(slotId); refreshPoolStatsBySlotId(slotId);
} }
@@ -378,7 +364,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
private void refreshPoolStatsBySlotId(Long slotId) { private void refreshPoolStatsBySlotId(Long slotId) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId); Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) { if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId, SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue()); schedulePoolMapper.refreshPoolStats(poolId);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -79,13 +79,11 @@ public class OpSchedule extends HisBaseEntity {
private String surgerySite; private String surgerySite;
/** 入院时间 */ /** 入院时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime admissionTime; private LocalDateTime admissionTime;
/** 入手术室时间 */ /** 入手术室时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime entryTime; private LocalDateTime entryTime;
/** 手术室编码 */ /** 手术室编码 */
@@ -144,23 +142,19 @@ public class OpSchedule extends HisBaseEntity {
private String assistant3Code; private String assistant3Code;
/** 手术开始时间 */ /** 手术开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime; private LocalDateTime startTime;
/** 手术结束时间 */ /** 手术结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime; private LocalDateTime endTime;
/** 麻醉开始时间 */ /** 麻醉开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime anesStart; private LocalDateTime anesStart;
/** 麻醉结束时间 */ /** 麻醉结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime anesEnd; private LocalDateTime anesEnd;
/** 手术状态 */ /** 手术状态 */

View File

@@ -173,9 +173,4 @@ public class ServiceRequest extends HisBaseEntity {
*/ */
private Integer generateSourceEnum; private Integer generateSourceEnum;
/**
* 备注最长50字
*/
private String remark;
} }

View File

@@ -39,22 +39,6 @@ public interface IServiceRequestService extends IService<ServiceRequest> {
*/ */
void updateCompleteRequestStatus(List<Long> serReqIdList, Long practitionerId, Date checkDate); void updateCompleteRequestStatus(List<Long> serReqIdList, Long practitionerId, Date checkDate);
/**
* 更新检查申请状态已校对护士校对检查申请后状态为CHECK_VERIFIED而非COMPLETED
*
* @param serReqIdList 服务请求id列表
* @param practitionerId 校对人
* @param checkDate 校对时间
*/
void updateCheckVerifiedStatus(List<Long> serReqIdList, Long practitionerId, Date checkDate);
/**
* 更新检查申请状态待接收护士执行检查申请后状态为PENDING_RECEIVE
*
* @param serReqIdList 服务请求id列表
*/
void updatePendingReceiveStatus(List<Long> serReqIdList);
/** /**
* 获取执行过的诊疗数据 * 获取执行过的诊疗数据
* *
@@ -109,7 +93,7 @@ public interface IServiceRequestService extends IService<ServiceRequest> {
* @param practitionerId 校对人 * @param practitionerId 校对人
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate, String backReason); void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate);
/** /**
* 更新服务状态:待发送 * 更新服务状态:待发送

View File

@@ -66,31 +66,6 @@ public class ServiceRequestServiceImpl extends ServiceImpl<ServiceRequestMapper,
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode())); .eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
} }
/**
* 更新检查申请状态已校对护士校对检查申请后状态为CHECK_VERIFIED而非COMPLETED
*
* @param serReqIdList 服务请求id列表
*/
@Override
public void updateCheckVerifiedStatus(List<Long> serReqIdList, Long practitionerId, Date checkDate) {
baseMapper.update(new ServiceRequest().setStatusEnum(RequestStatus.CHECK_VERIFIED.getValue())
.setPerformerCheckId(SecurityUtils.getLoginUser().getPractitionerId()).setCheckTime(DateUtils.getNowDate()),
new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serReqIdList)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
}
/**
* 更新检查申请状态待接收护士执行检查申请后状态为PENDING_RECEIVE
*
* @param serReqIdList 服务请求id列表
*/
@Override
public void updatePendingReceiveStatus(List<Long> serReqIdList) {
baseMapper.update(new ServiceRequest().setStatusEnum(RequestStatus.PENDING_RECEIVE.getValue()),
new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serReqIdList)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
}
/** /**
* 获取执行过的诊疗数据 * 获取执行过的诊疗数据
* *
@@ -197,15 +172,9 @@ public class ServiceRequestServiceImpl extends ServiceImpl<ServiceRequestMapper,
* @param checkDate 校对时间 * @param checkDate 校对时间
*/ */
@Override @Override
public void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate, String backReason) { public void updateDraftStatus(List<Long> serviceRequestIdList, Long practitionerId, Date checkDate) {
ServiceRequest updateEntity = new ServiceRequest() baseMapper.update(new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue())
.setStatusEnum(RequestStatus.DRAFT.getValue()) .setPerformerCheckId(SecurityUtils.getLoginUser().getPractitionerId()).setCheckTime(DateUtils.getNowDate()),
.setPerformerCheckId(SecurityUtils.getLoginUser().getPractitionerId())
.setCheckTime(DateUtils.getNowDate());
if (backReason != null && !backReason.isEmpty()) {
updateEntity.setReasonText(backReason);
}
baseMapper.update(updateEntity,
new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serviceRequestIdList) new LambdaUpdateWrapper<ServiceRequest>().in(ServiceRequest::getId, serviceRequestIdList)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode())); .eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
} }

View File

@@ -27,7 +27,6 @@
AND T1.delete_flag = '0' AND T1.delete_flag = '0'
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
AND T1.tenant_id = #{tenantId} AND T1.tenant_id = #{tenantId}
LIMIT 1
</select> </select>
</mapper> </mapper>

View File

@@ -4,17 +4,14 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.appointmentmanage.mapper.ScheduleSlotMapper"> <mapper namespace="com.openhis.appointmentmanage.mapper.ScheduleSlotMapper">
<!-- <!-- 统一状态值(兼容数字/英文字符串存储),输出 Integer避免 resultType 映射 NumberFormatException -->
统一状态值映射: DB 数值 → 规范化输出
0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
-->
<sql id="slotStatusNormExpr"> <sql id="slotStatusNormExpr">
CASE CASE
WHEN LOWER(CONCAT('', s.status)) IN ('0', 'unbooked', 'available') THEN 0 WHEN LOWER(CONCAT('', s.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', s.status)) IN ('1', 'booked') THEN 1 WHEN LOWER(CONCAT('', s.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'locked') THEN 2 WHEN LOWER(CONCAT('', s.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', s.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3 WHEN LOWER(CONCAT('', s.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4 WHEN LOWER(CONCAT('', s.status)) IN ('4', 'locked') THEN 4
WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5 WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5
ELSE NULL ELSE NULL
END END
@@ -34,9 +31,9 @@
CASE CASE
WHEN LOWER(CONCAT('', p.status)) IN ('0', 'unbooked', 'available') THEN 0 WHEN LOWER(CONCAT('', p.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', p.status)) IN ('1', 'booked') THEN 1 WHEN LOWER(CONCAT('', p.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'locked') THEN 2 WHEN LOWER(CONCAT('', p.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', p.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3 WHEN LOWER(CONCAT('', p.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4 WHEN LOWER(CONCAT('', p.status)) IN ('4', 'locked') THEN 4
WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5 WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5
ELSE NULL ELSE NULL
END END
@@ -152,11 +149,10 @@
s.id = #{id} s.id = #{id}
</select> </select>
<!-- 预约锁定: 0→#{lockedStatus} (AVAILABLE→LOCKED),由枚举传入 -->
<update id="lockSlotForBooking"> <update id="lockSlotForBooking">
UPDATE adm_schedule_slot UPDATE adm_schedule_slot
SET SET
status = #{lockedStatus}, status = 1,
update_time = now() update_time = now()
WHERE WHERE
id = #{slotId} id = #{slotId}
@@ -178,7 +174,6 @@
AND delete_flag = '0' AND delete_flag = '0'
</update> </update>
<!-- 签到: #{requiredStatus}→#{status} (LOCKED→BOOKED),前置条件由枚举传入 -->
<update id="updateSlotStatusAndCheckInTime"> <update id="updateSlotStatusAndCheckInTime">
UPDATE adm_schedule_slot UPDATE adm_schedule_slot
SET SET
@@ -187,7 +182,6 @@
update_time = NOW() update_time = NOW()
WHERE WHERE
id = #{slotId} id = #{slotId}
AND status = #{requiredStatus}
AND delete_flag = '0' AND delete_flag = '0'
</update> </update>
@@ -208,7 +202,7 @@
update_time = now() update_time = now()
WHERE WHERE
id = #{slotId} id = #{slotId}
AND status = 2 AND status = 1
AND delete_flag = '0' AND delete_flag = '0'
</update> </update>
@@ -305,16 +299,15 @@
<if test="query.phone != null and query.phone != ''"> <if test="query.phone != null and query.phone != ''">
AND o.phone LIKE CONCAT('%', #{query.phone}, '%') AND o.phone LIKE CONCAT('%', #{query.phone}, '%')
</if> </if>
<!-- 5. 时间过滤: 仅待约(0)受时间限制,已锁定(2)/已约(1)/已签到(3)/已退号(5)不受影响 --> <!-- 5. 按系统时间过滤Bug #398 #399 修复:仅未预约受时间过滤,已预约/已取号/已退号不受影响 -->
AND ( AND (
(<include refid="slotStatusNormExpr" /> = 0 AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW()))) (<include refid="slotStatusNormExpr" /> = 0 AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW())))
OR <include refid="slotStatusNormExpr" /> = 1 OR <include refid="slotStatusNormExpr" /> = 1
OR <include refid="slotStatusNormExpr" /> = 2
OR <include refid="slotStatusNormExpr" /> = 3 OR <include refid="slotStatusNormExpr" /> = 3
OR <include refid="slotStatusNormExpr" /> = 5 OR <include refid="slotStatusNormExpr" /> = 5
OR <include refid="orderStatusNormExpr" /> = 4 OR <include refid="orderStatusNormExpr" /> = 4
) )
<!-- 6. 状态筛选: unbooked(0) locked(2) booked(2) checked(1) cancelled(4) returned(5) --> <!-- 6. 状态过滤 -->
<if test="query.status != null and query.status != '' and query.status != 'all'"> <if test="query.status != null and query.status != '' and query.status != 'all'">
<choose> <choose>
<when test="'unbooked'.equals(query.status) or '未预约'.equals(query.status)"> <when test="'unbooked'.equals(query.status) or '未预约'.equals(query.status)">
@@ -325,15 +318,7 @@
) )
</when> </when>
<when test="'booked'.equals(query.status) or '已预约'.equals(query.status)"> <when test="'booked'.equals(query.status) or '已预约'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 2 AND <include refid="slotStatusNormExpr" /> = 1
AND <include refid="orderStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
)
</when>
<when test="'locked'.equals(query.status) or '已锁定'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 2
AND <include refid="orderStatusNormExpr" /> = 1 AND <include refid="orderStatusNormExpr" /> = 1
AND ( AND (
d.is_stopped IS NULL d.is_stopped IS NULL
@@ -341,7 +326,13 @@
) )
</when> </when>
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status)"> <when test="'checked'.equals(query.status) or '已取号'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 1 AND (
<include refid="slotStatusNormExpr" /> = 3
OR (
<include refid="slotStatusNormExpr" /> = 1
AND <include refid="orderStatusNormExpr" /> = 2
)
)
AND ( AND (
d.is_stopped IS NULL d.is_stopped IS NULL
OR d.is_stopped = FALSE OR d.is_stopped = FALSE
@@ -349,7 +340,7 @@
</when> </when>
<when test="'cancelled'.equals(query.status) or '已停诊'.equals(query.status) or '已取消'.equals(query.status)"> <when test="'cancelled'.equals(query.status) or '已停诊'.equals(query.status) or '已取消'.equals(query.status)">
AND ( AND (
<include refid="slotStatusNormExpr" /> = 4 <include refid="slotStatusNormExpr" /> = 2
OR d.is_stopped = TRUE OR d.is_stopped = TRUE
) )
</when> </when>

View File

@@ -162,24 +162,6 @@ export function getG(svg, viewConfig) {
// 设置数据 // 设置数据
export function getData(allData) { export function getData(allData) {
const rowsData = allData.rows; // allData, '【全部数据】' const rowsData = allData.rows; // allData, '【全部数据】'
// 兼容旧数据:将旧 typeCode 映射到新 typeCode心率 004→014脉搏 005→002呼吸 006→001
const OLD_CODE_MAP = { '004': '014', '005': '002', '006': '001' };
rowsData.forEach(row => {
if (row.rowBOS) {
const prependItems = [];
row.rowBOS.forEach(item => {
const newCode = OLD_CODE_MAP[item.typeCode];
// 始终添加映射条目,用 unshift 插入数组头部
// 这样 getType 的 find() 优先匹配映射后的编码(如脉冲、呼吸)
// 即使存在同编码的旧条目(如血压舒张压用 002、收缩压用 001
// 映射后的脉搏(002)和呼吸(001)条目排在前面,确保图表正确渲染
if (newCode) {
prependItems.push({ ...item, typeCode: newCode });
}
});
row.rowBOS.unshift(...prependItems);
}
});
const infoData = allData.grParamBOS; const infoData = allData.grParamBOS;
const typesData = getTypeDatas(allData.types, allData.grParamBOS.beginDate); const typesData = getTypeDatas(allData.types, allData.grParamBOS.beginDate);
const selectOp = allData.selectOp; const selectOp = allData.selectOp;

View File

@@ -10,27 +10,15 @@
@click="clickAct" @click="clickAct"
> >
<div v-if="data.bedOperationalStatus==='U'"> <div v-if="data.bedOperationalStatus==='U'">
<img <img :src="emptyBed" class="pf_card_emptyBed_img">
:src="emptyBed" <div class="pf_card_emptyBed_text">{{ data.bedName }}</div>
class="pf_card_emptyBed_img"
>
<div class="pf_card_emptyBed_text">
{{ data.bedName }}
</div>
</div> </div>
<div v-else> <div v-else>
<div <div v-if="data.isDischarge" class="pf_card_discharge">
v-if="data.isDischarge"
class="pf_card_discharge"
>
<span style="margin-left: 6px"></span> <span style="margin-left: 6px"></span>
</div> </div>
<div class="pf_card_card"> <div class="pf_card_card">
<CardSign <CardSign :color="getBedBackColor(data.triageLevel)" :title="data.bedName" :tail="getDisplay(data.triageLevel)" />
:color="getBedBackColor(data.triageLevel)"
:title="data.bedName"
:tail="getDisplay(data.triageLevel)"
/>
</div> </div>
<div class="pf_card_nameSexAndAge"> <div class="pf_card_nameSexAndAge">
<span class="pf_card_name">{{ data.patientName }}</span> <span class="pf_card_name">{{ data.patientName }}</span>
@@ -40,48 +28,20 @@
<span style="margin-right: 16px">入室时间</span> <span style="margin-right: 16px">入室时间</span>
{{ moment(data.checkInWardTime).format('YYYY-MM-DD HH:mm') }} {{ moment(data.checkInWardTime).format('YYYY-MM-DD HH:mm') }}
</div> </div>
<div class="pf_card_noCode"> <div class="pf_card_noCode">{{ data.hisId }}</div>
{{ data.hisId }} <div class="pf_card_rescueTimeText">{{ rescueTimeText() }}</div>
</div> <div v-if="data.diag!==''" class="pf_card_diagnosis">
<div class="pf_card_rescueTimeText"> <div class="card-rectangle-text">{{ data.diag }}</div>
{{ rescueTimeText() }}
</div>
<div
v-if="data.diag!==''"
class="pf_card_diagnosis"
>
<div class="card-rectangle-text">
{{ data.diag }}
</div>
<span style="margin-left: 4px">(诊断)</span> <span style="margin-left: 4px">(诊断)</span>
</div> </div>
<div <div v-if="isNewSign()" class="card-rectangle"></div>
v-if="isNewSign()" <div v-if="is72HourSign()" class="card-rectangle2">超72H</div>
class="card-rectangle"
>
</div>
<div
v-if="is72HourSign()"
class="card-rectangle2"
>
超72H
</div>
<hr class="pf_card_line"> <hr class="pf_card_line">
<div class="pf_card_nursingMeasuresString"> <div class="pf_card_nursingMeasuresString">{{ getStringByCode(data.nursingMeasures, nursingMeasures) }}</div>
{{ getStringByCode(data.nursingMeasures, nursingMeasures) }} <div class="pf_card_specialArrangementString">{{ getStringByCode(data.specialArrangement, specialArrangementList) }}</div>
</div> <div v-if="false" class="pf_card_btn" @click="moreClick">更多</div>
<div class="pf_card_specialArrangementString">
{{ getStringByCode(data.specialArrangement, specialArrangementList) }}
</div>
<div
v-if="false"
class="pf_card_btn"
@click="moreClick"
>
更多
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>

View File

@@ -6,7 +6,7 @@
:data="item" :data="item"
:bed-config="bedConfig" :bed-config="bedConfig"
@click="clickAct" @click="clickAct"
@more-click="moreClickAct" @moreClick="moreClickAct"
/> />
</div> </div>
</template> </template>

View File

@@ -1,19 +1,9 @@
<template> <template>
<div class="printCard"> <div class="printCard">
<div <div ref="refQr" style="float: left; margin: 30px 15px">
ref="refQr" <img :src="emptyBed" style="height: 120px" class="pf_card_emptyBed_img">
style="float: left; margin: 30px 15px"
>
<img
:src="emptyBed"
style="height: 120px"
class="pf_card_emptyBed_img"
>
</div> </div>
<div <div class="printView_content" style=" margin: 30px 0">
class="printView_content"
style=" margin: 30px 0"
>
<div> <div>
<span>床号</span> <span>床号</span>
<span>{{ printData.bedName }}</span> <span>{{ printData.bedName }}</span>

View File

@@ -1,9 +1,6 @@
<template> <template>
<div class="recordBill"> <div class="recordBill">
<div <div id="div1" class="printView_header">
id="div1"
class="printView_header"
>
<div style="text-align: center; height: 40px"> <div style="text-align: center; height: 40px">
护理交接班 护理交接班
</div> </div>
@@ -21,83 +18,35 @@
/> />
</div> </div>
</div> </div>
<div <div id="div2" class="printView_content">
id="div2" <table border="1" cellSpacing="0" width="98%" cellPadding="1" style=" border-collapse:collapse; font-size: 14px" bordercolor="#333333">
class="printView_content"
>
<table
border="1"
cellSpacing="0"
width="98%"
cellPadding="1"
style=" border-collapse:collapse; font-size: 14px"
bordercolor="#333333"
>
<thead> <thead>
<TR> <TR>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 40px" align="center">类别</DIV>
style="width: 40px"
align="center"
>
类别
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 50px" align="center">床号</DIV>
style="width: 50px"
align="center"
>
床号
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 60px" align="center">姓名</DIV>
style="width: 60px"
align="center"
>
姓名
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 90px" align="center">主诉</DIV>
style="width: 90px"
align="center"
>
主诉
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 90px" align="center">既往史</DIV>
style="width: 90px"
align="center"
>
既往史
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 90px" align="center">诊断</DIV>
style="width: 90px"
align="center"
>
诊断
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 155px" align="center">交接信息</DIV>
style="width: 155px"
align="center"
>
交接信息
</DIV>
</TD> </TD>
</TR> </TR>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="item in printData.shiftRecordItems" :key="item.id">
v-for="item in printData.shiftRecordItems"
:key="item.id"
>
<td v-html="item.typeDisplay" /> <td v-html="item.typeDisplay" />
<td v-html="item.bedName" /> <td v-html="item.bedName" />
<td v-html="item.patientName" /> <td v-html="item.patientName" />

View File

@@ -1,9 +1,6 @@
<template> <template>
<div class="recordBill"> <div class="recordBill">
<div <div :id="'exeSheetTitle' + printData.id" class="printView_header">
:id="'exeSheetTitle' + printData.id"
class="printView_header"
>
<div style="text-align: center; height: 60px"> <div style="text-align: center; height: 60px">
{{ userStore.hospitalName }}医嘱执行单 {{ userStore.hospitalName }}医嘱执行单
</div> </div>
@@ -19,119 +16,51 @@
<span style="display: inline-block; width: 140px">性别{{!printData.patientInfo.gender? '':printData.patientInfo.gender.display}}</span> <span style="display: inline-block; width: 140px">性别{{!printData.patientInfo.gender? '':printData.patientInfo.gender.display}}</span>
</div>--> </div>-->
</div> </div>
<div <div :id="'exeSheet' + printData.id" class="printView_content">
:id="'exeSheet' + printData.id" <table border="1" cellSpacing="0" width="97%" cellPadding="1" style=" border-collapse:collapse; font-size: 13px" bordercolor="#333333">
class="printView_content"
>
<table
border="1"
cellSpacing="0"
width="97%"
cellPadding="1"
style=" border-collapse:collapse; font-size: 13px"
bordercolor="#333333"
>
<thead> <thead>
<TR> <TR>
<TD rowspan="1"> <TD rowspan="1">
<DIV style="width: 65px;text-align: center"> <DIV style="width: 65px;text-align: center">医嘱日期</DIV>
医嘱日期
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 120px" align="center">医嘱</DIV>
style="width: 120px"
align="center"
>
医嘱
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 10px" align="center" />
style="width: 10px"
align="center"
/>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 70px" align="center">嘱托</DIV>
style="width: 70px"
align="center"
>
嘱托
</DIV>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 60px" align="center">用量</DIV>
style="width: 60px"
align="center"
>
用量
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 40px" align="center">用法</DIV>
style="width: 40px"
align="center"
>
用法
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 40px" align="center">频次</DIV>
style="width: 40px"
align="center"
>
频次
</DIV>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 65px" align="center">开立医生</DIV>
style="width: 65px"
align="center"
>
开立医生
</DIV>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 65px" align="center">执行时间</DIV>
style="width: 65px"
align="center"
>
执行时间
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 65px" align="center">执行护士</DIV>
style="width: 65px"
align="center"
>
执行护士
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 55px" align="center">终止时间</DIV>
style="width: 55px"
align="center"
>
终止时间
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 55px" align="center">终止人</DIV>
style="width: 55px"
align="center"
>
终止人
</DIV>
</TD> </TD>
</TR> </TR>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="item in printData.recordData" :key="item.id">
v-for="item in printData.recordData"
:key="item.id"
>
<td v-html="item.moTime" /> <td v-html="item.moTime" />
<td v-html="item.orderName" /> <td v-html="item.orderName" />
<td v-html="item.flag" /> <td v-html="item.flag" />
@@ -141,20 +70,12 @@
<td v-html="item.frequency" /> <td v-html="item.frequency" />
<td :id="item.id"> <td :id="item.id">
<span v-if="(item.docSignImage === ''||item.docSignImage === null)">{{ item.moDocName }}</span> <span v-if="(item.docSignImage === ''||item.docSignImage === null)">{{ item.moDocName }}</span>
<img <img v-if="(item.docSignImage !== ''&&item.docSignImage !== null)" :src="'data:image/png;base64,'+ item.docSignImage" style="height: 100%; width: 100%;object-fit: cover;">
v-if="(item.docSignImage !== ''&&item.docSignImage !== null)"
:src="'data:image/png;base64,'+ item.docSignImage"
style="height: 100%; width: 100%;object-fit: cover;"
>
</td> </td>
<td v-html="item.occurrence" /> <td v-html="item.occurrence" />
<td :id="item.id"> <td :id="item.id">
<span v-if="(item.perNurserSignImage === ''||item.perNurserSignImage === null)">{{ item.performName }}</span> <span v-if="(item.perNurserSignImage === ''||item.perNurserSignImage === null)">{{ item.performName }}</span>
<img <img v-if="(item.perNurserSignImage !== ''&&item.perNurserSignImage !== null)" :src="'data:image/png;base64,'+ item.perNurserSignImage" style="height: 100%; width: 100%;object-fit: cover;">
v-if="(item.perNurserSignImage !== ''&&item.perNurserSignImage !== null)"
:src="'data:image/png;base64,'+ item.perNurserSignImage"
style="height: 100%; width: 100%;object-fit: cover;"
>
</td> </td>
<td /> <td />
<td /> <td />
@@ -170,6 +91,10 @@ import { simplePrint, PRINT_TEMPLATE } from '@/utils/printUtils.js'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
export default { export default {
setup() {
const userStore = useUserStore();
return { userStore };
},
props: { props: {
printData: { printData: {
type: Object, type: Object,
@@ -178,10 +103,6 @@ export default {
} }
} }
}, },
setup() {
const userStore = useUserStore();
return { userStore };
},
data() { data() {
return {} return {}
}, },

View File

@@ -12,51 +12,23 @@
</div> </div>
</div> </div>
<div style="display: block; width: 120px; height: 60px; float:left; "> <div style="display: block; width: 120px; height: 60px; float:left; ">
<div <div :id="getId(printData.id)" style="float: left; margin: 5px;" />
:id="getId(printData.id)"
style="float: left; margin: 5px;"
/>
<span style="float: left; margin: 5px">{{ printData.priority }}</span> <span style="float: left; margin: 5px">{{ printData.priority }}</span>
</div> </div>
</div> </div>
<div :id="printData.id + 'div2'"> <div :id="printData.id + 'div2'">
<table <table border="1" cellSpacing="0" width="390px" cellPadding="1" style="margin-left: 8px; border-collapse:collapse; table-layout: fixed; font-size: 14px" bordercolor="#333333">
border="1"
cellSpacing="0"
width="390px"
cellPadding="1"
style="margin-left: 8px; border-collapse:collapse; table-layout: fixed; font-size: 14px"
bordercolor="#333333"
>
<thead> <thead>
<TR> <TR>
<Th <Th style="width: 160px" v-html="'药品名称'" />
style="width: 160px" <Th style="width: 75px" v-html="'用量'" />
v-html="'药品名称'" <Th style="width: 10px" v-html="''" />
/> <Th style="width: 50px" v-html="'频次'" />
<Th <Th style="width: 75px" v-html="'用法'" />
style="width: 75px"
v-html="'用量'"
/>
<Th
style="width: 10px"
v-html="''"
/>
<Th
style="width: 50px"
v-html="'频次'"
/>
<Th
style="width: 75px"
v-html="'用法'"
/>
</TR> </TR>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="item in printData.orderDetail" :key="item.id">
v-for="item in printData.orderDetail"
:key="item.id"
>
<td v-html="item.orderName" /> <td v-html="item.orderName" />
<td v-html="item.doseOnce + item.doseUnit" /> <td v-html="item.doseOnce + item.doseUnit" />
<td v-html="item.flag" /> <td v-html="item.flag" />

View File

@@ -1,9 +1,6 @@
<template> <template>
<div class="recordBill"> <div class="recordBill">
<div <div id="div1" class="printView_header">
id="div1"
class="printView_header"
>
<div style="text-align: center; font-size: 20px; height: 40px"> <div style="text-align: center; font-size: 20px; height: 40px">
{{ userStore.hospitalName }}输液执行单 {{ userStore.hospitalName }}输液执行单
</div> </div>
@@ -16,88 +13,38 @@
<span style="margin-left: 18px">科室{{ printData.patientInfo.deptName }}</span> <span style="margin-left: 18px">科室{{ printData.patientInfo.deptName }}</span>
</div> </div>
</div> </div>
<div <div id="div2" class="printView_content">
id="div2" <table border="1" cellSpacing="0" cellPadding="1" style=" border-collapse:collapse; font-size: 14px" bordercolor="#333333">
class="printView_content"
>
<table
border="1"
cellSpacing="0"
cellPadding="1"
style=" border-collapse:collapse; font-size: 14px"
bordercolor="#333333"
>
<thead> <thead>
<TR style="height: 30px"> <TR style="height: 30px">
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 35px" align="center">时间</DIV>
style="width: 35px"
align="center"
>
时间
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 280px" align="center">药品名称</DIV>
style="width: 280px"
align="center"
>
药品名称
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 10px" align="center" />
style="width: 10px"
align="center"
/>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 55px" align="center">剂量</DIV>
style="width: 55px"
align="center"
>
剂量
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 30px" align="center">频次</DIV>
style="width: 30px"
align="center"
>
频次
</DIV>
</TD> </TD>
<TD colspan="1"> <TD colspan="1">
<DIV <DIV style="width: 55px" align="center">用法</DIV>
style="width: 55px"
align="center"
>
用法
</DIV>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 70px" align="center">执行时间</DIV>
style="width: 70px"
align="center"
>
执行时间
</DIV>
</TD> </TD>
<TD rowspan="1"> <TD rowspan="1">
<DIV <DIV style="width: 55px" align="center">执行人</DIV>
style="width: 55px"
align="center"
>
执行人
</DIV>
</TD> </TD>
</TR> </TR>
</thead> </thead>
<tbody style=" border-collapse:collapse;"> <tbody style=" border-collapse:collapse;">
<tr <tr v-for="item in printData.recordData" :key="item.id">
v-for="item in printData.recordData"
:key="item.id"
>
<td v-html="item.moTime.substring(0,16)" /> <td v-html="item.moTime.substring(0,16)" />
<td v-html="item.orderName" /> <td v-html="item.orderName" />
<td v-html="item.flag" /> <td v-html="item.flag" />

View File

@@ -1,15 +1,9 @@
<template> <template>
<div> <div>
<div ref="print"> <div ref="print">
<div <div v-for="item in printData" :key="item.id">
v-for="item in printData"
:key="item.id"
>
<div class="myccs2"> <div class="myccs2">
<injectLabel <injectLabel :ref="item.id" :print-data="item" />
:ref="item.id"
:print-data="item"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,21 +1,10 @@
<template> <template>
<div> <div>
<div ref="print"> <div ref="print">
<div <div v-for="item in printData" :key="item.id">
v-for="item in printData"
:key="item.id"
>
<div class="myccs2"> <div class="myccs2">
<orderSheet <orderSheet v-if="!item.type" :ref="item.id" :print-data="item" />
v-if="!item.type" <exeOrderSheet v-if="item.type" :ref="item.id" :print-data="item" />
:ref="item.id"
:print-data="item"
/>
<exeOrderSheet
v-if="item.type"
:ref="item.id"
:print-data="item"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,5 @@
<template> <template>
<Graphics <Graphics v-if="graphicsDataDone" :value="resInfo" print @done="printPage" />
v-if="graphicsDataDone"
:value="resInfo"
print
@done="printPage"
/>
</template> </template>
<script setup> <script setup>

View File

@@ -21,10 +21,7 @@
<span>分诊时间</span> <span>分诊时间</span>
<span>{{ printData.triageTime }}</span> <span>{{ printData.triageTime }}</span>
</div> </div>
<img <img ref="refQr" style="position: absolute; top: 10px; left: 100px" />
ref="refQr"
style="position: absolute; top: 10px; left: 100px"
>
</div> </div>
</template> </template>
<script> <script>
@@ -33,6 +30,10 @@ import useUserStore from '@/store/modules/user';
export default { export default {
name: 'TriageTicket', name: 'TriageTicket',
setup() {
const userStore = useUserStore();
return { userStore };
},
props: { props: {
printData: { printData: {
type: Object, type: Object,
@@ -47,10 +48,6 @@ export default {
}, },
}, },
}, },
setup() {
const userStore = useUserStore();
return { userStore };
},
data() { data() {
return {}; return {};
}, },

Some files were not shown because too many files have changed in this diff Show More