Compare commits

...

78 Commits

Author SHA1 Message Date
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
Ranyunqiao
74cf599ea7 需求111 住院护士站-》护理记录维护权限 修改成功 2026-06-24 14:46:15 +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
Ranyunqiao
20dade7bf0 bug 800 802 803 804 805 2026-06-24 13:45:20 +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
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
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
287 changed files with 6317 additions and 170491 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

@@ -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

@@ -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

@@ -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);

View File

@@ -0,0 +1,264 @@
-- V107__fix_role_permission_alignment.sql
-- 全面修复角色-权限匹配问题菜单展示但API报403
-- ============================================================
-- 第一部分修复权限前缀不一致历史遗留的infection:前缀)
-- ============================================================
-- 修复EMR相关菜单权限infection:emr → emr
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'infection:emr:list';
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'infection:emr:edit';
UPDATE sys_menu SET perms = 'emr:sync:list' WHERE perms = 'infection:emr:sync:list';
-- 修复病案统计明细infection:mrhomepage → mrhomepage:mrhomepage
UPDATE sys_menu SET perms = 'mrhomepage:mrhomepage:list' WHERE perms = 'infection:mrhomepage:list';
-- 修复报表维度infection:report → reportmanage:report
UPDATE sys_menu SET perms = 'reportmanage:report:list' WHERE perms = 'infection:report:list';
UPDATE sys_menu SET perms = 'reportmanage:report:edit' WHERE perms = 'infection:report:edit';
-- 修复inpatient相关inpatient:emr → emr已由V101处理此处兜底
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'inpatient:emr:list';
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'inpatient:emr:edit';
-- ============================================================
-- 第二部分确保所有Controller需要的权限在sys_menu中存在
-- ============================================================
-- 检查并插入缺失的菜单权限(如果菜单不存在则创建)
-- 这些是后端Controller @PreAuthorize使用的权限但菜单表中可能缺失
-- administration模块
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)
SELECT '医务人员患者管理',
(SELECT menu_id FROM sys_menu WHERE menu_name = '系统管理' AND menu_type = 'M' LIMIT 1),
99, 'practitioner-patient', 'administration/practitioner-patient/index', 'C', '0', '0',
'administration:practitionerPatient:list', 'user', 'admin', NOW(), 'admin', NOW(),
'医务人员患者管理菜单'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'administration:practitionerPatient:list');
-- basicmanage模块
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)
SELECT '电子健康卡',
(SELECT menu_id FROM sys_menu WHERE menu_name = '基础管理' AND menu_type = 'M' LIMIT 1),
10, 'ehcard', 'basicmanage/ehcard/index', 'C', '0', '0',
'basicmanage:ehcard:list', 'card', 'admin', NOW(), 'admin', NOW(),
'电子健康卡管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'basicmanage:ehcard:list');
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)
SELECT '电子发票',
(SELECT menu_id FROM sys_menu WHERE menu_name = '基础管理' AND menu_type = 'M' LIMIT 1),
20, 'einvoice', 'basicmanage/einvoice/index', 'C', '0', '0',
'basicmanage:invoice:list', 'invoice', 'admin', NOW(), 'admin', NOW(),
'电子发票管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'basicmanage:invoice:list');
-- document模块病程记录
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)
SELECT '病程记录',
(SELECT menu_id FROM sys_menu WHERE menu_name = '电子病历管理' AND menu_type = 'M' LIMIT 1),
50, 'progress-note', 'document/progress-note/index', 'C', '0', '0',
'document:progressnote:list', 'note', 'admin', NOW(), 'admin', NOW(),
'病程记录管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'document:progressnote:list');
-- epidemic模块传染病报卡
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)
SELECT '传染病报卡',
(SELECT menu_id FROM sys_menu WHERE menu_name = '医院感染管理' AND menu_type = 'M' LIMIT 1),
10, 'epidemic', 'infection/epidemic/index', 'C', '0', '0',
'epidemic:list', 'alert', 'admin', NOW(), 'admin', NOW(),
'传染病报卡管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'epidemic:list');
-- flowable模块工作流表单
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)
SELECT '流程表单',
(SELECT menu_id FROM sys_menu WHERE menu_name = '系统管理' AND menu_type = 'M' LIMIT 1),
98, 'flowable-form', 'flowable/form/index', 'C', '0', '0',
'flowable:form:list', 'form', 'admin', NOW(), 'admin', NOW(),
'流程表单管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'flowable:form:list');
-- tcm模块中医
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)
SELECT '中医诊断',
(SELECT menu_id FROM sys_menu WHERE menu_name = '门诊医生工作站' AND menu_type = 'M' LIMIT 1),
99, 'tcm', 'tcm/diagnosis/index', 'C', '0', '0',
'tcm:list', '中医', 'admin', NOW(), 'admin', NOW(),
'中医诊断管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'tcm:list');
-- surgery模块手术安全核查
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)
SELECT '手术安全核查',
(SELECT menu_id FROM sys_menu WHERE menu_name = '手术管理' AND menu_type = 'M' LIMIT 1),
50, 'surgery-safety', 'surgery/safety-check/index', 'C', '0', '0',
'surgery:schedule:list', 'safety', 'admin', NOW(), 'admin', NOW(),
'手术安全核查管理'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'surgery:schedule:list');
-- ============================================================
-- 第三部分:为所有角色授予基础查看权限
-- ============================================================
-- 获取所有非管理员角色ID
-- 为每个角色授予关键模块的查看权限
-- 授予所有活跃角色emr:list权限电子病历查看
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.status = '0'
AND m.perms IN (
'emr:list',
'emr:edit',
'infection:cdss:list',
'infection:regional:list',
'reportmanage:report:list',
'mrhomepage:mrhomepage:list',
'epidemic:list',
'document:progressnote:list',
'basicmanage:ehcard:list',
'basicmanage:invoice:list',
'surgery:schedule:list',
'tcm: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
);
-- ============================================================
-- 第四部分:为医生角色授予专属权限
-- ============================================================
-- 医生角色:授予门诊医生工作站、住院医生工作站相关权限
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.status = '0'
AND r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '主任医师', '副主任医师')
AND m.perms IN (
'emr:list',
'emr:edit',
'infection:cdss:list',
'infection:cdss:edit',
'infection:check:list',
'infection:check:edit',
'document:progressnote:list',
'document:progressnote:add',
'document:progressnote:edit',
'tcm:list',
'tcm:edit',
'surgery:schedule:list',
'surgery:schedule:edit',
'epidemic:list',
'epidemic:edit',
'nursing:nursing:list',
'outpatient:telehealth:list',
'outpatient:telehealth: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
);
-- ============================================================
-- 第五部分:为护士角色授予专属权限
-- ============================================================
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.status = '0'
AND r.role_name IN ('护士', 'nurse', '护士长')
AND m.perms IN (
'nursing:nursing:list',
'nursing:nursing:edit',
'nursing:execution:list',
'nursing:execution:add',
'nursing:execution:edit',
'nursing:record:list',
'nursing:record:add',
'nursing:record:edit',
'inpatient:anesthesia:list',
'inpatient:anesthesia:edit',
'inpatient:clinical:list',
'inpatient:clinical:edit',
'inpatient:criticalvalue:list',
'inpatient:criticalvalue:edit',
'inpatient:bloodtransfusion:list',
'inpatient:bloodtransfusion:edit',
'emr:list',
'emr: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
);
-- ============================================================
-- 第六部分:为药房角色授予专属权限
-- ============================================================
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.status = '0'
AND r.role_name IN ('药房', 'pharmacy', '药师', '药剂师')
AND m.perms IN (
'infection:rationaldrug:edit',
'inpatient:clinical:list',
'inpatient:clinical:edit',
'inpatient:criticalvalue:list',
'emr: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
);
-- ============================================================
-- 第七部分:为管理员角色授予所有权限
-- ============================================================
-- 管理员角色获取所有菜单权限通过admin用户已有的 *:*:* 权限)
-- 但确保管理员角色在sys_role_menu中有所有菜单的关联
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id
FROM sys_menu m
WHERE m.status = '0'
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id
);
-- ============================================================
-- 第八部分修复doctor_enhanced菜单的重复问题V66/V76遗留
-- ============================================================
-- 删除可能存在的重复菜单保留perms正确的那个
DELETE FROM sys_menu
WHERE menu_name = '门诊医生增强'
AND perms = 'infection:emr:list'
AND menu_id IN (
SELECT menu_id FROM (
SELECT menu_id FROM sys_menu
WHERE menu_name = '门诊医生增强'
ORDER BY menu_id DESC
LIMIT 1 OFFSET 1
) t
);
-- ============================================================
-- 完成:刷新菜单缓存的提示
-- ============================================================
-- 执行完此脚本后,需要:
-- 1. 重启应用或调用 /system/menu/refreshCache 刷新菜单缓存
-- 2. 用户重新登录以加载最新权限

View File

@@ -0,0 +1,415 @@
-- V109: 病程记录模块假数据
-- 生成时间: 2026-06-22
-- 说明: 为emr/progress模块创建测试数据覆盖各种病程记录类型和状态
-- ==================== 1. 病程记录 sys_progress_note ====================
INSERT INTO sys_progress_note (
id, encounter_id, patient_id, patient_name, note_type, note_content,
author_user_id, author_name, author_title,
review_user_id, review_user_name,
sign_status, sign_time, deadline, is_overdue, overdue_hours,
template_id, version, tenant_id, delete_flag, create_by, create_time
)
VALUES
-- ===== 首次病程记录 (note_type=1, 时限8小时) =====
(8000000001, 6001, 5001, '张伟', 1,
'【首次病程记录】
患者张伟45岁因"反复上腹痛3年加重伴恶心呕吐1周"于2026-06-20 10:00入院。
一、病例特点
1. 中年男性,慢性病程,急性加重
2. 主要症状反复上腹痛3年加重伴恶心呕吐1周
3. 既往史:否认高血压、糖尿病史,否认手术外伤史
4. 查体T 36.8℃P 82次/分R 18次/分BP 125/80mmHg。腹软剑突下压痛(+),无反跳痛
二、诊断依据
1. 反复上腹痛病史
2. 剑突下压痛阳性
3. 胃镜检查提示:十二指肠球部溃疡
三、鉴别诊断
1. 胃溃疡:疼痛规律不同,胃镜可鉴别
2. 胃癌:需病理活检排除
四、诊疗计划
1. 完善相关检查血常规、肝肾功能、腹部B超
2. 抑酸护胃奥美拉唑40mg ivgtt qd
3. 对症支持治疗
4. 必要时请消化内科会诊',
1001, '张明', '主治医师',
NULL, NULL,
1, '2026-06-20 14:30:00', '2026-06-20 18:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-20 10:30:00'),
-- ===== 日常病程记录 (note_type=2, 时限72小时) =====
(8000000002, 6001, 5001, '张伟', 2,
'【日常病程记录 - 第1次】
患者诉上腹痛较前缓解,恶心呕吐症状消失。饮食改善,可进半流质饮食。
查体T 36.6℃P 78次/分R 18次/分BP 120/78mmHg。腹软剑突下轻压痛无反跳痛。
辅助检查回报:
- 血常规WBC 6.8×10^9/LN 65%Hb 135g/L
- 肝肾功能ALT 35U/LAST 28U/LCr 78μmol/L
- 腹部B超肝胆胰脾未见明显异常
目前诊断明确:十二指肠球部溃疡。抑酸治疗有效,继续当前方案。',
1001, '张明', '主治医师',
NULL, NULL,
1, '2026-06-21 09:00:00', '2026-06-23 10:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-21 08:30:00'),
(8000000003, 6001, 5001, '张伟', 2,
'【日常病程记录 - 第2次】
患者一般情况良好,无腹痛、恶心、呕吐。饮食恢复至普食,大便正常。
查体T 36.5℃P 75次/分R 16次/分BP 118/75mmHg。腹软无压痛。
治疗方案调整:
- 口服奥美拉唑20mg qd
- 停用静脉用药
- 嘱患者注意饮食规律,避免辛辣刺激食物',
1002, '李华', '住院医师',
1001, '张明',
1, '2026-06-22 10:00:00', '2026-06-24 10:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-22 09:00:00'),
-- ===== 上级医师查房记录 (note_type=3, 时限72小时) =====
(8000000004, 6001, 5001, '张伟', 3,
'【上级医师查房记录】
查房时间2026-06-21 15:00
查房医师:张明 主治医师
患者张伟,诊断:十二指肠球部溃疡。
查房意见:
1. 患者目前症状明显缓解,抑酸治疗有效
2. 建议完善C13呼气试验明确有无幽门螺杆菌感染
3. 如Hp阳性需行四联根除治疗
4. 继续目前治疗方案,注意观察病情变化
5. 如无特殊情况,可安排出院
签名:张明',
1001, '张明', '主治医师',
1001, '张明',
1, '2026-06-21 16:00:00', '2026-06-24 15:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-21 15:30:00'),
-- ===== 阶段小结 (note_type=5, 时限720小时/30天) =====
(8000000005, 6002, 5002, '李娜', 5,
'【阶段小结】
患者李娜52岁因"发现血糖升高5年控制不佳2月"于2026-05-15入院。
一、入院诊断
1. 2型糖尿病
2. 高血压病3级极高危
二、诊疗经过
1. 入院后完善相关检查HbA1c 8.5%空腹血糖12.3mmol/L
2. 调整降糖方案二甲双胍500mg tid + 格列美脲2mg qd + 甘精胰岛素20u qn
3. 控制血压硝苯地平控释片30mg qd + 缬沙坦80mg qd
4. 糖尿病饮食教育、运动指导
三、目前情况
- 空腹血糖7.2-8.5mmol/L餐后2h血糖10.2-12.8mmol/L
- 血压135/85mmHg左右
- 无低血糖发生
四、下一步计划
1. 继续调整降糖方案,目标空腹血糖<7.0mmol/L
2. 加强糖尿病足筛查
3. 完善眼底检查',
1003, '王芳', '副主任医师',
1003, '王芳',
1, '2026-06-10 14:00:00', '2026-06-15 09:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-10 09:00:00'),
-- ===== 抢救记录 (note_type=6, 时限6小时) =====
(8000000006, 6003, 5003, '王强', 6,
'【抢救记录】
抢救时间2026-06-18 14:30-15:20
抢救地点:神经内科病房
参加抢救人员:赵磊(副主任医师)、钱进(主治医师)、孙丽(护士长)等
患者王强68岁因"突发意识不清2小时"于2026-06-18 12:30入院。
抢救经过:
14:30 患者突然出现意识不清,呼之不应,左侧肢体瘫痪
14:32 立即给予吸氧、心电监护,建立静脉通路
14:35 血压185/110mmHg心率110次/分血氧饱和度92%
14:38 急查头颅CT右侧基底节区脑出血出血量约35ml
14:40 予以甘露醇250ml快速静滴脱水降颅压
14:45 乌拉地尔25mg缓慢静推控制血压
14:50 血压降至150/90mmHg患者意识稍有好转
15:00 联系ICU准备转科进一步治疗
15:20 患者生命体征相对平稳转ICU继续治疗
抢救结果抢救成功患者转ICU继续治疗',
1004, '赵磊', '副主任医师',
1004, '赵磊',
1, '2026-06-18 16:00:00', '2026-06-18 20:30:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-18 15:30:00'),
-- ===== 出院记录 (note_type=9, 时限24小时) =====
(8000000007, 6001, 5001, '张伟', 9,
'【出院记录】
入院日期2026-06-20 10:00
出院日期2026-06-22 14:00
住院天数2天
入院诊断:十二指肠球部溃疡
诊疗经过:
患者因反复上腹痛3年加重1周入院。入院后完善相关检查明确诊断为十二指肠球部溃疡。予以抑酸护胃、对症支持治疗后症状明显缓解。
出院情况:
患者一般情况良好,无腹痛、恶心、呕吐。饮食恢复普食,大便正常。
出院医嘱:
1. 奥美拉唑肠溶胶囊20mg qd×4周
2. 阿莫西林胶囊1g bid×2周如Hp阳性
3. 克拉霉素片500mg bid×2周如Hp阳性
4. 1月后复查胃镜
5. 注意饮食规律,避免辛辣刺激、戒烟限酒',
1001, '张明', '主治医师',
1001, '张明',
1, '2026-06-22 14:30:00', '2026-06-23 14:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-22 14:00:00'),
-- ===== 死亡讨论记录 (note_type=10, 时限168小时/7天) =====
(8000000008, 6005, 5005, '陈芳', 10,
'【死亡病例讨论记录】
讨论时间2026-06-10 14:00-15:30
讨论地点:神经内科示教室
主持人:赵磊 副主任医师
参加人员:赵磊、钱进、孙丽、周敏等
患者陈芳78岁因"突发右侧肢体无力伴言语不清6小时"于2026-06-05入院。
一、病例摘要
患者6小时前无明显诱因出现右侧肢体无力伴言语不清急诊入院。头颅CT示左侧大面积脑梗死。入院后予以溶栓、抗血小板、调脂稳斑等治疗。
二、治疗经过
- 6月5日急诊溶栓治疗
- 6月6日病情稳定转入普通病房
- 6月8日突发肺部感染予以抗感染治疗
- 6月9日出现多器官功能衰竭
- 6月10日经抢救无效死亡
三、死亡诊断
1. 急性大面积脑梗死
2. 肺部感染
3. 多器官功能衰竭
四、讨论总结
1. 患者高龄,基础疾病多,溶栓风险高
2. 溶栓后出血转化风险未能充分评估
3. 后续抗感染治疗时机可更积极
五、改进措施
1. 完善高龄患者溶栓风险评估量表
2. 加强溶栓后监测频率
3. 制定多学科联合查房制度',
1004, '赵磊', '副主任医师',
1004, '赵磊',
1, '2026-06-11 10:00:00', '2026-06-17 10:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-10 16:00:00'),
-- ===== 超时未签名的病程记录 =====
(8000000009, 6004, 5004, '刘洋', 2,
'【日常病程记录 - 待签名】
患者刘洋55岁诊断慢性阻塞性肺疾病急性加重。
今日查房:患者咳嗽、咳痰较前好转,痰量减少,无发热。呼吸平稳,可平卧。
查体T 36.7℃P 80次/分R 20次/分BP 130/85mmHg。双肺呼吸音粗可闻及散在湿啰音。
治疗调整:
- 继续抗感染治疗
- 雾化吸入tid
- 加强呼吸功能锻炼',
1002, '李华', '住院医师',
NULL, NULL,
0, NULL, '2026-06-19 10:00:00', true, 48,
NULL, 1, 1, '0', 'admin', '2026-06-19 09:00:00'),
(8000000010, 6006, 5006, '赵静', 1,
'【首次病程记录 - 超时】
患者赵静38岁因"转移性右下腹痛12小时"于2026-06-18 22:00入院。
一、病例特点
青年女性,急性病程
转移性右下腹痛12小时伴恶心、呕吐2次
查体:右下腹麦氏点压痛(+),反跳痛(+)
二、诊断
急性阑尾炎
三、诊疗计划
1. 完善术前检查
2. 禁食水
3. 抗感染治疗
4. 择期手术',
1003, '王芳', '副主任医师',
NULL, NULL,
0, NULL, '2026-06-19 06:00:00', true, 24,
NULL, 1, 1, '0', 'admin', '2026-06-18 22:30:00'),
-- ===== 更多日常病程记录 =====
(8000000011, 6007, 5007, '孙浩', 2,
'【日常病程记录】
患者孙浩62岁诊断冠心病、不稳定型心绞痛。
今日症状:胸闷、胸痛较前缓解,活动后仍有不适。
查体BP 128/82mmHgHR 76次/分,律齐。
心电图窦性心律ST-T改变较前改善。
治疗:
- 继续双联抗血小板治疗
- 阿托伐他汀20mg qn
- 美托洛尔缓释片47.5mg qd',
1001, '张明', '主治医师',
1001, '张明',
1, '2026-06-20 11:00:00', '2026-06-22 11:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-20 10:00:00'),
(8000000012, 6008, 5008, '周磊', 3,
'【上级医师查房记录】
查房时间2026-06-21 09:00
查房医师:赵磊 副主任医师
患者周磊48岁诊断腰椎间盘突出症。
查房意见:
1. 患者目前腰腿痛症状明显缓解
2. 直腿抬高试验较入院时改善
3. 建议加强腰背肌功能锻炼
4. 可考虑出院后继续康复治疗
5. 嘱患者避免久坐、弯腰负重',
1004, '赵磊', '副主任医师',
1004, '赵磊',
1, '2026-06-21 10:00:00', '2026-06-24 09:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-21 09:30:00'),
-- ===== 转科记录 (note_type=7) =====
(8000000013, 6009, 5009, '吴秀英', 7,
'【转科记录】
转出科室:呼吸内科
转入科室ICU
转科时间2026-06-20 16:00
转科原因:
患者因"重症肺炎、呼吸衰竭"入院经积极抗感染、呼吸支持治疗后病情仍较重需转ICU进一步监护治疗。
转科时情况:
T 38.5℃P 110次/分R 28次/分BP 95/60mmHgSpO2 88%
神志清楚,精神差,呼吸急促,双肺可闻及大量湿啰音
转科诊断:
1. 重症肺炎
2. I型呼吸衰竭
3. 脓毒症
转科医嘱:
1. 持续心电监护
2. 机械通气支持
3. 广谱抗感染治疗
4. 血管活性药物维持血压',
1002, '李华', '住院医师',
1004, '赵磊',
1, '2026-06-20 16:30:00', '2026-06-21 16:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-20 16:00:00'),
-- ===== 接收记录 (note_type=8) =====
(8000000014, 6009, 5009, '吴秀英', 8,
'【接收记录】
接收科室ICU
转出科室:呼吸内科
接收时间2026-06-20 16:30
接收医师:赵磊 副主任医师
接收时情况:
患者由呼吸内科转入T 38.4℃P 108次/分R 26次/分BP 98/62mmHgSpO2 90%
神志清楚,精神差,呼吸急促,双肺可闻及大量湿啰音
接收处理:
1. 立即予以机械通气支持
2. 完善动脉血气分析
3. 调整抗感染方案美罗培南1g q8h + 万古霉素1g q12h
4. 血管活性药物维持血压
5. 加强液体管理',
1004, '赵磊', '副主任医师',
1004, '赵磊',
1, '2026-06-20 17:00:00', '2026-06-21 16:30:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-20 16:30:00'),
-- ===== 疑难病例讨论 (note_type=4) =====
(8000000015, 6010, 5010, '郑伟', 4,
'【疑难病例讨论记录】
讨论时间2026-06-19 15:00-16:30
讨论地点:内科示教室
主持人:赵磊 副主任医师
参加人员:赵磊、钱进、孙丽、周敏、吴强等
患者郑伟58岁因"反复发热、关节痛2月皮疹1月"于2026-06-10入院。
一、病例特点
1. 中年男性,慢性病程
2. 主要症状反复发热最高39.2℃)、多关节肿痛、面部蝶形红斑
3. 辅助检查ANA 1:640抗dsDNA抗体阳性补体C3/C4降低
4. 肾脏受累尿蛋白2+,血肌酐升高
二、目前诊断
系统性红斑狼疮SLE伴狼疮性肾炎
三、讨论要点
1. 狼疮性肾炎分型:需行肾穿刺活检明确病理类型
2. 免疫抑制方案选择:环磷酰胺 vs 吗替麦考酚酯
3. 感染风险评估:长期免疫抑制治疗的感染预防
四、讨论总结
1. 同意目前SLE伴狼疮性肾炎诊断
2. 建议尽快行肾穿刺活检
3. 根据病理类型制定个体化免疫抑制方案
4. 加强感染监测和预防',
1004, '赵磊', '副主任医师',
1004, '赵磊',
1, '2026-06-19 17:00:00', '2026-06-22 15:00:00', false, 0,
NULL, 1, 1, '0', 'admin', '2026-06-19 16:30:00')
ON CONFLICT (id) DO NOTHING;
-- ==================== 2. 病程记录提醒 sys_progress_note_reminder ====================
INSERT INTO sys_progress_note_reminder (
id, encounter_id, patient_name, note_type, deadline,
status, remind_user_id, remind_user_name, created_time,
tenant_id, delete_flag, create_by, create_time
)
VALUES
-- 已提醒(1)
(8100000001, 6001, '张伟', 1, '2026-06-20 18:00:00', 1, 1001, '张明', '2026-06-20 14:30:00', 1, '0', 'admin', '2026-06-20 14:30:00'),
(8100000002, 6001, '张伟', 2, '2026-06-23 10:00:00', 1, 1001, '张明', '2026-06-21 09:00:00', 1, '0', 'admin', '2026-06-21 09:00:00'),
(8100000003, 6003, 5003, '王强', 6, '2026-06-18 20:30:00', 1, 1004, '赵磊', '2026-06-18 16:00:00', 1, '0', 'admin', '2026-06-18 16:00:00'),
-- 待提醒(0)
(8100000004, 6004, '刘洋', 2, '2026-06-19 10:00:00', 0, 1002, '李华', '2026-06-19 09:00:00', 1, '0', 'admin', '2026-06-19 09:00:00'),
(8100000005, 6006, '赵静', 1, '2026-06-19 06:00:00', 0, 1003, '王芳', '2026-06-18 22:30:00', 1, '0', 'admin', '2026-06-18 22:30:00'),
(8100000006, 6007, '孙浩', 2, '2026-06-22 11:00:00', 0, 1001, '张明', '2026-06-20 10:00:00', 1, '0', 'admin', '2026-06-20 10:00:00'),
(8100000007, 6008, '周磊', 3, '2026-06-24 09:00:00', 0, 1004, '赵磊', '2026-06-21 09:30:00', 1, '0', 'admin', '2026-06-21 09:30:00'),
(8100000008, 6009, '吴秀英', 7, '2026-06-21 16:00:00', 0, 1002, '李华', '2026-06-20 16:00:00', 1, '0', 'admin', '2026-06-20 16:00:00'),
(8100000009, 6010, '郑伟', 4, '2026-06-22 15:00:00', 0, 1004, '赵磊', '2026-06-19 16:30:00', 1, '0', 'admin', '2026-06-19 16:30:00')
ON CONFLICT (id) DO NOTHING;

View File

@@ -0,0 +1,9 @@
-- V110: 修复 clinical_pathway_execution 表缺少 create_by 列
-- 错误信息: ERROR: column "create_by" of relation "clinical_pathway_execution" does not exist
-- 添加缺失的 create_by 列
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
-- 确保其他 HisBaseEntity 列也存在
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,23 @@
-- V106__fix_order_closed_loop_permissions.sql
-- 修复医嘱闭环模块权限,将 sys_menu 表中的权限标识更新为与后端 OrderClosedLoopController 一致的权限,并授权给相关角色
-- 1. 更新已有的“执行追踪”和“闭环统计”菜单的权限标识,与后端控制器的 @PreAuthorize 权限一致
UPDATE sys_menu SET perms = 'inpatient:orderclosedloop:list' WHERE menu_id IN (20112, 20113);
-- 2. 添加按钮级权限:“编辑/执行/催办”权限
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES
(20114, '医嘱执行编辑', 20112, 1, '', '', 'F', '0', '0', 'inpatient:orderclosedloop:edit', '#', 'admin', CURRENT_TIMESTAMP, '医嘱闭环-执行追踪编辑按钮权限'),
(20115, '闭环统计催办', 20113, 1, '', '', 'F', '0', '0', 'inpatient:orderclosedloop:edit', '#', 'admin', CURRENT_TIMESTAMP, '医嘱闭环-闭环统计催办按钮权限')
ON CONFLICT (menu_id) DO NOTHING;
-- 3. 为“医生”、“护士”、“管理员”相关角色授予这些新引入的权限按钮
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 ('超级管理员', '管理员', 'admin', '医生', 'doctor', '门诊医生', '住院医生', '护士', 'nurse', '信息科管理员', '可用页面管理员')
AND m.menu_id IN (20114, 20115)
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,35 @@
-- V2026.0622.02__grant_closed_loop_menu_to_roles.sql
-- 修复 V2026.0622.01 的遗漏:将"执行追踪"(20112)和"闭环统计"(20113)菜单本身
-- 以及按钮权限(20114, 20115)一起授予相关角色。
-- 原脚本只给角色授予了 20114/20115 按钮,漏掉了 20112/20113 父菜单,
-- 导致 inpatient:orderclosedloop:list 权限无法加载到用户权限集合中。
-- 1. 将菜单 20112(执行追踪)、20113(闭环统计)、20114(医嘱执行编辑)、20115(闭环统计催办) 一起授权给相关角色
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 (
'超级管理员', '管理员', 'admin',
'医生', 'doctor', '门诊医生', '住院医生',
'护士', 'nurse',
'信息科管理员', '可用页面管理员'
)
AND m.menu_id IN (20112, 20113, 20114, 20115)
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. 兜底:对于 role_key 包含 doctor/nurse/admin 的角色也进行授权,
-- 防止某些角色 role_name 为中文自定义名称(如"内科医生")但 role_key 仍匹配的情况
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_key LIKE '%doctor%' OR r.role_key LIKE '%nurse%' OR r.role_key = 'admin')
AND m.menu_id IN (20112, 20113, 20114, 20115)
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,34 @@
-- 修复医嘱闭环模块权限
-- 1. 将 sys_menu 权限标识更新为与后端 OrderClosedLoopController 一致
-- 2. 新增按钮级权限菜单
-- 3. 为相关角色授予全部闭环菜单权限
-- 1. 修正"执行追踪"和"闭环统计"菜单的权限标识
UPDATE sys_menu SET perms = 'inpatient:orderclosedloop:list' WHERE menu_id IN (20112, 20113);
-- 2. 添加按钮级权限
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES
(20114, '医嘱执行编辑', 20112, 1, '', '', 'F', '0', '0', 'inpatient:orderclosedloop:edit', '#', 'admin', CURRENT_TIMESTAMP, '医嘱闭环-执行追踪编辑按钮权限'),
(20115, '闭环统计催办', 20113, 1, '', '', 'F', '0', '0', 'inpatient:orderclosedloop:edit', '#', 'admin', CURRENT_TIMESTAMP, '医嘱闭环-闭环统计催办按钮权限')
ON CONFLICT (menu_id) DO NOTHING;
-- 3. 为相关角色授予闭环模块全部菜单20112-20115
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, m.menu_id
FROM sys_role r
CROSS JOIN (SELECT unnest(ARRAY[20112, 20113, 20114, 20115]) AS menu_id) m
WHERE r.role_name IN ('超级管理员', '管理员', 'admin', '医生', 'doctor', '门诊医生', '住院医生', '护士', 'nurse', '信息科管理员', '可用页面管理员')
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
);
-- 4. 兜底:按 role_key 模糊匹配(覆盖自定义中文角色名)
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, m.menu_id
FROM sys_role r
CROSS JOIN (SELECT unnest(ARRAY[20112, 20113, 20114, 20115]) AS menu_id) m
WHERE (r.role_key LIKE '%doctor%' OR r.role_key LIKE '%nurse%' OR r.role_key = 'admin')
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,172 @@
-- V20260624_2__fix_nursing_record_permissions.sql
-- 修复护理记录tab权限问题韦雪账号访问住院护士站→护理记录tab提示"当前操作没有权限"
-- 根因NursingRecordController 使用 @PreAuthorize('nursing:record:list') 等权限,
-- 但 sys_menu 中缺少这些按钮级权限条目,导致 V108 的 CROSS JOIN 授权无效
-- ============================================================
-- 第一部分创建缺失的护理记录按钮级菜单权限F类型
-- ============================================================
-- 护理记录 - 查看
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)
SELECT '护理记录查询',
(SELECT menu_id FROM sys_menu WHERE menu_name = '护理记录' AND menu_type = 'C' LIMIT 1),
1, '#', '', 'F', '0', '0',
'nursing:record:list', '#', 'admin', NOW(), 'admin', NOW(),
'护理记录查询按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:record:list');
-- 护理记录 - 新增
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)
SELECT '护理记录新增',
(SELECT menu_id FROM sys_menu WHERE menu_name = '护理记录' AND menu_type = 'C' LIMIT 1),
2, '#', '', 'F', '0', '0',
'nursing:record:add', '#', 'admin', NOW(), 'admin', NOW(),
'护理记录新增按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:record:add');
-- 护理记录 - 编辑
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)
SELECT '护理记录修改',
(SELECT menu_id FROM sys_menu WHERE menu_name = '护理记录' AND menu_type = 'C' LIMIT 1),
3, '#', '', 'F', '0', '0',
'nursing:record:edit', '#', 'admin', NOW(), 'admin', NOW(),
'护理记录修改按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:record:edit');
-- 护理记录 - 删除
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)
SELECT '护理记录删除',
(SELECT menu_id FROM sys_menu WHERE menu_name = '护理记录' AND menu_type = 'C' LIMIT 1),
4, '#', '', 'F', '0', '0',
'nursing:record:remove', '#', 'admin', NOW(), 'admin', NOW(),
'护理记录删除按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:record:remove');
-- ============================================================
-- 第二部分创建缺失的护理执行按钮级菜单权限F类型
-- ============================================================
-- 护理执行 - 查看
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)
SELECT '护理执行查询',
(SELECT menu_id FROM sys_menu WHERE menu_name = '住院护士站' AND menu_type IN ('M', 'C') LIMIT 1),
51, '#', '', 'F', '0', '0',
'nursing:execution:list', '#', 'admin', NOW(), 'admin', NOW(),
'护理执行查询按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:execution:list');
-- 护理执行 - 新增
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)
SELECT '护理执行新增',
(SELECT menu_id FROM sys_menu WHERE menu_name = '住院护士站' AND menu_type IN ('M', 'C') LIMIT 1),
52, '#', '', 'F', '0', '0',
'nursing:execution:add', '#', 'admin', NOW(), 'admin', NOW(),
'护理执行新增按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:execution:add');
-- 护理执行 - 编辑
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)
SELECT '护理执行修改',
(SELECT menu_id FROM sys_menu WHERE menu_name = '住院护士站' AND menu_type IN ('M', 'C') LIMIT 1),
53, '#', '', 'F', '0', '0',
'nursing:execution:edit', '#', 'admin', NOW(), 'admin', NOW(),
'护理执行修改按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:execution:edit');
-- ============================================================
-- 第三部分创建缺失的护理通用按钮级菜单权限F类型
-- ============================================================
-- nursing:nursing:list
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)
SELECT '护理管理查询',
(SELECT menu_id FROM sys_menu WHERE menu_name = '住院护士站' AND menu_type IN ('M', 'C') LIMIT 1),
61, '#', '', 'F', '0', '0',
'nursing:nursing:list', '#', 'admin', NOW(), 'admin', NOW(),
'护理管理查询按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:nursing:list');
-- nursing:nursing:edit
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)
SELECT '护理管理修改',
(SELECT menu_id FROM sys_menu WHERE menu_name = '住院护士站' AND menu_type IN ('M', 'C') LIMIT 1),
62, '#', '', 'F', '0', '0',
'nursing:nursing:edit', '#', 'admin', NOW(), 'admin', NOW(),
'护理管理修改按钮'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'nursing:nursing:edit');
-- ============================================================
-- 第四部分:为护士角色授予所有护理相关权限
-- ============================================================
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.status = '0'
AND r.role_name IN ('护士', 'nurse', '护士长')
AND m.perms IN (
'nursing:record:list',
'nursing:record:add',
'nursing:record:edit',
'nursing:record:remove',
'nursing:execution:list',
'nursing:execution:add',
'nursing:execution:edit',
'nursing:nursing:list',
'nursing:nursing: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
);
-- ============================================================
-- 第五部分:为管理员角色授予新增的菜单权限
-- ============================================================
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id
FROM sys_menu m
WHERE m.perms IN (
'nursing:record:list',
'nursing:record:add',
'nursing:record:edit',
'nursing:record:remove',
'nursing:execution:list',
'nursing:execution:add',
'nursing:execution:edit',
'nursing:nursing:list',
'nursing:nursing:edit'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id
);
-- ============================================================
-- 第六部分:为医生角色也授予护理查看权限(支持跨角色查看)
-- ============================================================
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.status = '0'
AND r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '主任医师', '副主任医师')
AND m.perms IN (
'nursing:record:list',
'nursing:nursing: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
);
-- ============================================================
-- 完成提示
-- ============================================================
-- 执行完此脚本后,需要:
-- 1. 重启应用或调用 /system/menu/refreshCache 刷新菜单缓存
-- 2. 韦雪账号重新登录以加载最新权限

View File

@@ -0,0 +1,9 @@
-- Initialize update_time for all TCM syndromes to a default past timestamp so they don't sort before active ones in nulls-first ORDER BY
UPDATE cli_condition_definition
SET update_time = '2000-01-01 00:00:00'
WHERE source_enum = 6;
-- Set update_time for the 8 core TCM syndromes so that they sort at the top of the list
UPDATE cli_condition_definition
SET update_time = CURRENT_TIMESTAMP
WHERE source_enum = 6 AND name IN ('阴证', '阳证', '寒证', '热证', '虚证', '实证', '闭证', '脱证');

View File

@@ -0,0 +1,9 @@
-- 修复 电子健康卡 & 电子发票 的 parent_id 为 基础数据 (211)
UPDATE sys_menu
SET parent_id = (SELECT menu_id FROM sys_menu WHERE menu_name = '基础数据' AND menu_type = 'M' LIMIT 1)
WHERE menu_name IN ('电子健康卡', '电子发票') AND (parent_id IS NULL OR parent_id NOT IN (SELECT menu_id FROM sys_menu));
-- 修复 传染病报卡 的 parent_id 为 院感管理 (10001)
UPDATE sys_menu
SET parent_id = (SELECT menu_id FROM sys_menu WHERE menu_name = '院感管理' AND menu_type = 'M' LIMIT 1)
WHERE menu_name = '传染病报卡' AND (parent_id IS NULL OR parent_id NOT IN (SELECT menu_id FROM sys_menu));

View File

@@ -1,3 +1,3 @@
ALTER TABLE critical_value_handle_record ALTER COLUMN handler_id DROP NOT NULL; ALTER TABLE critical_value_handle_record ALTER COLUMN handler_id DROP NOT NULL;
ALTER TABLE critical_value_handle_record ADD COLUMN update_time TIMESTAMP; ALTER TABLE critical_value_handle_record ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
ALTER TABLE critical_value_handle_record ADD COLUMN update_by VARCHAR(64); ALTER TABLE critical_value_handle_record ADD COLUMN IF NOT EXISTS update_by VARCHAR(64);

View File

@@ -1,7 +1,7 @@
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark, delete_flag) 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 VALUES
('CDSS', 10001, 10, 'cdss', 'cdss/cdssAlerts/index', NULL, 1, 0, 'C', '0', '0', 'infection:cdss:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '临床决策支持', 0), ('CDSS', 10001, 10, 'cdss', 'cdss/cdssAlerts/index', 'C', '0', '0', 'infection:cdss:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '临床决策支持'),
('区域共享', 20081, 10, 'regionalshare', 'esbmanage/regionalshare/index', NULL, 1, 0, 'C', '0', '0', 'infection:regional:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '区域医疗信息共享', 0), ('区域共享', 20081, 10, 'regionalshare', 'esbmanage/regionalshare/index', 'C', '0', '0', 'infection:regional:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '区域医疗信息共享'),
('EMR数据仓库', 20201, 10, 'data-warehouse', 'emr/data-warehouse/index', NULL, 1, 0, 'C', '0', '0', 'infection:emr:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '病历数据仓库', 0), ('EMR数据仓库', 20201, 10, 'data-warehouse', 'emr/data-warehouse/index', 'C', '0', '0', 'infection:emr:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '病历数据仓库'),
('病案统计明细', 20051, 10, 'statistics-detail', 'mrhomepage/statistics-detail/index', NULL, 1, 0, 'C', '0', '0', 'infection:mrhomepage:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '病案统计明细', 0), ('病案统计明细', 20051, 10, 'statistics-detail', 'mrhomepage/statistics-detail/index', 'C', '0', '0', 'infection:mrhomepage:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '病案统计明细'),
('报表维度', 360, 10, 'ReportDimension', 'reportmanage/ReportDimension', NULL, 1, 0, 'C', '0', '0', 'infection:report:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '报表维度', 0); ('报表维度', 360, 10, 'ReportDimension', 'reportmanage/ReportDimension', 'C', '0', '0', 'infection:report:list', 'fa:', 'admin', CURRENT_TIMESTAMP, NULL, NULL, '报表维度');

View File

@@ -1,6 +1,19 @@
-- Create epidemic_report table if not exists
CREATE TABLE IF NOT EXISTS epidemic_report (
id BIGINT PRIMARY KEY,
patient_name VARCHAR(100),
id_card VARCHAR(20),
phone VARCHAR(20),
report_type VARCHAR(50),
report_time TIMESTAMP,
report_status VARCHAR(20),
create_time TIMESTAMP,
update_time TIMESTAMP
);
-- Add auto-screen fields to epidemic_report -- Add auto-screen fields to epidemic_report
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_result TEXT; ALTER TABLE epidemic_report ADD COLUMN IF NOT EXISTS screen_result TEXT;
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_time TIMESTAMP; ALTER TABLE epidemic_report ADD COLUMN IF NOT EXISTS screen_time TIMESTAMP;
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_level VARCHAR(20); ALTER TABLE epidemic_report ADD COLUMN IF NOT EXISTS screen_level VARCHAR(20);
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS address VARCHAR(500); ALTER TABLE epidemic_report ADD COLUMN IF NOT EXISTS address VARCHAR(500);
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(50); ALTER TABLE epidemic_report ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(50);

View File

@@ -1,11 +1,21 @@
UPDATE sys_menu SET perms = 'mrhomepage:mrhomepage:list' WHERE menu_name = '病案统计明细' AND perms = 'infection:mrhomepage:list'; UPDATE sys_menu SET perms = 'mrhomepage:mrhomepage:list' WHERE menu_name = '病案统计明细' AND perms = 'infection:mrhomepage:list';
UPDATE sys_menu SET perms = 'reportmanage:report:list' WHERE menu_name = '报表维度' AND perms = 'infection:report:list'; UPDATE sys_menu SET perms = 'reportmanage:report:list' WHERE menu_name = '报表维度' AND perms = 'infection:report:list';
INSERT INTO sys_role_menu (role_id, menu_id) VALUES INSERT INTO sys_role_menu (role_id, menu_id)
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = 'CDSS告警' LIMIT 1)), SELECT 1, menu_id FROM sys_menu WHERE menu_name = 'CDSS告警'
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = 'CDSS规则' LIMIT 1)), ON CONFLICT DO NOTHING;
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = '区域共享' LIMIT 1)), INSERT INTO sys_role_menu (role_id, menu_id)
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = 'EMR数据仓库' LIMIT 1)), SELECT 1, menu_id FROM sys_menu WHERE menu_name = 'CDSS规则'
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = '病案统计明细' LIMIT 1)), ON CONFLICT DO NOTHING;
(1, (SELECT menu_id FROM sys_menu WHERE menu_name = '报表维度' LIMIT 1)) INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '区域共享'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = 'EMR数据仓库'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '病案统计明细'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '报表维度'
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;

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