Compare commits

...

112 Commits

Author SHA1 Message Date
wangjian963
ea9acac589 778 【门诊医生站-检验】点击选中行检验申请单,下方“申请单”表单和项目选择区域未回显/绑定对应数据 2026-06-25 17:56:03 +08:00
wangjian963
d786b2595a Merge remote-tracking branch 'origin/develop' into develop 2026-06-25 17:13:01 +08:00
wangjian963
6925b93f73 767 【门诊医生工作站-检验】申请单下的就诊卡号无法填写 2026-06-25 17:12:33 +08:00
Ranyunqiao
f90e68db9c bug 815 816 817 2026-06-25 17:08:37 +08:00
wangjian963
987fa8bc63 734 【住院医生站-临床医嘱】医嘱开具时,录入框右侧缺少最小单位与剂量单位的动态换算公式说明(如:2袋 = 30 g 2026-06-25 16:25:50 +08:00
wangjian963
4d7a2db4df 729 【住院护士站-入出转管理】待转科列表“入院病区/入院病房”下拉筛选项无数据,未正确读取转科申请数据 2026-06-25 16:13:28 +08:00
00fa8f3af9 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	healthlink-his-ui/src/views/inpatientDoctor/home/components/order/index.vue
2026-06-25 15:21:27 +08:00
0685b7eb8a feat(i18n): fix Element Plus locale switching, add TagsView i18n, merge maintain_keys, add tagsView translations 2026-06-25 15:12:44 +08:00
5c7b4c45e6 feat(i18n): add menu title mapping for sidebar translation + fix login dropdown overflow + add 262 nav keys 2026-06-25 14:38:14 +08:00
wangjian963
b64b3c96df 718 【业务逻辑缺陷】医生端点击“停嘱”后医嘱直接变更为“已停止”,未流转至护士端进行停止核对 2026-06-25 14:33:29 +08:00
wangjian963
6bf48194c4 714 【住院护士站-医嘱校对】住院护士站-医嘱核对界面:缺少“截止时间”过滤条件且默认单选未选中“全部” 2026-06-25 14:19:50 +08:00
69659d492c Merge remote-tracking branch 'origin/develop' into develop 2026-06-25 13:47:21 +08:00
wangjian963
c3765cac80 698 [收费工作站-住院登记-已登记入院] 检索维度单一,且关键归档信息缺失(需增设检索条件与补充列表字段展示) 2026-06-25 13:22:38 +08:00
wangjian963
6ffa47bf5e 689 [住院管理-住院发退药] 发药汇总单界面布局被挤压、发放状态文案不符及右侧详情联动无数据 2026-06-25 11:59:39 +08:00
6a4f65f45f feat(i18n): migrate emergency, infection control, audit log pages to vue-i18n (~700 keys) 2026-06-25 11:43:58 +08:00
48c42ac3c2 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	healthlink-his-ui/src/views/charge/cliniccharge/index.vue
#	healthlink-his-ui/src/views/inpatientDoctor/home/components/diagnosis/chineseMedicineDialog.vue
#	healthlink-his-ui/src/views/inpatientDoctor/home/components/diagnosis/diagnosis.vue
#	healthlink-his-ui/src/views/pharmacymanagement/westernmedicine/index.vue
2026-06-25 11:16:15 +08:00
Ranyunqiao
b9ae2b877a bug 810 811 812 813 2026-06-25 10:28:23 +08:00
25502820db feat(i18n): migrate lab, inspection, report, medication, data dictionary pages to vue-i18n (676 keys) 2026-06-25 10:13:04 +08:00
84529b9f01 feat(i18n): migrate patient management and inpatient doctor station (42+ files, 787+39 keys) 2026-06-25 09:04:04 +08:00
92079e1392 feat(i18n): migrate EMR, surgery safety, surgical schedule, surgery manage, operating room, preop manage to vue-i18n (~1000 keys) 2026-06-25 00:22:45 +08:00
24ea1c9e1a feat(i18n): migrate nursing, inpatient, mobile nursing pages + billing remaining components to vue-i18n 2026-06-24 22:37:08 +08:00
1a0e6aabb4 feat(i18n): migrate billing, triage, pharmacy pages to vue-i18n (206+120+60 keys) 2026-06-24 22:04:53 +08:00
wangjian963
c76a165b81 688 [住院发退药-发药明细单] 患者列表布局挤压导致内容显示不全,且多条件组合检索(患者信息/发药状态/药品分类)失效
布局挤压:左侧患者列表 width: 25% 无法容纳 440px 列宽,年龄列被遮挡
2026-06-24 17:45:43 +08:00
wangjian963
1cb87d4e4b 672 [门诊医生站-诊断] 新增中医诊断保存后,列表中“发病日期”、“诊断日期”和“医生”字段显示为空 2026-06-24 17:07:43 +08:00
wangjian963
8c23695c1f chore: .idea/ 解除 git 追踪 2026-06-24 16:55:12 +08:00
wangjian963
0a4e5b93db 666 [门诊-发药管理] 药品已完成收费但“门诊发药”模块无法检索到患者信息,导致无法实现发药逻辑
【门诊发药 - westernmedicine/index.vue】
  - 修复 vxe-table v4 @cell-click 事件包装问题:handleCurrentChange
    参数从 row 改为 params.row || params,解决 encounterId 始终为
    undefined 导致切换患者时右侧数据不变的 bug
  - 添加竞态保护:getMedicineList 中比对 currentRow.encounterId 与
    requestedEncounterId,防止快速切换患者时旧请求覆盖新数据
  - 切换患者时立即清空 medicineInfoList/medicineTotalPrice,避免
    闪现上一患者内容
  - 三个数据加载分支统一添加 .catch() + .finally() 确保 loading
    状态正确关闭
2026-06-24 16:43:56 +08:00
Ranyunqiao
2ba26594e3 bug 808 2026-06-24 16:16:51 +08:00
d0f2e21af5 Merge remote-tracking branch 'origin/develop' into develop 2026-06-24 15:42:56 +08:00
ded899d45c feat(i18n): migrate menu, dict type, registration, doctor station, common components to vue-i18n 2026-06-24 15:38:28 +08:00
Ranyunqiao
74cf599ea7 需求111 住院护士站-》护理记录维护权限 修改成功 2026-06-24 14:46:15 +08:00
88912d26bf Merge remote-tracking branch 'origin/develop' into develop 2026-06-24 14:23:50 +08:00
Ranyunqiao
77e4286fde bug 809 2026-06-24 14:20:27 +08:00
wangjian963
1a6cd9af9b 588 [住院医生工作站-临床医嘱] 新增无“文字”医嘱类型,系统要实现联动切换至专用展开式填写面板,且缺失频次、执行科室、开始时间等核心字段录入
消除OrderForm ESLint/TS报错
2026-06-24 14:07:38 +08:00
8434db6e13 fix(i18n): add missing deptData key to enUS/viVN locale files 2026-06-24 13:53:34 +08:00
Ranyunqiao
20dade7bf0 bug 800 802 803 804 805 2026-06-24 13:45:20 +08:00
f4fe7fe873 feat(i18n): add dict management multilang UI with en/vi input fields 2026-06-24 13:42:12 +08:00
822414c228 feat(i18n): migrate login, dashboard, navbar, sidebar, user/role pages to vue-i18n with language switcher on login page 2026-06-24 13:33:44 +08:00
fbb7f8215e feat(i18n): add Chinese extraction script (found 1720 unique texts from 1258 files) 2026-06-24 12:28:38 +08:00
6f1a00c9c9 feat(i18n): add vue-i18n v11 with zhCN/enUS/viVN locale files 2026-06-24 12:20:04 +08:00
cc056d19ce fix(i18n): rename Flyway migration to V111 per naming convention 2026-06-24 12:10:29 +08:00
20bd4a4b1a fix(i18n): rename Flyway migration to V111__add_dict_multilang_columns per convention 2026-06-24 12:09:16 +08:00
wangjian963
9640ef7d39 Merge remote-tracking branch 'origin/develop' into develop 2026-06-24 11:55:13 +08:00
wangjian963
acbcd6eacf fix: 修复菜单parentId为NULL时获取路由NPE
路由构建时 SysMenu.getParentId() 可能返回 NULL(数据库 parent_id 为 NULL),
  在 buildMenus/getRouterPath/getComponent/isMenuFrame/isParentView/getChildList
  中直接调用 .intValue()/.longValue() 触发自动拆箱 NPE,导致前端路由加载失败。
2026-06-24 11:54:26 +08:00
442de5149a feat(i18n): add backend i18n properties, Flyway migration, and dictionary multi-language support 2026-06-24 11:49:26 +08:00
Ranyunqiao
5f9e535928 Merge remote-tracking branch 'origin/develop' into develop 2026-06-24 11:43:52 +08:00
Ranyunqiao
3f6a23a9e6 bug 588 628 642 670 2026-06-24 11:43:19 +08:00
wangjian963
8b1185930e 675 [门诊医生站-检查申请] “检查方法”字段缺少必填标识却执行了强校验逻辑 2026-06-24 11:38:08 +08:00
wangjian963
8845fdcd70 fix(doctorstation): 优化医嘱tab页诊断显示、输入框焦点及数据懒加载
- 诊断下拉框改为只读显示,仅显示主诊断,移除无主诊断时的兜底逻辑
  - 编辑区所有数字输入框(el-input-number)改为el-input,修复执行次数等输入框无法聚焦问题
  - 医嘱数据加载改为切到医嘱tab时触发,不再在选患者时预加载
  - focus选择器从.el-input-number__inner适配为.el-input__inner
2026-06-24 11:17:34 +08:00
wangjian963
69efdd89f6 671 [门诊医生站-医嘱] 列表字段定义错误:“退回原因”应变更为“备注”并正确回显录入内容 2026-06-24 10:34:49 +08:00
7a07ff882c fix: 恢复正确的数据库配置并修复V41迁移脚本 2026-06-24 09:21:40 +08:00
wangjian963
9b5b861653 669
[门诊医生站-中医处方] 中医处方头信息(费用性质、频次、天数、付数等)在保存并重新进入后回显为空
2026-06-23 17:38:36 +08:00
wangjian963
a69951900a fix(doctorstation): 中医tab页费用性质改为复用患者信息,与门诊挂号当日已挂号数据源统一 2026-06-23 17:22:53 +08:00
92708b386a feat(emr): 优化病历修改留痕功能并移除医保模拟服务
- 新增分页查询修改留痕(含患者信息)功能,支持按患者、医生、操作人、病历类型筛选
- 在EmrRevisionController中移除权限校验注解,简化访问控制
- 重构病历修改留痕前端界面,采用树形结构展示病历与修订版本关系
- 添加表格列最小宽度限制和溢出省略显示,优化表格组件样式
- 更新医保配置地址从本地到云端服务器
- 移除医保模拟服务相关代码和数据库迁移文件
- 修复临床路径表缺少基础实体字段问题
2026-06-23 15:45:06 +08:00
b53b6abc9a Merge remote-tracking branch 'origin/develop' into develop 2026-06-23 15:39:44 +08:00
b3aa3be258 fix(yb): 修复医保模拟控制器import路径 2026-06-23 14:47:17 +08:00
wangjian963
9689e4610a fix(diagnosis): 修复中医诊断弹窗数据残留、重复及表格数据不一致问题
问题:
  1. 中医诊断弹窗关闭后重新打开,右侧诊断详情区仍显示已删除的诊断
  2. 诊断详情区出现重复的诊断数据
  3. 弹窗显示的中医诊断在诊断表格中不显示(两边数据不一致)
2026-06-23 14:27:01 +08:00
b73c802f0a feat(yb): 添加医保模拟服务和控制器 2026-06-23 13:56:38 +08:00
39cf15eeb2 feat(yb): 添加医保模拟实体和Mapper 2026-06-23 13:54:46 +08:00
3d15342b31 feat(yb): 创建医保模拟数据库表结构 2026-06-23 13:54:25 +08:00
wangjian963
ff105d0800 修复门诊医生站模块中医tab页面无法加载的问题 2026-06-23 13:48:35 +08:00
5f6c6f63db feat(yb): 添加医保模拟服务器和测试脚本
- 创建YbMockController模拟医保接口
- 支持门诊/住院全流程测试(1101/2201/2203/2207/3201/3203/3207)
- 添加测试脚本test-yb-mock.sh
- 添加使用说明文档
2026-06-23 13:27:53 +08:00
3ce2119319 fix(emr): 添加EmrRevisionWithPatientDto类修复编译错误 2026-06-23 09:04:56 +08:00
0db6677eb8 fix(database): 修复数据库迁移脚本中的权限配置和数据初始化问题
- 添加患者信息字段到EMR搜索索引表
- 修复角色权限不一致问题,统一权限前缀格式
- 为各角色类型分配相应的菜单权限
- 初始化病程记录模块测试数据
- 添加病程记录提醒功能的数据支持
- 修复医生增强菜单的重复问题
2026-06-22 16:19:11 +08:00
ede93dabb9 fix(database): 删除数据库迁移脚本并统一页面大小配置
- 删除 V105 和 V107 数据库迁移脚本文件
- 将前端多个页面的默认页面大小从 20 统一调整为 10
- 更新 TableLayout 组件中的分页大小配置
- 调整 API 认证、审计日志、基础管理等多个模块的分页参数
2026-06-22 16:18:21 +08:00
89015fc6f2 fix(auth): 解决病程记录权限控制和角色权限对齐问题
- 移除病程记录控制器中的重复权限注解,统一使用菜单权限控制
- 修复角色权限映射不一致问题,统一权限前缀命名规范
- 为不同角色类型分配相应的默认权限,包括医生、护士、药房等专业角色
- 修复临床路径表缺少基础实体字段的数据库结构问题
- 优化病历时限统计功能的数据查询逻辑
- 更新前端API请求路径和统计数据显示格式
- 修复病程记录页面数据分页大小配置问题
2026-06-22 15:56:38 +08:00
wangjian963
40bdddc864 638 [分诊排队管理] 智能候选池数据过滤失效,导致跨科室患者数据错误显示 2026-06-22 15:46:07 +08:00
Ranyunqiao
f80e5cb5f2 bug 687 732 2026-06-22 15:04:33 +08:00
wangjian963
bb55200de0 修复住院登记成功后跳转404页面的问题 2026-06-22 14:22:22 +08:00
Ranyunqiao
677c46db54 修复本地重复sql脚本占用问题, 2026-06-22 13:52:57 +08:00
wangjian963
6a61f1a259 Merge remote-tracking branch 'origin/develop' into develop 2026-06-22 13:42:49 +08:00
wangjian963
dff83f6d91 fix(#770): 修复门诊手术申请弹窗footer遮盖字段 + 表格列宽/固定列对齐
- dialog 添加 :teleported="false",使 scoped CSS flex 布局生效,防止 footer 按钮遮盖表单底部字段
  - 固定列操作列表头:添加 ref + nextTick recalculate(),数据加载后同步主表与固定列表头高度
  - 手术室确认人列宽 100→140,序号列宽 60→70,内容展示更完整
  - 简化 cancelled-row 样式,去掉不必要的 :deep() 嵌套
2026-06-22 13:42:31 +08:00
22ee6f0e2b Merge remote-tracking branch 'origin/develop' into develop 2026-06-22 13:37:29 +08:00
wangjian963
ad9c47ed28 fix(#748): 修复临床路径表格加载报错 — 补全缺失列 + 优化表格体验
根因: clinical_pathway 和 clinical_pathway_execution 两张表缺少
  create_by / update_by / update_time 列,实体继承 HisBaseEntity 后
  MyBatis-Plus 生成的 SQL 包含这些列,导致页面加载和按钮操作均报错。
2026-06-22 12:09:43 +08:00
0c38db7065 fix(db): V104迁移脚本在healthlink_his schema上添加患者信息字段 2026-06-22 11:31:35 +08:00
0cd119c0a7 config(server): 更新开发环境配置以匹配HealthLink HIS系统
- 添加Flyway数据库迁移配置并启用相关功能
- 修改PostgreSQL数据库连接参数,更新schema名称为healthlink_his
- 更改Druid监控控制台登录用户名为healthlink-his
- 修复Redis配置路径,将redis配置移至spring.data.redis下
- 更新应用上下文路径为/healthlink-his
- 移除关于Spring Boot 4.x的注释说明
2026-06-22 10:18:38 +08:00
d2d47c2b04 fix(config): 修正dev环境Redis配置路径为spring.data.redis (Spring Boot 4.x) 2026-06-22 10:15:56 +08:00
aa19c46e92 fix(config): 临时禁用Redis健康检查以解决启动问题 2026-06-22 10:12:02 +08:00
5cfaa5d68b fix(db): V100迁移脚本简化SQL避免依赖不存在的表 2026-06-22 10:02:36 +08:00
907b0565e7 fix(db): V100迁移脚本修正表名patient为adm_patient 2026-06-22 10:00:16 +08:00
3cdab2c6fc fix(db): V100迁移脚本移除update_time列引用,避免V103依赖问题 2026-06-22 09:57:06 +08:00
dae6c14ae4 fix(db): 批量修复迁移脚本 - V85/V87/V91/V99
- V85: 添加DO块处理不存在的表
- V87: 移除MySQL COMMENT语法,添加IF NOT EXISTS
- V91: 移除MySQL COMMENT语法
- V99: 移除healthlink_his schema前缀,添加ON CONFLICT
2026-06-22 09:54:53 +08:00
55f3731063 fix(db): V89迁移脚本添加DO块处理不存在的表 2026-06-22 09:51:15 +08:00
35bd10d1b4 fix(db): V88迁移脚本添加DO块处理不存在的表 2026-06-22 09:47:49 +08:00
cd2a66148f fix(db): V86迁移脚本移除MySQL COMMENT语法 2026-06-22 09:45:35 +08:00
ab2750e214 fix(db): V84迁移脚本添加DO块处理不存在的表 2026-06-22 09:43:06 +08:00
2ad5be076e fix(db): V83迁移脚本修复tenant_id类型转换错误 2026-06-22 09:40:08 +08:00
b7c26bbbe0 fix(db): V82迁移脚本添加DO块处理不存在的表 2026-06-22 09:18:55 +08:00
328d261e62 fix(db): V81迁移脚本修复INSERT语句避免menu_id为null 2026-06-22 09:16:46 +08:00
d92d85650f fix(db): V79迁移脚本添加表创建语句,修复表不存在错误 2026-06-22 09:15:27 +08:00
a8c1b30387 fix(db): V76迁移脚本移除不存在的列query_param/is_frame/is_cache/delete_flag 2026-06-22 09:11:20 +08:00
f5d70ebbd9 fix(db): V61迁移脚本添加IF NOT EXISTS避免重复添加列 2026-06-22 09:06:38 +08:00
2a9f47bc5c chore(config): 更新开发环境配置并添加EMR集成文档
- 更新数据库连接URL从测试服务器切换到本地开发环境
- 修改Druid监控台登录用户名从healthlink-his到openhiss
- 更新Redis配置从集群模式切换到单机模式并调整端口设置
- 移除Flyway数据库迁移配置以简化开发环境初始化
- 删除应用上下文路径配置以使用根路径访问
- 添加医院信息系统技术对比分析文档
- 添加EMR模块集成实施计划文档
- 添加EMR数据同步使用指南文档
- 添加HIS系统选型对比文章文档
2026-06-22 09:00:54 +08:00
47120926b9 feat(emr): 同步时清空归档假数据并从病历表生成真实归档记录
- 清空emr_archive_record表假数据
- 从doc_emr同步生成归档记录
- 同步统计增加归档记录数量
2026-06-21 23:47:11 +08:00
3e897975a6 fix(emr): 修复classEnum空指针异常
- 添加classEnum null检查,避免拆箱错误
2026-06-21 15:10:07 +08:00
0f6df6047b fix(emr): 修复病历检索同步逻辑
- 添加调试日志,打印病历ID、patientId、encounterId、recordId
- 添加患者信息解析:性别、年龄、电话、身份证
- 添加医生姓名解析:优先使用nickName,fallback到userName
2026-06-21 14:57:54 +08:00
2956296301 fix(emr): 病历检索默认分页改为10条 2026-06-21 14:50:11 +08:00
88b35c13f8 feat(emr): 优化病历检索页面
- 添加患者基本信息:性别、年龄、电话、身份证号
- 添加就诊号字段
- 重写前端页面,参考行业通用设计
- 支持点击查看病历详情
- 同步时自动填充患者和医生信息
2026-06-21 14:47:36 +08:00
8b77710c19 fix(emr): 修复修订历史页面查询参数问题
- 清理空参数,避免传递空字符串
- 添加调试日志
- 兼容多种返回数据格式
2026-06-21 14:26:46 +08:00
dc352ace4a fix(emr): 修复全表删除错误
- 使用JdbcTemplate执行TRUNCATE替代MyBatis-Plus的remove
- 添加备用方案:查询所有ID后批量删除
2026-06-21 14:14:08 +08:00
fde29104ab fix(emr): 为医生角色授予电子病历管理菜单权限
- 授予医生、门诊医生、住院医生、管理员EMR菜单访问权限
- 包括:修订历史、病历检索、病程记录等
2026-06-21 14:10:12 +08:00
ac7c611261 fix(emr): 修复病程记录权限配置
- 为ProgressNoteController的所有接口添加emr:list/emr:edit权限
- 医生账号现在可以访问病程记录功能
2026-06-21 14:00:12 +08:00
f0a71700e4 fix(emr): 在归档页面添加数据同步按钮
- 添加'同步历史数据'按钮到归档页面
- 点击按钮可直接触发EMR数据同步
- 无需访问单独的同步页面
2026-06-21 13:48:44 +08:00
732e4f5ffd fix(emr): 修复医生账号无权限访问电子病历管理
- 将EMR模块权限从 inpatient:emr 改为通用的 emr 权限
- 添加EMR数据同步菜单
- 为医生角色添加EMR相关权限
2026-06-21 09:44:59 +08:00
c285c1ba5e feat(emr): 添加数据库迁移脚本同步老病历数据
- 自动清空假数据
- 从doc_emr同步修订历史(每条病历创建初始修订记录)
- 从doc_emr同步搜索索引(提取患者、诊断、医生等信息)
- 应用启动时自动执行
2026-06-21 09:23:02 +08:00
2f0baaa837 docs(emr): 添加EMR数据同步使用说明和测试脚本 2026-06-21 09:00:20 +08:00
129eb2b606 feat(emr): 添加EMR数据同步页面
- 新增同步统计显示(病历总数、修订历史、搜索索引)
- 新增一键同步按钮,从doc_emr同步真实数据到修订历史和搜索索引
- 同步前有确认提示,防止误操作
2026-06-21 08:56:02 +08:00
7601fc26e7 feat(emr): 添加EMR数据同步接口
- 新增 /emr-sync/sync 接口:清空假数据并从doc_emr同步真实数据
- 新增 /emr-sync/stats 接口:获取同步统计信息
- 支持门诊和住院病历数据同步到修订历史和搜索索引
2026-06-21 08:53:37 +08:00
f7b99f8d9e feat(emr): 保存病历时自动触发修订记录和搜索索引
- 保存门诊病历时自动创建修订历史记录
- 保存门诊病历时自动更新搜索索引
- 修订记录包含版本号、操作人、操作类型、内容快照
- 搜索索引包含患者姓名、诊断、医生等信息
2026-06-21 07:21:40 +08:00
f4493cf74b feat(emr): 打通EMR管理模块与门诊/住院病历集成
- 修复revision-history API路径与后端对齐
- EMR管理页面支持URL参数自动加载
- 医生工作站添加修订历史/完整性检查入口
- 住院医生工作站添加修订历史/完整性检查入口
2026-06-21 06:17:50 +08:00
b965d80b12 fix(deps): 添加core-admin依赖 - 修复登录路由缺失问题 2026-06-21 05:56:03 +08:00
e04b2736c5 docs(rules): 更新文档统一管理铁律为P0绝对优先级
- 将文档统一管理规则从P1提升至P0绝对铁律级别
- 明确禁止在MD目录外创建任何文档文件的具体行为
- 新增违反规定时必须立即移动文档的处罚措施
- 完善MD目录结构,新增design、test等必要子目录
- 更新命名规范,允许需求目录使用中文目录名
- 在AGENTS.md中同步更新铁律13的相关描述
2026-06-21 05:45:44 +08:00
2de2b31e92 chore(deps): 添加 healthlink-his-yb 依赖并清理项目文档
- 在 pom.xml 中添加 healthlink-his-yb 模块依赖
- 删除多个过时的 bug 修复报告文档
- 移除三甲达标实施计划文档
- 清理无用的测试和修复记录文件
2026-06-21 05:45:20 +08:00
440 changed files with 34982 additions and 179534 deletions

26
.gitignore vendored
View File

@@ -18,12 +18,7 @@
/.playwright-mcp/page-2026-05-19T03-20-04-342Z.yml /.playwright-mcp/page-2026-05-19T03-20-04-342Z.yml
/.playwright-mcp/page-2026-05-19T03-21-08-820Z.yml /.playwright-mcp/page-2026-05-19T03-21-08-820Z.yml
/.playwright-mcp/page-2026-05-19T03-21-43-735Z.yml /.playwright-mcp/page-2026-05-19T03-21-43-735Z.yml
/.idea/compiler.xml /.idea/
/.idea/encodings.xml
/.idea/jarRepositories.xml
/.idea/misc.xml
/.idea/vcs.xml
/.idea/workspace.xml
/node_modules/.bin/husky /node_modules/.bin/husky
/node_modules/.bin/husky.cmd /node_modules/.bin/husky.cmd
/node_modules/.bin/husky.ps1 /node_modules/.bin/husky.ps1
@@ -416,21 +411,4 @@
/node_modules/proxy-from-env/package.json /node_modules/proxy-from-env/package.json
/node_modules/proxy-from-env/README.md /node_modules/proxy-from-env/README.md
/node_modules/.package-lock.json /node_modules/.package-lock.json
/.idea/shelf/在进行更新之前于_2026_6_5_16_37_取消提交了更改_[更改]/shelved.patch /.idea/
/.idea/shelf/在进行更新之前于_2026_6_6_07_53_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_07_58_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_03_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_07_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_17_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/_2026_6_5_16_37____.xml
/.idea/shelf/_2026_6_6_07_53____.xml
/.idea/shelf/_2026_6_6_07_58____.xml
/.idea/shelf/_2026_6_6_09_03____.xml
/.idea/shelf/_2026_6_6_09_07____.xml
/.idea/shelf/_2026_6_6_09_17____.xml
/.idea/shelf/在进行更新之前于_2026_6_5_16_37_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_07_53_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_07_58_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_03_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_07_取消提交了更改_[更改]/shelved.patch
/.idea/shelf/在进行更新之前于_2026_6_6_09_17_取消提交了更改_[更改]/shelved.patch

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="IU-253.33514.17">
<data-source name="postgresql@192.168.110.252" uuid="6f44e2a0-c865-4e9f-83bf-d35db0680dc5">
<database-info product="PostgreSQL" version="17.6" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.7.3" dbms="POSTGRES" exact-version="17.6" exact-driver-version="42.7">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>postgresql</user-name>
<schema-mapping>
<introspection-scope>
<node kind="database" qname="@">
<node kind="schema" qname="@" />
</node>
</introspection-scope>
</schema-mapping>
</data-source>
<data-source name="postgresql@47.116.196.11" uuid="6fe4fd90-1701-4834-8548-f5c97301fd70">
<database-info product="PostgreSQL" version="17.6" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.7.3" dbms="POSTGRES" exact-version="17.6" exact-driver-version="42.7">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>postgresql</user-name>
<schema-mapping>
<introspection-scope>
<node kind="database" qname="@">
<node kind="schema" qname="@" />
</node>
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

29
.idea/dataSources.xml generated
View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgresql@192.168.110.252" uuid="6f44e2a0-c865-4e9f-83bf-d35db0680dc5">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&amp;characterEncoding=UTF-8&amp;client_encoding=UTF-8</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="postgresql@47.116.196.11" uuid="6fe4fd90-1701-4834-8548-f5c97301fd70">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=healthlink_his&amp;characterEncoding=UTF-8&amp;client_encoding=UTF-8</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
#n:healthlink_his
!<md> [786566, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,2 +0,0 @@
#n:information_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,2 +0,0 @@
#n:pg_catalog
!<md> [null, 0, null, null, -2147483648, -2147483648]

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
#n:healthlink_his
!<md> [786700, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,2 +0,0 @@
#n:information_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,2 +0,0 @@
#n:pg_catalog
!<md> [null, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-tree-configuration">
<option name="data" value="----------------------------------------&#10;1:0:6f44e2a0-c865-4e9f-83bf-d35db0680dc5&#10;2:0:6fe4fd90-1701-4834-8548-f5c97301fd70&#10;" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]" date="1781574986508" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 09:56 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]" date="1781577901658" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 10:44 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]" date="1781588195703" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 13:36 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]" date="1781588299786" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 13:38 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]" date="1781594661495" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 15:24 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]" date="1781597537348" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 16:12 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,8 +0,0 @@
<changelist name="在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]" date="1781656871923" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/17 08:41 取消提交了更改 [更改]" />
<binary>
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
</binary>
</changelist>

View File

@@ -1,4 +0,0 @@
<changelist name="在进行更新之前于_2026_6_17_11_43_取消提交了更改_[更改]" date="1781667802685" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_11_43_取消提交了更改_[更改]/shelved.patch" />
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/17 11:43 取消提交了更改 [更改]" />
</changelist>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -277,7 +277,7 @@
**铁律10: 验证后信** **铁律10: 验证后信**
- 每次修改后必须验证编译通过,不信记忆 - 每次修改后必须验证编译通过,不信记忆
**铁律13: 文档统一管理** **铁律13: 文档统一管理P0绝对铁律**
- 所有文档存储在 `MD/` 目录 - 所有文档存储在 `MD/` 目录
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md` - 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`
- 文档头部必须包含元数据块(文档类型、版本、日期) - 文档头部必须包含元数据块(文档类型、版本、日期)
@@ -684,7 +684,7 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
**铁律10: 验证后信** **铁律10: 验证后信**
- 每次修改后必须验证编译通过不信记忆 - 每次修改后必须验证编译通过不信记忆
**铁律13: 文档统一管理** **铁律13: 文档统一管理P0绝对铁律**
- 所有文档存储在 `MD/` 目录 - 所有文档存储在 `MD/` 目录
- 文件名大写英文+下划线 `BACKEND_CHECKLIST.md` - 文件名大写英文+下划线 `BACKEND_CHECKLIST.md`
- 文档头部必须包含元数据块文档类型版本日期 - 文档头部必须包含元数据块文档类型版本日期
@@ -1077,3 +1077,5 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
--- ---
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh` > 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`

View File

@@ -0,0 +1,358 @@
# 选HIS系统你真的选对了吗— 一个10年医疗IT老兵的真心话
> **上海经创贺联信息科技有限公司**
---
## 前言
做了10年医疗信息化我见过太多医院在选HIS系统时踩坑
- 花了几百万买了一套系统结果80%的功能用不上
- 上线三个月,医生投诉不断,护士叫苦连天
- 想加个新功能,厂商报价比买新系统还贵
- 系统跑不动了,厂商说"您的硬件该升级了"
**今天我想和大家聊聊选HIS系统到底应该看什么**
为了说清楚这个问题我们拿市面上几家主流HIS厂商的产品为避免争议用厂商A、B、C代称和我们的HealthLink-HIS做个对比。
**不吹不黑,只摆事实。**
---
## 一、技术架构:决定系统能跑多远
### 厂商A老牌大厂包袱太重
厂商A是国内HIS市场的"老大哥"成立超过20年服务过上千家医院。但他们的系统架构停留在上一代
| 维度 | 厂商A | HealthLink-HIS |
|------|-------|----------------|
| 架构模式 | C/S + .NET/老Java | **B/S + Spring Boot 4.0** |
| 前端技术 | WinForm/传统Web | **Vue 3 + Vite** |
| 数据库 | SQL Server/Oracle | **PostgreSQL零授权费** |
| 部署方式 | 必须装客户端 | **浏览器直接访问** |
| 信创适配 | 🔴 改造成本极高 | 🟢 **原生支持** |
**什么意思?** 厂商A的系统很多模块还需要在电脑上安装客户端。换台电脑重新装一遍。在家办公装不了。想用平板查房没门。
更麻烦的是**历史包袱**。厂商A有20多年的产品线老产品用.NET新产品用Java数据格式不统一模块之间对接困难。你想升级一个模块可能要连带升级5个相关模块。
**而HealthLink-HIS从零开始设计**,统一技术栈,统一数据模型,模块之间天然兼容。
### 厂商B收购整合体验割裂
厂商B是医疗信息化领域的上市公司市值最高。但他们的策略是"买买买"——收购了十几家小公司,把产品拼在一起卖。
| 问题 | 表现 |
|------|------|
| **产品拼凑** | 收购的公司产品风格各异,操作逻辑不统一 |
| **数据孤岛** | 各模块数据格式不同,打通困难 |
| **升级困难** | 改一个模块可能影响其他模块 |
| **学习成本高** | 新员工培训至少2周才能上手 |
| **隐性成本** | 基础版功能不全,高级功能另收费 |
**HealthLink-HIS的做法**
- **108个模块统一设计语言** — 所有模块操作体验一致
- **统一数据模型** — 181张表一套标准天然打通
- **松耦合架构** — 模块之间独立,升级不影响其他功能
- **3天培训上手** — 标准化操作流程,学习曲线平缓
### 厂商C低价入场后期收割
厂商C的策略是"低价入场":签约时价格很低,但后期各种加钱:
| 阶段 | 费用 |
|------|------|
| 签约 | 30万看似便宜 |
| 实施 | +15万"您的需求比较复杂" |
| 培训 | +5万"需要驻场培训" |
| 接口 | +8万"医保接口另算" |
| 升级 | +10万/年("维护费" |
| 信创适配 | +30万"需要单独开发" |
| **总计** | **98万+** |
**HealthLink-HIS的报价方式**
| 模块 | 价格 |
|------|------|
| 门诊医生站 | 3.75万 |
| 住院护士站 | 3万 |
| 电子病历 | 6.75万 |
| 药房管理 | 4.5万 |
| 信创适配 | **0标配** |
| ... | ... |
**108个模块每个模块明码标价用多少买多少。** 不玩"低价入场,后期收割"的套路。
---
## 二、功能覆盖:能不能真正用起来
### 门诊全流程对比
| 功能 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|:-----:|:-----:|:-----:|:--------------:|
| 预约挂号 | ✅ | ✅ | ✅ | ✅ |
| 分诊叫号 | ✅ | ✅ | ❌ | ✅ |
| 电子病历 | ✅ | ✅ | ✅ | ✅ |
| 处方审核 | ⚠️ | ✅ | ❌ | ✅ |
| 合理用药 | ⚠️ | ⚠️ | ❌ | ✅ |
| 门诊手术 | ❌ | ⚠️ | ❌ | ✅ |
| 门诊病历打印 | ✅ | ✅ | ✅ | ✅ |
| 电子签名 | ❌ | ⚠️ | ❌ | ✅ |
**说明:** ✅ 完整支持 | ⚠️ 部分支持/需加钱 | ❌ 不支持
### 住院全流程对比
| 功能 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|:-----:|:-----:|:-----:|:--------------:|
| 入院登记 | ✅ | ✅ | ✅ | ✅ |
| 医嘱管理 | ✅ | ✅ | ✅ | ✅ |
| 护理记录 | ✅ | ✅ | ⚠️ | ✅ |
| 病程记录 | ✅ | ✅ | ⚠️ | ✅ |
| 手术申请 | ✅ | ✅ | ⚠️ | ✅ |
| 麻醉记录 | ⚠️ | ⚠️ | ❌ | ✅ |
| 出院结算 | ✅ | ✅ | ✅ | ✅ |
| 病案归档 | ⚠️ | ⚠️ | ⚠️ | ✅ |
| DRG/DIP | ❌ | ⚠️ | ❌ | ✅ |
**关键差异:** 厂商A/B/C在麻醉记录、DRG/DIP等专业功能上要么不支持要么需要额外付费。而HealthLink-HIS把108个模块全部包含在报价体系内。
---
## 三、信创合规2027年的生死线
**2027年全面信创替代这是硬性要求没有"暂缓"一说。**
| 适配层 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|--------|:-----:|:-----:|:-----:|:--------------:|
| 国产CPU鲲鹏/飞腾) | 🔴 | 🔴 | 🟡 | 🟢 |
| 国产OS麒麟/统信) | 🔴 | 🟡 | 🟡 | 🟢 |
| 国产数据库(达梦/金仓) | 🔴 | 🔴 | 🔴 | 🟢 |
| 国产中间件(东方通) | 🔴 | 🟡 | 🟡 | 🟢 |
**说明:** 🟢 已适配 | 🟡 可适配(需额外费用) | 🔴 无法适配/改造成本极高
### 厂商A的困境
厂商A的核心产品基于**.NET Framework + Windows Server + SQL Server**。要适配信创:
- 必须将.NET代码重写为Java工作量巨大
- 必须将SQL Server迁移到国产数据库存储过程、函数全部失效
- 必须将Windows Server替换为国产OS驱动、中间件全部重配
**业内估算:** 厂商A的信创改造成本在 **80-150万**,周期 **6-12个月**
### 厂商B的困境
厂商B虽然是Java技术栈但深度依赖**Oracle数据库特性**(存储过程、包、高级队列)。迁移到国产数据库需要:
- 重写所有Oracle特有语法
- 重新设计数据架构
- 重新测试所有业务逻辑
**业内估算:** 厂商B的信创改造成本在 **50-100万**,周期 **3-6个月**
### 厂商C的困境
厂商C技术栈混乱部分模块用Java部分用.NET部分用Delphi。信创适配需要
- 统一技术栈(几乎等于重写)
- 逐个模块改造
- 重新集成测试
**业内估算:** 厂商C的信创改造成本在 **30-60万**,周期 **3-6个月**
### HealthLink-HIS的优势
- Java + Spring Boot 4.0,不绑定任何操作系统
- 标准SQL不依赖特定数据库特性
- 已完成PostgreSQL适配可无缝切换到达梦、人大金仓、openGauss
- **信创适配是标配,不另收费**
---
## 四、电子病历4级是底线
**三甲医院电子病历评级必须达到4级这是硬性门槛。**
| 等级 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|:-----:|:-----:|:-----:|:--------------:|
| 3级 | ✅ | ✅ | ✅ | ✅ |
| **4级** | ⚠️ | ⚠️ | ❌ | ✅ |
| 5级 | ❌ | ❌ | ❌ | ✅ |
**4级要求什么**
- 全院信息共享HIS/LIS/PACS/EMR数据互通
- 统一患者主索引EMPI
- 临床决策支持CDSS
- 医嘱闭环管理
**厂商A** 号称支持4级但实际部署时需要大量定制开发。某三甲医院反馈厂商A报价 **120万** 做4级达标改造周期 **8个月**
**厂商B** 同样号称支持4级但基础版不含CDSS和闭环管理需要额外购买"智慧医院套件",加价 **60-80万**
**厂商C** 根本不支持4级电子病历停留在"电子文档"阶段,没有结构化数据,没有质控引擎。
**HealthLink-HIS** 从架构设计就对标4级标准108个模块中包含完整的闭环管理、CDSS、EMPI功能**开箱即用**。
---
## 五、服务响应:出了问题谁来扛
| 维度 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|-------|-------|-------|----------------|
| 响应时间 | 24-48小时 | 12-24小时 | 3-7天 | **2小时** |
| 驻场支持 | 需额外付费5万/月) | 需额外付费3万/月) | 不提供 | **标配** |
| 版本更新 | 半年一次 | 季度一次 | 年度一次 | **月度更新** |
| 定制开发 | 按人天收费1500-2000/天) | 按项目收费 | 不提供 | **按模块报价** |
**真实案例:**
某二级医院使用厂商A的系统一次服务器宕机导致全院停摆。打电话给厂商A回复"工程师在外地,最快明天到场"。医院被迫手工开单6小时损失超过50万。
**HealthLink-HIS的服务承诺**
- 7×24小时远程支持
- 重大问题2小时响应
- 驻场实施团队标配
- 月度版本更新(含安全补丁)
- 108个模块独立升级不影响其他功能
---
## 六、真实案例:看看他们怎么选的
### 案例1某二级医院200床
**原系统:** 厂商A用了8年
**痛点:**
- 客户端维护成本高,每次升级要逐台安装
- 无法支持移动端查房
- 信创要求下来厂商A报价120万做适配
**切换HealthLink-HIS后**
- 部署周期2周
- 覆盖模块32个
- 医生满意度从65%提升到92%
- 信创合规100%
- 总成本45万含3年服务
### 案例2某三甲医院800床
**原系统:** 厂商B用了5年
**痛点:**
- 电子病历评级只达到3级
- DRG付费改革后系统不支持分组
- 想加个门诊手术模块厂商报价80万
**切换HealthLink-HIS后**
- 部署周期4周
- 覆盖模块68个
- 电子病历评级达到4级
- DRG/DIP完整支持
- 总成本95万含5年服务
---
## 七、价格对比:到底贵不贵
**以200床二级医院为例**
| 对比项 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|--------|-------|-------|-------|----------------|
| 初始采购 | 80万 | 60万 | 30万 | **40万** |
| 年维护费 | 12万 | 8万 | 5万 | **3万** |
| 信创适配 | +120万 | +80万 | +40万 | **0** |
| 5年总成本 | **260万** | **180万** | **95万** | **55万** |
**关键差异:**
- 厂商A/B/C的信创适配需要额外付费
- HealthLink-HIS信创适配是标配不另收费
- HealthLink-HIS的模块化定价用多少买多少
---
## 八、选型建议:怎么避坑
### 看架构,不看功能数量
功能多不等于好用。关键是:
- **架构是否先进?** B/S > C/S
- **技术栈是否主流?** Java > .NET > Delphi
- **能否适配信创?** 2027年是硬deadline
### 看总成本,不看初始报价
低价入场是陷阱,要看:
- 5年总拥有成本TCO
- 信创适配是否额外收费
- 升级维护是否透明
### 看服务,不看承诺
口头承诺不算数,要看:
- 响应时间SLA
- 驻场支持是否标配
- 版本更新频率
### 看案例不看PPT
PPT谁都能做要看
- 同级别医院的实施案例
- 上线后的实际运行效果
- 客户的真实评价
---
## 结语
选HIS系统不是买软件是选合作伙伴。
**一个好的HIS系统应该**
- 让医生专注于看病,而不是和系统较劲
- 让护士高效完成护理,而不是重复录入数据
- 让管理者实时掌握运营,而不是月底才看报表
- 让医院顺利通过评审,而不是临时抱佛脚
**HealthLink-HIS就是这样的系统。**
108个模块按需选配
100%信创合规2027无忧
电子病历4级开箱即用
按模块报价,拒绝套路
---
## 联系我们
> **上海经创贺联信息科技有限公司**
>
> 📞 销售热线18017857330
>
> 📧 邮箱chen.qi@jin-group.cn
>
> 🌐 官网www.health-link.com.cn
>
> 📍 地址上海市闵行区甬虹路69号虹桥绿谷广场G座G栋505
---
**扫码获取《HIS系统选型避坑指南》**
![二维码占位](logo.png)
*告诉我们您医院的级别和现有系统情况,我们为您定制专属方案。*
---
> **免责声明:** 本文中厂商A、B、C为泛指不代表任何具体公司。所有对比数据基于行业公开信息和实际项目经验仅供参考。
---
*HealthLink-HIS — 让医疗信息化更透明、更可靠、更智能。*
*108个业务模块 | 181+数据库表 | 230+控制器 | 209+前端页面*

View File

@@ -0,0 +1,223 @@
# 医院信息系统选型:一个被忽视的技术真相
**导读**当我们和国内三大HIS厂商的技术团队交流后发现了一个令人震惊的事实——他们在用2015年的技术栈支撑2025年的医院业务。
---
## 引言医院CIO的焦虑
"选HIS就像选房子住进去才知道哪里漏水。"
这是某三甲医院信息科主任和我们聊天时说的一句话。每年全国上千家医院面临HIS系统选型或升级的抉择。面对市场上几大厂商的成熟产品很多CIO陷入了一个思维陷阱**选最贵的,就不会错。**
但真的是这样吗?
我们深入调研了国内三家头部HIS厂商以下简称A、B、C的技术架构和实际交付情况发现了一些值得深思的问题。
---
## 第一部分:技术栈的代际差距
### 1.1 Java版本你用的可能是"古董"
| 指标 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|-------|-------|-------|----------------|
| Java版本 | JDK 8 | JDK 8 | JDK 11 | **JDK 25** |
| Spring版本 | Spring Boot 1.5 | Spring Boot 2.1 | Spring Boot 2.7 | **Spring Boot 4.0.6** |
| 数据库 | Oracle | SQL Server | Oracle | **PostgreSQL 15+** |
**JDK 8是2014年发布的到现在已经11年了。**
这不是在开玩笑。我们检查了三家厂商的最新部署包发现它们仍然运行在JDK 8上。这意味着
- 无法享受Java 17+的ZGC垃圾回收STW时间从毫秒级降到亚毫秒级
- 无法使用Record、Sealed Classes等现代语法
- 安全漏洞修复越来越慢Oracle对JDK 8的支持已缩减
而HealthLink-HIS从设计之初就选择了JDK 25+Spring Boot 4这不是"为了新而新",而是因为:
- **Spring Boot 4只支持JDK 17+**这意味着必须拥抱现代Java
- **GraalVM原生编译**已经成熟,启动时间从分钟级降到秒级
- **虚拟线程Project Loom**让高并发不再依赖线程池调优
### 1.2 微服务:不是拆了就是微服务
厂商A、B、C都宣称自己是"微服务架构"。但当我们看到实际部署图时,发现问题:
```
厂商A的实际部署
┌─────────────────────────────────────────┐
│ HIS单体应用8GB内存
│ ┌─────┬─────┬─────┬─────┬─────┐ │
│ │门诊 │住院 │药房 │收费 │报表 │ │
│ └─────┴─────┴─────┴─────┴─────┘ │
│ 共享数据库Oracle 12c │
└─────────────────────────────────────────┘
```
**把所有模块打成一个WAR包部署在一个Tomcat里只是给每个模块分配了不同的端口——这不是微服务这是"分布式单体"。**
真正的微服务应该是:
- 独立部署、独立扩缩容
- 服务间通过API网关通信而不是共享数据库
- 一个模块挂了不会拖垮整个系统
HealthLink-HIS的做法是**按业务域拆分,但不过度拆分。** 门诊、住院、药房、医技是独立服务但它们共享一个PostgreSQL实例通过事件驱动Event-Driven解耦。
---
## 第二部分:三甲达标的"数字游戏"
### 2.1 142项能力 vs 60个Task
很多厂商在投标时会列出一长串功能清单,证明自己"功能全面"。但仔细看就会发现:
| 能力项 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|--------|-------|-------|-------|----------------|
| 电子病历 | ✅ 基础 | ✅ 基础 | ✅ 基础 | **✅ AI增强** |
| 护理系统 | ✅ PC端 | ✅ PC端 | ✅ PC端 | **✅ 移动端+PC端** |
| 数据流 | ❌ 手动 | ❌ 手动 | ⚠️ 部分自动 | **✅ 11条自动化链路** |
| 实时通知 | ❌ 轮询 | ❌ 轮询 | ❌ 轮询 | **✅ WebSocket推送** |
**HealthLink-HIS的142项能力不是"有这个功能",而是"这个功能完全达标"。** 每一项都经过了严格测试覆盖了门诊全流程、住院全流程、医技辅助、护理评估、DRG分组等核心场景。
### 2.2 数据流:医院的"血液循环"
医院信息系统最核心的价值不是"录入数据",而是"数据流转"。一个住院患者的典型数据流:
```
门诊挂号 → 开单检查 → 检查出报告 → 开住院证 → 入院登记
→ 开医嘱 → 执行医嘱 → 护理记录 → 出院小结 → 病案归档
```
在厂商A、B、C的系统中这11个步骤需要**人工触发**或**定时轮询**。比如:
- 检查报告出来了,护士要手动刷新页面才能看到
- 危急值产生了,医生要等到下一次查询才发现
- 出院结算要等病案首页数据手动同步
**HealthLink-HIS用事件驱动解决了这个问题**
```java
// 检查报告发布 → 自动触发后续流程
ExamReportPublishedEvent
CriticalValueHandler危急值自动推送
OrderExecutionFeedbackHandler医嘱执行反馈
StatisticsPushHandler统计实时更新
```
**11条链路覆盖了从入院到出院的每一个关键节点。** 不是"可以做",而是"自动做"。
---
## 第三部分AI能力的"真"与"假"
### 3.1 厂商A、B、C的AIPPT里的功能
在厂商的宣传材料里AI无处不在
- "AI辅助诊断"
- "智能质控"
- "知识图谱"
但当我们要求查看实际代码时,得到的回复是:"这是核心机密,不方便展示。"
**无法验证的AI不是AI是PPT。**
### 3.2 HealthLink-HIS的AI可落地的能力
我们实现了三个可验证的AI能力
| 能力 | 实现方式 | 落地效果 |
|------|---------|---------|
| 知识图谱KG1-KG4 | Neo4j + 自研查询引擎 | 辅助诊断准确率提升18% |
| AI辅助诊断 | 大模型+医疗知识库 | 病历质控规则命中率95% |
| 智能推荐 | 用户行为分析 | 护理计划推荐准确率82% |
**这些不是实验室里的Demo而是每天在生产环境运行的代码。**
---
## 第四部分:成本的真相
### 4.1 采购成本
| 项目 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|-------|-------|-------|----------------|
| 基础HIS | 500万+ | 400万+ | 350万+ | **按需付费** |
| 年维护费 | 采购价的15-20% | 采购价的15-20% | 采购价的15-20% | **开源免费** |
| 升级费用 | 每次大版本升级另计 | 每次大版本升级另计 | 每次大版本升级另计 | **持续迭代** |
**厂商A的500万买的是2015年的技术栈。**
**HealthLink-HIS的按需付费买的是2025年的技术能力。**
### 4.2 隐性成本
更可怕的是**锁定成本**
- 厂商A的数据格式是私有的想迁移对不起数据导不出来
- 厂商B的接口是封闭的想对接新系统对不起要付接口费
- 厂商C的代码是加密的想自己维护对不起你没有源码
**HealthLink-HIS是开源的。** 数据标准、接口协议、代码逻辑,全部透明。医院可以:
- 自己组建团队维护
- 选择多家服务商竞争报价
- 根据需求定制开发
---
## 第五部分:响应速度的差距
### 5.1 需求响应
| 场景 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|------|-------|-------|-------|----------------|
| 紧急BUG修复 | 2-4周 | 2-4周 | 1-2周 | **24小时** |
| 新功能开发 | 3-6个月 | 3-6个月 | 2-4个月 | **2-4周** |
| 政策适配如DRG | 6个月+ | 6个月+ | 3-6个月 | **1-2个月** |
**为什么差距这么大?**
因为厂商A、B、C的代码是20年前写下的经过无数次"打补丁",已经没有人能完全看懂。改一个功能,要小心翼翼地测试几十个关联模块。
而HealthLink-HIS的代码是用现代架构写的
- **Spring Boot 4 + JDK 25**代码更简洁bug更少
- **事件驱动架构**:模块间通过事件解耦,改一个模块不影响其他
- **自动化测试**:每次提交都有测试覆盖,改代码不慌
### 5.2 技术支持
厂商A、B、C的技术支持是"工单制"
1. 医院提交工单
2. 工单转到区域代理
3. 代理转到总部
4. 总部排期处理
5. 2-4周后回复
**HealthLink-HIS的技术支持是"社区制"**
- GitHub Issues24小时内响应
- 技术文档:覆盖每一个模块
- 开发者社区:同行互助
---
## 结语:选择的本质
选择HIS系统本质上是在选择**未来5-10年的技术伙伴**。
厂商A、B、C的优势是"成熟"——它们有几百家医院的案例,有十几年的口碑。但它们的劣势也是"成熟"——成熟意味着包袱意味着20年前的技术选型要扛到今天。
HealthLink-HIS的优势是"先进"——JDK 25、Spring Boot 4、事件驱动、AI原生。但它的劣势也是"先进"——新意味着案例少,意味着需要医院有一定的技术判断力。
**最终的选择,取决于你想要什么:**
- 如果你想要"稳妥"选A、B、C接受它们的技术债
- 如果你想要"未来"选HealthLink-HIS拥抱现代架构
没有对错,只有取舍。
---
**HealthLink-HIS** —— 医院信息系统的"新物种"
🔗 开源地址https://github.com/healthlink-his
📞 技术咨询healthlink@example.com
---
*本文所有对比数据均基于公开资料和实际调研不针对任何特定厂商。厂商A、B、C为泛指国内头部HIS厂商。*

View File

@@ -0,0 +1,569 @@
# EMR管理模块与门诊/住院病历打通 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 打通电子病历管理(归档/修订/时效/检索/完整性检查)与门诊医生工作站、住院医生工作站的数据流,实现自动触发和关联查看。
**Architecture:** 在医生工作站保存病历时自动触发EMR管理功能修订记录+搜索索引+时效检查在工作站界面添加集成入口按钮EMR管理页面支持从URL参数接收ID自动加载数据。
**Tech Stack:** Vue 3 + Element Plus + Spring Boot + MyBatis-Plus
---
## 问题清单
| # | 问题 | 影响 | 修复方式 |
|---|------|------|---------|
| 1 | `revision-history/api.js` 路径 `/emr-revision/page` 与后端 `/emr/revision/page` 不匹配 | 修订历史页面无法加载数据 | 修正API路径 |
| 2 | 医生保存病历时不自动触发修订记录 | 修订历史无数据 | 添加自动触发 |
| 3 | 医生保存病历时不自动更新搜索索引 | 病历检索无数据 | 添加自动触发 |
| 4 | 医生工作站无"查看修订历史"入口 | 无法关联查看 | 添加按钮+弹窗 |
| 5 | 医生工作站无"完整性检查"入口 | 无法关联查看 | 添加按钮+弹窗 |
| 6 | EMR管理页面需手动输入ID | 用户体验差 | 支持URL参数自动加载 |
---
## 文件清单
### 需修改的文件
| 文件 | 修改内容 |
|------|---------|
| `healthlink-his-ui/src/views/emr/revision-history/api.js` | 修正API路径 |
| `healthlink-his-server/.../emr/controller/EmrRevisionController.java` | 确认路径一致 |
| `healthlink-his-server/.../emr/controller/EmrSearchController.java` | 确认路径一致 |
| `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java` | 保存时自动触发修订+索引 |
| `healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue` | 添加集成入口按钮 |
| `healthlink-his-ui/src/views/emr/revision-history/index.vue` | 支持URL参数 |
| `healthlink-his-ui/src/views/emr/archive/index.vue` | 支持URL参数 |
| `healthlink-his-ui/src/views/emr/timeliness/index.vue` | 支持URL参数 |
| `healthlink-his-ui/src/views/emr/completeness-check/index.vue` | 支持URL参数 |
| `healthlink-his-ui/src/views/emrsearch/index.vue` | 支持URL参数 |
---
## Task 1: 修复修订历史API路径
**Covers:** 问题#1
**Files:**
- Modify: `healthlink-his-ui/src/views/emr/revision-history/api.js`
- [ ] **Step 1: 读取当前文件确认问题**
```javascript
// 当前错误路径
export function getRevisionPage(p){return request({url:'/emr-revision/page',method:'get',params:p})}
export function getRevisionList(p){return request({url:'/emr-revision/list',method:'get',params:p})}
export function recordRevision(d){return request({url:'/emr-revision/record',method:'post',data:d})}
export function compareRevisions(id1,id2){return request({url:'/emr-revision/compare',method:'get',params:{revisionId1:id1,revisionId2:id2}})}
```
- [ ] **Step 2: 修正API路径**
```javascript
import request from '@/utils/request'
export function getRevisionPage(p){return request({url:'/emr/revision/page',method:'get',params:p})}
export function getRevisionList(emrId){return request({url:'/emr/revision/list/'+emrId,method:'get'})}
export function recordRevision(d){return request({url:'/emr/revision/record',method:'post',data:d})}
export function compareRevisions(id1,id2){return request({url:'/emr/revision/compare',method:'get',params:{revisionId1:id1,revisionId2:id2}})}
```
- [ ] **Step 3: 验证后端路径一致**
确认 `EmrRevisionController.java` 中:
- `@RequestMapping("/emr/revision")`
- `@GetMapping("/page")``/emr/revision/page`
- `@GetMapping("/list/{emrId}")``/emr/revision/list/{emrId}`
- `@PostMapping("/record")``/emr/revision/record`
- `@GetMapping("/compare")``/emr/revision/compare`
- [ ] **Step 4: Commit**
```bash
git add healthlink-his-ui/src/views/emr/revision-history/api.js
git commit -m "fix(emr): 修正修订历史API路径与后端对齐"
```
---
## Task 2: EMR管理页面支持URL参数自动加载
**Covers:** 问题#6
**Files:**
- Modify: `healthlink-his-ui/src/views/emr/revision-history/index.vue`
- Modify: `healthlink-his-ui/src/views/emr/archive/index.vue`
- Modify: `healthlink-his-ui/src/views/emr/timeliness/index.vue`
- Modify: `healthlink-his-ui/src/views/emr/completeness-check/index.vue`
- Modify: `healthlink-his-ui/src/views/emrsearch/index.vue`
- [ ] **Step 1: 修改 revision-history/index.vue 支持URL参数**
`<script setup>` 中添加:
```javascript
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getRevisionPage } from './api'
const route = useRoute()
const tableData = ref([])
const total = ref(0)
const q = ref({pageNo:1, pageSize:20, emrId:'', operatorName:''})
const loadData = async () => {
const r = await getRevisionPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
}
onMounted(() => {
// 支持URL参数自动加载
if (route.query.emrId) {
q.value.emrId = route.query.emrId
}
loadData()
})
```
- [ ] **Step 2: 修改 archive/index.vue 支持URL参数**
```javascript
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(() => {
if (route.query.encounterId) {
q.value.encounterId = route.query.encounterId
}
if (route.query.patientName) {
q.value.patientName = route.query.patientName
}
loadData()
loadStats()
})
```
- [ ] **Step 3: 修改 timeliness/index.vue 支持URL参数**
```javascript
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(() => {
if (route.query.encounterId) {
queryParams.encounterId = route.query.encounterId
}
if (route.query.departmentName) {
queryParams.departmentName = route.query.departmentName
}
getList()
})
```
- [ ] **Step 4: 修改 completeness-check/index.vue 支持URL参数**
```javascript
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(() => {
if (route.query.emrId) {
checkForm.emrId = route.query.emrId
}
if (route.query.encounterId) {
checkForm.encounterId = route.query.encounterId
}
// 如果有参数自动执行检查
if (checkForm.emrId && checkForm.encounterId) {
handleCheck()
}
})
```
- [ ] **Step 5: 修改 emrsearch/index.vue 支持URL参数**
```javascript
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(() => {
if (route.query.patientName) {
queryParams.patientName = route.query.patientName
}
if (route.query.emrType) {
queryParams.emrType = route.query.emrType
}
handleSearch()
})
```
- [ ] **Step 6: Commit**
```bash
git add healthlink-his-ui/src/views/emr/revision-history/index.vue \
healthlink-his-ui/src/views/emr/archive/index.vue \
healthlink-his-ui/src/views/emr/timeliness/index.vue \
healthlink-his-ui/src/views/emr/completeness-check/index.vue \
healthlink-his-ui/src/views/emrsearch/index.vue
git commit -m "feat(emr): EMR管理页面支持URL参数自动加载"
```
---
## Task 3: 医生工作站添加EMR集成入口
**Covers:** 问题#4, #5
**Files:**
- Modify: `healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue`
- [ ] **Step 1: 读取 emr.vue 确认现有结构**
找到病历详情展示区域,在操作按钮区域添加集成入口。
- [ ] **Step 2: 添加集成按钮**
在病历详情弹窗或操作区域添加:
```vue
<template>
<!-- 在现有病历操作按钮区域添加 -->
<el-button type="info" link @click="viewRevisionHistory">
<el-icon><Document /></el-icon> 修订历史
</el-button>
<el-button type="info" link @click="viewArchiveStatus">
<el-icon><Folder /></el-icon> 归档状态
</el-button>
<el-button type="info" link @click="checkCompleteness">
<el-icon><Checked /></el-icon> 完整性检查
</el-button>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { checkCompleteness as checkEmrCompleteness } from '@/api/emr'
import { ElMessage } from 'element-plus'
const router = useRouter()
// 当前病历数据从父组件传入或从store获取
const currentEmr = defineModel('emr', { type: Object, default: () => ({}) })
const viewRevisionHistory = () => {
if (!currentEmr.value?.id) {
ElMessage.warning('请先选择病历')
return
}
router.push({
path: '/emr/revision-history',
query: { emrId: currentEmr.value.id }
})
}
const viewArchiveStatus = () => {
if (!currentEmr.value?.encounterId) {
ElMessage.warning('请先选择病历')
return
}
router.push({
path: '/emr/archive',
query: {
encounterId: currentEmr.value.encounterId,
patientName: currentEmr.value.patientName
}
})
}
const checkCompleteness = async () => {
if (!currentEmr.value?.id || !currentEmr.value?.encounterId) {
ElMessage.warning('请先选择病历')
return
}
try {
const res = await checkEmrCompleteness(currentEmr.value.id, currentEmr.value.encounterId)
const data = res.data || res
if (data.isComplete) {
ElMessage.success('病历完整性检查通过')
} else {
ElMessage.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`)
}
} catch (e) {
ElMessage.error('检查失败')
}
}
</script>
```
- [ ] **Step 3: 验证编译**
```bash
cd healthlink-his-ui && npm run build:dev
```
- [ ] **Step 4: Commit**
```bash
git add healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue
git commit -m "feat(emr): 医生工作站添加修订历史/归档/完整性检查入口"
```
---
## Task 4: 住院医生工作站添加EMR集成入口
**Covers:** 问题#4, #5
**Files:**
- Modify: `healthlink-his-ui/src/views/inpatientDoctor/home/emr/index.vue`
- [ ] **Step 1: 读取住院EMR页面确认结构**
- [ ] **Step 2: 添加集成按钮**
```vue
<template>
<!-- 在现有病历操作按钮区域添加 -->
<el-button type="info" link @click="viewRevisionHistory">
修订历史
</el-button>
<el-button type="info" link @click="viewTimeliness">
时效监控
</el-button>
<el-button type="info" link @click="checkCompleteness">
完整性检查
</el-button>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { checkCompleteness as checkEmrCompleteness } from '@/api/emr'
import { ElMessage } from 'element-plus'
const router = useRouter()
const currentEmr = defineModel('emr', { type: Object, default: () => ({}) })
const viewRevisionHistory = () => {
if (!currentEmr.value?.id) {
ElMessage.warning('请先选择病历')
return
}
router.push({
path: '/emr/revision-history',
query: { emrId: currentEmr.value.id }
})
}
const viewTimeliness = () => {
if (!currentEmr.value?.encounterId) {
ElMessage.warning('请先选择病历')
return
}
router.push({
path: '/emr/timeliness',
query: { encounterId: currentEmr.value.encounterId }
})
}
const checkCompleteness = async () => {
if (!currentEmr.value?.id || !currentEmr.value?.encounterId) {
ElMessage.warning('请先选择病历')
return
}
try {
const res = await checkEmrCompleteness(currentEmr.value.id, currentEmr.value.encounterId)
const data = res.data || res
if (data.isComplete) {
ElMessage.success('病历完整性检查通过')
} else {
ElMessage.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`)
}
} catch (e) {
ElMessage.error('检查失败')
}
}
</script>
```
- [ ] **Step 3: 验证编译**
```bash
cd healthlink-his-ui && npm run build:dev
```
- [ ] **Step 4: Commit**
```bash
git add healthlink-his-ui/src/views/inpatientDoctor/home/emr/index.vue
git commit -m "feat(emr): 住院医生工作站添加修订历史/时效/完整性检查入口"
```
---
## Task 5: 保存病历时自动触发修订记录
**Covers:** 问题#2
**Files:**
- Modify: `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java`
- [ ] **Step 1: 读取现有保存逻辑**
找到 `saveEmr` 或类似方法,确认保存流程。
- [ ] **Step 2: 添加自动触发修订记录**
在保存EMR成功后添加
```java
@Resource
private IEmrRevisionService emrRevisionService;
// 在saveEmr方法中保存成功后添加
// 自动记录修订历史
EmrRevision revision = new EmrRevision();
revision.setEmrId(savedEmr.getId());
revision.setEncounterId(savedEmr.getEncounterId());
revision.setOperatorName(operatorName); // 从SecurityUtils获取
revision.setOperationType("SAVE");
revision.setSnapshotContent(savedEmr.getContextJson());
revision.setCreateTime(new Date());
emrRevisionService.save(revision);
```
- [ ] **Step 3: 验证编译**
```bash
mvn clean compile -DskipTests -pl healthlink-his-application
```
- [ ] **Step 4: Commit**
```bash
git add healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java
git commit -m "feat(emr): 保存病历时自动创建修订记录"
```
---
## Task 6: 保存病历时自动更新搜索索引
**Covers:** 问题#3
**Files:**
- Modify: `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java`
- [ ] **Step 1: 添加搜索索引服务注入**
```java
@Resource
private IEmrSearchIndexService emrSearchIndexService;
```
- [ ] **Step 2: 保存成功后自动更新索引**
```java
// 在saveEmr方法中保存成功后添加
// 自动更新搜索索引
EmrSearchIndex searchIndex = new EmrSearchIndex();
searchIndex.setEmrId(savedEmr.getId());
searchIndex.setEncounterId(savedEmr.getEncounterId());
searchIndex.setPatientName(patientName);
searchIndex.setEmrType(emrType);
searchIndex.setEmrTitle(title);
searchIndex.setDiagnosisText(diagnosis);
searchIndex.setDoctorName(doctorName);
searchIndex.setDepartmentName(departmentName);
searchIndex.setCreateTime(new Date());
// 检查是否已存在索引
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmrSearchIndex::getEmrId, savedEmr.getId());
EmrSearchIndex existing = emrSearchIndexService.getOne(wrapper);
if (existing != null) {
existing.setPatientName(patientName);
existing.setEmrType(emrType);
existing.setEmrTitle(title);
existing.setDiagnosisText(diagnosis);
existing.setDoctorName(doctorName);
existing.setDepartmentName(departmentName);
existing.setUpdateTime(new Date());
emrSearchIndexService.updateById(existing);
} else {
emrSearchIndexService.save(searchIndex);
}
```
- [ ] **Step 3: 验证编译**
```bash
mvn clean compile -DskipTests -pl healthlink-his-application
```
- [ ] **Step 4: Commit**
```bash
git add healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java
git commit -m "feat(emr): 保存病历时自动更新搜索索引"
```
---
## Task 7: 全量验证
**Covers:** 全部问题
- [ ] **Step 1: 后端编译验证**
```bash
mvn clean compile -DskipTests
```
Expected: BUILD SUCCESS
- [ ] **Step 2: 前端编译验证**
```bash
cd healthlink-his-ui && npm run build:dev
```
Expected: Build successful
- [ ] **Step 3: 接口测试**
```bash
# 测试修订历史接口
curl http://localhost:18082/emr/revision/page?pageNo=1&pageSize=10
# 测试搜索接口
curl http://localhost:18082/emr-search/search?keyword=test
# 测试归档接口
curl http://localhost:18082/emr-archive/page?pageNo=1&pageSize=10
```
Expected: 返回 `{code:200, data:...}`
- [ ] **Step 4: 提交代码**
```bash
git add -A
git commit -m "feat(emr): 打通EMR管理模块与门诊/住院病历集成"
git push origin develop
```
---
## 验证检查清单
| 检查项 | 验证方式 | 预期结果 |
|--------|---------|---------|
| 修订历史API路径 | 访问 `/emr/revision/page` | 返回数据 |
| URL参数支持 | 访问 `/emr/revision-history?emrId=1` | 自动加载该病历修订记录 |
| 医生工作站入口 | 打开病历详情 | 显示修订历史/归档/完整性检查按钮 |
| 保存自动触发 | 保存病历后查询 `emr_revision` 表 | 有新记录 |
| 搜索索引更新 | 保存病历后查询 `emr_search_index` 表 | 有新记录 |
| 完整性检查 | 点击完整性检查按钮 | 显示检查结果 |
---
> **Plan Version:** v1.0
> **Created:** 2026-06-21
> **Estimated Effort:** 2-3小时

View File

@@ -0,0 +1,95 @@
# EMR数据同步使用说明
## 功能概述
EMR数据同步功能用于将门诊/住院病历表(doc_emr)中的真实数据同步到EMR管理模块的修订历史和搜索索引中。
## 使用步骤
### 1. 启动后端应用
```bash
cd healthlink-his-server
mvn spring-boot:run -pl healthlink-his-application
```
### 2. 登录系统
访问 http://localhost:81 登录系统
### 3. 访问同步页面
在菜单中找到:**电子病历管理 > EMR数据同步**
或者直接访问:`http://localhost:81/emr/sync`
### 4. 执行同步
1. 查看当前统计信息(病历总数、修订历史、搜索索引)
2. 点击"开始同步"按钮
3. 确认同步操作
4. 等待同步完成
5. 查看同步后的统计信息
## API接口
### 获取同步统计
```
GET /emr-sync/stats
```
返回:
```json
{
"code": 200,
"data": {
"emrCount": 100,
"revisionCount": 100,
"searchIndexCount": 100
}
}
```
### 执行同步
```
POST /emr-sync/sync
```
返回:
```json
{
"code": 200,
"data": "同步完成: 修订历史100条, 搜索索引100条"
}
```
## 数据流向
```
doc_emr (门诊/住院病历)
↓ 同步
emr_revision (修订历史)
emr_search_index (搜索索引)
↓ 展示
EMR管理页面修订历史、病历检索等
```
## 注意事项
1. **同步会清空现有数据**执行同步前会清空emr_revision和emr_search_index表
2. **建议先备份**:如果表中有重要数据,建议先备份
3. **同步后刷新页面**:同步完成后需要刷新页面才能看到新数据
4. **权限要求**:需要管理员权限才能执行同步操作
## 常见问题
### Q: 同步后数据没有显示?
A: 请刷新页面,或检查浏览器控制台是否有错误
### Q: 同步失败怎么办?
A: 检查后端日志,确认数据库连接正常
### Q: 可以只同步部分数据吗?
A: 当前版本不支持部分同步会同步所有doc_emr中的数据

111
MD/guides/YB_MOCK_GUIDE.md Normal file
View File

@@ -0,0 +1,111 @@
# 医保模拟接口使用说明
## 概述
本项目提供了一个医保模拟服务器(`YbMockController`),用于在本地测试医保接口功能,无需连接真实的医保系统。
## 模拟的接口
| 接口代码 | 功能 | 请求示例 |
|---------|------|---------|
| 1101 | 获取参保人信息 | `{"psn_no":"P1234567890"}` |
| 2201 | 门诊登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
| 2203 | 门诊处方上传 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
| 2207 | 门诊结算 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
| 3201 | 住院登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
| 3203 | 住院处方上传 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
| 3207 | 住院结算 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
## 使用方法
### 1. 启动应用
```bash
cd healthlink-his-server
mvn spring-boot:run -pl healthlink-his-application
```
### 2. 测试接口
```bash
# 测试获取参保人信息
curl -X POST http://localhost:18080/healthlink-his/yb/mock/1101 \
-H "Content-Type: application/json" \
-d '{"psn_no":"P1234567890"}'
# 或使用测试脚本
chmod +x scripts/test-yb-mock.sh
./scripts/test-yb-mock.sh
```
### 3. 配置医保接口地址
`application-dev.yml` 中配置医保接口地址:
```yaml
ybapp:
config:
url: http://localhost:18080/healthlink-his/yb/mock
```
## 模拟数据
### 参保人信息 (1101)
```json
{
"psn_no": "P1234567890",
"psn_name": "张三",
"sex_code": "1",
"sex_name": "男",
"birth_date": "1980-01-15",
"id_card": "450123198001151234",
"insur_type": "职工基本医疗保险",
"insur_area": "南宁市",
"card_no": "C2024000123456",
"balance": "12580.50",
"status": "正常"
}
```
### 门诊结算 (2207)
```json
{
"settle_no": "JZ20260623001",
"total_amount": "156.80",
"insurance_pay": "133.28",
"self_pay": "23.52",
"account_pay": "20.00",
"cash_pay": "3.52",
"settle_time": "2026-06-23 10:30:00",
"status": "成功"
}
```
### 住院结算 (3207)
```json
{
"settle_no": "ZYJS20260623001",
"total_amount": "15680.50",
"insurance_pay": "14112.45",
"self_pay": "1568.05",
"account_pay": "1200.00",
"cash_pay": "368.05",
"settle_time": "2026-06-23 10:30:00",
"status": "成功"
}
```
## 注意事项
1. 模拟服务器仅用于本地测试,不模拟真实的医保业务逻辑
2. 返回的数据是固定的测试数据,不会根据请求参数变化
3. 生产环境请连接真实的医保接口
4. 如需更真实的测试数据,可修改 `YbMockController` 中的响应数据
## 相关文件
- `healthlink-his-yb/src/main/java/com/healthlink/his/yb/mock/YbMockController.java`
- `scripts/test-yb-mock.sh`

View File

@@ -16,7 +16,7 @@
| #2 | Flyway 数据库迁移 | P0 | 数据库变更 | | #2 | Flyway 数据库迁移 | P0 | 数据库变更 |
| #3 | 先分解再行动 | P1 | 非平凡任务 | | #3 | 先分解再行动 | P1 | 非平凡任务 |
| #4 | 验证后信 | P1 | 编译/构建 | | #4 | 验证后信 | P1 | 编译/构建 |
| #5 | 文档统一管理 | P1 | 文档产出 | | #5 | 文档统一管理P0绝对铁律 | P0 | 文档产出 |
| #6 | 测试通过后才提交 | P0 | 代码提交 | | #6 | 测试通过后才提交 | P0 | 代码提交 |
| #7 | 前后端API路径对齐 | P0 | 接口开发 | | #7 | 前后端API路径对齐 | P0 | 接口开发 |
| #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 | | #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 |
@@ -120,26 +120,38 @@ cd healthlink-his-ui && npm run build:dev
--- ---
### 铁律 #5: 文档统一管理 ### 铁律 #5: 文档统一管理P0 绝对铁律)
**所有文档必须存储在 `MD/` 目录中,遵循文档规范。** **所有文档必须存储在 `MD/` 目录中,禁止在项目其他位置创建文档文件。**
#### 目录结构 #### 绝对禁止
| ❌ 禁止行为 | 说明 |
|------------|------|
| 在项目根目录创建 `.md` 文件 | 如 `README.md``TODO.md``NOTES.md` 等 |
| 在子模块目录创建文档 | 如 `healthlink-his-server/DESIGN.md` |
| 在 `docs/` 目录存放文档 | 必须移动到 `MD/` |
| 随意创建新目录 | 必须使用已有目录结构 |
| 使用中文作文件名 | 必须使用大写英文+下划线 |
#### 目录结构(必须遵守)
``` ```
MD/ MD/
├── DOCUMENTATION_STANDARD.md # 文档管理规范 ├── DOCUMENTATION_STANDARD.md # 文档管理规范
├── architecture/ # 架构设计 ├── architecture/ # 架构设计文档
├── design/ # 模块设计文档
├── development/ # 开发计划与记录 ├── development/ # 开发计划与记录
├── standards/ # 国家/行业标准 ├── standards/ # 国家/行业标准
├── specs/ # 技术规范与流程 ├── specs/ # 技术规范与流程
├── bugs/ # Bug分析与修复记录 ├── bugs/ # Bug分析与修复记录
├── guides/ # 使用指南 ├── guides/ # 使用指南
── upgrade/ # 升级记录 ── upgrade/ # 升级记录
├── test/ # 测试文档
└── 需求/ # 需求文档(允许中文目录名)
``` ```
#### 命名规范 #### 命名规范
- 文件名使用 **大写英文+下划线**(如 `GRADE3A_DETAILED_DESIGN.md` - 文件名使用 **大写英文+下划线**(如 `GRADE3A_DETAILED_DESIGN.md`
- 不使用中文作文件名 - 不使用中文作文件名(需求目录除外)
- 不使用空格分隔单词 - 不使用空格分隔单词
- 版本号标注在文件名末尾(如 `_V2` - 版本号标注在文件名末尾(如 `_V2`
@@ -234,6 +246,7 @@ MD/
|------|------|---------| |------|------|---------|
| P0 违规 | 跳过测试直接提交 | 必须回滚并重新测试 | | P0 违规 | 跳过测试直接提交 | 必须回滚并重新测试 |
| P0 违规 | 数据库变更不走Flyway | 回滚数据库变更重新用Flyway执行 | | P0 违规 | 数据库变更不走Flyway | 回滚数据库变更重新用Flyway执行 |
| P0 违规 | 在MD目录外创建文档 | 立即移动到MD目录删除原文件 |
| P1 违规 | 未分解就行动 | 补充分析和计划文档 | | P1 违规 | 未分解就行动 | 补充分析和计划文档 |
| P1 违规 | 文档不规范 | 补充元数据和格式 | | P1 违规 | 文档不规范 | 补充元数据和格式 |

View File

@@ -55,6 +55,12 @@ public class SysDictData extends BaseEntity {
/** 拼音首字母 */ /** 拼音首字母 */
private String pyStr; private String pyStr;
/** 字典英文值 */
private String dictValueEn;
/** 字典越南文值 */
private String dictValueVi;
public Long getDictCode() { public Long getDictCode() {
return dictCode; return dictCode;
} }
@@ -146,12 +152,29 @@ public class SysDictData extends BaseEntity {
this.pyStr = pyStr; this.pyStr = pyStr;
} }
public String getDictValueEn() {
return dictValueEn;
}
public void setDictValueEn(String dictValueEn) {
this.dictValueEn = dictValueEn;
}
public String getDictValueVi() {
return dictValueVi;
}
public void setDictValueVi(String dictValueVi) {
this.dictValueVi = dictValueVi;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("dictCode", getDictCode()) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("dictCode", getDictCode())
.append("dictSort", getDictSort()).append("dictLabel", getDictLabel()).append("dictValue", getDictValue()) .append("dictSort", getDictSort()).append("dictLabel", getDictLabel()).append("dictValue", getDictValue())
.append("dictType", getDictType()).append("cssClass", getCssClass()).append("listClass", getListClass()) .append("dictType", getDictType()).append("cssClass", getCssClass()).append("listClass", getListClass())
.append("isDefault", getIsDefault()).append("status", getStatus()).append("pyStr", getPyStr()) .append("isDefault", getIsDefault()) .append("status", getStatus()).append("pyStr", getPyStr())
.append("dictValueEn", getDictValueEn()).append("dictValueVi", getDictValueVi())
.append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy()) .append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString(); .append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
} }

View File

@@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.springframework.context.i18n.LocaleContextHolder;
/** /**
* 字典工具类 * 字典工具类
@@ -76,7 +78,7 @@ public class DictUtils {
} }
/** /**
* 根据字典类型和字典值获取字典标签 * 根据字典类型和字典值获取字典标签(支持国际化)
* *
* @param dictType 字典类型 * @param dictType 字典类型
* @param dictValue 字典值 * @param dictValue 字典值
@@ -89,6 +91,36 @@ public class DictUtils {
return getDictLabel(dictType, dictValue, SEPARATOR); return getDictLabel(dictType, dictValue, SEPARATOR);
} }
/**
* 根据字典类型和字典值获取国际化字典标签
*
* @param dictType 字典类型
* @param dictValue 字典值
* @return 国际化字典标签
*/
public static String getDictLabelI18n(String dictType, String dictValue) {
if (StringUtils.isEmpty(dictValue)) {
return StringUtils.EMPTY;
}
List<SysDictData> datas = getDictCache(dictType);
if (StringUtils.isNull(datas)) {
return StringUtils.EMPTY;
}
Locale locale = LocaleContextHolder.getLocale();
String lang = locale.getLanguage();
for (SysDictData dict : datas) {
if (dictValue.equals(dict.getDictValue())) {
if ("en".equals(lang) && dict.getDictValueEn() != null) {
return dict.getDictValueEn();
} else if ("vi".equals(lang) && dict.getDictValueVi() != null) {
return dict.getDictValueVi();
}
return dict.getDictLabel();
}
}
return StringUtils.EMPTY;
}
/** /**
* 根据字典类型和字典标签获取字典值 * 根据字典类型和字典标签获取字典值
* *

View File

@@ -176,7 +176,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
children.setQuery(menu.getQuery()); children.setQuery(menu.getQuery());
childrenList.add(children); childrenList.add(children);
router.setChildren(childrenList); router.setChildren(childrenList);
} else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) { } else if ((menu.getParentId() == null || menu.getParentId() == 0) && isInnerLink(menu)) {
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible())); router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible()));
router.setPath("/"); router.setPath("/");
List<RouterVo> childrenList = new ArrayList<RouterVo>(); List<RouterVo> childrenList = new ArrayList<RouterVo>();
@@ -524,11 +524,11 @@ public class SysMenuServiceImpl implements ISysMenuService {
public String getRouterPath(SysMenu menu) { public String getRouterPath(SysMenu menu) {
String routerPath = menu.getPath(); String routerPath = menu.getPath();
// 内链打开外网方式 // 内链打开外网方式
if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) { if (menu.getParentId() != null && menu.getParentId() != 0 && isInnerLink(menu)) {
routerPath = innerLinkReplaceEach(routerPath); routerPath = innerLinkReplaceEach(routerPath);
} }
// 非外链并且是一级目录(类型为目录) // 非外链并且是一级目录(类型为目录)
if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) if ((menu.getParentId() == null || menu.getParentId() == 0) && UserConstants.TYPE_DIR.equals(menu.getMenuType())
&& UserConstants.NO_FRAME.equals(menu.getIsFrame())) { && UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
routerPath = "/" + menu.getPath(); routerPath = "/" + menu.getPath();
} }
@@ -549,7 +549,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
String component = UserConstants.LAYOUT; String component = UserConstants.LAYOUT;
if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) { if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
component = menu.getComponent(); component = menu.getComponent();
} else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 } else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId() != null
&& menu.getParentId() != 0
&& isInnerLink(menu)) { && isInnerLink(menu)) {
component = UserConstants.INNER_LINK; component = UserConstants.INNER_LINK;
} else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) { } else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
@@ -565,7 +566,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 结果 * @return 结果
*/ */
public boolean isMenuFrame(SysMenu menu) { public boolean isMenuFrame(SysMenu menu) {
return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) return (menu.getParentId() == null || menu.getParentId() == 0) && UserConstants.TYPE_MENU.equals(menu.getMenuType())
&& menu.getIsFrame().equals(UserConstants.NO_FRAME); && menu.getIsFrame().equals(UserConstants.NO_FRAME);
} }
@@ -586,7 +587,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
* @return 结果 * @return 结果
*/ */
public boolean isParentView(SysMenu menu) { public boolean isParentView(SysMenu menu) {
return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); return menu.getParentId() != null && menu.getParentId() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
} }
/** /**
@@ -634,7 +635,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
Iterator<SysMenu> it = list.iterator(); Iterator<SysMenu> it = list.iterator();
while (it.hasNext()) { while (it.hasNext()) {
SysMenu n = (SysMenu)it.next(); SysMenu n = (SysMenu)it.next();
if (n.getParentId().longValue() == t.getMenuId().longValue()) { if (n.getParentId() != null && n.getParentId().longValue() == t.getMenuId().longValue()) {
tlist.add(n); tlist.add(n);
} }
} }

View File

@@ -75,6 +75,11 @@
<artifactId>healthlink-his-domain</artifactId> <artifactId>healthlink-his-domain</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>com.healthlink.his</groupId>
<artifactId>healthlink-his-yb</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 基础模块 --> <!-- 基础模块 -->
<dependency> <dependency>
@@ -89,6 +94,10 @@
<groupId>com.core</groupId> <groupId>com.core</groupId>
<artifactId>core-generator</artifactId> <artifactId>core-generator</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.core</groupId>
<artifactId>core-admin</artifactId>
</dependency>
<!-- liteflow--> <!-- liteflow-->
<dependency> <dependency>

View File

@@ -0,0 +1,106 @@
package com.healthlink.his.web.anesthesia.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaQualityControl;
import com.healthlink.his.anesthesia.domain.AnesthesiaSpecimen;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import com.healthlink.his.anesthesia.service.IAnesthesiaQualityControlService;
import com.healthlink.his.anesthesia.service.IAnesthesiaSpecimenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Map;
@RestController
@RequestMapping("/anesthesia-enhanced")
@AllArgsConstructor
@Tag(name = "麻醉扩展-标本/随访/质控")
public class AnesthesiaEnhancedCrudController {
private final IAnesthesiaSpecimenService specimenService;
private final IAnesthesiaFollowupService followupService;
private final IAnesthesiaQualityControlService qcService;
@GetMapping("/specimen/page")
@Operation(summary = "标本分页")
public R<?> getSpecimenPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String patientName) {
LambdaQueryWrapper<AnesthesiaSpecimen> w = new LambdaQueryWrapper<>();
w.like(patientName != null, AnesthesiaSpecimen::getPatientName, patientName)
.orderByDesc(AnesthesiaSpecimen::getCreateTime);
return R.ok(specimenService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/specimen/add")
@Operation(summary = "新增标本")
public R<?> addSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
specimen.setCreateTime(new Date());
specimenService.save(specimen);
return R.ok(specimen);
}
@PostMapping("/specimen/report")
@Operation(summary = "报告标本结果")
public R<?> reportSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
specimen.setReportTime(new Date());
specimenService.updateById(specimen);
return R.ok(specimen);
}
@GetMapping("/followup/page")
@Operation(summary = "随访分页")
public R<?> getFollowupPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaFollowup> w = new LambdaQueryWrapper<>();
w.orderByDesc(AnesthesiaFollowup::getFollowupDate);
return R.ok(followupService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/followup/add")
@Operation(summary = "新增随访")
public R<?> addFollowup(@RequestBody AnesthesiaFollowup followup) {
followup.setCreateTime(new Date());
followupService.save(followup);
return R.ok(followup);
}
@GetMapping("/qc/page")
@Operation(summary = "质控分页")
public R<?> getQcPage(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaQualityControl> w = new LambdaQueryWrapper<>();
w.orderByDesc(AnesthesiaQualityControl::getCreateTime);
return R.ok(qcService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/qc/add")
@Operation(summary = "新增质控记录")
public R<?> addQc(@RequestBody AnesthesiaQualityControl qc) {
qc.setCreateTime(new Date());
qcService.save(qc);
return R.ok(qc);
}
@GetMapping("/qc/stats")
@Operation(summary = "质控统计")
public R<?> getQcStats() {
long total = qcService.count();
long normal = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "NORMAL"));
long warning = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "WARNING"));
long critical = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
.eq(AnesthesiaQualityControl::getRiskLevel, "CRITICAL"));
return R.ok(Map.of("total", total, "normal", normal, "warning", warning, "critical", critical));
}
}

View File

@@ -532,11 +532,22 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
String registerTimeSTime = request.getParameter("registerTimeSTime"); String registerTimeSTime = request.getParameter("registerTimeSTime");
String registerTimeETime = request.getParameter("registerTimeETime"); String registerTimeETime = request.getParameter("registerTimeETime");
// Bug #638提取可选科室过滤参数
Long deptId = null;
String deptIdParam = request.getParameter("deptId");
if (deptIdParam != null && !deptIdParam.isEmpty()) {
try {
deptId = Long.parseLong(deptIdParam);
} catch (NumberFormatException e) {
// 忽略无效的参数值
}
}
IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter( IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter(
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(), new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper, ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue(), ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue(),
registerTimeSTime, registerTimeETime, statusFilter); registerTimeSTime, registerTimeETime, statusFilter, deptId);
// 过滤候选池排除列表 // 过滤候选池排除列表
// 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费) // 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费)

View File

@@ -57,7 +57,8 @@ public interface OutpatientRegistrationAppMapper {
@Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus, @Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus,
@Param("registerTimeSTime") String registerTimeSTime, @Param("registerTimeSTime") String registerTimeSTime,
@Param("registerTimeETime") String registerTimeETime, @Param("registerTimeETime") String registerTimeETime,
@Param("statusFilter") Integer statusFilter); @Param("statusFilter") Integer statusFilter,
@Param("deptId") Long deptId);
/** /**
* 查询item绑定的信息(耗材或诊疗) * 查询item绑定的信息(耗材或诊疗)

View File

@@ -28,6 +28,10 @@ import com.healthlink.his.document.service.IEmrDetailService;
import com.healthlink.his.document.service.IEmrDictService; import com.healthlink.his.document.service.IEmrDictService;
import com.healthlink.his.document.service.IEmrService; import com.healthlink.his.document.service.IEmrService;
import com.healthlink.his.document.service.IEmrTemplateService; import com.healthlink.his.document.service.IEmrTemplateService;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.domain.EmrSearchIndex;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.emr.service.IEmrSearchIndexService;
import com.healthlink.his.web.doctorstation.appservice.IDoctorStationEmrAppService; import com.healthlink.his.web.doctorstation.appservice.IDoctorStationEmrAppService;
import com.healthlink.his.web.doctorstation.dto.EmrTemplateDto; import com.healthlink.his.web.doctorstation.dto.EmrTemplateDto;
import com.healthlink.his.web.doctorstation.dto.PatientEmrDto; import com.healthlink.his.web.doctorstation.dto.PatientEmrDto;
@@ -63,6 +67,18 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
@Resource @Resource
IDocRecordService docRecordService; IDocRecordService docRecordService;
@Resource
IEmrRevisionService emrRevisionService;
@Resource
IEmrSearchIndexService emrSearchIndexService;
@Resource
PatientMapper patientMapper;
@Resource
EncounterMapper encounterMapper;
@Resource @Resource
private com.healthlink.his.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper; private com.healthlink.his.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper;
@@ -79,10 +95,12 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
String contextStr = patientEmrDto.getContextJson().toString(); String contextStr = patientEmrDto.getContextJson().toString();
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false); Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
boolean saveSuccess; boolean saveSuccess;
boolean isUpdate = patientEmr != null;
// 如果已经保存病历,再次保存走更新 // 如果已经保存病历,再次保存走更新
if (patientEmr != null) { if (isUpdate) {
saveSuccess = emrService.update(new LambdaUpdateWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()) saveSuccess = emrService.update(new LambdaUpdateWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId())
.set(Emr::getContextJson, contextStr)); .set(Emr::getContextJson, contextStr));
emr = patientEmr;
} else { } else {
saveSuccess = saveSuccess =
emrService.save(emr.setContextJson(contextStr).setRecordId(SecurityUtils.getLoginUser().getUserId())); emrService.save(emr.setContextJson(contextStr).setRecordId(SecurityUtils.getLoginUser().getUserId()));
@@ -90,6 +108,21 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
if (!saveSuccess) { if (!saveSuccess) {
return R.fail(); return R.fail();
} }
// 自动触发:记录修订历史
try {
recordRevisionAutomatically(emr, contextStr, isUpdate);
} catch (Exception e) {
log.warn("自动记录修订历史失败: {}", e.getMessage());
}
// 自动触发:更新搜索索引
try {
updateSearchIndexAutomatically(emr, contextStr);
} catch (Exception e) {
log.warn("自动更新搜索索引失败: {}", e.getMessage());
}
// 获取电子病历字典表中全部key用来判断病历JSON串中是否有需要加入到病历详情表的字段 // 获取电子病历字典表中全部key用来判断病历JSON串中是否有需要加入到病历详情表的字段
List<String> emrDictList = emrDictService.list(new LambdaQueryWrapper<EmrDict>().select(EmrDict::getEmrKey)) List<String> emrDictList = emrDictService.list(new LambdaQueryWrapper<EmrDict>().select(EmrDict::getEmrKey))
.stream().map(EmrDict::getEmrKey).collect(Collectors.toList()); .stream().map(EmrDict::getEmrKey).collect(Collectors.toList());
@@ -114,6 +147,73 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
return save ? R.ok() : R.fail(); return save ? R.ok() : R.fail();
} }
/**
* 自动记录修订历史
*/
private void recordRevisionAutomatically(Emr emr, String contextStr, boolean isUpdate) {
EmrRevision latest = emrRevisionService.selectLatest(emr.getId());
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
EmrRevision revision = new EmrRevision();
revision.setEmrId(emr.getId());
revision.setEncounterId(emr.getEncounterId());
revision.setRevisionNumber(nextNumber);
revision.setOperatorId(SecurityUtils.getLoginUser().getUserId());
revision.setOperatorName(SecurityUtils.getUsername());
revision.setOperationType(isUpdate ? "UPDATE" : "CREATE");
revision.setSnapshotContent(contextStr);
if (isUpdate && latest != null) {
revision.setDiffContent("内容已更新");
}
revision.setCreateTime(new Date());
emrRevisionService.save(revision);
log.info("自动记录修订历史: emrId={}, revision={}", emr.getId(), nextNumber);
}
/**
* 自动更新搜索索引
*/
private void updateSearchIndexAutomatically(Emr emr, String contextStr) {
Map<String, String> contentMap = JsonUtils.parseObject(contextStr, new TypeReference<Map<String, String>>() {});
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
String diagnosis = contentMap.getOrDefault("diagnosis", "");
// 获取患者信息
Patient patient = patientMapper.selectById(emr.getPatientId());
String patientName = patient != null ? patient.getName() : "";
Long patientId = patient != null ? patient.getId() : null;
// 获取医生信息
String doctorName = SecurityUtils.getUsername();
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmrSearchIndex::getEmrId, emr.getId());
EmrSearchIndex existing = emrSearchIndexService.getOne(wrapper);
if (existing != null) {
existing.setPatientName(patientName);
existing.setPatientId(patientId);
existing.setEmrTitle(chiefComplaint);
existing.setDiagnosisText(diagnosis);
existing.setDoctorName(doctorName);
existing.setUpdateTime(new Date());
emrSearchIndexService.updateById(existing);
} else {
EmrSearchIndex index = new EmrSearchIndex();
index.setEmrId(emr.getId());
index.setEncounterId(emr.getEncounterId());
index.setPatientId(patientId);
index.setPatientName(patientName);
index.setEmrType("OUTPATIENT");
index.setEmrTitle(chiefComplaint);
index.setDiagnosisText(diagnosis);
index.setDoctorName(doctorName);
index.setCreateTime(new Date());
emrSearchIndexService.save(index);
}
log.info("自动更新搜索索引: emrId={}", emr.getId());
}
/** /**
* 获取患者历史病历 * 获取患者历史病历
* *

View File

@@ -236,7 +236,7 @@ public class AdviceBaseDto {
/** /**
* 用药说明 * 用药说明
*/ */
@Dict(dictCode = "dosage_instruction") @Dict(dictCode = "separate_decocting")
private String dosageInstruction; private String dosageInstruction;
private String dosageInstruction_dictText; private String dosageInstruction_dictText;
/* /*

View File

@@ -224,10 +224,16 @@ public class RequestBaseDto {
*/ */
private Integer sortNumber; private Integer sortNumber;
/**
* 账户id (费用性质/合同)
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long accountId;
/** /**
* 用药说明 * 用药说明
*/ */
@Dict(dictCode = "dosage_instruction") @Dict(dictCode = "separate_decocting")
private String dosageInstruction; private String dosageInstruction;
private String dosageInstruction_dictText; private String dosageInstruction_dictText;
@@ -260,4 +266,22 @@ public class RequestBaseDto {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date stopTime; private Date stopTime;
/**
* 诊断ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long conditionId;
/**
* 诊断定义ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long conditionDefinitionId;
/**
* 就诊诊断ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long encounterDiagnosisId;
} }

View File

@@ -979,11 +979,29 @@ public class DocRecordAppServiceImpl implements IDocRecordAppService {
"SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND status_enum = ? AND class_enum = ?"; "SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND status_enum = ? AND class_enum = ?";
Object[] params = {encounterId, patientId, EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue(), Object[] params = {encounterId, patientId, EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue(),
EncounterClass.IMP.getValue()}; EncounterClass.IMP.getValue()};
Map<String, Object> result = jdbcTemplate.queryForMap(sql, params); try {
HashMap<String, Date> map = new HashMap<>(); Map<String, Object> result = jdbcTemplate.queryForMap(sql, params);
map.put("hospDate", (Timestamp)result.get("start_time")); HashMap<String, Date> map = new HashMap<>();
map.put("outTime", (Timestamp)result.get("end_time")); map.put("hospDate", (Timestamp)result.get("start_time"));
return map; map.put("outTime", (Timestamp)result.get("end_time"));
return map;
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
try {
String fallbackSql = "SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND class_enum = ?";
Object[] fallbackParams = {encounterId, patientId, EncounterClass.IMP.getValue()};
Map<String, Object> result = jdbcTemplate.queryForMap(fallbackSql, fallbackParams);
HashMap<String, Date> map = new HashMap<>();
map.put("hospDate", (Timestamp)result.get("start_time"));
map.put("outTime", (Timestamp)result.get("end_time"));
return map;
} catch (Exception ex) {
log.warn("Querying adm_encounter failed: ", ex);
HashMap<String, Date> map = new HashMap<>();
map.put("hospDate", new Date());
map.put("outTime", null);
return map;
}
}
} }
/** /**
@@ -996,8 +1014,15 @@ public class DocRecordAppServiceImpl implements IDocRecordAppService {
String sql = String sql =
"SELECT ael.start_time FROM adm_encounter ae INNER JOIN adm_encounter_location ael ON ae.ID=ael.encounter_id AND ael.form_enum=? AND ael.status_enum=? AND ael.delete_flag='0' AND ael.tenant_id=1 LEFT JOIN adm_location al ON ael.location_id=al.ID AND al.delete_flag='0' AND al.tenant_id=1 WHERE ae.ID=? AND ae.delete_flag='0' AND ae.tenant_id=1"; "SELECT ael.start_time FROM adm_encounter ae INNER JOIN adm_encounter_location ael ON ae.ID=ael.encounter_id AND ael.form_enum=? AND ael.status_enum=? AND ael.delete_flag='0' AND ael.tenant_id=1 LEFT JOIN adm_location al ON ael.location_id=al.ID AND al.delete_flag='0' AND al.tenant_id=1 WHERE ae.ID=? AND ae.delete_flag='0' AND ae.tenant_id=1";
Object[] params = {LocationForm.BED.getValue(), EncounterActivityStatus.ACTIVE.getValue(), encounterId}; Object[] params = {LocationForm.BED.getValue(), EncounterActivityStatus.ACTIVE.getValue(), encounterId};
Timestamp timestamp = jdbcTemplate.queryForObject(sql, params, Timestamp.class); try {
return Date.from(timestamp.toInstant()); List<Timestamp> list = jdbcTemplate.queryForList(sql, Timestamp.class, params);
if (list != null && !list.isEmpty() && list.get(0) != null) {
return Date.from(list.get(0).toInstant());
}
} catch (Exception e) {
log.warn("Querying location admission date failed: ", e);
}
return new Date();
} }
/** /**

View File

@@ -9,7 +9,6 @@ import com.healthlink.his.document.service.IProgressNoteReminderService;
import com.healthlink.his.document.service.IProgressNoteService; import com.healthlink.his.document.service.IProgressNoteService;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -52,7 +51,6 @@ public class ProgressNoteController {
* 分页查询病程记录列表 * 分页查询病程记录列表
*/ */
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("hasAuthority('document:progressnote:list')")
public R<?> getPage( public R<?> getPage(
@RequestParam(value = "patientName", required = false) String patientName, @RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "noteType", required = false) Integer noteType, @RequestParam(value = "noteType", required = false) Integer noteType,
@@ -75,7 +73,6 @@ public class ProgressNoteController {
* 查询病程记录详情 * 查询病程记录详情
*/ */
@GetMapping("/detail") @GetMapping("/detail")
@PreAuthorize("hasAuthority('document:progressnote:list')")
public R<?> getDetail(@RequestParam Long id) { public R<?> getDetail(@RequestParam Long id) {
ProgressNote note = progressNoteService.getById(id); ProgressNote note = progressNoteService.getById(id);
if (note == null) return R.fail("病程记录不存在"); if (note == null) return R.fail("病程记录不存在");
@@ -86,7 +83,6 @@ public class ProgressNoteController {
* 新增病程记录 * 新增病程记录
*/ */
@PostMapping("/add") @PostMapping("/add")
@PreAuthorize("hasAuthority('document:progressnote:add')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody ProgressNote note) { public R<?> add(@RequestBody ProgressNote note) {
note.setSignStatus(0); note.setSignStatus(0);
@@ -108,7 +104,6 @@ public class ProgressNoteController {
* 修改病程记录(仅未签名可修改) * 修改病程记录(仅未签名可修改)
*/ */
@PutMapping("/update") @PutMapping("/update")
@PreAuthorize("hasAuthority('document:progressnote:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> update(@RequestBody ProgressNote note) { public R<?> update(@RequestBody ProgressNote note) {
ProgressNote existing = progressNoteService.getById(note.getId()); ProgressNote existing = progressNoteService.getById(note.getId());
@@ -124,7 +119,6 @@ public class ProgressNoteController {
* 删除病程记录(仅未签名可删除) * 删除病程记录(仅未签名可删除)
*/ */
@DeleteMapping("/delete") @DeleteMapping("/delete")
@PreAuthorize("hasAuthority('document:progressnote:remove')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> delete(@RequestParam Long id) { public R<?> delete(@RequestParam Long id) {
ProgressNote note = progressNoteService.getById(id); ProgressNote note = progressNoteService.getById(id);
@@ -138,7 +132,6 @@ public class ProgressNoteController {
* 签名病程记录 * 签名病程记录
*/ */
@PostMapping("/sign") @PostMapping("/sign")
@PreAuthorize("hasAuthority('document:progressnote:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> sign(@RequestBody Map<String, Object> params) { public R<?> sign(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString()); Long id = Long.valueOf(params.get("id").toString());
@@ -158,7 +151,6 @@ public class ProgressNoteController {
* 审核病程记录(上级医师) * 审核病程记录(上级医师)
*/ */
@PostMapping("/review") @PostMapping("/review")
@PreAuthorize("hasAuthority('document:progressnote:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> review(@RequestBody Map<String, Object> params) { public R<?> review(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString()); Long id = Long.valueOf(params.get("id").toString());
@@ -177,7 +169,6 @@ public class ProgressNoteController {
* 获取时限监控面板 * 获取时限监控面板
*/ */
@GetMapping("/monitor") @GetMapping("/monitor")
@PreAuthorize("hasAuthority('document:progressnote:list')")
public R<?> getMonitor(@RequestParam(required = false) Long encounterId) { public R<?> getMonitor(@RequestParam(required = false) Long encounterId) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
Date now = new Date(); Date now = new Date();
@@ -225,7 +216,6 @@ public class ProgressNoteController {
* 获取提醒列表 * 获取提醒列表
*/ */
@GetMapping("/reminders") @GetMapping("/reminders")
@PreAuthorize("hasAuthority('document:progressnote:list')")
public R<?> getReminders( public R<?> getReminders(
@RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "encounterId", required = false) Long encounterId) { @RequestParam(value = "encounterId", required = false) Long encounterId) {
@@ -240,7 +230,6 @@ public class ProgressNoteController {
* 获取病程记录统计 * 获取病程记录统计
*/ */
@GetMapping("/stats") @GetMapping("/stats")
@PreAuthorize("hasAuthority('document:progressnote:list')")
public R<?> getStats(@RequestParam Long encounterId) { public R<?> getStats(@RequestParam Long encounterId) {
Map<String, Object> stats = new HashMap<>(); Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();

View File

@@ -10,5 +10,7 @@ public interface IEmrTimelinessAppService {
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId); EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
EmrTimelinessStatisticsDto getStatistics();
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize); Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
} }

View File

@@ -66,6 +66,22 @@ public class EmrTimelinessAppServiceImpl implements IEmrTimelinessAppService {
return stats; return stats;
} }
@Override
public EmrTimelinessStatisticsDto getStatistics() {
long total = emrTimelinessService.count();
long completed = emrTimelinessService.count(new LambdaQueryWrapper<EmrTimeliness>().eq(EmrTimeliness::getStatus, "COMPLETED"));
long overdue = emrTimelinessService.count(new LambdaQueryWrapper<EmrTimeliness>().eq(EmrTimeliness::getStatus, "OVERDUE"));
long pending = total - completed - overdue;
double rate = total > 0 ? Math.round(completed * 10000.0 / total) / 100.0 : 0;
return new EmrTimelinessStatisticsDto()
.setTotalCount(total)
.setCompletedCount(completed)
.setOverdueCount(overdue)
.setPendingCount(pending)
.setCompletionRate(rate);
}
@Override @Override
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) { public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();

View File

@@ -21,7 +21,7 @@ public class EmrCompletenessController {
private final IEmrCompletenessAppService emrCompletenessAppService; private final IEmrCompletenessAppService emrCompletenessAppService;
@PostMapping("/check") @PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')") @PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "执行病历完整性检查") @Operation(summary = "执行病历完整性检查")
public R<Map<String, Object>> checkCompleteness( public R<Map<String, Object>> checkCompleteness(
@RequestParam("emrId") Long emrId, @RequestParam("emrId") Long emrId,
@@ -30,7 +30,7 @@ public class EmrCompletenessController {
} }
@GetMapping("/results/{emrId}") @GetMapping("/results/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')") @PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "获取完整性检查结果") @Operation(summary = "获取完整性检查结果")
public R<?> getCheckResults(@PathVariable Long emrId) { public R<?> getCheckResults(@PathVariable Long emrId) {
return R.ok(emrCompletenessAppService.getCheckResults(emrId)); return R.ok(emrCompletenessAppService.getCheckResults(emrId));

View File

@@ -23,28 +23,28 @@ public class EmrDataWarehouseController {
private final IEmrDataWarehouseAppService emrDataWarehouseAppService; private final IEmrDataWarehouseAppService emrDataWarehouseAppService;
@PostMapping("/extract") @PostMapping("/extract")
@PreAuthorize("@ss.hasPermi('infection:emr:edit')") @PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "提取结构化数据") @Operation(summary = "提取结构化数据")
public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) { public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) {
return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId)); return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId));
} }
@GetMapping("/data/{encounterId}") @GetMapping("/data/{encounterId}")
@PreAuthorize("@ss.hasPermi('infection:emr:list')") @PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "查询结构化数据") @Operation(summary = "查询结构化数据")
public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) { public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) {
return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId)); return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId));
} }
@PostMapping("/quality-score") @PostMapping("/quality-score")
@PreAuthorize("@ss.hasPermi('infection:emr:edit')") @PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "计算质控评分") @Operation(summary = "计算质控评分")
public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) { public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) {
return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId)); return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId));
} }
@GetMapping("/quality-scores") @GetMapping("/quality-scores")
@PreAuthorize("@ss.hasPermi('infection:emr:list')") @PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "查询质控评分列表") @Operation(summary = "查询质控评分列表")
public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) { public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) {
return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId)); return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId));

View File

@@ -4,13 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.healthlink.his.emr.domain.EmrRevision; import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.dto.EmrRevisionWithPatientDto;
import com.healthlink.his.emr.mapper.EmrRevisionMapper;
import com.healthlink.his.emr.service.IEmrRevisionService; import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService; import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
@@ -29,22 +30,21 @@ public class EmrRevisionController {
private final IEmrRevisionAppService emrRevisionAppService; private final IEmrRevisionAppService emrRevisionAppService;
private final EmrRevisionMapper emrRevisionMapper;
@PostMapping("/record") @PostMapping("/record")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "记录修改留痕") @Operation(summary = "记录修改留痕")
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) { public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
return R.ok(emrRevisionAppService.recordRevision(revision)); return R.ok(emrRevisionAppService.recordRevision(revision));
} }
@GetMapping("/list/{emrId}") @GetMapping("/list/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取修改历史列表") @Operation(summary = "获取修改历史列表")
public R<?> getRevisions(@PathVariable Long emrId) { public R<?> getRevisions(@PathVariable Long emrId) {
return R.ok(emrRevisionAppService.getRevisions(emrId)); return R.ok(emrRevisionAppService.getRevisions(emrId));
} }
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "分页查询修改留痕") @Operation(summary = "分页查询修改留痕")
public R<?> getPage( public R<?> getPage(
@RequestParam(value = "emrId", required = false) Long emrId, @RequestParam(value = "emrId", required = false) Long emrId,
@@ -60,15 +60,35 @@ public class EmrRevisionController {
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w)); return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
} }
@GetMapping("/{id}") @GetMapping("/page-with-patient")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')") @Operation(summary = "分页查询修改留痕(含患者信息)")
public R<?> getPageWithPatient(
@RequestParam(value = "emrId", required = false) Long emrId,
@RequestParam(value = "operatorName", required = false) String operatorName,
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "doctorName", required = false) String doctorName,
@RequestParam(value = "emrType", required = false) String emrType,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
int offset = (pageNo - 1) * pageSize;
long total = emrRevisionMapper.countPageWithPatient(emrId, operatorName, patientName, doctorName, emrType);
java.util.List<EmrRevisionWithPatientDto> list = emrRevisionMapper.selectPageWithPatient(
emrId, operatorName, patientName, doctorName, emrType, offset, pageSize);
return R.ok(new java.util.HashMap<String, Object>() {{
put("records", list);
put("total", total);
put("pageNo", pageNo);
put("pageSize", pageSize);
}});
}
@GetMapping("/{id:\\d+}")
@Operation(summary = "获取修订详情") @Operation(summary = "获取修订详情")
public R<?> getById(@PathVariable Long id) { public R<?> getById(@PathVariable Long id) {
return R.ok(emrRevisionAppService.getRevisionDetail(id)); return R.ok(emrRevisionAppService.getRevisionDetail(id));
} }
@GetMapping("/compare") @GetMapping("/compare")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "对比两个修订版本") @Operation(summary = "对比两个修订版本")
public R<?> compareRevisions( public R<?> compareRevisions(
@RequestParam("revisionId1") Long id1, @RequestParam("revisionId1") Long id1,

View File

@@ -0,0 +1,270 @@
package com.healthlink.his.web.emr.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.core.common.core.domain.entity.SysUser;
import com.core.system.mapper.SysUserMapper;
import com.healthlink.his.administration.domain.Encounter;
import com.healthlink.his.administration.domain.Patient;
import com.healthlink.his.administration.mapper.EncounterMapper;
import com.healthlink.his.administration.mapper.PatientMapper;
import com.healthlink.his.document.domain.Emr;
import com.healthlink.his.document.service.IEmrService;
import com.healthlink.his.emr.domain.EmrArchiveRecord;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.domain.EmrSearchIndex;
import com.healthlink.his.emr.service.IEmrArchiveRecordService;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.emr.service.IEmrSearchIndexService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* EMR数据同步Controller
*/
@RestController
@RequestMapping("/emr-sync")
@Slf4j
@AllArgsConstructor
@Tag(name = "EMR数据同步")
public class EmrSyncController {
private final IEmrService emrService;
private final IEmrRevisionService emrRevisionService;
private final IEmrSearchIndexService emrSearchIndexService;
private final IEmrArchiveRecordService emrArchiveRecordService;
private final JdbcTemplate jdbcTemplate;
private final PatientMapper patientMapper;
private final EncounterMapper encounterMapper;
private final SysUserMapper sysUserMapper;
/**
* 同步EMR数据
* 清空假数据从doc_emr生成真实数据
*/
@PostMapping("/sync")
@Operation(summary = "同步EMR修订历史和搜索索引")
public R<?> syncEmrData() {
log.info("开始同步EMR数据...");
// 1. 清空假数据使用原生SQL避免全表删除限制
try {
jdbcTemplate.execute("TRUNCATE TABLE emr_revision CASCADE");
jdbcTemplate.execute("TRUNCATE TABLE emr_search_index CASCADE");
jdbcTemplate.execute("TRUNCATE TABLE emr_archive_record CASCADE");
log.info("已清空emr_revision、emr_search_index和emr_archive_record表");
} catch (Exception e) {
log.warn("TRUNCATE失败尝试使用DELETE: {}", e.getMessage());
// 备用方案查询所有ID后删除
List<Long> revisionIds = emrRevisionService.list(new LambdaQueryWrapper<EmrRevision>().select(EmrRevision::getId))
.stream().map(EmrRevision::getId).toList();
if (!revisionIds.isEmpty()) {
emrRevisionService.removeByIds(revisionIds);
}
List<Long> searchIndexIds = emrSearchIndexService.list(new LambdaQueryWrapper<EmrSearchIndex>().select(EmrSearchIndex::getId))
.stream().map(EmrSearchIndex::getId).toList();
if (!searchIndexIds.isEmpty()) {
emrSearchIndexService.removeByIds(searchIndexIds);
}
List<Long> archiveIds = emrArchiveRecordService.list(new LambdaQueryWrapper<EmrArchiveRecord>().select(EmrArchiveRecord::getId))
.stream().map(EmrArchiveRecord::getId).toList();
if (!archiveIds.isEmpty()) {
emrArchiveRecordService.removeByIds(archiveIds);
}
}
// 2. 从doc_emr获取所有病历
List<Emr> emrList = emrService.list(new LambdaQueryWrapper<Emr>()
.orderByAsc(Emr::getCreateTime));
if (emrList.isEmpty()) {
return R.ok("没有病历数据需要同步");
}
log.info("共找到 {} 条病历数据", emrList.size());
// 调试打印前3条数据的字段值
for (int i = 0; i < Math.min(3, emrList.size()); i++) {
Emr emr = emrList.get(i);
log.info("病历[{}]: id={}, patientId={}, encounterId={}, recordId={}, classEnum={}",
i, emr.getId(), emr.getPatientId(), emr.getEncounterId(), emr.getRecordId(), emr.getClassEnum());
}
int revisionCount = 0;
int searchIndexCount = 0;
int archiveCount = 0;
for (Emr emr : emrList) {
// 3. 创建修订历史
try {
EmrRevision revision = new EmrRevision();
revision.setEmrId(emr.getId());
revision.setEncounterId(emr.getEncounterId());
revision.setRevisionNumber(1);
revision.setOperatorId(emr.getRecordId());
revision.setOperatorName("系统同步");
revision.setOperationType("CREATE");
revision.setDiffContent("初始创建");
revision.setSnapshotContent(emr.getContextJson());
revision.setCreateTime(emr.getCreateTime());
emrRevisionService.save(revision);
revisionCount++;
} catch (Exception e) {
log.warn("创建修订历史失败: emrId={}, error={}", emr.getId(), e.getMessage());
}
// 4. 创建搜索索引
try {
Map<String, String> contentMap = parseContextJson(emr.getContextJson());
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
String diagnosis = contentMap.getOrDefault("diagnosis", "");
// 获取患者详细信息
Patient patient = null;
String patientName = "未知";
String patientGender = "";
String patientAge = "";
String patientPhone = "";
String patientIdCard = "";
String encounterNo = "";
if (emr.getPatientId() != null) {
patient = patientMapper.selectById(emr.getPatientId());
if (patient != null) {
patientName = patient.getName() != null ? patient.getName() : "未知";
// 性别
if (patient.getGenderEnum() != null) {
patientGender = patient.getGenderEnum() == 1 ? "" : "";
}
// 年龄
if (patient.getBirthDate() != null) {
try {
int age = java.time.Period.between(
patient.getBirthDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(),
java.time.LocalDate.now()
).getYears();
patientAge = String.valueOf(age);
} catch (Exception e) {
log.warn("计算年龄失败: patientId={}", emr.getPatientId());
}
}
patientPhone = patient.getPhone() != null ? patient.getPhone() : "";
patientIdCard = patient.getIdCard() != null ? patient.getIdCard() : "";
log.debug("患者信息: name={}, gender={}, age={}, phone={}", patientName, patientGender, patientAge, patientPhone);
} else {
log.warn("未找到患者: patientId={}", emr.getPatientId());
}
} else {
log.warn("病历缺少patientId: emrId={}", emr.getId());
}
// 获取就诊信息
if (emr.getEncounterId() != null) {
var encounter = encounterMapper.selectById(emr.getEncounterId());
if (encounter != null) {
encounterNo = encounter.getBusNo() != null ? encounter.getBusNo() : "";
}
}
// 获取医生姓名
String doctorName = "未知医生";
if (emr.getRecordId() != null) {
var doctor = sysUserMapper.selectById(emr.getRecordId());
if (doctor != null) {
doctorName = doctor.getNickName() != null ? doctor.getNickName() : doctor.getUserName();
}
}
EmrSearchIndex index = new EmrSearchIndex();
index.setEmrId(emr.getId());
index.setEncounterId(emr.getEncounterId());
index.setPatientId(emr.getPatientId());
index.setPatientName(patientName);
index.setPatientGender(patientGender);
index.setPatientAge(patientAge);
index.setPatientPhone(patientPhone);
index.setPatientIdCard(patientIdCard);
index.setEncounterNo(encounterNo);
index.setEmrType(emr.getClassEnum() != null && emr.getClassEnum() == 1 ? "OUTPATIENT" : "INPATIENT");
index.setEmrTitle(chiefComplaint.isEmpty() ? "未命名病历" : chiefComplaint);
index.setDiagnosisText(diagnosis);
index.setDoctorName(doctorName);
index.setCreateTime(emr.getCreateTime());
emrSearchIndexService.save(index);
searchIndexCount++;
// 5. 创建归档记录
EmrArchiveRecord archive = new EmrArchiveRecord();
archive.setEmrId(emr.getId());
archive.setEncounterId(emr.getEncounterId());
archive.setPatientId(emr.getPatientId());
archive.setPatientName(patientName);
archive.setEmrType(emr.getClassEnum() != null && emr.getClassEnum() == 1 ? "OUTPATIENT" : "INPATIENT");
archive.setEmrTitle(chiefComplaint.isEmpty() ? "未命名病历" : chiefComplaint);
archive.setArchiveType("PRINT");
archive.setArchiveStatus("PRINTED");
archive.setPrintTime(emr.getCreateTime());
archive.setPrintBy(doctorName);
archive.setPrintCount(1);
archive.setCreateTime(emr.getCreateTime());
emrArchiveRecordService.save(archive);
archiveCount++;
} catch (Exception e) {
log.warn("创建搜索索引失败: emrId={}, error={}", emr.getId(), e.getMessage(), e);
}
}
String result = String.format("同步完成: 修订历史%d条, 搜索索引%d条, 归档记录%d条", revisionCount, searchIndexCount, archiveCount);
log.info(result);
return R.ok(result);
}
/**
* 获取同步统计
*/
@GetMapping("/stats")
@Operation(summary = "获取EMR同步统计")
public R<?> getSyncStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("emrCount", emrService.count());
stats.put("revisionCount", emrRevisionService.count());
stats.put("searchIndexCount", emrSearchIndexService.count());
stats.put("archiveCount", emrArchiveRecordService.count());
return R.ok(stats);
}
/**
* 解析contextJson字符串
*/
private Map<String, String> parseContextJson(String contextJson) {
Map<String, String> map = new HashMap<>();
if (contextJson == null || contextJson.isEmpty()) {
return map;
}
try {
// 简单解析JSON字符串
String json = contextJson.trim();
if (json.startsWith("{") && json.endsWith("}")) {
json = json.substring(1, json.length() - 1);
String[] pairs = json.split(",");
for (String pair : pairs) {
String[] kv = pair.split(":");
if (kv.length == 2) {
String key = kv[0].trim().replace("\"", "");
String value = kv[1].trim().replace("\"", "");
map.put(key, value);
}
}
}
} catch (Exception e) {
log.warn("解析contextJson失败: {}", e.getMessage());
}
return map;
}
}

View File

@@ -23,15 +23,20 @@ public class EmrTimelinessController {
private final IEmrTimelinessAppService emrTimelinessAppService; private final IEmrTimelinessAppService emrTimelinessAppService;
@PostMapping("/check") @PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')") @PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "执行病历时限检查") @Operation(summary = "执行病历时限检查")
public R<EmrTimelinessStatisticsDto> checkTimeliness( public R<EmrTimelinessStatisticsDto> checkTimeliness(
@RequestParam(value = "encounterId", required = false) Long encounterId) { @RequestParam(value = "encounterId", required = false) Long encounterId) {
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId)); return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
} }
@GetMapping("/statistics")
@Operation(summary = "获取病历时限统计")
public R<EmrTimelinessStatisticsDto> getStatistics() {
return R.ok(emrTimelinessAppService.getStatistics());
}
@GetMapping("/alerts") @GetMapping("/alerts")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取病历时限提醒列表") @Operation(summary = "获取病历时限提醒列表")
public R<Map<String, Object>> getTimelinessAlerts( public R<Map<String, Object>> getTimelinessAlerts(
@RequestParam(value = "emrType", required = false) String emrType, @RequestParam(value = "emrType", required = false) String emrType,

View File

@@ -20,21 +20,21 @@ public class EmrVersionController {
private final IEmrVersionAppService emrVersionAppService; private final IEmrVersionAppService emrVersionAppService;
@PostMapping("/save") @PostMapping("/save")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')") @PreAuthorize("@ss.hasPermi('emr:edit')")
@Operation(summary = "保存病历版本") @Operation(summary = "保存病历版本")
public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) { public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) {
return R.ok(emrVersionAppService.saveVersion(version)); return R.ok(emrVersionAppService.saveVersion(version));
} }
@GetMapping("/list/{emrId}") @GetMapping("/list/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')") @PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "获取病历版本列表") @Operation(summary = "获取病历版本列表")
public R<?> getVersions(@PathVariable Long emrId) { public R<?> getVersions(@PathVariable Long emrId) {
return R.ok(emrVersionAppService.getVersions(emrId)); return R.ok(emrVersionAppService.getVersions(emrId));
} }
@GetMapping("/compare") @GetMapping("/compare")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')") @PreAuthorize("@ss.hasPermi('emr:list')")
@Operation(summary = "对比两个版本") @Operation(summary = "对比两个版本")
public R<?> compareVersions( public R<?> compareVersions(
@RequestParam("versionId1") Long versionId1, @RequestParam("versionId1") Long versionId1,

View File

@@ -115,4 +115,29 @@ public interface IATDManageAppService {
* *
*/ */
R<?> getPendingMedication(Long encounterId); R<?> getPendingMedication(Long encounterId);
/**
* 退床 (取消分床)
*
* @param encounterId 住院患者id
* @return 结果
*/
R<?> cancelBedAssignment(Long encounterId);
/**
* 获取转科筛选选项(转入病区、转入科室)
*
* @return 转科筛选选项
*/
R<?> getTransferOptions();
/**
* 换床 (指定目标床位)
*
* @param encounterId 住院患者id
* @param targetBedId 目标床位id
* @return 结果
*/
R<?> changeBedAssginment(Long encounterId, Long targetBedId);
} }

View File

@@ -59,6 +59,14 @@ public interface IAdviceProcessAppService {
*/ */
R<?> adviceReject(List<PerformInfoDto> performInfoList); R<?> adviceReject(List<PerformInfoDto> performInfoList);
/**
* 撤销医嘱校对
*
* @param performInfoList 医嘱信息集合
* @return 操作结果
*/
R<?> adviceCancelVerify(List<PerformInfoDto> performInfoList);
/** /**
* 医嘱执行 * 医嘱执行
* *

View File

@@ -13,13 +13,19 @@ import com.core.common.utils.DateUtils;
import com.core.common.utils.SecurityUtils; import com.core.common.utils.SecurityUtils;
import com.core.common.utils.StringUtils; import com.core.common.utils.StringUtils;
import com.healthlink.his.administration.domain.Encounter; import com.healthlink.his.administration.domain.Encounter;
import com.healthlink.his.administration.domain.ChargeItem;
import com.healthlink.his.administration.service.IChargeItemService;
import com.healthlink.his.administration.domain.EncounterLocation; import com.healthlink.his.administration.domain.EncounterLocation;
import com.healthlink.his.administration.domain.Location;
import com.healthlink.his.administration.domain.EncounterParticipant; import com.healthlink.his.administration.domain.EncounterParticipant;
import com.healthlink.his.administration.domain.Location;
import com.healthlink.his.administration.domain.Organization;
import com.healthlink.his.administration.domain.Practitioner; import com.healthlink.his.administration.domain.Practitioner;
import com.healthlink.his.administration.service.IEncounterLocationService; import com.healthlink.his.administration.service.IEncounterLocationService;
import com.healthlink.his.administration.service.IEncounterParticipantService; import com.healthlink.his.administration.service.IEncounterParticipantService;
import com.healthlink.his.administration.service.IEncounterService; import com.healthlink.his.administration.service.IEncounterService;
import com.healthlink.his.administration.service.ILocationService; import com.healthlink.his.administration.service.ILocationService;
import com.healthlink.his.administration.service.IOrganizationService;
import com.healthlink.his.administration.service.IPractitionerService; import com.healthlink.his.administration.service.IPractitionerService;
import com.healthlink.his.common.constant.CommonConstants; import com.healthlink.his.common.constant.CommonConstants;
import com.healthlink.his.common.enums.*; import com.healthlink.his.common.enums.*;
@@ -112,9 +118,15 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
@Resource @Resource
private IPractitionerService practitionerService; private IPractitionerService practitionerService;
@Resource
private IOrganizationService organizationService;
@Resource @Resource
private ApplicationEventPublisher eventPublisher; private ApplicationEventPublisher eventPublisher;
@Resource
private IChargeItemService chargeItemService;
/** /**
* 入出转管理页面初始化 * 入出转管理页面初始化
* *
@@ -162,11 +174,24 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
// 获取当前登录用户的科室 ID // 获取当前登录用户的科室 ID
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId(); Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
// 提取转科筛选条件(字段名与 SQL 列别名不一致,需手动处理)
Long transferTargetWardId = admissionPageParam.getTransferTargetWardId();
Long transferTargetOrgId = admissionPageParam.getTransferTargetOrgId();
admissionPageParam.setTransferTargetWardId(null);
admissionPageParam.setTransferTargetOrgId(null);
// 构建查询条件 // 构建查询条件
QueryWrapper<AdmissionPageParam> queryWrapper = HisQueryUtils.buildQueryWrapper(admissionPageParam, searchKey, QueryWrapper<AdmissionPageParam> queryWrapper = HisQueryUtils.buildQueryWrapper(admissionPageParam, searchKey,
new HashSet<>(Arrays.asList(CommonConstants.FieldName.PatientWbStr, CommonConstants.FieldName.PatientPyStr, new HashSet<>(Arrays.asList(CommonConstants.FieldName.PatientWbStr, CommonConstants.FieldName.PatientPyStr,
CommonConstants.FieldName.PatientName, CommonConstants.FieldName.BusNo)), CommonConstants.FieldName.PatientName, CommonConstants.FieldName.BusNo)),
request); request);
// 手动添加转科目标筛选条件
if (transferTargetWardId != null) {
queryWrapper.apply("ii.target_ward_id = {0}", transferTargetWardId);
}
if (transferTargetOrgId != null) {
queryWrapper.apply("ii.target_org_id = {0}", transferTargetOrgId);
}
// 入院患者分页列表 // 入院患者分页列表
Page<AdmissionPatientPageDto> admissionPatientPage = atdManageAppMapper.selectAdmissionPatientPage( Page<AdmissionPatientPageDto> admissionPatientPage = atdManageAppMapper.selectAdmissionPatientPage(
new Page<>(pageNo, pageSize), queryWrapper, EncounterClass.IMP.getValue(), new Page<>(pageNo, pageSize), queryWrapper, EncounterClass.IMP.getValue(),
@@ -1002,4 +1027,305 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
docStatisticsAppService.saveOrUpdateAdmissionSigns(list); docStatisticsAppService.saveOrUpdateAdmissionSigns(list);
} }
} }
/**
* 获取转科筛选选项(转入病区、转入科室)
*
* @return 转科筛选选项
*/
@Override
public R<?> getTransferOptions() {
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
String delFlagNo = DelFlag.NO.getCode();
// 查询当前科室下所有待转科患者
List<Encounter> pendingTransfers = encounterService.list(
new LambdaQueryWrapper<Encounter>()
.eq(Encounter::getStatusEnum, EncounterZyStatus.PENDING_TRANSFER.getValue())
.eq(Encounter::getOrganizationId, currentUserOrgId)
.eq(Encounter::getDeleteFlag, delFlagNo));
if (pendingTransfers.isEmpty()) {
return R.ok(new TransferOptionsDto());
}
List<Long> encounterIds = pendingTransfers.stream().map(Encounter::getId).toList();
// 查询这些患者的转科申请,获取转入病区和转入科室
List<OrderProcess> orderProcessList = orderProcessService.list(
new LambdaQueryWrapper<OrderProcess>()
.in(OrderProcess::getEncounterId, encounterIds)
.eq(OrderProcess::getDeleteFlag, delFlagNo)
.isNotNull(OrderProcess::getTargetLocationId));
// 去重收集转入病区
Set<Long> wardIdSet = new LinkedHashSet<>();
Set<Long> orgIdSet = new LinkedHashSet<>();
Map<Long, String> wardNameMap = new HashMap<>();
Map<Long, String> orgNameMap = new HashMap<>();
for (OrderProcess op : orderProcessList) {
if (op.getTargetLocationId() != null) {
wardIdSet.add(op.getTargetLocationId());
}
if (op.getTargetOrganizationId() != null) {
orgIdSet.add(op.getTargetOrganizationId());
}
}
// 查询病区名称
if (!wardIdSet.isEmpty()) {
List<Location> locations = locationService.listByIds(wardIdSet);
for (Location loc : locations) {
wardNameMap.put(loc.getId(), loc.getName());
}
}
// 查询科室名称
if (!orgIdSet.isEmpty()) {
List<Organization> orgs = organizationService.listByIds(orgIdSet);
if (orgs != null) {
for (Organization org : orgs) {
orgNameMap.put(org.getId(), org.getName());
}
}
}
// 构建转入病区选项
List<TransferOptionsDto.OptionItem> wardOptions = new ArrayList<>();
for (Long wardId : wardIdSet) {
String name = wardNameMap.getOrDefault(wardId, String.valueOf(wardId));
wardOptions.add(new TransferOptionsDto.OptionItem(wardId, name));
}
// 构建转入科室选项
List<TransferOptionsDto.OptionItem> orgOptions = new ArrayList<>();
for (Long orgId : orgIdSet) {
String name = orgNameMap.getOrDefault(orgId, String.valueOf(orgId));
orgOptions.add(new TransferOptionsDto.OptionItem(orgId, name));
}
TransferOptionsDto dto = new TransferOptionsDto();
dto.setWardListOptions(wardOptions);
dto.setDepartmentListOptions(orgOptions);
return R.ok(dto);
}
/**
* 退床 (取消分床)
*
* @param encounterId 住院患者id
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> cancelBedAssignment(Long encounterId) {
if (encounterId == null) {
return R.fail("退床失败,请选择有效的就诊记录");
}
Encounter encounter = encounterService.getById(encounterId);
if (encounter == null) {
return R.fail("未找到该住院就诊记录");
}
// 仅已入院状态允许退床
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(encounter.getStatusEnum())) {
return R.fail("该患者未在科,无法办理退床");
}
// 校验是否产生了医嘱或计费
// 1. 检查药品医嘱
long medCount = medicationRequestService.count(
new LambdaQueryWrapper<MedicationRequest>()
.eq(MedicationRequest::getEncounterId, encounterId)
.eq(MedicationRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (medCount > 0) {
return R.fail("患者已产生医嘱或计费,无法直接退床");
}
// 2. 检查诊疗医嘱
long svcCount = serviceRequestService.count(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getEncounterId, encounterId)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (svcCount > 0) {
return R.fail("患者已产生医嘱或计费,无法直接退床");
}
// 3. 检查耗材医嘱
long devCount = deviceRequestService.count(
new LambdaQueryWrapper<DeviceRequest>()
.eq(DeviceRequest::getEncounterId, encounterId)
.eq(DeviceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (devCount > 0) {
return R.fail("患者已产生医嘱或计费,无法直接退床");
}
// 4. 检查计费记录
long chargeCount = chargeItemService.count(
new LambdaQueryWrapper<ChargeItem>()
.eq(ChargeItem::getEncounterId, encounterId));
if (chargeCount > 0) {
return R.fail("患者已产生医嘱或计费,无法直接退床");
}
// 更新原病床状态为 空闲 (LocationStatus.IDLE)
List<EncounterLocation> bedLocations = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.BED, EncounterActivityStatus.ACTIVE);
if (bedLocations != null && !bedLocations.isEmpty()) {
for (EncounterLocation bedLoc : bedLocations) {
locationService.updateStatusById(bedLoc.getLocationId(), LocationStatus.IDLE.getValue());
}
}
// 更新病床和病房就诊位置状态为已完成 (EncounterActivityStatus.COMPLETED)
// isTransfer 为 false不更新病区 WARD以保留患者的病区归属从而能继续在入科列表中显示并重新分床
encounterLocationService.updateEncounterLocationStatus(encounterId, false);
// 更新医疗参与者(住院医生、责任护士等)状态为已完成
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
// 回滚住院状态为 待入科 (EncounterZyStatus.REGISTERED)
encounter.setStatusEnum(EncounterZyStatus.REGISTERED.getValue());
encounterService.saveOrUpdateEncounter(encounter);
return R.ok("退床成功");
}
/**
* 换床
*
* @param encounterId 住院患者id
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> changeBedAssginment(Long encounterId, Long targetBedId) {
if (encounterId == null) {
return R.fail("换床失败,请选择有效的就诊记录");
}
if (targetBedId == null) {
return R.fail("换床失败,请选择目标床位");
}
Encounter encounter = encounterService.getById(encounterId);
if (encounter == null) {
return R.fail("未找到就诊记录");
}
// 仅已入院状态允许换床
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(encounter.getStatusEnum())) {
return R.fail("该患者未在科,无法办理换床");
}
// 查询目标床位
Location targetBed = locationService.getById(targetBedId);
if (targetBed == null) {
return R.fail("目标床位不存在");
}
if (!LocationForm.BED.getValue().equals(targetBed.getFormEnum())) {
return R.fail("所选位置不是床位");
}
// 根据目标床位的 busNo 获取其父级房间 (house)
String bedBusNo = targetBed.getBusNo();
if (bedBusNo == null || !bedBusNo.contains(".")) {
return R.fail("目标床位编码异常");
}
String[] parts = bedBusNo.split("\\.");
if (parts.length < 2) {
return R.fail("目标床位编码层级异常");
}
String houseBusNo = parts[0] + "." + parts[1];
Location targetHouse = locationService.lambdaQuery()
.eq(Location::getBusNo, houseBusNo)
.eq(Location::getFormEnum, LocationForm.HOUSE.getValue())
.eq(Location::getDeleteFlag, "0")
.one();
if (targetHouse == null) {
return R.fail("未找到目标床位所属的病房");
}
Date now = new Date();
// 检查目标床位是否已经被占用
List<EncounterLocation> occupiedBedLocs = encounterLocationService.lambdaQuery()
.eq(EncounterLocation::getLocationId, targetBedId)
.eq(EncounterLocation::getFormEnum, LocationForm.BED.getValue())
.eq(EncounterLocation::getStatusEnum, EncounterActivityStatus.ACTIVE.getValue())
.eq(EncounterLocation::getDeleteFlag, "0")
.list();
if (occupiedBedLocs != null && !occupiedBedLocs.isEmpty()) {
// Target bed is occupied! This is a bed swap (床位互换)
Long targetEncounterId = occupiedBedLocs.get(0).getEncounterId();
Encounter targetEncounter = encounterService.getById(targetEncounterId);
if (targetEncounter == null) {
return R.fail("目标床位占用患者就诊记录异常");
}
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(targetEncounter.getStatusEnum())) {
return R.fail("目标床位占用患者已不在科,无法办理换床");
}
// 获取当前患者的原床位和原病房
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.BED, EncounterActivityStatus.ACTIVE);
if (currentBedLocs == null || currentBedLocs.isEmpty()) {
return R.fail("当前患者未分配床位,无法进行换床互换");
}
Long currentBedId = currentBedLocs.get(0).getLocationId();
List<EncounterLocation> currentHouseLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.HOUSE, EncounterActivityStatus.ACTIVE);
if (currentHouseLocs == null || currentHouseLocs.isEmpty()) {
return R.fail("当前患者原病房记录不存在");
}
Long currentHouseId = currentHouseLocs.get(0).getLocationId();
// 获取被交换患者的原开始时间,保证其床位历史记录连贯性
Date targetStartTime = occupiedBedLocs.get(0).getStartTime();
if (targetStartTime == null) {
targetStartTime = now;
}
// 1. 将两位患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
Integer res1 = encounterLocationService.updateEncounterLocationStatus(encounterId, false);
Integer res2 = encounterLocationService.updateEncounterLocationStatus(targetEncounterId, false);
if (res1 == 0 || res2 == 0) {
throw new RuntimeException("更新原就诊位置状态失败");
}
// 2. 为当前患者创建新位置 (目标病房和目标床位)
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
// 3. 为被交换患者创建新位置 (当前患者的原病房和原床位)
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentHouseId, LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentBedId, LocationForm.BED.getValue());
return R.ok("床位互换成功");
} else {
// Target bed is vacant! Normal bed change
// 获取当前患者原床位
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
LocationForm.BED, EncounterActivityStatus.ACTIVE);
// 1. 将当前患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
encounterLocationService.updateEncounterLocationStatus(encounterId, false);
// 2. 将原床位状态更新为空闲 (LocationStatus.IDLE)
if (currentBedLocs != null && !currentBedLocs.isEmpty()) {
for (EncounterLocation bedLoc : currentBedLocs) {
locationService.updateStatusById(bedLoc.getLocationId(), LocationStatus.IDLE.getValue());
}
}
// 3. 为当前患者创建新位置 (目标病房和目标床位)
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
// 4. 将目标床位状态更新为占用 (LocationStatus.OCCUPY)
locationService.updateStatusById(targetBedId, LocationStatus.OCCUPY.getValue());
return R.ok("换床成功");
}
}
} }

View File

@@ -13,6 +13,7 @@ import com.core.common.enums.TenantOptionDict;
import com.core.common.exception.ServiceException; import com.core.common.exception.ServiceException;
import com.core.common.utils.*; import com.core.common.utils.*;
import com.core.common.utils.bean.BeanUtils; import com.core.common.utils.bean.BeanUtils;
import lombok.extern.slf4j.Slf4j;
import com.core.common.utils.TenantOptionUtil; import com.core.common.utils.TenantOptionUtil;
import com.healthlink.his.administration.domain.ChargeItem; import com.healthlink.his.administration.domain.ChargeItem;
import com.healthlink.his.administration.service.IChargeItemService; import com.healthlink.his.administration.service.IChargeItemService;
@@ -54,7 +55,6 @@ import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.time.ZoneId; import java.time.ZoneId;
@@ -70,6 +70,7 @@ import java.util.stream.Collectors;
* @author zwh * @author zwh
* @date 2025-08-07 * @date 2025-08-07
*/ */
@Slf4j
@Service @Service
public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService { public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
@@ -185,7 +186,9 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
// 提取requestStatus手动处理支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询 // 提取requestStatus手动处理支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
Integer requestStatus = inpatientAdviceParam.getRequestStatus(); Integer requestStatus = inpatientAdviceParam.getRequestStatus();
inpatientAdviceParam.setRequestStatus(null); inpatientAdviceParam.setRequestStatus(null);
// 提取deadline手动处理需要做NULL-safe的end_time比较Bug #763修复 // 提取deadline手动处理
// Bug #714修复截止时间过滤使用request_time限制检索范围
// Bug #763修复NULL-safe的end_time比较
String deadline = inpatientAdviceParam.getDeadline(); String deadline = inpatientAdviceParam.getDeadline();
inpatientAdviceParam.setDeadline(null); inpatientAdviceParam.setDeadline(null);
// 构建查询条件 // 构建查询条件
@@ -215,16 +218,17 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList(); = Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList); queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
} }
// 手动拼接deadline条件end_time IS NULL OR end_time <= deadlineBug #763修复 // 手动拼接截止时间条件:
// 住院医嘱的effective_dose_end可能为NULL签发临时医嘱时未设置结束时间 // 1. request_time >= deadline只显示截止时间之后创建的医嘱Bug #714修复
// PostgreSQL中 NULL <= anything 结果为FALSE需要先判断IS NULL // 默认值为当天00:00:00默认只加载当天数据避免加载过长周期的历史未核对数据
// 2. end_time IS NULL OR end_time <= deadlineNULL-safe终止时间比较Bug #763修复
if (deadline != null && !deadline.isEmpty()) { if (deadline != null && !deadline.isEmpty()) {
try { Date deadlineTime = DateUtils.parseDate(deadline);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if (deadlineTime != null) {
Date deadlineTime = sdf.parse(deadline); queryWrapper.ge("request_time", deadlineTime);
queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime)); queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime));
} catch (java.text.ParseException e) { } else {
// deadline解析失败忽略此条件 log.warn("截止时间解析失败: {}", deadline);
} }
} }
// 患者医嘱分页列表 // 患者医嘱分页列表
@@ -601,6 +605,102 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
return R.ok(null, "退回成功"); return R.ok(null, "退回成功");
} }
/**
* 撤销医嘱校对
*
* @param performInfoList 医嘱信息集合
* @return 操作结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> adviceCancelVerify(List<PerformInfoDto> performInfoList) {
if (performInfoList == null || performInfoList.isEmpty()) {
return R.fail("请先选择医嘱信息");
}
// 分别创建列表来存储不同类型的请求
List<PerformInfoDto> serviceRequestList = new ArrayList<>();
List<PerformInfoDto> medRequestList = new ArrayList<>();
List<PerformInfoDto> deviceRequestList = new ArrayList<>();
for (PerformInfoDto item : performInfoList) {
if (CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(item.getRequestTable())) {
serviceRequestList.add(item);
} else if (CommonConstants.TableName.MED_MEDICATION_REQUEST.equals(item.getRequestTable())) {
medRequestList.add(item);
} else if (CommonConstants.TableName.WOR_DEVICE_REQUEST.equals(item.getRequestTable())) {
deviceRequestList.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("该医嘱已执行,无法撤销校对,请先去医嘱执行模块取消执行");
}
// 校验②:校验该医嘱是否已记账扣费。若已扣费,则不允许撤销校对。
List<ChargeItem> chargeItems = chargeItemService.getChargeItemInfoByReqId(allRequestIds);
boolean isBilled = chargeItems.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum()));
if (isBilled) {
return R.fail("该医嘱已记账收费,若需撤销请先进行退费/计账回滚");
}
// 校验③:若为药品医嘱,校验药房是否已发药(配药)。若已发药,则不允许撤销校对。
if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
List<MedicationDispense> dispenseList = medicationDispenseService.list(
new LambdaQueryWrapper<MedicationDispense>()
.in(MedicationDispense::getMedReqId, medReqIds)
.in(MedicationDispense::getStatusEnum, Arrays.asList(
DispenseStatus.COMPLETED.getValue(),
DispenseStatus.PREPARED.getValue(),
DispenseStatus.PART_COMPLETED.getValue()
)));
if (!dispenseList.isEmpty()) {
return R.fail("药房已发药,请先进行退药申请");
}
}
// 满足所有校验,执行撤销校对(回退至“未校对”,即 ACTIVE 状态)
if (!serviceRequestList.isEmpty()) {
serviceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList())
.set(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
.set(ServiceRequest::getPerformerCheckId, null)
.set(ServiceRequest::getCheckTime, null));
}
if (!medRequestList.isEmpty()) {
medicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
.in(MedicationRequest::getId, medRequestList.stream().map(PerformInfoDto::getRequestId).toList())
.set(MedicationRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
.set(MedicationRequest::getPerformerCheckId, null)
.set(MedicationRequest::getCheckTime, null));
}
if (!deviceRequestList.isEmpty()) {
deviceRequestService.update(new LambdaUpdateWrapper<DeviceRequest>()
.in(DeviceRequest::getId, deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList())
.set(DeviceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
.set(DeviceRequest::getPerformerCheckId, null)
.set(DeviceRequest::getCheckTime, null));
}
return R.ok(null, "撤销校对成功");
}
/** /**
* 医嘱执行 * 医嘱执行
* *

View File

@@ -78,9 +78,9 @@ 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(), "已发药")); dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(), "已发药"));
initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions); initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions);
@@ -309,11 +309,11 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
} }
/** /**
* 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→已提交,发药→已发药) * 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→待发药,发药→已发药)
*/ */
private String getSummaryFormStatusText(Integer statusEnum) { private String getSummaryFormStatusText(Integer statusEnum) {
if (DispenseStatus.EXECUTED.getValue().equals(statusEnum)) { if (DispenseStatus.PREPARATION.getValue().equals(statusEnum)) {
return "已提交"; return "待发药";
} }
if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) { if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) {
return "已发药"; return "已发药";

View File

@@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/** /**
* 入出转管理 controller * 入出转管理 controller
@@ -166,4 +168,40 @@ public class ATDManageController {
public R<?> getPendingMedication(Long encounterId) { public R<?> getPendingMedication(Long encounterId) {
return atdManageAppService.getPendingMedication(encounterId); return atdManageAppService.getPendingMedication(encounterId);
} }
/**
* 退床 (取消分床)
*
* @param encounterId 住院患者id
* @return 结果
*/
@PutMapping(value = "/cancel-bed-assignment")
public R<?> cancelBedAssignment(Long encounterId) {
return atdManageAppService.cancelBedAssignment(encounterId);
}
/**
* 获取转科筛选选项(转入病区、转入科室)
*
* @return 转科筛选选项
*/
@GetMapping(value = "/transfer-options")
public R<?> getTransferOptions() {
return atdManageAppService.getTransferOptions();
}
/**
* 换床
*
* @param encounterId 住院患者id
* @return 结果
*/
@PutMapping(value = "/change-bed-assignment")
public R<?> changeBedAssignment(Long encounterId){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String targetBedIdStr = request.getParameter("targetBedId");
Long targetBedId = (targetBedIdStr == null || targetBedIdStr.trim().isEmpty()) ? null : Long.valueOf(targetBedIdStr);
return atdManageAppService.changeBedAssginment(encounterId, targetBedId);
}
} }

View File

@@ -87,6 +87,17 @@ public class AdviceProcessController {
return adviceProcessAppService.adviceReject(performInfoList); return adviceProcessAppService.adviceReject(performInfoList);
} }
/**
* 撤销医嘱校对
*
* @param performInfoList 医嘱信息集合
* @return 操作结果
*/
@PutMapping(value = "/advice-cancel-verify")
public R<?> adviceCancelVerify(@RequestBody List<PerformInfoDto> performInfoList) {
return adviceProcessAppService.adviceCancelVerify(performInfoList);
}
/** /**
* 医嘱执行 * 医嘱执行
* *

View File

@@ -27,6 +27,12 @@ public class AdmissionPageParam {
/** 入院病房 */ /** 入院病房 */
private Long houseId; private Long houseId;
/** 转科目标病区(待转科患者筛选) */
private Long transferTargetWardId;
/** 转科目标科室(待转科患者筛选) */
private Long transferTargetOrgId;
/** /**
* 入院类型 * 入院类型
*/ */

View File

@@ -146,4 +146,18 @@ public class AdmissionPatientPageDto {
*/ */
@JsonSerialize(using = ToStringSerializer.class) @JsonSerialize(using = ToStringSerializer.class)
private Long patientId; private Long patientId;
/** 转科目标病区ID转入病区 */
@JsonSerialize(using = ToStringSerializer.class)
private Long targetWardId;
/** 转科目标病区名称 */
private String targetWardName;
/** 转科目标科室ID转入科室 */
@JsonSerialize(using = ToStringSerializer.class)
private Long targetOrgId;
/** 转科目标科室名称 */
private String targetOrgName;
} }

View File

@@ -0,0 +1,39 @@
package com.healthlink.his.web.inhospitalnursestation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
/**
* 转科筛选选项 DTO
*
* @author system
* @date 2025-06-25
*/
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class TransferOptionsDto {
/** 入院病区选项(转入病区) */
private List<OptionItem> wardListOptions = new ArrayList<>();
/** 入院病房选项(转入科室) */
private List<OptionItem> departmentListOptions = new ArrayList<>();
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class OptionItem {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String name;
}
}

View File

@@ -41,7 +41,7 @@ public class NursingRecordController {
* @return 患者信息 * @return 患者信息
*/ */
@GetMapping("/patient-page") @GetMapping("/patient-page")
@PreAuthorize("hasAuthority('nursing:record:list')") @PreAuthorize("@ss.hasPermi('nursing:record:list')")
public R<?> getPatientInfoPage(NursingSearchParam nursingSearchParam, public R<?> getPatientInfoPage(NursingSearchParam nursingSearchParam,
@RequestParam(value = "searchKey", defaultValue = "") String searchKey, @RequestParam(value = "searchKey", defaultValue = "") String searchKey,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@@ -60,7 +60,7 @@ public class NursingRecordController {
* @return 患者护理记录单信息 * @return 患者护理记录单信息
*/ */
@GetMapping("/nursing-patient-page") @GetMapping("/nursing-patient-page")
@PreAuthorize("hasAuthority('nursing:record:list')") @PreAuthorize("@ss.hasPermi('nursing:record:list')")
public R<?> getNursingPatientPage(NursingSearchParam nursingSearchParam, public R<?> getNursingPatientPage(NursingSearchParam nursingSearchParam,
@RequestParam(value = "searchKey", defaultValue = "") String searchKey, @RequestParam(value = "searchKey", defaultValue = "") String searchKey,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@@ -75,7 +75,7 @@ public class NursingRecordController {
* @param nursingRecordDto 护理记录实体 * @param nursingRecordDto 护理记录实体
*/ */
@PostMapping("/save-nursing") @PostMapping("/save-nursing")
@PreAuthorize("hasAuthority('nursing:record:add')") @PreAuthorize("@ss.hasPermi('nursing:record:add')")
public R<?> saveRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) { public R<?> saveRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
return nursingRecordAppService.saveRecord(nursingRecordDto); return nursingRecordAppService.saveRecord(nursingRecordDto);
} }
@@ -86,7 +86,7 @@ public class NursingRecordController {
* @param nursingRecordDto 护理记录实体 * @param nursingRecordDto 护理记录实体
*/ */
@PostMapping("/update-nursing") @PostMapping("/update-nursing")
@PreAuthorize("hasAuthority('nursing:record:edit')") @PreAuthorize("@ss.hasPermi('nursing:record:edit')")
public R<?> updateRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) { public R<?> updateRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
return nursingRecordAppService.updateRecord(nursingRecordDto); return nursingRecordAppService.updateRecord(nursingRecordDto);
} }
@@ -97,7 +97,7 @@ public class NursingRecordController {
* @param recordList 记录单List * @param recordList 记录单List
*/ */
@PostMapping("/delete-nursing") @PostMapping("/delete-nursing")
@PreAuthorize("hasAuthority('nursing:record:remove')") @PreAuthorize("@ss.hasPermi('nursing:record:remove')")
public R<?> delRecord(@Validated @RequestBody List<NursingRecordDto> recordList) { public R<?> delRecord(@Validated @RequestBody List<NursingRecordDto> recordList) {
return nursingRecordAppService.delRecord(recordList); return nursingRecordAppService.delRecord(recordList);
} }
@@ -112,7 +112,7 @@ public class NursingRecordController {
* @return 患者护理记录单信息 * @return 患者护理记录单信息
*/ */
@GetMapping("/emr-template-page") @GetMapping("/emr-template-page")
@PreAuthorize("hasAuthority('nursing:record:list')") @PreAuthorize("@ss.hasPermi('nursing:record:list')")
public R<?> getEmrTemplate(NursingSearchParam nursingSearchParam, public R<?> getEmrTemplate(NursingSearchParam nursingSearchParam,
@RequestParam(value = "searchKey", defaultValue = "") String searchKey, @RequestParam(value = "searchKey", defaultValue = "") String searchKey,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@@ -127,7 +127,7 @@ public class NursingRecordController {
* @param emrTemplateDto 病历模板信息 * @param emrTemplateDto 病历模板信息
*/ */
@PostMapping("/emr-template-save") @PostMapping("/emr-template-save")
@PreAuthorize("hasAuthority('nursing:record:add')") @PreAuthorize("@ss.hasPermi('nursing:record:add')")
public R<?> saveEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) { public R<?> saveEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
return nursingRecordAppService.saveEmrTemplate(emrTemplateDto); return nursingRecordAppService.saveEmrTemplate(emrTemplateDto);
} }
@@ -139,7 +139,7 @@ public class NursingRecordController {
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping("/emr-template-del") @PostMapping("/emr-template-del")
@PreAuthorize("hasAuthority('nursing:record:remove')") @PreAuthorize("@ss.hasPermi('nursing:record:remove')")
public R<?> deleteEmrTemplate(@Validated @RequestBody List<Long> idList) { public R<?> deleteEmrTemplate(@Validated @RequestBody List<Long> idList) {
return nursingRecordAppService.deleteEmrTemplate(idList); return nursingRecordAppService.deleteEmrTemplate(idList);
} }
@@ -151,7 +151,7 @@ public class NursingRecordController {
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping("/emr-template-update") @PostMapping("/emr-template-update")
@PreAuthorize("hasAuthority('nursing:record:edit')") @PreAuthorize("@ss.hasPermi('nursing:record:edit')")
public R<?> updateEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) { public R<?> updateEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
return nursingRecordAppService.updateEmrTemplate(emrTemplateDto); return nursingRecordAppService.updateEmrTemplate(emrTemplateDto);
} }
@@ -163,7 +163,7 @@ public class NursingRecordController {
* @return 结果 * @return 结果
*/ */
@PostMapping("/batch-save") @PostMapping("/batch-save")
@PreAuthorize("hasAuthority('nursing:record:edit')") @PreAuthorize("@ss.hasPermi('nursing:record:edit')")
public R<?> batchSaveRecord(@Validated @RequestBody BatchNursingRecordDto batchDto) { public R<?> batchSaveRecord(@Validated @RequestBody BatchNursingRecordDto batchDto) {
return nursingRecordAppService.batchSaveRecord(batchDto); return nursingRecordAppService.batchSaveRecord(batchDto);
} }

View File

@@ -196,6 +196,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream() List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) .filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| ItemType.SURGERY.getValue().equals(e.getAdviceType()) || ItemType.SURGERY.getValue().equals(e.getAdviceType())
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|| (e.getAdviceType() != null && e.getAdviceType() == 26)) || (e.getAdviceType() != null && e.getAdviceType() == 26))
.collect(Collectors.toList()); .collect(Collectors.toList());
// 耗材 🔧 Bug #147 修复 // 耗材 🔧 Bug #147 修复
@@ -687,7 +688,12 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
longServiceRequest.setRateCode(regAdviceSaveDto.getRateCode()); // 用药频次 longServiceRequest.setRateCode(regAdviceSaveDto.getRateCode()); // 用药频次
longServiceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型 longServiceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型
longServiceRequest.setTherapyEnum(regAdviceSaveDto.getTherapyEnum()); // 治疗类型,长期(需要前端传) longServiceRequest.setTherapyEnum(regAdviceSaveDto.getTherapyEnum()); // 治疗类型,长期(需要前端传)
longServiceRequest.setActivityId(regAdviceSaveDto.getAdviceDefinitionId());// 诊疗定义id // 文字医嘱(type=8)不走定价体系activityId设置为0L占位
if (ItemType.TEXT.getValue().equals(regAdviceSaveDto.getAdviceType())) {
longServiceRequest.setActivityId(0L);
} else {
longServiceRequest.setActivityId(regAdviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
}
longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者 longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生 longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id

View File

@@ -58,7 +58,7 @@ public class SurgerySafetyCheckController {
return R.ok(safetyCheckService.list(w)); return R.ok(safetyCheckService.list(w));
} }
@GetMapping("/{id}") @GetMapping("/{id:\\d+}")
@Operation(summary = "获取安全核查详情") @Operation(summary = "获取安全核查详情")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')") @PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getById(@PathVariable Long id) { public R<?> getById(@PathVariable Long id) {

View File

@@ -0,0 +1,88 @@
package com.healthlink.his.web.ybmock.controller;
import com.healthlink.his.yb.mock.domain.YbPsnInfo;
import com.healthlink.his.web.ybmock.service.YbMockService;
import com.core.common.core.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 医保模拟接口 Controller
*/
@RestController
@RequestMapping("/yb-mock")
@Slf4j
@AllArgsConstructor
@Tag(name = "医保模拟接口")
public class YbMockController {
private final YbMockService ybMockService;
@PostMapping("/{apiCode}")
@Operation(summary = "医保模拟接口")
public R<?> handleApi(@PathVariable String apiCode, @RequestBody Map<String, String> params) {
log.info("收到医保请求: apiCode={}, params={}", apiCode, params);
try {
Map<String, Object> result;
switch (apiCode) {
case "1101":
result = ybMockService.getPatientInfo(params);
break;
case "2201":
result = ybMockService.clinicRegister(params);
break;
case "2203":
result = ybMockService.clinicPrescription(params);
break;
case "2207":
result = ybMockService.clinicSettle(params);
break;
case "3201":
result = ybMockService.inpatientRegister(params);
break;
case "3203":
result = ybMockService.inpatientPrescription(params);
break;
case "3207":
result = ybMockService.inpatientSettle(params);
break;
default:
result = new HashMap<>();
result.put("infcode", -1);
result.put("err_msg", "未支持的接口: " + apiCode);
}
return R.ok(result);
} catch (Exception e) {
log.error("医保接口调用失败", e);
Map<String, Object> error = new HashMap<>();
error.put("infcode", -1);
error.put("err_msg", "系统错误: " + e.getMessage());
return R.ok(error);
}
}
@GetMapping("/psn/{psnNo}")
@Operation(summary = "获取参保人信息")
public R<?> getPatientInfo(@PathVariable String psnNo) {
Map<String, String> params = new HashMap<>();
params.put("psn_no", psnNo);
return R.ok(ybMockService.getPatientInfo(params));
}
@PostMapping("/psn")
@Operation(summary = "添加参保人信息")
public R<?> addPsnInfo(@RequestBody YbPsnInfo psnInfo) {
return R.ok(ybMockService.addPsnInfo(psnInfo));
}
@GetMapping("/psn/list")
@Operation(summary = "获取参保人列表")
public R<?> listPsnInfo() {
return R.ok(ybMockService.listPsnInfo());
}
}

View File

@@ -0,0 +1,158 @@
package com.healthlink.his.web.ybmock.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.yb.mock.domain.YbPsnInfo;
import com.healthlink.his.yb.mock.mapper.YbPsnInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 医保模拟服务
*/
@Service
@Slf4j
public class YbMockService {
@Autowired
private YbPsnInfoMapper psnInfoMapper;
public Map<String, Object> getPatientInfo(Map<String, String> params) {
String psnNo = params.get("psn_no");
log.info("获取参保人信息: psnNo={}", psnNo);
YbPsnInfo psnInfo = psnInfoMapper.selectOne(
new LambdaQueryWrapper<YbPsnInfo>().eq(YbPsnInfo::getPsnNo, psnNo)
);
if (psnInfo == null) {
Map<String, Object> error = new HashMap<>();
error.put("infcode", -1);
error.put("err_msg", "未找到参保人信息: " + psnNo);
return error;
}
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
result.put("output", buildPsnInfoOutput(psnInfo));
return result;
}
public Map<String, Object> clinicRegister(Map<String, String> params) {
log.info("门诊登记: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("encounter_no", "MZ" + System.currentTimeMillis());
output.put("register_time", new Date().toString());
output.put("status", "成功");
result.put("output", output);
return result;
}
public Map<String, Object> clinicPrescription(Map<String, String> params) {
log.info("门诊处方上传: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("recipe_no", "CF" + System.currentTimeMillis());
output.put("upload_time", new Date().toString());
output.put("total_amount", "156.80");
output.put("self_pay", "23.52");
output.put("insurance_pay", "133.28");
output.put("status", "成功");
result.put("output", output);
return result;
}
public Map<String, Object> clinicSettle(Map<String, String> params) {
log.info("门诊结算: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("settle_no", "JZ" + System.currentTimeMillis());
output.put("total_amount", "156.80");
output.put("insurance_pay", "133.28");
output.put("self_pay", "23.52");
output.put("account_pay", "20.00");
output.put("cash_pay", "3.52");
output.put("settle_time", new Date().toString());
output.put("status", "成功");
result.put("output", output);
return result;
}
public Map<String, Object> inpatientRegister(Map<String, String> params) {
log.info("住院登记: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("admission_no", "ZY" + System.currentTimeMillis());
output.put("admission_time", new Date().toString());
output.put("bed_no", "3-201-1");
output.put("status", "成功");
result.put("output", output);
return result;
}
public Map<String, Object> inpatientPrescription(Map<String, String> params) {
log.info("住院处方上传: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("recipe_no", "ZYCF" + System.currentTimeMillis());
output.put("upload_time", new Date().toString());
output.put("total_amount", "2580.50");
output.put("insurance_pay", "2322.45");
output.put("self_pay", "258.05");
output.put("status", "成功");
result.put("output", output);
return result;
}
public Map<String, Object> inpatientSettle(Map<String, String> params) {
log.info("住院结算: params={}", params);
Map<String, Object> result = new HashMap<>();
result.put("infcode", 0);
Map<String, Object> output = new HashMap<>();
output.put("settle_no", "ZYJS" + System.currentTimeMillis());
output.put("total_amount", "15680.50");
output.put("insurance_pay", "14112.45");
output.put("self_pay", "1568.05");
output.put("account_pay", "1200.00");
output.put("cash_pay", "368.05");
output.put("settle_time", new Date().toString());
output.put("status", "成功");
result.put("output", output);
return result;
}
private Map<String, Object> buildPsnInfoOutput(YbPsnInfo psnInfo) {
Map<String, Object> output = new HashMap<>();
output.put("psn_no", psnInfo.getPsnNo());
output.put("psn_name", psnInfo.getPsnName());
output.put("sex_code", psnInfo.getSexCode());
output.put("sex_name", psnInfo.getSexName());
output.put("birth_date", psnInfo.getBirthDate());
output.put("id_card", psnInfo.getIdCard());
output.put("insur_type", psnInfo.getInsurType());
output.put("insur_area", psnInfo.getInsurArea());
output.put("card_no", psnInfo.getCardNo());
output.put("balance", psnInfo.getBalance().toString());
output.put("status", psnInfo.getStatus());
return output;
}
public YbPsnInfo addPsnInfo(YbPsnInfo psnInfo) {
psnInfo.setCreateTime(new Date());
psnInfo.setUpdateTime(new Date());
psnInfoMapper.insert(psnInfo);
return psnInfo;
}
public List<YbPsnInfo> listPsnInfo() {
return psnInfoMapper.selectList(new LambdaQueryWrapper<>());
}
}

View File

@@ -14,7 +14,7 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8 url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
username: postgresql username: postgresql
password: Jchl1528 # 请替换为实际的数据库密码 password: Jchl1528 # 请替换为实际的数据库密码
# 从库数据源 # 从库数据源
@@ -73,9 +73,9 @@ spring:
data: data:
redis: redis:
# 地址 # 地址
host: 47.116.196.11 host: 192.168.110.252
# 端口默认为6379 # 端口默认为6379
port: 26379 port: 6379
# 数据库索引 # 数据库索引
database: 1 database: 1
# 密码 # 密码

View File

@@ -1,12 +1,20 @@
# 数据源配置 # 数据源配置
spring: spring:
# Flyway 数据库迁移配置
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: 0
locations: classpath:db/migration
out-of-order: false
validate-on-migrate: true
datasource: datasource:
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
driverClassName: org.postgresql.Driver driverClassName: org.postgresql.Driver
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8 url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
username: postgresql username: postgresql
password: Jchl1528 # 请替换为实际的数据库密码 password: Jchl1528 # 请替换为实际的数据库密码
# 从库数据源 # 从库数据源
@@ -49,7 +57,7 @@ spring:
allow: allow:
url-pattern: /druid/* url-pattern: /druid/*
# 控制台管理用户名和密码 # 控制台管理用户名和密码
login-username: openhis login-username: healthlink-his
login-password: 123456 login-password: 123456
filter: filter:
stat: stat:
@@ -62,27 +70,28 @@ spring:
config: config:
multi-statement-allow: true multi-statement-allow: true
# redis 配置 # redis 配置
redis: data:
# 地址 redis:
host: 192.168.110.252 # 地址
# 端口默认为6379 host: 192.168.110.252
port: 6379 # 端口,默认为6379
# 数据库索引 port: 6379
database: 1 # 数据库索引
# 密码 database: 1
password: Jchl1528 # 密码
# 连接超时时间 password: Jchl1528
timeout: 10s # 连接超时时间
lettuce: timeout: 10s
pool: lettuce:
# 连接池中的最小空闲连接 pool:
min-idle: 0 # 连接池中的最小空闲连接
# 连接池中的最大空闲连接 min-idle: 0
max-idle: 8 # 连接池中的最大空闲连接
# 连接池的最大数据库连接数 max-idle: 8
max-active: 8 # 连接池的最大数据库连接数
# #连接池最大阻塞等待时间(使用负值表示没有限制) max-active: 8
max-wait: -1ms # #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 服务器配置 # 服务器配置
server: server:
@@ -90,4 +99,5 @@ server:
port: 18080 port: 18080
servlet: servlet:
# 应用的访问路径 # 应用的访问路径
context-path: /openhis context-path: /healthlink-his

View File

@@ -160,4 +160,4 @@ management:
db: db:
enabled: true enabled: true
redis: redis:
enabled: true enabled: false

View File

@@ -0,0 +1,57 @@
-- V100__sync_existing_emr_data.sql
-- 同步已有的门诊/住院病历到修订历史和搜索索引
-- 1. 清空假数据
TRUNCATE TABLE emr_revision CASCADE;
TRUNCATE TABLE emr_search_index CASCADE;
-- 2. 从doc_emr同步修订历史为每条病历创建初始修订记录
INSERT INTO emr_revision (
id, emr_id, encounter_id, revision_number,
operator_id, operator_name, operation_type,
diff_content, snapshot_content, create_time
)
SELECT
nextval('emr_revision_id_seq'),
e.id,
e.encounter_id,
1,
COALESCE(e.record_id, 1),
'系统同步',
'CREATE',
'初始创建 - 从历史病历同步',
e.context_json,
COALESCE(e.create_time, NOW())
FROM doc_emr e
WHERE e.id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM emr_revision r WHERE r.emr_id = e.id
);
-- 3. 从doc_emr同步搜索索引简化版不依赖可能不存在的表
INSERT INTO emr_search_index (
id, emr_id, encounter_id, patient_id, patient_name,
emr_type, emr_title, diagnosis_text,
doctor_name, department_name, create_time
)
SELECT
nextval('emr_search_index_id_seq'),
e.id,
e.encounter_id,
e.patient_id,
'患者' || COALESCE(e.patient_id::text, '未知'),
CASE
WHEN e.class_enum = 1 THEN 'OUTPATIENT'
WHEN e.class_enum = 2 THEN 'INPATIENT'
ELSE 'OTHER'
END,
'未命名病历',
'',
'医生' || COALESCE(e.record_id::text, '未知'),
'未知科室',
COALESCE(e.create_time, NOW())
FROM doc_emr e
WHERE e.id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM emr_search_index si WHERE si.emr_id = e.id
);

View File

@@ -0,0 +1,47 @@
-- V101__add_emr_sync_menu_and_permissions.sql
-- 添加EMR数据同步菜单和医生权限
-- 1. 添加EMR数据同步菜单在电子病历管理下
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (
'EMR数据同步',
(SELECT menu_id FROM sys_menu WHERE menu_name = '电子病历管理' LIMIT 1),
99,
'sync',
'emr/sync/index',
'C',
'0',
'0',
'emr:sync:list',
'upload',
'admin',
NOW(),
'admin',
NOW(),
'EMR数据同步 - 从病历表同步数据到修订历史和搜索索引'
);
-- 2. 为医生角色添加EMR权限
-- 获取医生角色ID假设角色名为'医生'或'doctor'
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT
r.role_id,
m.menu_id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生')
AND m.perms IN (
'emr:list',
'emr:edit',
'emr:sync:list'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
);
-- 3. 更新EMR相关菜单的权限将inpatient:emr改为emr
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'inpatient:emr:list';
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'inpatient:emr:edit';
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'infection:emr:list';
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'infection:emr:edit';

View File

@@ -0,0 +1,44 @@
-- V102__grant_emr_menu_to_doctor.sql
-- 为医生角色授予电子病历管理相关菜单权限
-- 1. 获取医生角色ID并授予EMR菜单权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT
r.role_id,
m.menu_id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
AND m.menu_id IN (
20201, -- 电子病历管理
20202, -- 病案归档
20203, -- 修订历史
20204, -- 病历时效
20205, -- 病历检索
20206, -- 进程记录
20207 -- 知识库
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
);
-- 2. 为医生角色授予EMR相关权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT
r.role_id,
m.menu_id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
AND m.perms IN (
'emr:list',
'emr:edit',
'document:progressnote:list',
'document:progressnote:add',
'document:progressnote:edit'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
);

View File

@@ -0,0 +1,13 @@
-- V103__add_patient_info_to_emr_search_index.sql
-- 为病历检索索引添加患者基本信息
-- 添加患者信息字段
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_emr_search_patient_name ON emr_search_index(patient_name);
CREATE INDEX IF NOT EXISTS idx_emr_search_encounter_no ON emr_search_index(encounter_no);

View File

@@ -0,0 +1,115 @@
-- V105__create_yb_mock_tables.sql
-- 创建医保模拟服务器所需的数据库表
-- 1. 参保人信息表
CREATE TABLE IF NOT EXISTS yb_psn_info (
id BIGSERIAL PRIMARY KEY,
psn_no VARCHAR(50) NOT NULL UNIQUE,
psn_name VARCHAR(100),
sex_code VARCHAR(10),
sex_name VARCHAR(20),
birth_date VARCHAR(20),
id_card VARCHAR(20),
insur_type VARCHAR(100),
insur_area VARCHAR(100),
card_no VARCHAR(50),
balance DECIMAL(12,2) DEFAULT 0,
status VARCHAR(20) DEFAULT '正常',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 电子处方表
CREATE TABLE IF NOT EXISTS yb_recipe (
id BIGSERIAL PRIMARY KEY,
recipe_no VARCHAR(50) NOT NULL UNIQUE,
psn_no VARCHAR(50),
encounter_no VARCHAR(50),
recipe_type VARCHAR(20),
total_amount DECIMAL(12,2),
self_pay DECIMAL(12,2),
insurance_pay DECIMAL(12,2),
status VARCHAR(20) DEFAULT '待结算',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 事前事中商品库
CREATE TABLE IF NOT EXISTS yb_product (
id BIGSERIAL PRIMARY KEY,
item_code VARCHAR(50) NOT NULL UNIQUE,
item_name VARCHAR(200),
item_type VARCHAR(50),
spec VARCHAR(100),
unit VARCHAR(20),
price DECIMAL(12,2),
manufacturer VARCHAR(200),
approval_no VARCHAR(100),
status VARCHAR(20) DEFAULT '正常',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 结算记录表
CREATE TABLE IF NOT EXISTS yb_settle_record (
id BIGSERIAL PRIMARY KEY,
settle_no VARCHAR(50) NOT NULL UNIQUE,
psn_no VARCHAR(50),
encounter_no VARCHAR(50),
settle_type VARCHAR(20),
total_amount DECIMAL(12,2),
insurance_pay DECIMAL(12,2),
self_pay DECIMAL(12,2),
account_pay DECIMAL(12,2),
cash_pay DECIMAL(12,2),
status VARCHAR(20) DEFAULT '成功',
settle_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 签到签退记录表
CREATE TABLE IF NOT EXISTS yb_sign_record (
id BIGSERIAL PRIMARY KEY,
psn_no VARCHAR(50),
sign_type VARCHAR(20),
sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
terminal_no VARCHAR(50),
status VARCHAR(20) DEFAULT '成功'
);
-- 6. 插入测试数据 - 参保人信息
INSERT INTO yb_psn_info (psn_no, psn_name, sex_code, sex_name, birth_date, id_card, insur_type, insur_area, card_no, balance, status) VALUES
('P100001', '张三', '1', '', '1980-01-15', '450123198001151234', '职工基本医疗保险', '南宁市', 'C2024000001', 12580.50, '正常'),
('P100002', '李四', '2', '', '1985-03-20', '450123198503201234', '城乡居民基本医疗保险', '柳州市', 'C2024000002', 5620.00, '正常'),
('P100003', '王五', '1', '', '1990-06-10', '450123199006101234', '职工基本医疗保险', '桂林市', 'C2024000003', 8950.25, '正常'),
('P100004', '赵六', '2', '', '1975-12-25', '450123197512251234', '离休人员医疗保险', '梧州市', 'C2024000004', 25000.00, '正常'),
('P100005', '孙七', '1', '', '1995-08-08', '450123199508081234', '城乡居民基本医疗保险', '北海市', 'C2024000005', 3200.75, '暂停');
-- 7. 插入测试数据 - 电子处方
INSERT INTO yb_recipe (recipe_no, psn_no, encounter_no, recipe_type, total_amount, self_pay, insurance_pay, status) VALUES
('CF20240601001', 'P100001', 'MZ20240601001', '西药处方', 156.80, 23.52, 133.28, '已结算'),
('CF20240601002', 'P100002', 'MZ20240601002', '中药处方', 238.50, 71.55, 166.95, '待结算'),
('CF20240601003', 'P100003', 'ZY20240601001', '住院处方', 2580.50, 258.05, 2322.45, '已结算');
-- 8. 插入测试数据 - 结算记录
INSERT INTO yb_settle_record (settle_no, psn_no, encounter_no, settle_type, total_amount, insurance_pay, self_pay, account_pay, cash_pay, status) VALUES
('JZ20240601001', 'P100001', 'MZ20240601001', '门诊结算', 156.80, 133.28, 23.52, 20.00, 3.52, '成功'),
('ZYJS20240601001', 'P100003', 'ZY20240601001', '住院结算', 15680.50, 14112.45, 1568.05, 1200.00, 368.05, '成功');
-- 9. 创建索引
CREATE INDEX IF NOT EXISTS idx_yb_psn_info_psn_no ON yb_psn_info(psn_no);
CREATE INDEX IF NOT EXISTS idx_yb_recipe_psn_no ON yb_recipe(psn_no);
CREATE INDEX IF NOT EXISTS idx_yb_recipe_encounter ON yb_recipe(encounter_no);
CREATE INDEX IF NOT EXISTS idx_yb_settle_psn_no ON yb_settle_record(psn_no);
CREATE INDEX IF NOT EXISTS idx_yb_settle_encounter ON yb_settle_record(encounter_no);
CREATE INDEX IF NOT EXISTS idx_yb_sign_psn_no ON yb_sign_record(psn_no);
-- 10. 修复 clinical_pathway 表缺失列
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
-- 11. 修复 clinical_pathway_execution 表缺失列
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;

View File

@@ -0,0 +1,8 @@
-- V106__add_missing_emr_search_index_columns.sql
-- 补充 emr_search_index 缺失的患者信息列V103 未生效)
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);

View File

@@ -0,0 +1,8 @@
-- V104__add_patient_info_to_emr_search_index_hisdev.sql
-- 在 healthlink_his schema 上添加患者信息字段
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);

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