129 Commits

Author SHA1 Message Date
669d669422 refactor(home): 更新工作流任务API导入路径
- 将 '@/api/workflow/task' 导入路径更改为 '@/api/workflow/task.js'
2026-02-01 15:06:15 +08:00
98fe9f3301 feat(router): 添加医生工作站等功能模块路由配置
- 新增医生工作站路由,包含待写病历功能
- 添加全部功能模块路由,支持功能列表和配置页面
- 集成待办事项模块路由,完善工作流功能
- 配置相关API接口和服务类,实现用户配置管理
- 实现待写病历列表展示和相关业务逻辑
- 完善首页统计数据显示功能
2026-02-01 15:05:57 +08:00
6f7d723c6b Merge remote-tracking branch 'origin/develop' into develop 2026-02-01 14:51:14 +08:00
0a08088ada feat(menu): 添加菜单完整路径功能和待写病历管理
- 在SysMenu实体类中新增fullPath字段用于存储完整路径
- 实现buildMenuTreeWithFullPath方法构建带完整路径的菜单树
- 添加getMenuFullPath和generateFullPath服务方法获取和生成完整路径
- 在菜单控制器中增加获取完整路径的API接口
- 前端菜单组件显示完整路径并在新增修改时使用后端返回的路径
- 添加待写病历管理功能包括获取待写病历列表、数量统计和检查接口
- 在医生工作站界面集成待写病历选项卡和相关处理逻辑
- 更新首页统计数据接口路径并添加待写病历数量获取功能
- 重构首页快捷功能配置为动态从数据库获取用户自定义配置
- 优化菜单列表查询使用异步方式处理带完整路径的菜单数据
- 添加菜单完整路径的数据库映射配置和前端API调用支持
2026-02-01 14:50:22 +08:00
HuangShun
48309fcaa4 需求56 检查项目设置-》检查类型维护;在check_type表中增加一个parent_id字段用于父行与子行绑定;修改执行科室下拉字典的数据来源 2026-01-30 15:31:01 +08:00
wangjian963
28160e082c 收费工作站-门诊挂号:点击【确认】退号无响应 2026-01-30 12:02:37 +08:00
wangjian963
29ecfd90f2 修复收费工作站-门诊挂号:挂号类型字段内容重复显示 2026-01-30 11:10:36 +08:00
f690b78b18 上传文件至 md/需求/media 2026-01-29 22:39:47 +08:00
6f71c678bd 上传文件至 md/需求 2026-01-29 22:39:03 +08:00
1c781c1224 套餐设置套餐管理完善 2026-01-29 15:32:21 +08:00
HuangShun
638f853af6 需求17 门诊医生站-》患者列表,修复门诊医生站的初诊/复诊标识没有完成数据存储和显示 2026-01-29 14:59:07 +08:00
HuangShun
96a8f75aa1 修复Bug128 门诊管理-》门诊划价:【新增】诊疗项目在点击【签发】报错 2026-01-29 11:47:40 +08:00
15a6445e26 Merge remote-tracking branch 'origin/develop' into develop 2026-01-28 15:31:04 +08:00
0e4b0ad6fd feat(accomplishList): 设置默认按申请时间倒序排列
- 添加申请时间字段排序功能
- 设置默认排序方式为降序
- 优化控制台日志输出格式
- 移除多余的等号字符
2026-01-28 15:30:54 +08:00
HuangShun
7da461a9cb 需求56 检查项目设置-》检查类型维护;界面代码被误修,回滚提交 2026-01-28 15:00:03 +08:00
b0040bcd48 Merge remote-tracking branch 'origin/develop' into develop 2026-01-28 14:37:48 +08:00
fa5394cc35 feat(organization): 优化科室分类查询功能
- 修改 getOrganizationTree 方法参数,将 classEnum 字符串改为 classEnumList 列表
- 实现多选科室分类的精确匹配查询逻辑
- 添加分页查询时只显示未删除记录的过滤条件
- 更新控制器中对逗号分隔参数的解析逻辑
- 修复查询条件构造中的逻辑错误
- 配置 Lombok 注解处理器路径
- 重命名诊断服务方法名以提高可读性
- 修复医保模块中诊断查询方法调用
- 修复集合初始化语法错误
2026-01-28 14:36:25 +08:00
81cc8b08a0 Merge remote-tracking branch 'origin/develop' into develop 2026-01-28 14:29:29 +08:00
0cecf3bcad 套餐设置套餐管理完善 2026-01-28 14:26:41 +08:00
HuangShun
b8d7e3cdf1 修复125 【住院登记】的入院科室无法选择,入院病区也无法选择 2026-01-28 14:05:35 +08:00
weixin_45799331
df2a4c1694 Merge remote-tracking branch 'origin/develop' into develop 2026-01-28 12:12:13 +08:00
weixin_45799331
a6a4e0ed58 sse实时开发 微修 2026-01-28 12:11:50 +08:00
HuangShun
ba31371b6f 修复124 系统管理-》业务规则配置-》执行科室配置:项目名称显示数字 2026-01-28 09:38:00 +08:00
9bd5caaa1b Fix 住院工作站中,住院病历的入院记录不吻合问题 2026-01-27 18:41:29 +08:00
164e4a4b75 Merge remote-tracking branch 'origin/develop' into develop 2026-01-27 17:32:15 +08:00
4f0cc1a0c4 refactor(ui): 优化按钮样式和数据加载逻辑
- 将多个按钮组件从 type="text" 改为 link 属性,提升界面美观性
- 修复 PatientList 组件中姓名显示的文本截断功能
- 在住院记录模板中添加对 patientInfo 变化的监听,自动更新表单数据
- 优化打印机列表获取逻辑,添加连接状态检查和警告信息
- 移除不必要的防抖和重复请求防护逻辑,简化代码实现
- 修复多处组件中对 patientInfo 属性访问的安全性问题
- 优化病历数据加载时机,移除防抖包装直接调用加载函数
- 改进数据设置逻辑,避免覆盖未传入字段的原有值
- 调整组件属性定义,使 patientInfo 参数变为可选并设置默认值
- 优化患者切换时的组件重置和数据加载流程
2026-01-27 17:32:03 +08:00
HuangXinQuan
6dedb92b54 87 门诊医生站-》增加医生常用语权限管理 2026-01-27 17:10:33 +08:00
HuangShun
0f0dc70c7e 修复123 系统管理-》基础数据-》门诊号源管理:修改出诊医生字段内容未保存成功 2026-01-27 16:20:09 +08:00
HuangShun
acfce391dc 需求17 门诊医生站-》患者列表;从adm_encounter表中查询到first_enum字段用以判断初复诊 2026-01-27 15:39:04 +08:00
weixin_45799331
b0f2eabf6b sse实时开发 2026-01-27 13:31:03 +08:00
weixin_45799331
c5db404290 88 分诊排队管理-》科室叫号显示屏 表triage_queue_item中添加了联合索引queue_date,organization_id,tenant_id,queue_order。添加了room_no,practitioner_id字段。新增的实体类刚刚没提交 2026-01-27 13:28:44 +08:00
weixin_45799331
c4c3073be0 88 分诊排队管理-》科室叫号显示屏 表triage_queue_item中添加了联合索引queue_date,organization_id,tenant_id,queue_order。添加了room_no,practitioner_id字段。 2026-01-27 11:15:15 +08:00
41494ebf7c 诊疗下面没有项目的代码冲突 2026-01-27 11:05:43 +08:00
HuangShun
4de4d9099e 修复122 门诊医生站-》医嘱TAB:点击选中项目后查询框自动关闭 2026-01-27 10:56:25 +08:00
497af01f9b chore(config): 调整应用日志级别配置
- 将 com.openhis 包的日志级别从 debug 调整为 info
- 将 com.baomidou.mybatisplus 的日志级别从 debug 调整为 info
- 将 com.openhis.web.regdoctorstation.mapper 的日志级别从 debug 调整为 info
- 将 org.springframework.jdbc.core 的日志级别从 debug 调整为 info
- 将 com.alibaba.druid 的日志级别从 debug 调整为 info
- 将 com.alibaba.druid.sql 的日志级别从 debug 调整为 info
2026-01-27 10:53:37 +08:00
ffc1f29b80 chore(config): 更新开发环境数据库和Redis连接配置
- 修改PostgreSQL数据库连接地址从47.116.196.11到192.168.110.252
- 修改Redis服务器地址从47.116.196.11到192.168.110.252
- 修改Redis端口从26379调整为默认端口6379
2026-01-27 10:45:56 +08:00
86bca03b04 Backup local changes before resolving remote repository issue 2026-01-27 10:19:54 +08:00
11c2758289 test 2026-01-27 10:19:54 +08:00
802f845231 检验项目设置-套餐设置-套餐管理 2026-01-27 09:26:27 +08:00
HuangShun
ea5215a1b0 修复101 门诊医生站-》门诊病历配置了未显示 2026-01-26 16:05:41 +08:00
a9fb093d9c 上传文件至 md/需求/media 2026-01-26 15:12:39 +08:00
chenjinyang
f4bf064f08 Merge remote-tracking branch 'origin/develop' into develop 2026-01-26 13:50:02 +08:00
chenjinyang
4dd824d296 完成帮助中心的改造 2026-01-26 13:49:43 +08:00
3ab6c2d424 刪除测试上传md内容 2026-01-26 13:42:48 +08:00
12b2bf255c 上传文件至 md/需求/aaa 2026-01-26 13:41:20 +08:00
sindir
c878dc19d7 86门诊医生站-》西药方 2026-01-26 13:25:02 +08:00
sindir
c1efd84332 55门诊医生站-》中医:诊断字段 2026-01-26 13:21:45 +08:00
1616f66fc4 诊疗下没有项目功能完善 2026-01-26 11:59:13 +08:00
wangjian963
2df1ed645f Merge remote-tracking branch 'origin/develop' into develop 2026-01-26 11:08:59 +08:00
wangjian963
4f7fc1c09a 修改目录管理-诊疗目录-划价标记字段。 2026-01-26 11:08:46 +08:00
bd873f81d2 诊疗下没有项目功能完善 2026-01-26 10:10:42 +08:00
1975fda73c feat(common): 添加审计字段工具类和基础服务
- 实现AuditFieldUtil工具类,支持通过反射设置创建人、创建时间、更新人、更新时间字段
- 支持驼峰命名和下划线命名的字段格式
- 只有当字段值为null或空字符串时才进行设置,避免覆盖已有值
- 实现BaseService基类,自动在保存和更新操作时设置审计字段
- 继承MyBatis-Plus的ServiceImpl,提供save、saveBatch、updateById方法的审计字段自动设置
- 集成SecurityUtils获取当前登录用户信息用于设置操作人字段
2026-01-25 23:17:30 +08:00
ffce6f81c3 feat(core): 完善自动填充机制和时间格式化处理
- 替换 ServiceImpl 继承为 BaseService 以支持自动填充功能
- 在 HisBaseEntity 中添加 JsonFormat 注解统一时间格式化
- 重构 MybastisColumnsHandler 实现完整的自动填充逻辑,包括 createTime、updateTime、createBy、updateBy 和 tenantId 字段
- 添加详细的日志记录和异常处理机制
- 在 PractitionerAppServiceImpl 中增强租户ID和审计字段的设置逻辑
- 优化时间解析工具类 openhis.js 以正确处理 ISO 8601 格式时间字符串
- 更新数据库映射文件以支持下划线字段名映射
- 重构 SysUserServiceImpl 实现完整的审计字段自动填充机制
2026-01-25 23:13:04 +08:00
ca043de624 feat(inpatientDoctor): 添加住院医生模块本地病人信息状态管理
- 新增本地病人信息状态管理文件 localPatient.js
- 实现护士等级状态管理功能
- 添加选择患者信息本地状态管理
- 提供更新护士等级和患者信息的方法
2026-01-25 16:43:12 +08:00
054b51c63d Merge remote-tracking branch 'origin/develop' into develop 2026-01-25 16:42:23 +08:00
5cf2dd165c fix(core): 修复审计字段缺失和组件状态管理问题
- 在Account、ChargeItem、EncounterParticipant和Encounter服务中添加审计字段验证
- 确保tenantId、createBy和createTime字段在插入数据库前正确设置
- 修复EMR模块中删除模板API的导出问题
- 更新患者信息状态管理,统一使用localPatientInfo替换patientInfo
- 在EMR组件中实现防抖机制优化历史记录刷新性能
- 修复病历模板切换时的表单数据重置逻辑
- 在首页统计组件中使用markRaw包装图标组件
- 为住院记录模板添加默认表单数据结构
- 修复SVG患者图标路径错误
2026-01-25 16:41:19 +08:00
27b094744c 创建 md 下文件夹 2026-01-24 14:54:44 +08:00
wangjian963
55e3533600 Merge remote-tracking branch 'origin/develop' into develop 2026-01-23 17:11:51 +08:00
wangjian963
1522183432 门诊管理-》门诊划价:新增耗材点击【保存】报错的问题(设置租户id,创建者,创建时间) 2026-01-23 17:08:29 +08:00
6382741b71 上传文件至 md/需求 2026-01-23 16:49:43 +08:00
16c854d55f feat(router): 添加基础管理和发票管理路由配置
- 新增基础管理模块路由配置,包含发票管理子页面
- 配置动态路由中的发票管理组件路径
- 移除监控模块的路由配置
- 修复路由数组结构,移除多余的逗号
2026-01-23 16:17:00 +08:00
73617e1b0f Merge remote-tracking branch 'origin/develop' into develop 2026-01-23 16:13:57 +08:00
abd5bd9f2f feat(system): 添加菜单显示状态控制功能并完善租户ID设置
- 在MetaVo中添加visible字段用于控制菜单显示状态
- 修改SysMenuServiceImpl中的路由构建逻辑,传递visible信息到前端
- 更新SidebarItem.vue组件,根据visible属性控制菜单项显示
- 在多个医嘱管理相关服务类中显式设置租户ID以确保多租户隔离
- 调整字典管理相关路由配置,优化页面跳转路径
- 在菜单管理界面添加显示状态查询和表格列展示功能
2026-01-23 16:12:56 +08:00
HuangShun
9000d66c0c 修复107 系统管理-》基础数据-》科室管理:科室分类筛选条件无效中搜索条件清空问题 2026-01-23 14:06:07 +08:00
huhuihua
61be9ff552 46 门诊医生站-》开立诊断:优化 修改数据库中一些字段不能为null 2026-01-23 13:58:33 +08:00
huhuihua
9408cf6c2d Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-01-23 13:58:25 +08:00
HuangShun
66c70a2b4a 修复103 门诊医生站-》药品医嘱开立内容重复/【确认】无响应 2026-01-23 13:28:17 +08:00
f6d9321f95 代码回滚 2026-01-23 11:08:54 +08:00
ccff9a7246 fix(hospitalizationEmr): 解决病历模板切换时数据显示残留问题
- 在切换模板前重置表单数据,避免显示之前的数据
- 先清空当前组件再设置新组件,确保组件完全重新渲染
- 使用 nextTick 确保 DOM 更新后再设置新组件
- 添加 JSON 解析异常处理,解析失败时清空组件数据
- 当没有历史记录或加载出错时清空表单数据
- 患者信息变化时重置当前组件和表单数据
- 没有患者信息时也重置组件和表单数据
2026-01-22 22:36:54 +08:00
2884f610f5 refactor(pharmacy): 激活药品仓库各类订单的供应分类选项
- 激活损益订单中的通用损益、盘点损益和制剂消耗分类选项
- 更新采购订单中的库存供应和非库存供应分类选项
- 激活退货订单中的普通分类选项
- 激活退仓订单中的普通分类选项
- 激活入库订单中的外购药品、自制药品、代销药品、其他药品和赠送药品入库分类选项
- 激活出库订单中的院内出库、院外出库和其他出库分类选项
- 激活盘点订单中的普通盘点和月度盘点分类选项
- 在枚举类中启用所有被注释的供应分类并完善文档注释
2026-01-22 22:24:22 +08:00
wangjian963
035738f990 修改库房管理-》采购管理-》采购入库:仓库字段值为中心耗材库在采购管理未显示的问题,修改了采购入库仓库字段值的布局样式,修改了在批量保存入库业务中添加申请人等核心数据数据,
修改了获取入库数据的查询SQL语句。
2026-01-22 19:38:58 +08:00
sindir
d0c6f57f6b 解决了无法接诊的问题 2026-01-22 16:37:20 +08:00
huhuihua
58c1e02415 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-01-22 15:58:55 +08:00
huhuihua
1e459b8883 46 门诊医生站-》开立诊断:优化 2026-01-22 15:51:37 +08:00
huabuweixin
0d57e984a6 76 门诊预约挂号 2026-01-22 15:09:52 +08:00
4450e3cc50 上传文件至 md/需求 2026-01-22 15:08:57 +08:00
wangjian963
902ee0587e 修改新增耗材“一次性静脉采血器” 点击保存categoryEnum为空值的问题。 2026-01-22 14:13:10 +08:00
49550fcc2e 诊疗下面没有诊疗项目 2026-01-22 14:03:38 +08:00
sindir
1dd7ee3428 90,分诊排队管理-》医生叫号界面 2026-01-22 12:14:01 +08:00
wangjian963
8dff5d466a Merge remote-tracking branch 'origin/develop' into develop 2026-01-22 09:22:22 +08:00
wangjian963
19ada4ace9 移除调用字典接口功能 2026-01-22 09:20:35 +08:00
c92ff38133 Merge remote-tracking branch 'origin/develop' into develop 2026-01-21 17:50:58 +08:00
1c07108e58 refactor(PatientList): 重构患者列表卡片布局结构
- 将原有的 header-top 和 header-bottom 结构替换为 info-row 统一布局
- 新增姓名、性别年龄、房间床号、住院号、保险类型等独立信息行
- 使用 el-text 组件优化姓名显示效果
- 为性别标签添加女性样式标识
- 调整床位信息展示方式,支持溢出省略
- 修改溢出属性从 hidden 为 visible 确保内容正常显示
- 优化标签样式和间距布局
- 隐藏已废弃的旧布局元素
- 调整 pending 患者列表的换行和对齐方式
2026-01-21 17:50:51 +08:00
weixin_45799331
34dd969cb4 门诊医生站-》医嘱:诊疗开具医嘱点击项目选择下拉框,获取时间过慢40多秒,解决了之后正常情况获取4秒 2026-01-21 17:27:28 +08:00
a0b546266d fix(mapper): 修复患者主信息查询的重复数据问题
- 在 getRegPatientMainInfo 查询中添加 DISTINCT ON 子句按 patient_id 去重
- 为分页功能添加 getRegPatientMainInfoCount 计数查询
- 修复 SQL 拼接条件的语法错误,将 ${ew.customSqlSegment} 替换为标准的动态 SQL 标签
- 调整字典标签查询逻辑,先查询指定表再回退到默认字典缓存
- 优化查询性能,避免不必要的数据重复和错误的 SQL 语法
- 添加缺失的 ORDER BY 子句确保查询结果的一致性
2026-01-21 16:22:05 +08:00
wangjian963
fc9ce6241e Merge remote-tracking branch 'origin/develop' into develop 2026-01-21 13:38:23 +08:00
wangjian963
5187ff1ae3 添加申请单表单患者信息自动填充姓名、就诊卡号、费用性质,以及申请医生和申请科室的输入框格式,
依据申请单和申请单明细表字段修改表单变量名。
2026-01-21 13:34:31 +08:00
huabuweixin
73b1d01044 修复
104 系统管理-》业务规则配置-》取药科室配置:开立科室字段内容显示不全
2026-01-21 10:02:04 +08:00
huabuweixin
b88ad89146 修复
107
系统管理-》基础数据-》科室管理:科室分类筛选条件无效
2026-01-20 18:02:41 +08:00
huabuweixin
de8039c513 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-01-20 18:01:46 +08:00
3464153d93 Merge remote-tracking branch 'origin/develop' into develop 2026-01-20 17:31:07 +08:00
6b868e378f feat(router): 添加新功能模块路由配置
- 添加套餐管理相关路由到公共路由,确保始终可用
- 新增基础管理模块,包含发票管理功能
- 添加系统监控模块,包含操作日志、登录日志、定时任务功能
- 新增系统工具模块,包含代码生成功能
- 扩展系统管理模块,增加租户用户、合同管理、用户角色授权等功能
- 添加字典数据、任务日志、代码生成编辑等隐藏路由
- 统一修复代码格式,调整对象属性间的空格格式
- 完善路由元信息配置,添加权限控制和图标支持
2026-01-20 17:30:42 +08:00
f6403fa059 Fix 禅道 108 系统管理-》业务规则配置-》执行科室配置:【添加新项目】按钮无法操作 2026-01-20 17:12:26 +08:00
sindir
bc92b9aa62 正确转到字典类型页面 2026-01-20 16:29:30 +08:00
sindir
46145ff636 正确转到字典类型页面 2026-01-20 16:19:54 +08:00
huhuihua
3ad32fac9f 46 门诊医生站补充 2026-01-20 10:02:48 +08:00
d1223aec07 挂号补单功能的完善 2026-01-20 09:31:37 +08:00
649f7bcf5b fix(database): 修复患者首页查询重复数据和关联查询问题
- 在ATDManageAppMapper.xml中添加DISTINCT关键字解决入院患者信息重复问题
- 重构PatientHomeAppMapper.xml中的复杂查询逻辑,使用子查询替代多层JOIN提高性能
- 修复vital signs查询中的字段关联错误,将base_service_req_id改为request_id
- 优化前端implementDepartment组件的数据加载逻辑,添加异步处理和错误捕获
- 为诊疗项目下拉框添加数据加载状态检查,防止空数据导致的界面异常
- 实现防抖机制和数据量限制,提升大数据量下的响应性能
- 添加并行数据加载,减少页面初始化时间
2026-01-20 08:24:07 +08:00
a3dce8de60 fix(inhospitalnurse): 优化住院护士站患者管理和床位分配功能
- 移除住院参与者更新失败时的异常返回,改为静默处理
- 更新床位分配提示信息,为用户提供更清晰的操作指导
- 实现实施科室下拉选择器的远程搜索功能,提升大数据量下的用户体验
- 添加节点切换时的未保存数据确认提醒,防止数据丢失
- 优化实施科室管理页面的选项过滤和加载状态管理
2026-01-19 23:18:38 +08:00
f81dd54f0c Merge remote-tracking branch 'origin/develop' into develop 2026-01-19 22:36:12 +08:00
803e4d0bb5 refactor(inhospitalnursestation): 优化入院护士站应用的数据库查询性能
- 将CTE查询重构为子查询以提高执行效率
- 为位置和医生查询添加LIMIT 1约束以减少数据量
- 移除不必要的GROUP BY子句以简化查询逻辑
- 在前端组件中实现异步数据加载和错误处理机制
- 使用可选链操作符处理空值情况避免报错
- 添加防抖机制解决单击双击冲突问题
- 优化患者列表和床位列表的并行加载逻辑
- 清理调试用的console.log语句并替换为有意义的信息
2026-01-19 22:36:04 +08:00
deebcde41f 依赖标记更新 2026-01-19 21:48:43 +08:00
095c43bbf3 修复医嘱下获取读取慢问题,同时解决系列字典表读取慢问题 2026-01-19 21:48:11 +08:00
aa3beb848b fix(patient): 修复患者信息新增和更新逻辑
- 修改handlePatientInfo方法中的患者对象初始化逻辑
- 添加患者ID存在时的查询验证机制
- 区分新增和更新操作分别调用不同的服务方法
- 移除重复的身份证号查询条件优化性能
- 统一患者信息保存和更新的操作流程
2026-01-19 21:41:05 +08:00
sindir
ae96bbd0bb 医生常用语管理 - 行内编辑改为弹窗编辑,优化编辑体验。新增编辑弹窗,点击编辑按钮弹窗回显数据,修改更便捷 2026-01-19 17:25:37 +08:00
sindir
1a2c444269 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-01-19 15:47:38 +08:00
sindir
9cba8fea12 完善医生常用语管理功能
### 后端修改
1. 实体类DoctorPhrase:字段/注解调整
2. 枚举完善:新增DoctorPhraseAppTypeEnum/DoctorPhraseBizTypeEnum,统一业务分类
3. 服务实现类:修正类名拼写(DoctorPhraesAppS → DoctorPhraseAppServiceImpl
### 前端修改
1. 常用语管理页面(doctorphrase/index.vue):
   业务分类硬编码替换为枚举(主诉/现病史/术前/术后/既往史)
2026-01-19 15:43:34 +08:00
huabuweixin
f11b7380a4 Merge remote-tracking branch 'origin/develop' into develop 2026-01-19 15:01:28 +08:00
da17b2b89c fix(inpatient): 修复就诊位置更新逻辑
- 修改就诊位置表更新方式,直接更新指定ID的记录
- 添加位置ID设置功能
- 使用updateById方法替代saveOrUpdate方法提高准确性
2026-01-19 14:22:39 +08:00
9e4a010a8d Merge remote-tracking branch 'origin/develop' into develop 2026-01-19 11:39:58 +08:00
7e76083c37 feat(doctorstation): 优化医生工作站处方列表功能
- 调整诊疗定义表结构,添加序号和服务范围字段
- 修改费用项目查询逻辑,使用INNER JOIN替代LEFT JOIN并优化排序
- 增加批处理批次大小从500到1000,提升查询性能
- 修复处方类型筛选中的诊疗和耗材顺序错误
- 优化处方行数据重置逻辑,避免残留数据问题
- 移除不必要的README标题元素
2026-01-19 11:39:29 +08:00
de105adbdc 上传文件至 md/需求 2026-01-19 11:28:29 +08:00
f3eeee7405 refactor(doctorstation): 优化医嘱基础列表组件性能和数据处理
- 实现虚拟滚动表格以提升大数据量渲染性能
- 添加数据缓存机制减少重复API请求
- 增强节流防抖功能优化搜索响应
- 重构数据过滤逻辑支持本地快速检索
- 添加加载状态提示改善用户体验
- 优化表格列宽度设置提升界面美观度
- 修复医保等级显示和价格获取逻辑
- 后端服务增加分批处理避免大量参数问题
- 添加空值安全检查防止运行时错误
- 统一数据结构处理药品耗材诊疗不同类型
2026-01-19 10:37:46 +08:00
97f04d0b15 fix(inpatient): 修复就诊位置更新逻辑
- 修改就诊位置表更新方式,直接更新指定ID的记录
- 添加位置ID设置功能
- 使用updateById方法替代saveOrUpdate方法提高准确性
2026-01-18 19:44:32 +08:00
5667e04d12 fix(organization): 修复组织查询中class_enum字段的多值匹配逻辑
- 将FIND_IN_SET函数替换为LIKE操作符组合,提高PostgreSQL兼容性
- 添加子查询包装器支持多种匹配模式
- 实现精确匹配、前缀匹配、后缀匹配和中间匹配四种查询方式
- 确保逗号分隔的枚举值能够正确匹配查询条件
- 优化查询性能并提升代码可读性
2026-01-18 14:06:44 +08:00
59157fda56 feat(organization): 支持科室分类多选功能
- 修改前端界面组件支持科室分类多选下拉框
- 更新后端接口参数类型从Integer改为String以支持多选值
- 实现FIND_IN_SET查询方式处理多选分类条件
- 添加parseClassEnumValues函数处理字符串或数组格式转换
- 在医院住院对话框中扩展筛选条件支持多选分类
- 优化错误信息显示逻辑提供更详细的错误提示
- 在患者列表组件中添加入院日期和主治医生信息展示
- 修复多个服务调用中科室分类参数传递的数据类型问题
2026-01-18 13:39:57 +08:00
2fe6d45ad4 fix(doctorstation): 修复诊断组件和住院办理功能的数据处理问题
- 修复诊断组件中el-popover模板语法错误,添加template标签
- 优化患者历史数据处理逻辑,确保数组类型安全并正确构建树形结构
- 完善住院办理流程中的组织机构数据获取和筛选逻辑
- 添加详细的控制台日志用于调试住院办理功能
- 修复办理住院按钮的禁用状态计算逻辑
- 优化患者卡片点击事件处理,确保就诊ID正确传递
- 添加诊断信息完整性检查并提供用户引导
- 修复检验申请组件中的监听器和暴露方法逻辑
2026-01-18 00:37:54 +08:00
982ee316f7 fix(doctorstation): 解决参数验证和数据获取问题
- 在前端api.js中添加encounterId参数验证,避免无效参数导致的错误
- 在后端服务层添加参数检查,当encounterId为空时返回空数据而非报错
- 修改控制器参数注解,将required设置为false以允许空值传递
- 优化住院办理流程中的错误处理和参数验证
- 改进检验申请单获取时的数据验证和错误提示
- 更新maven编译器插件版本并添加必要的模块参数
- 统一错误处理机制,提供更友好的用户提示信息
2026-01-17 16:07:57 +08:00
64c7db68e8 上传文件至 md/需求 2026-01-17 00:03:51 +08:00
cb6b6ced67 上传文件至 md/需求 2026-01-16 18:54:27 +08:00
itcast
8fcfb481c9 门诊医生站-》开立诊断 页面调整 2026-01-16 16:32:36 +08:00
itcast
be0514bc08 门诊医生站-》开立诊断 页面调整 2026-01-16 15:46:43 +08:00
ljj
2b3add4808 91 分诊排队管理-》门诊医生站:【完诊】患者队列状态的变化
68 检验项目设置-检验类型 / 检验项目设置-检验项目
2026-01-16 11:31:40 +08:00
b33cb6f9a1 测试合并v13 2026-01-15 17:04:46 +08:00
072e71b025 测试合并v12 2026-01-15 16:58:14 +08:00
47394de43c Merge pull request 'document' (#2) from document into develop
Reviewed-on: #2
2026-01-15 07:37:39 +00:00
f0f1dde6b6 测试合并 2026-01-15 07:37:39 +00:00
1ab1165697 测试合并 2026-01-15 07:37:39 +00:00
a8f1b1fdfa feat(doctorstation): 添加医嘱类型对应的药品分类筛选功能
- 在处方列表组件中根据医嘱类型自动设置categoryCode筛选条件
- 为西药类型设置categoryCode为'2'
- 为中成药类型设置categoryCode为'1'
- 为耗材和诊疗类型清空categoryCode筛选条件
- 更新基础医嘱列表组件以接收并应用categoryCode查询参数
- 实现医嘱类型改变时的联动筛选逻辑
2026-01-15 15:34:34 +08:00
350 changed files with 26879 additions and 8267 deletions

View File

@@ -0,0 +1,29 @@
---
name: full-stack-developer
description: Use this agent when you need comprehensive full-stack development assistance including frontend, backend, database design, API integration, deployment planning, and architectural decisions. This agent excels at analyzing complex technical requirements, designing scalable solutions, implementing clean code across multiple technologies, and providing expert guidance on best practices for modern web applications.
color: Blue
---
You are an elite full-stack software engineer with extensive experience across all layers of modern web application development. You possess deep expertise in frontend technologies (React, Vue, Angular, HTML/CSS, JavaScript/TypeScript), backend systems (Node.js, Python, Java, .NET, Ruby), databases (SQL and NoSQL), cloud platforms (AWS, Azure, GCP), and DevOps practices.
Your primary responsibilities include:
- Analyzing complex technical requirements and proposing optimal architectural solutions
- Writing clean, efficient, maintainable code across frontend and backend systems
- Designing robust APIs and data models
- Optimizing performance and ensuring security best practices
- Providing guidance on scalability, testing, and deployment strategies
- Troubleshooting complex issues spanning multiple technology stacks
When working on projects, you will:
1. First understand the complete scope and requirements before proposing solutions
2. Consider scalability, maintainability, and security implications of your designs
3. Follow industry best practices for code organization, documentation, and testing
4. Suggest appropriate technologies based on project requirements and constraints
5. Provide implementation details with proper error handling and edge case considerations
6. Recommend optimization strategies for performance and resource utilization
For frontend development, focus on responsive design, accessibility, state management, and user experience. For backend work, emphasize proper architecture patterns, database design, authentication/authorization, and API design principles. When addressing databases, consider normalization, indexing, query optimization, and data consistency.
Always prioritize clean code principles, proper separation of concerns, and modular design. When uncertain about requirements, ask clarifying questions to ensure your solution meets the actual needs. Provide code examples that demonstrate best practices and include comments where necessary for understanding.
In your responses, balance technical depth with practical applicability. Consider trade-offs between different approaches and explain your recommendations. When reviewing existing code, identify potential improvements related to performance, security, maintainability, and adherence to best practices.

View File

@@ -0,0 +1,32 @@
---
name: his-architect-developer
description: Use this agent when designing, developing, reviewing, or troubleshooting Hospital Information System (HIS) applications. This agent specializes in full-stack development for healthcare systems including database design, backend APIs, frontend interfaces, security compliance, and integration with medical devices or third-party systems.
color: Blue
---
You are an elite Healthcare Information System (HIS) Development Architect and Full-Stack Engineer with extensive experience in designing and implementing comprehensive hospital management solutions. You possess deep expertise in healthcare software architecture, regulatory compliance (HIPAA, FDA, etc.), medical data standards (HL7, FHIR), and secure system integration.
Your responsibilities include:
- Designing scalable, secure, and compliant HIS architectures
- Developing robust backend services and APIs
- Creating intuitive frontend interfaces for healthcare professionals
- Ensuring patient data security and privacy compliance
- Integrating with medical devices and external healthcare systems
- Optimizing system performance for high-availability environments
- Troubleshooting complex technical issues in healthcare IT infrastructure
When working on HIS projects, you will:
1. Prioritize patient safety and data security above all other considerations
2. Follow healthcare industry standards and regulations (HIPAA, HITECH, FDA guidelines)
3. Implement proper audit trails and logging for all patient-related operations
4. Design fail-safe mechanisms and disaster recovery procedures
5. Ensure accessibility compliance for users with varying technical expertise
6. Plan for high availability and minimal downtime in critical systems
For database design, focus on normalized schemas that support medical record integrity, implement proper indexing for fast queries, and ensure backup/recovery procedures meet healthcare requirements. When developing APIs, follow RESTful principles while incorporating OAuth 2.0 or similar authentication methods suitable for healthcare environments.
For frontend development, prioritize usability for healthcare workers who may be operating under stress, ensuring clear workflows and minimizing cognitive load. Implement responsive designs that work across various devices commonly used in healthcare settings.
Always consider scalability requirements for growing healthcare institutions and plan for future expansion. When troubleshooting, approach problems systematically considering the potential impact on patient care.
In your responses, provide detailed explanations of your architectural decisions, code implementations, and recommendations. Include relevant healthcare industry best practices and explain how your solutions address specific regulatory requirements.

View File

@@ -0,0 +1,33 @@
---
name: his-developer-architect
description: Use this agent when developing or architecting Hospital Information System (HIS) solutions using Vue3, Spring Boot, and MyBatis technologies. This agent specializes in healthcare system development, understanding medical workflows, patient management systems, and hospital operational processes. Ideal for designing secure, scalable, and compliant healthcare applications.
color: Blue
---
You are an elite Healthcare Information System (HIS) developer and architect with deep expertise in Vue3, Spring Boot, and MyBatis technologies. You specialize in building robust, secure, and scalable hospital management systems that handle critical healthcare operations including patient records, medical workflows, billing, pharmacy management, and administrative processes.
Your responsibilities include:
- Designing and implementing full-stack HIS solutions using Vue3 for modern, responsive frontends and Spring Boot with MyBatis for secure, efficient backends
- Ensuring compliance with healthcare industry standards such as HIPAA, HL7, FHIR, and local health data protection regulations
- Creating secure authentication and authorization systems for healthcare staff with role-based access controls
- Optimizing database designs for handling large volumes of sensitive patient data efficiently
- Implementing audit trails and logging systems required for healthcare environments
- Building integration capabilities between different hospital systems and external healthcare providers
Technical Guidelines:
- Follow Vue3 best practices using Composition API, TypeScript, and state management with Pinia
- Implement Spring Boot microservices architecture with proper security configurations (Spring Security)
- Use MyBatis effectively with proper transaction management and connection pooling
- Apply healthcare-specific design patterns and architectural principles
- Prioritize data integrity, security, and system reliability over performance optimizations when there's a conflict
- Implement comprehensive error handling and logging for healthcare regulatory compliance
When designing solutions, consider:
- Patient privacy and data security requirements
- High availability and disaster recovery needs for critical healthcare systems
- Scalability to handle varying loads during peak times
- Integration with existing hospital infrastructure and legacy systems
- User experience for healthcare professionals who need quick, reliable access to information
- Regulatory compliance and audit requirements specific to healthcare systems
You will provide detailed technical recommendations, code implementations, architectural diagrams, and best practices tailored specifically to healthcare information systems. Always prioritize patient safety and data security in your solutions.

6
.qwen/settings.json Normal file
View File

@@ -0,0 +1,6 @@
{
"tools": {
"approvalMode": "yolo"
},
"$version": 2
}

376
CODEBUDDY.md Normal file
View File

@@ -0,0 +1,376 @@
# CODEBUDDY.md
This file provides guidance to CodeBuddy Code when working with code in this repository.
## Project Overview
This is a comprehensive Hospital Information System (HIS) built with a Java Spring Boot backend and Vue 3 frontend.
- **Backend**: Java 17, Spring Boot 2.5.15, multi-module Maven architecture
- **Frontend**: Vue 3, Vite, Element Plus, Pinia state management
- **Database**: PostgreSQL (recommended v16.2)
- **Cache**: Redis
## Repository Structure
```
.
├── openhis-server-new/ # Backend multi-module Maven project
│ ├── openhis-application/ # Main application module with startup class
│ ├── openhis-domain/ # Business domain modules (administration, clinical, financial, etc.)
│ ├── openhis-common/ # Shared utilities and common code
│ ├── core-admin/ # Core administration module
│ ├── core-framework/ # Framework configuration and security
│ ├── core-system/ # System management module
│ ├── core-quartz/ # Scheduled tasks
│ ├── core-generator/ # Code generation utilities
│ ├── core-common/ # Core utilities
│ └── core-flowable/ # Workflow engine integration
└── openhis-ui-vue3/ # Vue 3 frontend
├── src/
│ ├── api/ # API service layer
│ ├── components/ # Reusable components
│ ├── router/ # Vue Router configuration
│ ├── store/ # Pinia state management
│ ├── utils/ # Utility functions
│ └── views/ # Page components
└── vite/ # Vite plugins configuration
```
## Build and Development Commands
### Backend (Java)
**Build the entire backend:**
```bash
cd openhis-server-new
mvn clean package -DskipTests
```
**Run the backend application (development):**
```bash
cd openhis-server-new/openhis-application
mvn spring-boot:run
```
**Alternative: Run directly from IDE:**
- Run the main method in `openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
**Start scripts:**
- Linux/Mac: `openhis-server-new/start.sh`
- Windows: `openhis-server-new/start.bat`
### Frontend (Vue 3)
**Install dependencies:**
```bash
cd openhis-ui-vue3
npm install
```
**Development server (with hot reload):**
```bash
npm run dev
```
- Runs on port 81 by default
- Proxies `/dev-api` requests to `http://localhost:18080/openhis`
**Build for production:**
```bash
npm run build:prod # Production build
npm run build:stage # Staging build
npm run build:test # Test environment build
npm run build:dev # Development build
npm run build:spug # Spug environment build
```
**Preview production build:**
```bash
npm run preview
```
## Architecture Overview
### Backend Architecture
The backend uses a multi-module Maven architecture with clear separation of concerns:
1. **openhis-application**: Entry point with `OpenHisApplication.java` (d:\his\openhis-server-new\openhis-application\src\main\java\com\openhis\OpenHisApplication.java:20)
- Scans `com.core` and `com.openhis` packages
- Configures async processing and YAML service configuration
- Runs on port 18080 with context path `/openhis`
2. **openhis-domain**: Business domain modules organized by medical functionality:
- `administration`: Administrative functions
- `appointmentmanage`: Appointment management
- `check`: Medical examination/checkup
- `clinical`: Clinical workflows
- `crosssystem`: Cross-system integration
- `document`: Document management
- `financial`: Financial/billing
- `lab`: Laboratory operations
- `medication`: Medication management
- `triageandqueuemanage`: Patient triage and queue management
- `yb`, `ybcatalog`, `ybelep`: Insurance (Yi Bao) integration
- `workflow`: Workflow management
- `jlau`, `nenu`: Additional domain modules
- `template`: Template management
3. **Core Modules** (com.core package):
- `core-system`: User, role, menu, and permission management
- `core-framework`: Security, exception handling, and framework configurations
- `core-common`: Shared utilities and base classes
- `core-quartz`: Scheduled task management
- `core-generator`: Code generation tools
- `core-flowable`: Workflow engine integration
- `core-admin`: Administrative functions
4. **openhis-common**: Domain-specific shared code and utilities under `com.openhis.common` package
**Key Technologies:**
- MyBatis-Plus 3.5.5 for ORM with enhanced CRUD operations
- Druid 1.2.27 connection pool with monitoring at `/druid/*`
- Flowable 6.8.0 for workflow management
- LiteFlow 2.12.4.1 for business rule orchestration
- Swagger 3.0.0 for API documentation
- JWT 0.9.1 for authentication
- Hutool 5.3.8 utility library
- Fastjson2 2.0.58 for JSON processing
- Pinyin4j 2.5.1 for Chinese character to Pinyin conversion
### Frontend Architecture
The frontend uses Vue 3 with composition API and modern tooling:
**Key Files:**
- Entry point: `openhis-ui-vue3/src/main.js`
- Router configuration: `openhis-ui-vue3/src/router/index.js`
- Store initialization: `openhis-ui-vue3/src/store/store.js`
- Vite configuration: `openhis-ui-vue3/vite.config.js`
**State Management:**
- Pinia for global state (replaces Vuex)
- Store modules: `app`, `dict`, `permission`, `settings`, `tagsView`, `user`
- Modules located in `openhis-ui-vue3/src/store/modules/`
**Routing:**
- Vue Router 4.3.0
- Two types of routes:
- `constantRoutes`: Public routes (login, 404, etc.)
- `dynamicRoutes`: Permission-based routes loaded dynamically
- Route meta fields: `title`, `icon`, `permissions`, `noCache`, `activeMenu`
**API Integration:**
- Axios 0.27.2 for HTTP requests
- Base API URL configured via environment variables (`VITE_APP_BASE_API`)
- Proxy configuration in vite.config.js for development
- `/dev-api``http://localhost:18080/openhis`
- `/ybplugin``http://localhost:5000` (insurance plugin)
- Request/response interceptors in `openhis-ui-vue3/src/utils/request.js`
- API service files organized by module in `openhis-ui-vue3/src/api/`
- `administration`, `appoinmentmanage`, `monitor`, `system`, `tool`
- Shared APIs: `home.js`, `login.js`, `menu.js`, `public.js`
**Component Architecture:**
- Element Plus as the UI framework
- Custom components in `openhis-ui-vue3/src/components/`
- Global components registered in main.js:
- Pagination, TreeSelect, FileUpload, ImageUpload, ImagePreview
- RightToolbar, Editor, DictTag
## Configuration
### Backend Configuration
**Main configuration file:** `openhis-server-new/openhis-application/src/main/resources/application.yml`
**Environment-specific profiles:**
- `application-dev.yml` - Development environment
- `application-test.yml` - Test environment
- `application-prd.yml` - Production environment
**Key configuration sections:**
- Database: PostgreSQL connection (URL, username, password, pool settings)
- Redis: Cache configuration (host, port, database index)
- Server: Port (18080), context path (/openhis), thread pool
- MyBatis-Plus: Mapper scanning (`com.core.**.domain,com.openhis.**.domain`), type aliases, logical delete
- Logging: Debug levels for com.openhis and com.baomidou.mybatisplus
- Swagger: API documentation at `/swagger-ui/index.html`
- Druid: Database monitoring at `/druid/*` (credentials: openhis/123456)
- Flowable: Workflow engine settings (schema update disabled)
- LiteFlow: Business rule configuration at `config/flow.el.xml`
- Token: JWT configuration (secret, expire time, header)
- File upload: Max file size (10MB), max request size (20MB)
### Frontend Configuration
**Environment files** (in `openhis-ui-vue3/`):
- `.env.dev` - Dev environment
- `.env.development` - Development environment variables
- `.env.staging` - Staging environment variables
- `.env.production` - Production environment variables
- `.env.test` - Test environment variables
- `.env.spug` - Spug environment variables
**Key environment variables:**
- `VITE_APP_TITLE`: Application title (e.g., "医院信息管理系统")
- `VITE_APP_BASE_API`: Backend API base URL (e.g., `/dev-api`)
- `VITE_APP_ENV`: Environment identifier
**Vite configuration:**
- Development server: Port 81, host true, auto-open
- Proxy: `/dev-api``http://localhost:18080/openhis`
- Path aliases: `@``./src`, `~``./`
## Database
**Initialization script:** `数据库初始话脚本请使用navicat16版本导入.sql` (located at repository root)
- Use Navicat version 16 to import
- Contains schema and initial demonstration data
**Database connection (dev environment):**
- Type: PostgreSQL
- URL: `jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=hisdev`
- Driver: `org.postgresql.Driver`
- Schema: `hisdev`
## Common Development Tasks
### Running Full Stack Locally
**Terminal 1 - Start backend:**
```bash
cd openhis-server-new/openhis-application
mvn spring-boot:run
```
**Terminal 2 - Start frontend:**
```bash
cd openhis-ui-vue3
npm run dev
```
Access the application at:
- Frontend: http://localhost:81
- Backend API: http://localhost:18080/openhis
- Swagger UI: http://localhost:18080/openhis/swagger-ui/index.html
- Druid monitoring: http://localhost:18080/openhis/druid/login.html
### Adding a New Backend Feature
1. Create domain entity in appropriate module under `openhis-domain/[module]/domain/`
2. Create mapper interface in `openhis-domain/[module]/mapper/`
3. Create mapper XML in `openhis-domain/[module]/resources/mapper/` (if custom SQL needed)
4. Create service interface and implementation in `openhis-domain/[module]/service/`
5. Create controller in `openhis-application/src/main/java/com/openhis/web/[module]/`
6. Add MyBatis-Plus annotations if using enhanced features
7. Test endpoints via Swagger UI at `http://localhost:18080/openhis/swagger-ui/index.html`
**Note:** Controllers are organized under `com.openhis.web` by business module (e.g., `web.administration`, `web.clinicalmanage`, `web.patientmanage`, etc.)
### Adding a New Frontend Page
1. Create Vue component in `openhis-ui-vue3/src/views/[module]/`
2. Add API service methods in `openhis-ui-vue3/src/api/`
3. Add route to `openhis-ui-vue3/src/router/index.js` (constantRoutes or dynamicRoutes)
4. Add Pinia store module if state management needed
5. Register global components if reusable
### Testing
**Backend:**
```bash
cd openhis-server-new
mvn test
```
**Frontend:**
- Run unit tests (if configured):
```bash
cd openhis-ui-vue3
npm test
```
## Key Patterns and Conventions
### Backend
- Package structure follows domain-driven design
- Service layer uses `@Service` annotation
- Controllers use `@RestController` with request mapping
- MyBatis-Plus base mapper: `BaseMapper<T>`
- Logical delete field: `validFlag` (1 = active, 0 = deleted)
- Use `@EnableAsync` for async processing
- JWT token stored in `Authorization` header
### Frontend
- Use Vue 3 Composition API (`<script setup>`)
- Element Plus components with Chinese locale (zhCn)
- API calls through centralized request utility in `src/utils/request.js`
- Route-based permission control
- Dictionary data through `useDict()` composable
- Global properties: `$download`, `$downloadGet`, `$parseTime`, `$resetForm`, `$handleTree`, `$formatDateStr`
- CSS in SCSS with global styles in `src/assets/styles/index.scss`
- Registered global components: DictTag, Pagination, TreeSelect, FileUpload, ImageUpload, ImagePreview, RightToolbar, Editor
- Hiprint plugin for printing functionality (window.hiprint)
## Important Files
### Backend
- Startup class: `openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
- Main config: `openhis-server-new/openhis-application/src/main/resources/application.yml`
- MyBatis config: `openhis-server-new/openhis-application/src/main/resources/mybatis/mybatis-config.xml`
- Parent POM: `openhis-server-new/pom.xml`
### Frontend
- Entry point: `openhis-ui-vue3/src/main.js`
- Router: `openhis-ui-vue3/src/router/index.js`
- Request utils: `openhis-ui-vue3/src/utils/request.js`
- Vite config: `openhis-ui-vue3/vite.config.js`
- Environment files: `openhis-ui-vue3/.env.*`
## External Integrations
- **PostgreSQL 42.2.27**: Primary database
- **MySQL Connector 9.4.0**: MySQL database support (alternative)
- **Redis**: Caching and session management
- **Flowable 6.8.0**: Workflow engine
- **LiteFlow 2.12.4.1**: Business rule engine
- **Swagger 3.0.0**: API documentation
- **Druid 1.2.27**: Database connection pool and monitoring
- **Element Plus 2.12.0**: Vue 3 UI component library
- **Pinia 2.2.0**: State management
- **Vite 5.0.4**: Build tool and dev server
- **Hutool 5.3.8**: Java utility library
- **Fastjson2 2.0.58**: JSON processing
- **Pinyin4j 2.5.1**: Chinese character to Pinyin conversion
- **iText 5.5.12**: PDF generation
- **Apache POI 4.1.2**: Excel file processing
## Additional Notes
### WebView Integration
- Frontend supports WebView environment (e.g., embedded in desktop applications)
- Chrome WebView integration with C# accessor (`chrome.webview.hostObjects.CSharpAccessor`)
- Mounted to Vue instance as `csAccessor` global property
### File Upload
- Backend upload path: Configured in `core.profile` property (default: `D:/home/uploadPath`)
- Max file size: 10MB per file, 20MB total request
- File upload component: `FileUpload` (global component)
### Authentication
- JWT token stored in `Authorization` header
- Token configuration: `token.secret`, `token.expireTime`, `token.header`
- Password lockout: 5 failed attempts, 10-minute lock time
### Logging
- Backend logs: Configured in `logback.xml`
- Debug logging enabled for: `com.openhis`, `com.baomidou.mybatisplus`, `com.alibaba.druid`
- Druid slow SQL threshold: 1000ms
### Code Generation
- Backend code generator: `core-generator` module
- Access via Swagger or `/tool/gen` route
- Uses Velocity templates in `openhis-application/src/main/resources/vm/`

View File

@@ -0,0 +1,223 @@
# MyBatis-Plus 自动填充处理器优化指南
## 概述
本文档说明如何优化 `MybastisColumnsHandler` 以确保所有实体的审计字段create_by、create_time、update_by、update_time能够正确自动填充。
## 问题背景
在 OpenHIS 系统中,当保存实体时可能会遇到以下错误:
```
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
```
这是因为数据库表中的审计字段设置了 NOT NULL 约束,但在某些情况下自动填充机制未能正确设置这些字段。
## 解决方案
通过优化 `MybastisColumnsHandler` 来确保总是使用当前登录用户的用户名填充 `create_by` 字段,使用当前时间填充 `create_time` 字段。
## 实施步骤
### 1. 替换现有处理器
`D:\his\openhis-server-new\core-framework\src\main\java\com\core\framework\handler\MybastisColumnsHandler.java` 文件替换为以下内容:
```java
package com.core.framework.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils;
import com.core.framework.config.TenantContext;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* MyBatis-Plus 自动填充处理器
* 用于自动填充创建时间和更新时间,以及创建人和更新人
*/
@Component
public class MybastisColumnsHandler implements MetaObjectHandler {
// 设置数据新增时的字段自动赋值规则
@Override
public void insertFill(MetaObject metaObject) {
// 填充创建时间
Date currentTime = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
// 获取当前登录用户名
String username = getCurrentUsername();
// 填充创建人
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "create_by", String.class, username);
// 确保tenantId被设置
Integer tenantId = getCurrentTenantId();
if (tenantId == null) {
throw new RuntimeException("无法获取当前租户ID请确保用户已登录或正确设置租户上下文");
}
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
}
// 设置数据修改时的字段自动赋值规则
@Override
public void updateFill(MetaObject metaObject) {
// 填充更新时间
Date currentTime = new Date();
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
// 填充更新人
String username = getCurrentUsername();
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
this.strictUpdateFill(metaObject, "update_by", String.class, username);
}
/**
* 获取当前登录用户名
* @return 当前登录用户名,如果无法获取则返回 "system"
*/
private String getCurrentUsername() {
String username = "system"; // 默认值
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
username = loginUser.getUsername();
} else {
// 尝试从请求中获取用户信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 可以在这里添加额外的逻辑来从请求中获取用户信息
// 例如从请求头、session等获取用户信息
}
}
} catch (Exception e) {
// 记录异常但不中断处理流程
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
// 可以考虑记录日志
}
return username;
}
/**
* 获取当前租户 ID
*/
private Integer getCurrentTenantId() {
Integer result = null;
// 首先尝试从线程局部变量中获取租户ID适用于定时任务等场景
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
if (threadLocalTenantId != null) {
result = threadLocalTenantId;
} else {
// 获取当前登录用户的租户ID优先使用SecurityUtils中储存的LoginUser的租户ID
try {
if (SecurityUtils.getAuthentication() != null) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
result = loginUser.getTenantId();
}
}
} catch (Exception e) {
// 记录异常但不中断处理
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
}
if (result == null) {
// 尝试从请求头中获取租户ID
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (request != null) {
// 从请求头获取租户ID假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
String tenantIdHeader = request.getHeader("X-Tenant-ID");
String requestMethodName = request.getHeader("Request-Method-Name");
// 登录
if ("login".equals(requestMethodName)) {
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
try {
result = Integer.parseInt(tenantIdHeader);
} catch (NumberFormatException e) {
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
}
}
}
}
}
}
}
// 如果仍然没有获取到租户ID返回默认值
if (result == null) {
System.out.println("警告: 未能获取当前租户ID将使用默认租户ID 1");
result = 1; // 默认租户ID
}
return result;
}
}
```
### 2. 验证处理器是否被正确扫描
确保在主应用类或配置类中启用了自动填充功能:
```java
@SpringBootApplication
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
@EnableTransactionManagement // 启用事务管理
public class OpenHisApplication {
public static void main(String[] args) {
SpringApplication.run(OpenHisApplication.class, args);
}
}
```
### 3. 测试验证
创建一个简单的测试来验证自动填充是否正常工作:
```java
@SpringBootTest
public class AuditFieldTest {
@Autowired
private PractitionerMapper practitionerMapper;
@Test
public void testAuditFieldsAutoFill() {
Practitioner practitioner = new Practitioner();
practitioner.setName("Test Practitioner");
// 保存实体
practitionerMapper.insert(practitioner);
// 验证审计字段是否被正确填充
assertThat(practitioner.getCreateBy()).isNotNull();
assertThat(practitioner.getCreateBy()).isNotEqualTo("");
assertThat(practitioner.getCreateTime()).isNotNull();
// 清理测试数据
practitionerMapper.deleteById(practitioner.getId());
}
}
```
## 注意事项
1. **安全上下文**:确保在调用保存方法时用户已登录,这样 `SecurityUtils.getLoginUser()` 才能返回有效的用户对象。
2. **异常处理**:处理器中包含了异常处理,如果无法获取当前用户,将使用 "system" 作为默认值。
3. **租户ID**处理器也处理租户ID的自动填充这对于多租户系统很重要。
4. **兼容性**处理器同时支持驼峰命名createBy和下划线命名create_by的字段以兼容不同的配置。
## 总结
通过优化 `MybastisColumnsHandler`,我们可以确保所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误,同时保持数据完整性和审计跟踪功能。

184
QWEN.md Normal file
View File

@@ -0,0 +1,184 @@
# Qwen Code Context for HIS (Hospital Information System)
## Project Overview
This is a comprehensive Hospital Information System (HIS) called OpenHIS, built with a Java Spring Boot backend and Vue 3 frontend. The system is designed to manage hospital operations including patient management, appointments, clinical workflows, billing, and administrative tasks.
### Technology Stack
**Backend:**
- Java 17
- Spring Boot 2.5.15
- PostgreSQL (recommended v16.2)
- Redis
- MyBatis-Plus 3.5.5 for ORM
- Druid 1.2.27 for database connection pooling
- Flowable 6.8.0 for workflow management
- LiteFlow 2.12.4.1 for business rule orchestration
- Swagger 3.0.0 for API documentation
- JWT 0.9.1 for authentication
**Frontend:**
- Vue 3 with Composition API
- Vite 5.0.4 as build tool
- Element Plus 2.12.0 as UI component library
- Pinia 2.2.0 for state management
- Axios 0.27.2 for HTTP requests
- Sass for styling
## Repository Structure
```
.
├── openhis-server-new/ # Backend multi-module Maven project
│ ├── openhis-application/ # Main application module with startup class
│ ├── openhis-domain/ # Business domain modules (administration, clinical, financial, etc.)
│ ├── openhis-common/ # Shared utilities and common code
│ ├── core-admin/ # Core administration module
│ ├── core-framework/ # Framework configuration and security
│ ├── core-system/ # System management module
│ ├── core-quartz/ # Scheduled tasks
│ ├── core-generator/ # Code generation utilities
│ ├── core-common/ # Core utilities
│ └── core-flowable/ # Workflow engine integration
├── openhis-ui-vue3/ # Vue 3 frontend
│ ├── src/
│ │ ├── api/ # API service layer
│ │ ├── components/ # Reusable components
│ │ ├── router/ # Vue Router configuration
│ │ ├── store/ # Pinia state management
│ │ ├── utils/ # Utility functions
│ │ └── views/ # Page components
│ └── vite/ # Vite plugins configuration
├── sql/ # Database scripts
├── 发版记录/ # Release records
└── 迁移记录-DB变更记录/ # Database migration records
```
## Building and Running
### Backend Setup
1. **Prerequisites:**
- JDK 17 (required)
- PostgreSQL v16.2 (required)
- Redis (stable version)
2. **Database Setup:**
- Import the database initialization script using Navicat 16 or later
- Script location: `sql/20251224init脚本(使用Navicat Premium 17导入).sql`
- Configure database connection in `application.yml` or `application-dev.yml`
3. **Build and Run:**
```bash
cd openhis-server-new
mvn clean package -DskipTests
cd openhis-application
mvn spring-boot:run
```
Or run directly from IDE by executing `OpenHisApplication.java`
### Frontend Setup
1. **Prerequisites:**
- Node.js v16.15 (recommended)
2. **Installation and Run:**
```bash
cd openhis-ui-vue3
npm install
npm run dev
```
3. **Access the application:**
- Frontend: http://localhost:81
- Backend API: http://localhost:18080/openhis
- Swagger UI: http://localhost:18080/openhis/swagger-ui/index.html
## Development Conventions
### Backend Architecture
The backend follows a multi-module Maven architecture with clear separation of concerns:
1. **openhis-application**: Entry point with `OpenHisApplication.java`
- Scans `com.core` and `com.openhis` packages
- Configured to run on port 18080 with context path `/openhis`
2. **openhis-domain**: Business domain modules organized by medical functionality:
- `administration`: Administrative functions
- `appointmentmanage`: Appointment management
- `check`: Medical examination/checkup
- `clinical`: Clinical workflows
- `crosssystem`: Cross-system integration
- `document`: Document management
- `financial`: Financial/billing
- `lab`: Laboratory operations
- `medication`: Medication management
- `triageandqueuemanage`: Patient triage and queue management
- `yb`, `ybcatalog`, `ybelep`: Insurance (Yi Bao) integration
- `workflow`: Workflow management
3. **Core Modules** (com.core package):
- `core-system`: User, role, menu, and permission management
- `core-framework`: Security, exception handling, and framework configurations
- `core-common`: Shared utilities and base classes
- `core-quartz`: Scheduled task management
- `core-generator`: Code generation tools
- `core-flowable`: Workflow engine integration
- `core-admin`: Administrative functions
### Frontend Architecture
The frontend uses Vue 3 with composition API and modern tooling:
1. **State Management:** Pinia for global state with modules for app, dict, permission, settings, tagsView, and user
2. **Routing:** Vue Router 4.3.0 with public routes and dynamic permission-based routes
3. **API Integration:** Axios with request/response interceptors and API services organized by module
4. **Component Architecture:** Element Plus as UI framework with custom components in `src/components/`
## Key Configuration Files
### Backend Configuration
- Main config: `openhis-server-new/openhis-application/src/main/resources/application.yml`
- Environment-specific: `application-dev.yml`, `application-test.yml`, `application-prd.yml`
- Database connection settings, Redis configuration, server settings, and MyBatis-Plus configuration
### Frontend Configuration
- Environment files: `.env.*` in `openhis-ui-vue3/`
- Vite configuration: `vite.config.js`
- Main entry: `src/main.js`
- Router: `src/router/index.js`
## Common Development Tasks
### Adding a New Backend Feature
1. Create domain entity in appropriate module under `openhis-domain/[module]/domain/`
2. Create mapper interface in `openhis-domain/[module]/mapper/`
3. Create service interface and implementation in `openhis-domain/[module]/service/`
4. Create controller in `openhis-application/src/main/java/com/openhis/web/[module]/`
5. Test endpoints via Swagger UI
### Adding a New Frontend Page
1. Create Vue component in `openhis-ui-vue3/src/views/[module]/`
2. Add API service methods in `openhis-ui-vue3/src/api/`
3. Add route to `openhis-ui-vue3/src/router/index.js`
4. Add Pinia store module if state management needed
## Important Notes
- The system uses logical deletion with a `validFlag` field (1 = active, 0 = deleted)
- JWT tokens are stored in the `Authorization` header
- The system supports WebView environments with C# accessor integration
- File uploads are configured with max 10MB per file and 20MB total request size
- Password lockout occurs after 5 failed attempts with a 10-minute lock time
- The system includes a code generator accessible via `/tool/gen` route
- Printing functionality is implemented using the hiprint plugin

View File

@@ -0,0 +1,202 @@
# OpenHIS 系统审计字段填充最佳实践
## 概述
本文档介绍如何在 OpenHIS 系统中确保所有实体的审计字段create_by、create_time、update_by、update_time能够正确自动填充。
## 自动填充机制
### 1. 基础实体类
所有需要审计字段的实体类都应该继承自 `HisBaseEntity`
```java
import com.core.common.core.domain.HisBaseEntity;
@Data
@TableName("adm_practitioner")
public class Practitioner extends HisBaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
// 其他业务字段...
}
```
### 2. 自动填充处理器
系统使用 `MybastisColumnsHandler` 来自动填充审计字段:
```java
@Component
public class MybastisColumnsHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 填充创建时间和创建人
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
String username = getCurrentUsername(); // 获取当前用户名
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "create_by", String.class, username);
}
@Override
public void updateFill(MetaObject metaObject) {
// 填充更新时间和更新人
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "update_time", Date.class, new Date());
String username = getCurrentUsername(); // 获取当前用户名
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
this.strictUpdateFill(metaObject, "update_by", String.class, username);
}
private String getCurrentUsername() {
String username = "system";
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
username = loginUser.getUsername();
}
} catch (Exception ignored) {
}
return username;
}
}
```
## 确保自动填充正常工作的要点
### 1. 检查实体类继承关系
确保所有实体类都正确继承了 `HisBaseEntity`
```java
// 正确的做法
public class Practitioner extends HisBaseEntity { ... }
// 如果不能继承 HisBaseEntity则需要手动添加审计字段
public class CustomEntity {
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
@TableField(fill = FieldFill.UPDATE)
private Date updateTime;
}
```
### 2. 验证安全上下文
确保在执行数据库操作时有有效的安全上下文:
```java
@Service
public class PractitionerService {
public void savePractitioner(Practitioner practitioner) {
// 确保调用此方法时用户已登录
// SecurityUtils.getLoginUser() 应该能返回有效的 LoginUser 对象
// MyBatis-Plus 会在保存时自动调用 MybastisColumnsHandler
practitionerMapper.insert(practitioner);
}
}
```
### 3. 检查配置
确保自动填充处理器被正确配置:
```yaml
# application.yml
mybatis-plus:
global-config:
db-config:
# 其他配置...
configuration:
# 其他配置...
```
### 4. 手动填充(特殊情况)
在某些特殊情况下,如果自动填充不工作,可以手动设置:
```java
@Service
public class PractitionerService {
public void savePractitionerManually(Practitioner practitioner) {
// 手动设置审计字段
Date now = new Date();
String currentUser = getCurrentUsername();
practitioner.setCreateTime(now);
practitioner.setCreateBy(currentUser);
practitioner.setUpdateTime(now);
practitioner.setUpdateBy(currentUser);
practitionerMapper.insert(practitioner);
}
}
```
## 常见问题及解决方案
### 问题1自动填充不生效
**原因:**
- 实体类没有继承 `HisBaseEntity`
- `MybastisColumnsHandler` 没有被Spring管理缺少@Component注解
- 没有有效的安全上下文
**解决方案:**
- 确保实体类继承 `HisBaseEntity`
- 检查 `MybastisColumnsHandler` 是否有 `@Component` 注解
- 确保在调用保存方法时用户已登录
### 问题2获取不到当前用户
**原因:**
- 用户未登录
- 安全上下文配置错误
**解决方案:**
- 在调用保存方法前确保用户已登录
- 检查安全配置是否正确
### 问题3批量操作时审计字段未填充
**原因:**
- 批量操作可能绕过了自动填充机制
**解决方案:**
- 对于批量操作,手动设置审计字段
- 或者使用 MyBatis-Plus 的批量操作方法,确保它们支持自动填充
## 测试验证
创建一个简单的测试来验证自动填充是否正常工作:
```java
@SpringBootTest
public class AuditFieldTest {
@Autowired
private PractitionerMapper practitionerMapper;
@Test
public void testAuditFieldsAutoFill() {
Practitioner practitioner = new Practitioner();
practitioner.setName("Test Practitioner");
// 保存实体
practitionerMapper.insert(practitioner);
// 验证审计字段是否被正确填充
assertThat(practitioner.getCreateBy()).isNotNull();
assertThat(practitioner.getCreateTime()).isNotNull();
// 清理测试数据
practitionerMapper.deleteById(practitioner.getId());
}
}
```
## 总结
通过遵循以上最佳实践,可以确保 OpenHIS 系统中的所有实体在保存时都能正确填充审计字段,避免因缺少这些字段而引发的数据库约束错误。

113
audit_field_solution.md Normal file
View File

@@ -0,0 +1,113 @@
# 关于数据库审计字段create_by, create_time等的处理方案
## 问题描述
在使用OpenHIS系统时可能会遇到如下错误
```
org.postgresql.util.PSQLException: ERROR: null value in column "create_by" of relation "adm_practitioner" violates not-null constraint
```
## 问题分析
1. 数据库表中的审计字段如create_by, create_time设置了NOT NULL约束
2. 应用程序层面使用了MyBatis-Plus的自动填充功能来设置这些字段
3. 当自动填充机制失效时,就会出现违反非空约束的错误
## 解决方案
### 方案一:修复自动填充机制(推荐)
系统已经实现了自动填充机制,位于 `MybastisColumnsHandler.java`
```java
// 设置数据新增时候的,字段自动赋值规则
@Override
public void insertFill(MetaObject metaObject) {
// 同时填充驼峰和下划线命名的字段,以兼容不同的配置
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "create_time", Date.class, new Date());
String username = "system";
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
username = loginUser.getUsername();
}
} catch (Exception ignored) {
}
// 使用 fillStrategy 确保即使字段为 null 也会被填充
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "create_by", String.class, username);
// 如果 strictInsertFill 没有生效,使用 setFieldValByName 强制设置
if (metaObject.hasGetter("createBy") && metaObject.getValue("createBy") == null) {
this.setFieldValByName("createBy", username, metaObject);
}
if (metaObject.hasGetter("create_by") && metaObject.getValue("create_by") == null) {
this.setFieldValByName("create_by", username, metaObject);
}
...
}
```
确保所有实体类都继承自 `HisBaseEntity``BaseEntity`,这样就能自动获得审计字段。
### 方案二:移除数据库约束(谨慎使用)
如果确实需要允许审计字段为NULL可以移除数据库约束
```sql
-- 移除 adm_practitioner 表中 create_by 列的 NOT NULL 约束
ALTER TABLE "public"."adm_practitioner"
ALTER COLUMN "create_by" DROP NOT NULL;
-- 同样处理 create_time 列(如果需要)
ALTER TABLE "public"."adm_practitioner"
ALTER COLUMN "create_time" DROP NOT NULL;
```
### 方案三:批量修复所有表的约束
如果多个表都存在这个问题,可以使用以下脚本:
```sql
-- 为所有表的审计字段移除NOT NULL约束
-- 注意:执行前请备份数据库!
-- 1. 检查所有包含审计字段的表
SELECT
table_name,
column_name,
is_nullable
FROM
information_schema.columns
WHERE
column_name IN ('create_by', 'create_time', 'update_by', 'update_time')
AND table_schema = 'public'
AND is_nullable = 'NO'; -- NO 表示 NOT NULL 约束
-- 2. 根据需要移除特定表的约束
-- 示例移除多个表的create_by约束
ALTER TABLE "public"."adm_practitioner" ALTER COLUMN "create_by" DROP NOT NULL;
ALTER TABLE "public"."adm_patient" ALTER COLUMN "create_by" DROP NOT NULL;
-- 添加更多表的处理...
```
## 最佳实践
### 1. 确保实体类继承基础类
所有实体类应继承 `HisBaseEntity``BaseEntity`
```java
@Data
@TableName("adm_practitioner")
public class Practitioner extends HisBaseEntity {
// 其他字段...
}
```
### 2. 检查安全上下文
确保在保存数据时有有效的安全上下文,这样自动填充处理器才能获取到当前用户信息。
### 3. 验证自动填充配置
确保 `MybastisColumnsHandler` 在Spring容器中被正确注册使用@Component注解)。
## 总结
- 推荐保持数据库中的NOT NULL约束确保数据完整性
- 依赖MyBatis-Plus的自动填充机制来设置审计字段
- 确保所有实体类继承基础实体类
- 在必要时才考虑移除数据库约束

38
debug_api_return.md Normal file
View File

@@ -0,0 +1,38 @@
# 检查后端API返回数据结构
## 问题分析
尽管我们更新了DTO和SQL查询前端仍然没有显示创建时间可能的原因
1. API响应中没有包含createTime字段
2. SQL查询没有正确返回createTime字段
3. 数据库中createTime字段本身为null
4. JSON序列化问题
## 检查步骤
### 1. 检查数据库中数据
首先检查数据库中sys_user表的createTime字段是否正确填充
```sql
SELECT user_id, user_name, nick_name, create_time
FROM sys_user
WHERE create_time IS NOT NULL
LIMIT 10;
```
### 2. 检查API端点
API端点是GET /base-data-manage/practitioner/user-practitioner-page
这个端点在PractitionerController中定义调用practitionerAppService.getUserPractitionerPage()
### 3. 检查SQL查询
在PractitionerAppMapper.xml中我们已经添加了createTime字段
```xml
T2.create_time
```
### 4. 验证DTO映射
UserAndPractitionerDto中已添加createTime字段
```java
private Date createTime;
```
### 5. 检查JSON序列化
检查是否有@JsonFormat注解或其他序列化配置问题

View File

@@ -0,0 +1,290 @@
# 深度排查 MyBatis-Plus 自动填充不生效问题
## 问题概述
尽管对 MyBatis-Plus 的自动填充处理器进行了多次优化和配置,但 `create_by``create_time` 字段仍然没有被自动填充。
## 深度排查步骤
### 1. 检查 AOP 代理是否生效
MyBatis-Plus 的自动填充功能依赖于 AOP 代理。如果实体类的方法被直接调用而非通过代理调用,自动填充可能不会生效。
### 2. 验证 Service 层实现
确保使用的是 MyBatis-Plus 提供的通用 Service 方法,而不是自定义的 SQL。
### 3. 检查 @TableField 注解配置
确认实体类中的字段注解配置正确。
### 4. 检查事务配置
某些事务配置可能会影响 AOP 代理的生效。
## 解决方案
### 方案一:在 Service 层手动设置审计字段
创建一个工具类来统一处理审计字段的设置:
```java
@Component
public class AuditFieldUtil {
public static void setCreateInfo(Object entity) {
if (entity == null) return;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser != null ? loginUser.getUsername() : "system";
Date currentTime = new Date();
// 使用反射设置字段值
Field createByField = getField(entity.getClass(), "createBy");
if (createByField != null) {
createByField.setAccessible(true);
if (createByField.get(entity) == null || "".equals(createByField.get(entity))) {
createByField.set(entity, username);
}
}
Field createTimeField = getField(entity.getClass(), "createTime");
if (createTimeField != null) {
createTimeField.setAccessible(true);
if (createTimeField.get(entity) == null) {
createTimeField.set(entity, currentTime);
}
}
// 处理下划线命名的字段
Field createByFieldUnderscore = getField(entity.getClass(), "create_by");
if (createByFieldUnderscore != null) {
createByFieldUnderscore.setAccessible(true);
if (createByFieldUnderscore.get(entity) == null || "".equals(createByFieldUnderscore.get(entity))) {
createByFieldUnderscore.set(entity, username);
}
}
Field createTimeFieldUnderscore = getField(entity.getClass(), "create_time");
if (createTimeFieldUnderscore != null) {
createTimeFieldUnderscore.setAccessible(true);
if (createTimeFieldUnderscore.get(entity) == null) {
createTimeFieldUnderscore.set(entity, currentTime);
}
}
} catch (Exception e) {
System.err.println("设置审计字段时发生异常: " + e.getMessage());
}
}
private static Field getField(Class<?> clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if (clazz.getSuperclass() != null) {
return getField(clazz.getSuperclass(), fieldName);
}
return null;
}
}
}
```
然后在 Service 实现中使用:
```java
@Service
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
implements IPractitionerService {
@Autowired
private AuditFieldUtil auditFieldUtil;
@Override
@Transactional
public boolean save(Practitioner entity) {
// 在保存前手动设置审计字段
auditFieldUtil.setCreateInfo(entity);
return super.save(entity);
}
@Override
@Transactional
public boolean saveBatch(Collection<Practitioner> entityList) {
entityList.forEach(auditFieldUtil::setCreateInfo);
return super.saveBatch(entityList);
}
}
```
### 方案二:重写 BaseMapper 方法
如果 Service 层的方法不起作用,可以直接在 Mapper 层处理:
```java
@Mapper
public interface PractitionerMapper extends BaseMapper<Practitioner> {
@Insert({
"<script>",
"INSERT INTO adm_practitioner (",
"id, active_flag, name, name_json, gender_enum, birth_date, deceased_date,",
"phone, address, address_province, address_city, address_district, address_street,",
"address_json, py_str, wb_str, bus_no, yb_no, user_id, tenant_id, delete_flag,",
"create_by, create_time, update_by, update_time, org_id,",
"phar_prac_cert_no, prsc_dr_cert_code, dr_profttl_code, kpd_code, signature, pos_no",
") VALUES (",
"#{id}, #{activeFlag}, #{name}, #{nameJson}, #{genderEnum}, #{birthDate}, #{deceasedDate},",
"#{phone}, #{address}, #{addressProvince}, #{addressCity}, #{addressDistrict}, #{addressStreet},",
"#{addressJson}, #{pyStr}, #{wbStr}, #{busNo}, #{ybNo}, #{userId}, #{tenantId}, #{deleteFlag},",
"#{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{orgId},",
"#{pharPracCertNo}, #{prscDrCertCode}, #{drProfttlCode}, #{kpdCode}, #{signature}, #{posNo}",
")",
"</script>"
})
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertWithAuditFields(Practitioner record);
}
```
### 方案三:使用 MyBatis 拦截器
创建一个 MyBatis 拦截器来自动填充字段:
```java
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class AuditFieldInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
String sqlCommandType = ms.getSqlCommandType().toString();
if ("INSERT".equals(sqlCommandType)) {
setCreateAuditFields(parameter);
} else if ("UPDATE".equals(sqlCommandType)) {
setUpdateAuditFields(parameter);
}
return invocation.proceed();
}
private void setCreateAuditFields(Object parameter) {
if (parameter == null) return;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser != null ? loginUser.getUsername() : "system";
Date currentTime = new Date();
// 设置 createBy 和 createTime
setFieldValue(parameter, "createBy", username);
setFieldValue(parameter, "create_time", username);
setFieldValue(parameter, "createTime", currentTime);
setFieldValue(parameter, "create_time", currentTime);
} catch (Exception e) {
e.printStackTrace();
}
}
private void setUpdateAuditFields(Object parameter) {
if (parameter == null) return;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser != null ? loginUser.getUsername() : "system";
Date currentTime = new Date();
// 设置 updateBy 和 updateTime
setFieldValue(parameter, "updateBy", username);
setFieldValue(parameter, "update_by", username);
setFieldValue(parameter, "updateTime", currentTime);
setFieldValue(parameter, "update_time", currentTime);
} catch (Exception e) {
e.printStackTrace();
}
}
private void setFieldValue(Object obj, String fieldName, Object value) {
try {
Field field = getField(obj.getClass(), fieldName);
if (field != null) {
field.setAccessible(true);
if (field.get(obj) == null) { // 只在原值为 null 时设置
field.set(obj, value);
}
}
} catch (Exception e) {
// 忽略无法设置的字段
}
}
private Field getField(Class<?> clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if (clazz.getSuperclass() != null) {
return getField(clazz.getSuperclass(), fieldName);
}
return null;
}
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {}
}
```
## 推荐实施顺序
1. 首先尝试方案一Service 层手动设置),这是最简单且可控的方式
2. 如果方案一不行尝试方案三MyBatis 拦截器),它在更底层起作用
3. 方案二是最后的选择,需要重写具体的插入逻辑
## 验证方法
创建一个测试来验证自动填充是否生效:
```java
@SpringBootTest
public class AuditFieldTest {
@Autowired
private IPractitionerService practitionerService;
@Test
public void testAuditFieldFill() {
Practitioner practitioner = new Practitioner();
practitioner.setName("Test Practitioner");
// 记录保存前的值
System.out.println("保存前 - createBy: " + practitioner.getCreateBy());
System.out.println("保存前 - createTime: " + practitioner.getCreateTime());
boolean success = practitionerService.save(practitioner);
// 从数据库重新查询以验证
Practitioner saved = practitionerService.getById(practitioner.getId());
System.out.println("保存后 - createBy: " + saved.getCreateBy());
System.out.println("保存后 - createTime: " + saved.getCreateTime());
Assertions.assertTrue(success);
Assertions.assertNotNull(saved.getCreateBy());
Assertions.assertNotNull(saved.getCreateTime());
}
}
```
通过这些方案,应该能够解决自动填充不生效的问题。

143
diagnose_autofill_issue.md Normal file
View File

@@ -0,0 +1,143 @@
# 诊断 MyBatis-Plus 自动填充问题
## 问题现象
尽管 `MybastisColumnsHandler` 已经实现并配置了自动填充功能,但 `create_by``create_time` 字段仍然没有被正确填充。
## 可能的原因及解决方案
### 1. 检查组件扫描配置
确保 `MybastisColumnsHandler` 类被Spring容器正确管理
```java
@Component // 确保这个注解存在
public class MybastisColumnsHandler implements MetaObjectHandler {
// ...
}
```
### 2. 检查包扫描路径
在主应用类中确保扫描到了处理器所在的包:
```java
@SpringBootApplication
@MapperScan("com.openhis.*.mapper") // 确保扫描到你的mapper
@ComponentScan(basePackages = {"com.core", "com.openhis"}) // 确保扫描到处理器
public class OpenHisApplication {
public static void main(String[] args) {
SpringApplication.run(OpenHisApplication.class, args);
}
}
```
### 3. 验证实体类配置
确保实体类正确继承了 `HisBaseEntity` 并且字段上有正确的注解:
```java
@Data
@TableName("adm_practitioner")
public class Practitioner extends HisBaseEntity {
// 不需要在子类中重复定义 createBy, createTime 等字段
// 因为它们已在 HisBaseEntity 中定义并带有 @TableField(fill = FieldFill.INSERT)
}
```
### 4. 检查安全上下文
自动填充处理器依赖于安全上下文来获取当前用户。确保在执行保存操作时用户已登录:
```java
// 在保存之前,确保用户已登录
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
// 用户未登录,可能需要手动设置审计字段
}
```
### 5. 手动测试自动填充
创建一个简单的测试来验证自动填充是否正常工作:
```java
@SpringBootTest
public class AutoFillTest {
@Autowired
private PractitionerMapper practitionerMapper;
@Test
public void testAutoFill() {
Practitioner practitioner = new Practitioner();
practitioner.setName("Test Practitioner");
// 检查在保存前字段是否为空
System.out.println("Before insert - createBy: " + practitioner.getCreateBy());
System.out.println("Before insert - createTime: " + practitioner.getCreateTime());
// 执行插入操作
int result = practitionerMapper.insert(practitioner);
// 检查保存后字段是否被填充
System.out.println("After insert - createBy: " + practitioner.getCreateBy());
System.out.println("After insert - createTime: " + practitioner.getCreateTime());
assertThat(result).isEqualTo(1);
assertThat(practitioner.getCreateBy()).isNotNull();
assertThat(practitioner.getCreateTime()).isNotNull();
}
}
```
### 6. 临时解决方案
如果自动填充仍然不工作,可以在服务层手动设置这些字段:
```java
@Service
public class PractitionerServiceImpl extends ServiceImpl<PractitionerMapper, Practitioner>
implements IPractitionerService {
@Override
public void savePractitioner(Practitioner practitioner) {
// 手动设置审计字段
if (practitioner.getCreateBy() == null || practitioner.getCreateBy().isEmpty()) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
practitioner.setCreateBy(loginUser.getUsername());
} else {
practitioner.setCreateBy("system"); // 默认值
}
}
if (practitioner.getCreateTime() == null) {
practitioner.setCreateTime(new Date());
}
// 执行保存操作
this.save(practitioner);
}
}
```
### 7. 检查 MyBatis-Plus 版本兼容性
确保使用的 MyBatis-Plus 版本与自动填充功能兼容。当前项目使用的是 3.5.5 版本,应该支持自动填充功能。
### 8. 调试自动填充处理器
`MybastisColumnsHandler` 中添加日志来调试是否被调用:
```java
@Override
public void insertFill(MetaObject metaObject) {
System.out.println("MybastisColumnsHandler.insertFill() called"); // 调试日志
Date currentTime = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
String username = getCurrentUsername();
System.out.println("Setting createBy to: " + username); // 调试日志
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "create_by", String.class, username);
// ... 其他代码
}
```
通过以上步骤,应该能够诊断并解决自动填充不工作的问题。

View File

@@ -0,0 +1,145 @@
package com.core.framework.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils;
import com.core.framework.config.TenantContext;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* MyBatis-Plus 自动填充处理器
* 用于自动填充创建时间和更新时间,以及创建人和更新人
*/
@Component
public class MybastisColumnsHandler implements MetaObjectHandler {
// 设置数据新增时的字段自动赋值规则
@Override
public void insertFill(MetaObject metaObject) {
// 填充创建时间
Date currentTime = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, currentTime);
this.strictInsertFill(metaObject, "create_time", Date.class, currentTime);
// 获取当前登录用户名
String username = getCurrentUsername();
// 填充创建人
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "create_by", String.class, username);
// 确保tenantId被设置
Integer tenantId = getCurrentTenantId();
if (tenantId == null) {
throw new RuntimeException("无法获取当前租户ID请确保用户已登录或正确设置租户上下文");
}
this.strictInsertFill(metaObject, "tenantId", Integer.class, tenantId);
this.strictInsertFill(metaObject, "tenant_id", Integer.class, tenantId);
}
// 设置数据修改时的字段自动赋值规则
@Override
public void updateFill(MetaObject metaObject) {
// 填充更新时间
Date currentTime = new Date();
this.strictUpdateFill(metaObject, "updateTime", Date.class, currentTime);
this.strictUpdateFill(metaObject, "update_time", Date.class, currentTime);
// 填充更新人
String username = getCurrentUsername();
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
this.strictUpdateFill(metaObject, "update_by", String.class, username);
}
/**
* 获取当前登录用户名
* @return 当前登录用户名,如果无法获取则返回 "system"
*/
private String getCurrentUsername() {
String username = "system"; // 默认值
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
username = loginUser.getUsername();
} else {
// 尝试从请求中获取用户信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 可以在这里添加额外的逻辑来从请求中获取用户信息
// 例如从请求头、session等获取用户信息
}
}
} catch (Exception e) {
// 记录异常但不中断处理流程
System.err.println("获取当前登录用户时发生异常: " + e.getMessage());
// 可以考虑记录日志
}
return username;
}
/**
* 获取当前租户 ID
*/
private Integer getCurrentTenantId() {
Integer result = null;
// 首先尝试从线程局部变量中获取租户ID适用于定时任务等场景
Integer threadLocalTenantId = TenantContext.getCurrentTenant();
if (threadLocalTenantId != null) {
result = threadLocalTenantId;
} else {
// 获取当前登录用户的租户ID优先使用SecurityUtils中储存的LoginUser的租户ID
try {
if (SecurityUtils.getAuthentication() != null) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
result = loginUser.getTenantId();
}
}
} catch (Exception e) {
// 记录异常但不中断处理
System.err.println("获取当前登录用户租户ID时发生异常: " + e.getMessage());
}
if (result == null) {
// 尝试从请求头中获取租户ID
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (request != null) {
// 从请求头获取租户ID假设header名称为"X-Tenant-ID" ; 登录接口前端把租户id放到请求头里
String tenantIdHeader = request.getHeader("X-Tenant-ID");
String requestMethodName = request.getHeader("Request-Method-Name");
// 登录
if ("login".equals(requestMethodName)) {
if (tenantIdHeader != null && !tenantIdHeader.isEmpty()) {
try {
result = Integer.parseInt(tenantIdHeader);
} catch (NumberFormatException e) {
System.err.println("解析请求头中的租户ID时发生异常: " + e.getMessage());
}
}
}
}
}
}
}
// 如果仍然没有获取到租户ID返回默认值
if (result == null) {
System.out.println("警告: 未能获取当前租户ID将使用默认租户ID 1");
result = 1; // 默认租户ID
}
return result;
}
}

13
fragment.java Normal file
View File

@@ -0,0 +1,13 @@
package com.openhis;
/**
* 示例类 - 引用 OpenHisApplication
*/
public class Fragment {
public static void main(String[] args) {
// 引用 OpenHisApplication
Class<?> applicationClass = com.openhis.OpenHisApplication.class;
System.out.println("Application class: " + applicationClass.getName());
}
}

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试合并1</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,273 @@
## 门诊手术中临时医嘱生成界面PRD文档
### 一、页面概述
**页面名称**:门诊手术中临时医嘱生成界面
**页面目标**:帮助麻醉医师在手术过程中快速生成临时医嘱,完成药品计费引用、医嘱预览和电子签名确认的全流程操作
**适用场景**:门诊手术过程中需要追加药品医嘱时使用
**页面类型**:表单页+数据展示页
**核心功能**
1. 患者手术信息展示
2. 已引用计费药品列表展示与汇总
3. 临时医嘱预览与编辑功能
4. 医师电子签名确认流程
5. 数据刷新与退出操作
**用户价值**:简化手术中医嘱生成流程,确保医嘱准确性,实现无纸化操作,提高手术室工作效率
原型图地址https://static.pm-ai.cn/prototype/20260122/e1d7f10b85e9efea543bf47bd6831600/index.html
**流程图:**
```mermaid
flowchart TD
Start(["Start"]) --> Enter["进入门诊手术中临时医嘱生成界面"]
Enter --> ShowBase["展示患者基本信息"]
ShowBase --> ShowQuoted["显示已引用计费药品列表"]
ShowQuoted --> ShowPreview["显示医嘱预览表格"]
ShowPreview --> UserOp{用户操作}
UserOp -- "引用计费" --> GetLatest{"获取最新计费药品数据\n获取成功?"}
GetLatest -- "否" --> ErrTip1["显示错误提示"]
GetLatest -- "是" --> UpdateTable["更新药品表格和汇总"]
UserOp -- "编辑" --> PopEdit["弹出医嘱编辑表单"]
PopEdit --> EditVal{"验证通过?"}
EditVal -- "否" --> ErrTip2["返回错误提示"]
EditVal -- "是" --> SaveClick{"点击保存?"}
SaveClick -- "是" --> GenTemp["生成临时药品医嘱"]
SaveClick -- "否" --> UserOp
GenTemp --> UpdatePreview["更新医嘱预览表格"]
UpdatePreview --> UpdateRecord["更新手术记录"]
UpdateRecord --> ShowResult["显示生成结果"]
UserOp -- "一键签名并生成医嘱" --> PopPwd["弹出账户密码输入框"]
PopPwd --> PopConfirm{"弹出确认对话框"}
PopConfirm -- "否" --> UserOp
PopConfirm -- "是" --> GenTemp
UserOp -- "刷新" --> Reload["重新加载界面数据"]
Reload --> ShowQuoted
UserOp -- "退出" --> ExitConfirm{"确认退出?"}
ExitConfirm -- "否" --> UserOp
ExitConfirm -- "是" --> ReturnUp["返回上级页面"]
ErrTip1 --> UserOp
ErrTip2 --> PopEdit
ShowResult --> UserOp
ReturnUp --> End([结束])
```
### 二、整体布局分析
**页面宽度**:自适应布局
**要区域划分**
1. 顶部信息区15%):患者基本信息+操作按钮区
2. 计费药品展示区35%):已引用计费药品表格+金额汇总
3. 医嘱预览区35%):待生成医嘱的预览表格
4. 签名确认区15%):医师签名信息+操作按钮
**布局特点**:上下分块布局,采用卡片式设计,主要区域间有明确分隔线
**响应式要求**768px以下时患者信息改为纵向排列操作按钮换行显示
### 三、页面区域详细描述
#### 1. 顶部信息区
**区域位置**:页面顶部
**区域尺寸**高度180px包含20px内边距
**区域功能**:展示患者基本信息+提供主要操作入口
**包含元素**
- **标题栏**
- 元素类型:标题文本
- 显示内容:“门诊术中临时医嘱”
- 样式特征白色文字1.5rem字号,居中显示,渐变蓝色背景
- **患者信息卡**
- 元素类型:信息展示区块
- 显示内容:患者姓名、就诊卡号、手术单号、科室、医师、角色
- 患者:样例值-张三
- 就诊卡号:样例值-202507010122
- 手术单号:样例值- S202507010135
- 科室: 样例值-手术室(OR101)-取值于手术安排的手术间号字段
- 医师:样例值-李麻(3015)
- 角色:样例值-麻醉医师
- 样式特征半透明白色背景圆角8px内部flex布局
- **操作按钮组**
- **[刷新按钮]**
- 元素类型:主要操作按钮
- 显示内容:↻ 刷新
- 交互行为:点击后重新加载当前界面的数据
- 样式特征:蓝色渐变背景,悬停有上浮效果
- **[引用计费按钮]**
- 元素类型:次要操作按钮
- 显示内容:引用计费
- 交互行为:点击后拉取当前患者最新计费药品的数据
#### 2. 计费药品展示区
**区域位置**:顶部信息区下方
**区域尺寸**高度约420px包含标题和表格
**区域功能**:展示待生成医嘱的计费药品清单
**包含元素**
- **表格标题**
- 显示内容:“一、已引用计费药品(待生成医嘱)”
- 样式特征1.2rem字号,底部边框线
- **药品数据表格**
**取值于门诊术中计费界面生成的药品计费数据adm_charge_item费用项管理、med_medication_request药品请求管理具体与系统实际业务数据为主。**
**(参考)关联字段adm_charge_item. encounter_id = med_medication_request. encounter_id and 就诊ID**
**adm_charge_item. service_table = 'med_medication_request' and --记录药品数据**
**adm_charge_item. bus_no = med_medication_request. bus_no -- adm_charge_item. bus_no的值之前多加了CI**
- 展示方式:斑马纹表格
- 数据字段:
- 序号:数字 - 自动生成
- 药品名称:文本 - 如"罗哌卡因注射液"
- 规格:文本 - 如"10ml"
- 数量:数字 - 可编辑
- 批号:文本 - 如"L240715"
- 单价:数字 - 如"38"
- 小计:数字 - 自动计算
- 医保:标签 - “甲/乙/自费”
- 样式特征:表头浅灰色背景,医保类型有颜色区分(蓝色=医保,绿色=自费)
- **金额汇总栏**
- 显示内容:
- 医保内金额(蓝色强调)
- 自费金额(绿色强调)
- 总计金额(红色强调)
- 位置:表格底部右对齐
#### 3. 医嘱预览区
**区域位置**:计费药品展示区下方
**区域尺寸**高度约420px包含标题和表格
**区域功能**:展示即将生成的药品医嘱
**包含元素**
\*生成门诊药品医嘱表相关的数据,满足**计费药品明细 ↔ 药品医嘱** 一一对应的要求。
可以对照参考:需结合门诊医生站开立药品医嘱时生成的药品医嘱表
- **表格标题**
- 显示内容:“二、临时医嘱预览(已生成)”
- **医嘱表格**
- 展示方式:斑马纹表格
- 数据字段:
- 序号:数字
- 医嘱名称:文本(取已引用计费药品的药品名称)
- 剂量:数字(自动计算=规格×数量)
- 单位:文本(根据药品类型自动判断)
- 用法:下拉选择(不可编辑)
- 频次:固定"临时"
- 执行时间:自动生成当前时间
- 操作:编辑/删除按钮
- 操作功能:
- 编辑:弹出表单修改剂量、用法等字段
- 删除:二次确认后移除该条医嘱
#### 4. 签名确认区
**区域位置**:页面底部
**区域尺寸**高度约180px
**区域功能**:完成医嘱确认和电子签名
**包含元素**
- **签名信息卡**
- 显示内容:医师姓名工号、签名状态、签名时间
- 样式特征:浅灰色背景,圆角边框
- **[一键签名按钮]**
- 元素类型:主要操作按钮
- 显示内容:“一键签名并生成医嘱”
- 交互行为:点击后弹出账户密码输入框
- 样式特征:绿色背景,悬停效果
- **[取消按钮]**
- 元素类型:次要操作按钮
- 显示内容:“取消”
- 交互行为:返回上级页面
### 四、交互功能详细说明
#### 1. 引用计费功能
**功能描述**:从术中计费药品获取患者当前最新的计费药品数据
**触发条件**:点击"引用计费"按钮
**操作流程**
1. 点击按钮获取患者当前最新的计费药品数据
2. 成功返回后更新药品表格数据
3. 自动计算并更新费用汇总
**反馈机制**:成功提示弹窗"已成功引用最新计费药品信息!"
**异常处理**:请求失败时显示错误提示“获取计费数据失败,请重试”,保留原数据
#### 2. 医嘱生成功能
**功能描述**:将计费药品转为正式医嘱
**触发条件**:点击"一键签名并生成医嘱"按钮
**操作流程**
1. 自动生成药品医嘱预览(带默认用法和剂量)
2. 弹出账户密码输入框
3. 验证通过后生成临时药品医嘱数据
4. 成功返回后显示生成结果
**数据转换规则**
- 剂量 = 规格数值 × 数量(如"10ml"×2 → 20ml
- 单位:根据药品名称自动判断(默认获取当前药品在《药品目录》维护剂量单位的值)
- 用法:根据药品名称自动判断(默认获取当前药品在《药品目录》维护用法的值,如果未维护默认空)
- 医嘱名称:取值药品名称
- 频次默认ST
- 执行时间:默认当前系统时间
#### 3. 医嘱编辑功能
**功能描述**:修改已生成的医嘱明细
**触发条件**:点击"编辑"按钮
**操作流程**
1. 弹出编辑表单(带当前值医嘱值)
2. 修改后点击保存更新表格
3. 自动重新计算相关字段得值
**字段限制**
- 剂量:必须为数字
- 用法:限定下拉选项,取值于字典管理:用药途径(用法)的值
- 频次:固定为"ST"不可编辑
### 五、数据结构说明
**关键数据字段**
| **字段名** | **说明** | **数据类型** | **示例值** | **是否必填** | **备注** |
|---------------|----------|--------------|--------------------|--------------|------------------|
| patientId | 患者ID | string | “202507010122” | 是 | 就诊卡号 |
| surgeryNo | 手术单号 | string | “S202507010135” | 是 | |
| medicineName | 药品名称 | string | “罗哌卡因注射液” | 是 | |
| spec | 规格 | string | “10ml” | 是 | 需包含数值和单位 |
| batchNo | 批号 | string | “L240715” | 是 | |
| insuranceType | 医保类型 | string | “乙” | 是 | 甲/乙/自费 |
| usage | 用法 | string | “静脉推注” | 是 | |
| execTime | 执行时间 | datetime | “2025-07-01 08:41” | 是 | 精确到分钟 |
### 六、开发实现要点
**样式规范**
- **主色调**\#4a90e2按钮/标题)
- **辅助色**\#5cb85c成功操作、\#e74c3c警告
- **字体规范**标题1.5rem/正文0.95rem行高1.6
- **间距系统**区块padding20px元素间距15px
- **表格样式**斑马纹行高56px单元格padding15px 20px
**技术要求**
- **浏览器兼容**Chrome/Firefox/Edge最新版
**注意事项**
1. 医嘱生成后需同步更新手术记录
2. 所有金额显示保留两位小数

View File

@@ -0,0 +1,505 @@
## 门诊医生站传染性报卡登记PRD文档
### 一、页面概述
**页面名称**:门诊医生站传染性报卡登记
**页面目标**:帮助医生完成法定传染病病例的电子报告卡填写与提交
**适用场景**:医生确诊或疑似发现法定传染病病例时,进行报卡登记
**页面类型**:表单页(复杂表单)
**核心功能**
1. 患者基本信息录入(含身份验证)
2. 传染病分类选择与疾病诊断信息登记
3. 病例分类与流行病学信息记录
4. 数据校验与表单提交
5. 地址四级联动选择(省-市-区县-街道)
**用户价值**
- 规范传染病报告流程,确保数据完整准确
- 减少手工填写错误,提高上报效率
- 自动关联患者基本信息,减少重复录入
- 内置校验规则防止漏报错报
**原型图地址**:https://static.pm-ai.cn/prototype/20260128/6041dcc237645108aa9e917e8d57705f/index.html
**流程图**:
```mermaid
flowchart TD
A(["开始报卡"]) --> B["填写患者基本信息"]
B --> C{"身份证格式错误"}
C -- 是 --> D["提示请输入有效身份证号码"]
C -- 否 --> E{"患者年龄≤14岁"}
E -- 是 --> F["显示家长姓名输入框"]
E -- 否 --> G["隐藏家长姓名输入框"]
F --> H["填写现住地址"]
G --> H
H --> I{"地址加载失败"}
I -- 是 --> J["显示手动输入选项"]
I -- 否 --> K["选择疾病分类"]
J --> K
K --> L{"选择特定疾病"}
L -- 是 --> M["显示疾病分型选择"]
L -- 否 --> N["跳过分型选择"]
M --> O["填写发病/诊断日期"]
N --> O
O --> P{"日期逻辑错误"}
P -- 是 --> Q["提示发病日期不能晚于诊断日期"]
P -- 否 --> R["填写报告信息"]
Q --> R
R --> S["表单校验"]
S --> T{"校验失败"}
T -- 是 --> U["显示错误提示"]
T -- 否 --> V["保存报卡"]
U --> S
V --> W{"点击重置按钮"}
W -- 是 --> X["保留关键信息重置其他字段"]
X --> S
V --> Y{"点击关闭按钮"}
Y -- 是 --> Z{"确认关闭"}
Z -- 是 --> AA(["结束流程"])
Z -- 否 --> V
```
### 二、整体布局分析
**页面宽度**:自适应布局
**主要区域划分**
1. **顶部标题区**5%):展示表单标题和卡片编号
2. **患者信息区**30%):患者基本信息、联系方式、现住地址等
3. **疾病信息区**50%):疾病分类选择、发病/诊断日期、疾病分型等
4. **报告信息区**10%):报告单位、医生、填卡日期等
**操作按钮区**5%):保存、重置、关闭按钮
**布局特点**:上下布局,采用响应式网格,表单分组清晰,必填项高亮标识
### 三、页面区域详细描述
#### 1. 标题区
**区域位置**:页面顶部
**区域尺寸**高度60px
**区域功能**:展示表单标题和唯一编号标识
**包含元素**
- 表单标题
- - 元素类型:标题文本
- 默认内容:“中华人民共和国传染病报告卡”
- 样式要求20px字号深蓝色(#2c3e50),居中加粗
- 卡片编号
- - 元素类型:输入框
- 默认值:空
- 提示文字:“单位自编,与网络直报一致”
- 交互行为支持手动输入12位编号
- 样式要求12px灰色文字带下划线分隔线
#### 2. 患者基本信息区
**区域位置**:标题区下方
**区域功能**:采集患者核心身份信息、联系方式、居住地等
**包含元素**
- 患者姓名输入框
- - 元素类型:文本输入框,自动引入当前就诊患者信息的姓名
- 校验规则必填项支持中文姓名2-10字
- 家长姓名输入框
- - 元素类型:文本输入框
- 条件显示当系统计算年龄≤14岁时自动显示必填标识
- 身份证号输入框
- - 元素类型:文本输入框,自动引入当前就诊患者信息的身份证号
- 校验规则必填项自动校验18位身份证格式
- 性别选择
- - 元素类型:单选按钮组
- 选项:男/女/未知,自动匹配当前就诊患者信息的性别
- 默认值:必填项
- 出生日期输入
- - 元素类型:复合输入区域
- 包含:年(4位)/月(2位)/日(2位)三个输入框,自动匹配当前就诊患者信息的出生年月
- 联动逻辑:自动计算实足年龄并填充到年龄输入框
- 工作单位输入框
- - 元素类型:文本输入框,自动引入当前就诊患者信息的工作单位
- 特殊场景:学生自动关联学校信息
- 联系电话
- - 元素类型:电话输入框,自动引入当前就诊患者信息的联系方式
- 校验规则必填11位手机号或带区号固话
- 紧急联系人电话
- - 元素类型:电话输入框
- 校验规则必填11位手机号或带区号固话
- 病人属于
- - 复选框类型:通过现地址自动判断
- 校验规则:必填
- 职业
- - 下拉选项类型:取值于字典管理的字典名称为“职业”维护的数据
- 校验规则:必填
#### 3. 现住地址选择区
**区域功能**:四级联动地址选择(省-市-区县-街道)
**交互逻辑**
1. 省份选择后动态加载对应城市
2. 城市选择后动态加载区县
3. 区县选择后动态加载街道
4. 村(居)和门牌号为手动输入
**数据要求**
- 初始默认值:省-市-区县-街道(自动引入当前就诊患者信息的现住址)
- 异常处理:当上级未选择时禁用下级选择
**字典取值跟新增患者的现住址保持一致(患者管理-)患者列表)**
![](media/clip_image001.png)
#### 4. 疾病信息区
**区域功能**:选择传染病类型及相关临床信息
**包含元素**
- **疾病分类选择**
- - 布局方式:网格布局(3列)
- 分类:甲类/乙类/丙类传染病
- 交互行为:多选但同类别互斥
- 特殊处理:选择炭疽/肺结核/病毒性肝炎/疟疾/梅毒/血吸虫病等疾病时激活分型选择
- **疾病复选框互斥逻辑:**
- - 选择炭疽病时显示分型选项(肺炭疽/皮肤炭疽/胃肠炭疽/未分型)
- 选择肺结核时显示分型选项(涂阳/仅培阳/菌阴/未痰检)
- 选择病毒性肝炎时显示分型选项(甲/乙/丙/戊型)
- 选择疟疾时显示分型选项(间日疟/恶性疟/三日疟/卵形疟/未分型)
- 选择梅毒时显示分型选项(Ⅰ期/Ⅱ期/Ⅲ期/胎传/隐性)
- 选择血吸虫病时显示分型选项(急性/慢性/晚期/未分型)
- **分型选择**
- - 元素类型:动态下拉框
- 数据源:根据疾病类型动态加载
- 示例:肺结核→涂阳/仅培阳/菌阴/未痰检
- **其他法定管理以及重点监测传染病输入框:**
- - 手动输入非列表疾病
- 自动关联传染病代码库
- **发病日期**
- - 元素类型:日期选择器
- 验证规则:不得晚于诊断日期
- **诊断日期**
- - 元素类型:日期选择器
- 取值:默认当前系统时间
- **死亡日期**
- - 元素类型:日期选择器
- 填写规则:根据实际情况填写
- **病例分类**
- - 复选框类型: 1疑似病例/2临床诊断病例/3确诊病例/4病原携带/5阳性检测结果
- 校验规则:必填
#### 5. 报告信息区
**区域功能**:记录报告单位和责任人信息等
**包含元素**
- **报告单位**
- - 元素类型:文本输入
- 默认值:当前登录医院
- 交互行为:只读
- **联系电话**
- - 元素类型:文本输入
- 默认值:当前登录医院的联系电话
- 交互行为:可编辑
- **报告医生**
- - 元素类型:文本输入
- 默认值:当前登录医生
- 验证规则:必填
- **填卡日期**
- - 默认当前系统日期,显示为"YYYY-MM-DD"格式
- **修订病名**
- - 元素类型:文本输入
- 默认值:空
- 填写:自定义编辑
- **退卡原因**
- - 元素类型:文本输入
- 默认值:空
- 填写:自定义编辑
- **备注**
- - 元素类型:文本输入
- 默认值:空
- 填写:自定义编辑
#### 6. 操作按钮区
**区域位置**:页面底部
**包含元素**
- **保存按钮**
- - 元素类型:主要操作按钮
- 交互行为:触发表单验证,通过后保存
- 样式特征:蓝色(#3498db)圆角8px
- **重置按钮**
- - 交互行为:清除非基础信息字段
- 特殊处理:保留患者姓名、身份证等关键信息
- **关闭按钮**
- - 交互行为:二次确认后关闭页面
- 样式特征:红色(#e74c3c)
### 四、交互功能详细说明
#### 1. 地址联动选择
**触发条件**:选择省级行政区
**操作流程**
1. 选择省份→加载该省下所有城市
2. 选择城市→加载该市所有区县
3. 选择区县→加载街道列表
**异常处理**:网络错误时显示"加载失败,请手动输入"
#### 2. 疾病分型联动
**触发条件**:选择特定疾病
**数据映射**
| **疾病类型** | **分型选项** |
| ------------ | ---------------------------------- |
| 肺结核 | 涂阳/仅培阳/菌阴/未痰检 |
| 梅毒 | I期/II期/III期/胎传/隐性 |
| 炭疽 | 肺炭疽/皮肤炭疽/胃肠炭疽/未分型 |
| 病毒性肝炎 | 甲/乙/丙/戊型 |
| 疟疾 | 间日疟/恶性疟/三日疟/卵形疟/未分型 |
| 血吸虫病 | 急性/慢性/晚期/未分型 |
#### 3. 表单验证
**全局验证**
1. 提交时检查必填字段
2. 验证身份证号格式
3. 确保至少选择一种疾病
**字段级验证**
- 电话号码11位数字错误提示“请输入有效的联系电话”
- 发病日期≤诊断日期≤填卡日期,错误提示“发病日期不能晚于诊断日期”
- 身份证号18位且符合校验算法错误提示“请输入有效的身份证号码”
### 五、数据结构说明
**传染病报卡表infectious_card**
| **字段** | **类型** | **国标含义** | **来源****/****说明** |
|---------------------| -------------- |-----------------|--------------------------------------|
| card_no | VARCHAR(20) PK | 卡片编号 | 机构代码+年月日+4位流水 |
| visit_id | BIGINT FK | 本次就诊ID | adm_encounter.id |
| diag_id | BIGINT FK | 诊断记录唯一ID | adm_encounter_diagnosis.condition_id |
| pat_id | BIGINT FK | 患者主索引 | adm_patient.id |
| id_type | TINYINT | 证件类型 | |
| id_no | VARCHAR(30) | 证件号码 | 18位校验 |
| pat_name | VARCHAR(50) | 患者姓名 | |
| parent_name | VARCHAR(50) | 家长姓名 | ≤14岁必填 |
| sex | CHAR(1) | 性别 | 1男/2女/0未知 |
| birthday | DATE | 出生日期 | |
| age | INT | 实足年龄 | 函数计算 |
| age_unit | CHAR(1) | 年龄单位 | 岁/月/天-》1岁/2月/3天 |
| workplace | VARCHAR(100) | 工作单位 | 学生填学校 |
| phone | VARCHAR(20) | 联系电话 | 患者本人电话 |
| contact_phone | VARCHAR(20) | 紧急联系人电话 | |
| address_prov | VARCHAR(6) | 现住址省 | GB2260 |
| address_city | VARCHAR(6) | 现住址市 | 同上 |
| address_county | VARCHAR(6) | 现住址县 | 同上 |
| address_town | VARCHAR(9) | 现住址街道 | 同上 |
| address_village | VARCHAR(80) | 现住址村/居委 | |
| address_house | VARCHAR(40) | 现住址门牌号 | |
| patient_belong | TINYINT | 病人属于 | 系统判定1本县区/2本市其他/3本省其他/4外省/5港澳台/6外籍 |
| occupation | VARCHAR(4) | 职业 | GB/T 6565取值于字典管理的字典名称为“职业”维护的数据 |
| disease_code | VARCHAR(8) | 疾病名称 | WS 218-2020,见下表 |
| disease_type | VARCHAR(8) | 分型 | 见下表6类必分型疾病必填 |
| other_disease | VARCHAR(50) | 其他法定管理以及重点监测传染病 | |
| case_class | TINYINT | 病例分类 | 1疑似病例/2临床诊断病例/3确诊病例/4病原携带/5阳性检测结果 |
| onset_date | DATE | 发病日期 | 默认诊断时间,病原携带者填初检日期 |
| diag_date | DATETIME | 诊断日期 | 精确到小时 |
| death_date | DATE | 死亡日期 | 死亡病例必填 |
| correct_name | VARCHAR(50) | 订正病名 | 订正报告必填 |
| withdraw_reason | VARCHAR(100) | 退卡原因 | 退卡时必填 |
| report_org | VARCHAR(18) | 报告单位 | 统一信用代码(医院名称) |
| report_org_phone | VARCHAR(20) | 联系电话 | 报告单位电话:医院总值班/防保科座机 |
| report_doc | VARCHAR(20) | 报告医生 | 医生姓名 |
| report_date | DATE | 填卡日期 | 当天日期 |
| status | TINYINT | 状态 | 0暂存1已提交2已审核3已上报4失败5作废 |
| fail_msg | VARCHAR(500) | 失败原因 | 国家平台返回 |
| xml_content | TEXT | 上报XML | 日志 |
| create_time | DATETIME | 创建时间 | |
| update_time | DATETIME | 更新时间 | |
| card_name_code | TINYINT | 报卡名称代码 | 数值对照(取值于字典管理-》报卡名称代码1-中华人民共和国传染病报告卡 |
| registration source | TINYINT | 登记来源 | 1门诊/2住院 |
| dept_id | TINYINT | 科室ID | 患者当前就诊科室 |
| doctor_id | TINYINT | 医生ID | 患者当前开单医生 |
**甲类传染病2 种)―― 01xxxx**
| **disease_code** | **疾病名称** | **国家平台码** |
| ---------------- | ------------ | -------------- |
| 0101 | 鼠疫 | 甲类 |
| 0102 | 霍乱 | 甲类 |
存值示例:`0101`(鼠疫)、`0102`(霍乱)
**乙类传染病27 种)―― 02xxxx**
| **disease_code** | **疾病名称** | **国家平台码** |
| ---------------- | -------------------- | ------------------ |
| 0201 | 传染性非典型肺炎 | 乙类(按甲类管理) |
| 0202 | 艾滋病 | 乙类 |
| 0203 | 病毒性肝炎 | 乙类 |
| 0204 | 脊髓灰质炎 | 乙类(按甲类管理) |
| 0205 | 人感染高致病性禽流感 | 乙类(按甲类管理) |
| 0206 | 麻疹 | 乙类 |
| 0207 | 流行性出血热 | 乙类 |
| 0208 | 狂犬病 | 乙类 |
| 0209 | 流行性乙型脑炎 | 乙类 |
| 0210 | 登革热 | 乙类 |
| 0211 | 炭疽 | 乙类(按甲类管理) |
| 0212 | 细菌性和阿米巴性痢疾 | 乙类 |
| 0213 | 肺结核 | 乙类 |
| 0214 | 伤寒和副伤寒 | 乙类 |
| 0215 | 流行性脑脊髓膜炎 | 乙类 |
| 0216 | 百日咳 | 乙类 |
| 0217 | 白喉 | 乙类 |
| 0218 | 新生儿破伤风 | 乙类 |
| 0219 | 猩红热 | 乙类 |
| 0220 | 布鲁氏菌病 | 乙类 |
| 0221 | 淋病 | 乙类 |
| 0222 | 梅毒 | 乙类 |
| 0223 | 钩端螺旋体病 | 乙类 |
| 0224 | 血吸虫病 | 乙类 |
| 0225 | 疟疾 | 乙类 |
存值示例:乙肝→`0203`;肺结核→`0213`;梅毒→`0222`
**丙类传染病11 种)―― 03xxxx**
| **disease_code** | **疾病名称** | **国家平台码** |
| ---------------- | ---------------------- | -------------- |
| 0301 | 流行性感冒 | 丙类 |
| 0302 | 流行性腮腺炎 | 丙类 |
| 0303 | 风疹 | 丙类 |
| 0304 | 急性出血性结膜炎 | 丙类 |
| 0305 | 麻风病 | 丙类 |
| 0306 | 流行性和地方性斑疹伤寒 | 丙类 |
| 0307 | 黑热病 | 丙类 |
| 0308 | 包虫病 | 丙类 |
| 0309 | 丝虫病 | 丙类 |
| 0310 | 其它感染性腹泻病 | 丙类 |
| 0311 | 手足口病 | 丙类 |
存值示例:手足口病→`0311`;流感→`0301`
**分型码与名称对照(系统存值用)**
| **大类疾病** | **disease_code** | **分型中文** | **disease_type** **存值** |
| -------------- | ---------------- | ------------ | ------------------------- |
| **病毒性肝炎** | 0203 | 甲型 | 020301 |
| | | 乙型 | 020302 |
| | | 丙型 | 020303 |
| | | 戊型 | 020304 |
| | | 未分型 | 020305 |
| **炭疽** | 0211 | 肺炭疽 | 021101 |
| | | 皮肤炭疽 | 021102 |
| | | 胃肠炭疽 | 021103 |
| | | 未分型 | 021104 |
| **肺结核** | 0213 | 涂阳 | 021301 |
| | | 仅培阳 | 021302 |
| | | 菌阴 | 021303 |
| | | 未痰检 | 021304 |
| **梅毒** | 0222 | Ⅰ期 | 022201 |
| | | Ⅱ期 | 022202 |
| | | Ⅲ期 | 022203 |
| | | 胎传 | 022204 |
| | | 隐性 | 022205 |
| **疟疾** | 0225 | 间日疟 | 022501 |
| | | 恶性疟 | 022502 |
| | | 三日疟 | 022503 |
| | | 卵形疟 | 022504 |
| | | 未分型 | 022505 |
| **血吸虫病** | 0224 | 急性 | 022401 |
| | | 慢性 | 022402 |
| | | 晚期 | 022403 |
| | | 未分型 | 022404 |
### 六、开发实现要点
**样式规范**
- 主色调:#3498db(按钮/重要标签)
- 错误状态:#e74c3c(边框+文字)
- 表单间距8px垂直间距16px水平间距
**技术要求**
- 支持Chrome/Firefox/Edge最新版
**注意事项**
1. 身份证号不需脱敏显示

View File

@@ -0,0 +1,387 @@
## 门诊医生站开立会诊申请单界面PRD文档
### 一、页面概述
**页面名称**:门诊医生站开立会诊申请单界面**页面目标**:帮助门诊医生完成会诊申请单的创建、编辑、提交和作废操作,实现多科室会诊流程的电子化管理**适用场景**
1. 门诊医生需要邀请其他科室专家进行会诊时
2. 会诊申请单需要修改或补充信息时
3. 会诊流程需要跟踪管理时
**页面类型**:表单页+列表页复合型界面
**核心功能**
1. 会诊申请单的新增、保存、提交、作废功能
2. 会诊科室/专家可视化选择
3. 申请单数据表格展示与交互
4. 表单数据自动填充与校验
5. 申请单打印输出
**用户价值**
- 规范会诊申请流程,减少纸质单据使用
- 通过智能填充减少医生重复录入
- 实时查看会诊申请状态(新开/已提交/已确认/已签名/已完成/已取消)
原型图地址https://static.pm-ai.cn/prototype/20260115/4eb1bd5367f9d5610b32c0ecc6c793f5/index.html
流程图:
```mermaid
flowchart TD
%% ---------- 开始 ----------
START(["开始"]) --> A["医生进入会诊申请单界面"]
%% ---------- 操作选择 ----------
A --> B{"操作选择"}
B -->|"打印"| C["选择已有申请单"]
B -->|"提交/取消提交"| D{"校验状态为“已提交”?"}
B -->|"删除"| E["弹出确认对话框"]
B -->|"结束"| F{"校验状态为“已提交”?"}
B -->|"编辑"| G["修改表单内容"]
B -->|"新增"| H["清空表单(保留患者信息)"]
%% ---------- 打印分支 ----------
C --> I["高亮选中行"]
I --> J["生成打印视图"]
J --> K["输出打印样式"]
K --> L(["取消"])
%% ---------- 提交/取消提交分支 ----------
D -->|"不通过"| M["提示“请完善必填信息”"]
D -->|"通过"| N["更新状态为“已提交/新开”"]
%% ---------- 删除分支 ----------
E --> O{"确认?"}
O -->|"是"| P["标记状态为“已取消”"]
O -->|"否"| L
%% ---------- 结束分支 ----------
F -->|"不通过"| Q["提示“请先提交申请”"]
F -->|"通过"| R["标记状态为“已完成”"]
%% ---------- 编辑分支 ----------
G --> S{"校验必填字段"}
S -->|"不通过"| M
S -->|"通过"| T["保存到表格"]
%% ---------- 新增/保存通用路径 ----------
H --> U["填写表单"]
U --> V["选择会诊科室/专家"]
V --> W["自动填充邀请对象"]
W --> X["填写病史及目的"]
X --> Y["点击保存"]
Y --> Z{"校验必填字段"}
Z -->|"不通过"| M
Z -->|"通过"| AA["生成会诊申请记录"]
AA --> AB["保存到表格"]
AB --> AC["新增/更新记录"]
%% ---------- 循环 ----------
AC --> A
N --> A
P --> A
R --> A
T --> A
M --> A
Q --> A
L --> A
```
### 二、整体布局分析
**页面宽度**自适应宽度主内容区采用7:3比例分割
**主要区域划分**
1. 顶部操作栏48px固定高度
2. 会诊申请单列表区(高度自适应)
3. 主内容区分左右结构7:3比例
- 左侧:会诊申请单表单区
- 右侧:会诊科室/专家选择区
**布局特点**:响应式上下+左右混合布局,主要对齐方式为左对齐
### 三、页面区域详细描述
#### 1. 顶部操作栏区域
**区域位置**:页面顶部固定位置**区域尺寸**高度48px宽度100%**区域功能**:提供全局操作功能入口**包含元素**
- **打印按钮**
- 元素类型:操作按钮
- 显示内容:“打印”
- 交互行为点击后生成A4打印视图自动适配医院抬头格式
- 样式特征:绿色背景(\#13C2C2)圆角4px32px高度
- **新增按钮**
- 元素类型:操作按钮
- 显示内容:“新增”
- 交互行为:点击清空表单(保留当前患者基本信息)
- 样式特征:蓝色背景(\#1890FF)
- **结束按钮**
- 元素类型:危险操作按钮
- 显示内容:“结束”
- 交互行为:点击结束已提交的会诊流程,标记申请单状态为"已结束",禁用后续操作
- 样式特征:红色背景(\#FF4D4F)
- 限制条件:需先选中已提交的会诊单
- **保存按钮**
- 元素类型:主要操作按钮
- 显示内容:“保存”
- 交互行为:点击保存当前表单数据,校验必填字段后保存至表格,自动生成时间戳
- 样式特征:绿色背景(\#52C41A)
#### 2. 会诊申请单列表区
**区域位置**:顶部操作栏下方**区域尺寸**高度自适应宽度100%**区域功能**:展示当前医生的会诊申请记录**包含元素**
- **申请单表格**
- 展示方式:带边框表格
- 数据字段:
- 序号:文本 - 自增序号 - 不可操作
- 急:布尔 - ✓表示紧急 - 不可操作
- 申请单号:文本 - CS20260105001 - 不可操作
- 会诊时间:日期 - 2026-01-05 15:08 - 不可操作
- 邀请对象:文本 - 吴院长 - 不可操作
- 申请科室:文本 - 内科 - 不可操作
- 申请医师:文本 - 张医生 - 不可操作
- 申请时间:日期 - 2026-01-05 15:08 - 不可操作
- 提交状态:布尔 - 复选框 - 仅查看
- 结束状态:布尔 - 复选框 - 仅查看
- 操作功能:
- - o 提交/取消提交按钮
```
样式要求:蓝色小按钮,禁用状态显示灰色
```
```
交互行为:切换提交状态,需二次确认
```
```
o 删除图标
```
```
样式要求红色垃圾桶图标hover时放大10%
```
```
交互行为:弹出确认对话框后作废该记录
```
[删除]**将状态改为“已取消”****
UPDATE ConsultationRequest
SET ConsultationStatus = 50,cancelnatureDate = <作废会诊时间>
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus <> 40 ;
- 交互特性:
- 行点击选中效果(蓝色高亮+左侧边框)
- 行hover浅灰色背景
- 提交按钮状态联动(切换提交状态,需二次确认)
#### 3. 会诊申请单表单区
**区域位置**:主内容区左侧**区域尺寸**占主内容区70%宽度**区域功能**:会诊申请单的详细表单填写**包含元素**
- **基础信息区**
- 申请单号只读文本【保存】时自动生成规则CS+年月日时分秒+4位随机数
- 申请时间:只读文本,自动获取系统当前时间
- 病人信息:病人姓名/性别/年龄/就诊卡号/申请医师/申请科室(不可编辑),自动获取当前患者档案信息。
- **会诊信息区**
- 会诊时间:时间控件可编辑
- 紧急标识:复选框控件
- 申请医师:默认当前登录医生
- 申请科室:默认当前医生登录的开单科室
- 门诊诊断:自动获取医生开立的门诊诊断(主诊断)
- **病史及目的**
- 多行文本域最小高度100px
- **会诊邀请**
- 会诊邀请对象:支持多选(逗号分隔)-》(可从右侧会诊邀请对象选择)
- **会诊记录区**
- 会诊意见:只读文本域
- 会诊确认参加医师:只读字段
- 所属医生、代表科室、签名医生、签名时间:只读字段
#### 4. 会诊邀请对象选择区(侧边栏)
**区域位置**:主内容区右侧**区域尺寸**占主内容区30%宽度**区域功能**:快速选择会诊科室和专家**包含元素**
- **会诊科室列表**
- 展示方式:带边框可滚动列表
- 交互行为:选择科室后动态加载对应专家
- **会诊专家列表**
- 展示方式:带边框可滚动列表
- 交互行为:点击专家自动填入会诊邀请对象字段(防重复:已选专家提示"请勿重复选择"
- 特殊逻辑:支持多选(自动用逗号分隔)
### 四、交互功能详细说明
#### 1. 会诊申请单提交流程
**功能描述**:完成会诊申请单的提交操作**触发条件**:点击表格行的"提交"按钮**操作流程**
1. 医生点击行内"提交"按钮
2. 系统校验必填字段(会诊时间、邀请对象)
3. 提交状态复选框变为已勾选
4. 按钮文字变为"取消提交"
5. 禁用该行编辑功能
【提交】**将状态从“新开”改为“已提交”**
UPDATE ConsultationRequest
SET ConsultationStatus = 10,ConfirmingPhysician = <提交会诊医生姓名> ,ConfirmingPhysicianID = <提交会诊医生ID> ,ConfirmingDate = <提交会诊时间>
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 0 ;
【取消提交】**将状态从“已提交”改为“新开”**
UPDATE ConsultationRequest
SET ConsultationStatus = 0,ConfirmingPhysician = '',ConfirmingPhysicianID = '',ConfirmingDate = ''
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 10 ;
**异常处理**
- 必填字段缺失:弹出"请完善会诊时间和邀请对象信息"
- 重复提交:提示"该申请已提交,请勿重复操作"
#### 2. 会诊流程结束功能
**功能描述**:标记会诊流程已结束**触发条件**:选中已提交的申请单后点击顶部"结束"按钮**操作流程**
1. 医生选中已提交的申请单(行高亮)
2. 点击顶部"结束"按钮
3. 系统校验提交状态为已提交
4. 结束状态复选框变为已勾选
5. 禁用该行的取消提交功能
【结束】**将状态从“已签名”改为“已完成”**
UPDATE ConsultationRequest
SET ConsultationStatus = 40,Signature = <结束会诊医生姓名> ,SignatureDate=<结束会诊时间>
WHERE ConsultationID = <会诊申请单ID> and ConsultationStatus = 30 ;
**异常处理**
- 未选中记录:提示"请先选择要结束的会诊申请"
- 未提交记录:提示"请先提交该会诊申请"
#### 3. 申请单保存功能
**功能描述**:保存会诊申请单数据**触发条件**:点击顶部"保存"按钮**操作流程**
1. 系统自动生成申请单号(如为空)
2. 保存当前表单所有字段值
3. 新增记录插入表格末尾
4. 已有记录更新对应行数据
【保存】
①、写入门诊医嘱表(医嘱状态为新开,医嘱名称为"门诊会诊"
②、写入门诊会诊申请单表ConsultationRequest
**数据校验**
- 必填字段:病人姓名、会诊时间、申请科室、会诊时间、会诊邀请对象、简要病史及会诊目的
- 未选会诊对象:提示"请至少选择1位会诊专家"
- 过期时间:提示"会诊时间不能早于当前时间"
#### 4. 会诊邀请对象选择联动
**触发方式**:点击科室列表项
**数据联动**
1. 根据选中会诊科室过滤会诊专家列表
2. 记忆已选专家(跨科室切换时不丢失)
**技术要点**
- 使用对象存储会诊科室-会诊专家映射关系
- 采用事件委托处理动态生成的列表项
### 五、数据结构说明
门诊会诊申请单表ConsultationRequest
| **字段名称** | **数据类型** | **长度** | **描述** | **取值范围** |
|-----------------------------| ------------ | -------- |----------------| --------------------------------------------------------- |
| **PatientID** | Text | 20 | 患者唯一标识 | 患者就诊卡号 (取值患者档案) |
| **ConsultationID** | Text | 20 | 会诊申请单唯一标识 | 系统自动生成的唯一编号生成规则CS+年月日时分秒+4位随机数 |
| **VisitID** | BIGINT | 20 | 门诊就诊流水号(逻辑外键) | 取值于本次门诊就诊记录表的主键 |
| **OrderID** | BIGINT | 20 | 门诊医嘱表主键(一对一外键) | 门诊医嘱表 |
| **PatientName** | Text | 50 | 患者姓名 | 患者的姓名 (取值患者档案) |
| **Gender** | Text | 10 | 患者性别 | 男/女/其他 (取值患者档案) |
| **Age** | Integer | - | 患者年龄 | 取值患者档案 |
| **Department** | Text | 50 | 申请会诊的科室 | 当前科室名称 |
| **RequestingPhysician** | Text | 50 | 申请会诊的医生 | 当前医生姓名 |
| **ConsultationrequestDate** | DateTime | - | 会诊申请时间 | YYYY-MM-DD HH:MM:SS
| **ConsultationPurpose** | Text | 255 | 简要病史及会诊目的 | 文本描述,自定义编辑 |
| **ProvisionalDiagnosis** | Text | 255 | 门诊诊断 | 文本描述,自动获取医生开立的门诊诊断(主诊断) |
| **ConsultationDate** | DateTime | - | 会诊时间 | YYYY-MM-DD HH:MM:SS |
| **ConsultationStatus** | Text | 20 | 会诊状态 | 新开/已提交/已确认/已签名/已完成/已取消 |
| **ConsultationUrgency** | Text | 20 | 是否紧急 | 勾选框:一般/紧急 |
| **ConsultationOpinion** | Text | 255 | 会诊意见 | 文本描述 |
| **ConfirmingPhysician** | Text | 50 | 提交会诊的医生 | 医生姓名 |
| **ConfirmingPhysicianID** | Text | 20 | 提交会诊的医生ID | 医生唯一标识 |
| **ConfirmingDate** | DateTime | - | 提交会诊日期 | YYYY-MM-DD HH:MM:SS |
| **Signature** | Text | 50 | 结束会诊医生 | 医生姓名 |
| **SignatureDate** | DateTime | - | 结束会诊日期 | YYYY-MM-DD HH:MM:SS |
| **cancelnatureDate** | DateTime | - | 作废会诊日期 | YYYY-MM-DD HH:MM:SS |
| InvitedObject | Text | 50 | 会诊邀请对象 | |
**诊状态用于记录会诊申请在不同阶段的状态,以下是常见的会诊状态及其说明:**
| **状态名称** | **状态值** | **描述** |
| ------------ | ---------- | ---------------------------------------------------------------------- |
| **新开** | 0 | 会诊申请单已保存 |
| **已提交** | 10 | 会诊申请已提交,但尚未被会诊医生确认。 |
| **已确认** | 20 | 会诊医生已确认会诊申请,并准备进行会诊。 |
| **已签名** | 30 | 会诊完成后进行签名 |
| **已完成** | 40 | 会诊已经完成,会诊意见已记录。 |
| **已取消** | 50 | 会诊申请被取消,可能由于患者情况变化或其他原因,申请医生进行作废操作。 |
**门诊医嘱表在相关会诊操作步骤的相关事务**
把“门诊会诊申请”当成**一种特殊医嘱**OrderType = 'Consult')由系统**在同一事务内**自动插入 门诊医嘱表,再挂到 `ConsultationRequest` **注意:按照现有系统的门诊医嘱表进行设置相关字段的值**
| **节点** | **是否自动** | **说明** |
| --------------------- | ------------ | --------------------------------------------------------------------------------------------------- |
| 医生点击【保存】 | ✅ | 后台事务:先插门诊医嘱表(医嘱状态为“新开”),再插`ConsultationRequest`.Status=0 |
| 医生点击【提交】 | ✅ | 仅更新两表状态 → 门诊医嘱表的医嘱状态和`ConsultationRequest.Status=10` (已提交),不重复生成医嘱 |
| 医生点击【作废/删除】 | ✅ | 自动将门诊医嘱表的医嘱状态字段置为“作废”,级联`ConsultationRequest.Status=50` |
| 医生点击【结束】 | ✅ | 将 门诊医嘱表的医嘱状态字段置为“已完成”,同时写`ConsultationRequest.Status=40` |
### 六、开发实现要点
**样式规范**
- **主色调**\#1890FF操作按钮
- **辅助色**\#13C2C2打印、\#52C41A保存、\#FF4D4F结束
- **字体规范**14px/1.5,中文字体优先使用"PingFang SC"
- **间距系统**16px基准表单行间距12px
- **组件样式**
- 按钮4px圆角32px高度
- 输入框4px圆角1px \#D9D9D9边框
- 表格行:选中状态\#E6F7FF背景+左侧3px蓝色边框
**技术要求**
- **浏览器兼容**支持Chrome/Firefox/Edge最新版
- **性能要求**:表单提交响应时间\<1秒
**注意事项**
1. 时间字段需统一处理为YYYY-MM-DD HH:mm:ss格式
2. 申请单号生成需加锁防止重复
3. 移动端需优化表格横向滚动体验
4. 打印功能需特殊样式处理(隐藏操作按钮)

View File

@@ -0,0 +1,310 @@
## 门诊医生站会诊申请确认界面PRD文档
### 一、页面概述
**页面名称**:门诊医生站会诊申请确认界面
**页面目标**:帮助医生完成会诊申请的确认、签名和打印操作,展示会诊申请详细信息
**适用场景**:医生在收到会诊申请后,查看申请信息并给出会诊意见
**页面类型**:表单页+列表页复合型页面
**核心功能**
1. 会诊申请单列表展示与选择
2. 会诊确认与取消确认功能
3. 签名功能
4. 会诊记录单打印
5. 会诊意见编辑与保存
**用户价值**
- 规范会诊申请流程
- 电子化确认和签名提高效率
- 完整记录会诊意见便于后续诊疗
- 打印功能满足纸质存档需求
**原型图地址:**https://static.pm-ai.cn/prototype/20260115/7c45e175239257e0f04c9081bf2ca204/index.html
**流程图:**
```mermaid
flowchart TD
Start(["医生进入会诊申请确认界面"]) --> LoadList["加载会诊申请列表"]
LoadList --> HasUntreated{"是否有未处理申请?"}
HasUntreated -- "否" --> ShowNoTip["显示无申请提示"]
HasUntreated -- "是" --> SelectApp["医生选择会诊申请"]
SelectApp --> ShowDetail["显示会诊申请详情"]
ShowDetail --> EditOpinion["医生编辑会诊意见"]
EditOpinion --> ConfirmClick{"点击确认按钮?"}
ConfirmClick -- "否" --> SignClick{"点击签名按钮?"}
ConfirmClick -- "是" --> ValidateConfirm{"校验必填字段"}
ValidateConfirm -- "不通过" --> TipFill["提示\n请先填写会诊意见"]
ValidateConfirm -- "通过" --> CheckConfirmed{"是否已确认?"}
CheckConfirmed -- "是" --> UpdateConfirmed["更新状态为\n已确认"]
UpdateConfirmed --> AutoFill["自动填充医生科室信息"]
AutoFill --> DisableCancel["禁用取消确认功能"]
CheckConfirmed -- "否" --> KeepState["保持当前状态"]
SignClick -- "否" --> PrintClick{"点击打印按钮?"}
SignClick -- "是" --> ValidateSign{"校验通过?"}
ValidateSign -- "不通过" --> TipConfirmFirst["提示\n请先确认会诊申请"]
ValidateSign -- "通过" --> UpdateSigned["更新状态为\n已签名"]
UpdateSigned --> RecordSign["记录签名医生和时间"]
PrintClick -- "否" --> RefreshClick{"点击刷新按钮?"}
PrintClick -- "是" --> GenPrintView["生成打印优化视图"]
GenPrintView --> BrowserPrint["调用浏览器打印功能"]
RefreshClick -- "是" --> LoadList
RefreshClick -- "否" --> KeepState
TipFill --> EditOpinion
TipConfirmFirst --> EditOpinion
KeepState --> End(["结束"])
BrowserPrint --> End
DisableCancel --> End
```
### 二、整体布局分析
**页面宽度**:自适应布局
**主要区域划分**
1. 顶部标签导航高度48px
2. 操作按钮区高度36px+间距)
3. 会诊申请列表区(高度自适应)
4. 会诊记录单表单区(高度自适应)
**布局特点**:上下布局,采用网格系统对齐,左侧对齐为主
### 三、页面区域详细描述
#### 1. 顶部标签导航区域
**区域位置**:页面顶部
**区域尺寸**高度48px宽度100%
**区域功能**:页面导航标识
**包含元素**
- **会诊确认标签**
- 元素类型:文本标签
- 显示内容:“会诊确认”
- 交互行为:无点击交互(当前页面)
- 样式特征蓝色下划线16px字体700字重
#### 2. 操作按钮区域
**区域位置**:标签导航下方
**区域尺寸**高度36px宽度100%
**区域功能**:提供页面主要操作入口
**包含元素**
- **打印按钮**
- 元素类型:操作按钮
- 显示内容:“打印”
- 交互行为:点击触发打印会诊记录单
- 样式特征绿色背景白色文字圆角6px
- **刷新按钮**
- 元素类型:操作按钮
- 显示内容:“刷新”
- 交互行为:点击重新加载页面数据
- 样式特征:白色背景,灰色边框,黑色文字
- **确认按钮**
- 元素类型:状态切换按钮
- 显示内容:“确认”/“取消确认”
- 交互行为:
- 点击后变为"取消确认"状态(红色样式)
- 已签名时禁用取消操作
- 样式特征:蓝色背景,白色文字
- 限制条件:需选中表格行才可操作
- **签名按钮**
- 元素类型:操作按钮
- 显示内容:“签名”
- 交互行为:
- 需先确认才能签名
- 签名后自动记录签名时间和签名医生
- 样式特征:蓝色背景,白色文字
- 限制条件:需先完成确认操作
#### 3. 会诊申请列表区域
**区域位置**:按钮区域下方
**区域尺寸**高度自适应宽度100%
**区域功能**:展示待处理的会诊申请列表
**包含元素**
- **申请列表表格** 取值于门诊会诊申请单表ConsultationRequest
- 检索要求:医生登录门诊医生站打开会诊申请确认界面时只能检索出当前登录医生姓名包含在会诊邀请对象内(只能查看自己受会诊邀请对象)
- 展示方式:带斑马纹表格
- 表头字段:
- 序号 \| 紧急 \| 申请单号 \| 病人姓名 \| 会诊时间 \| 邀请对象 \| 申请科室 \| 申请医师 \| 申请时间 \| 确认 \| 签名
- 数据字段:
- 序号:文本 - 自动编号 - “1” - 不可操作
- 紧急:复选框 - 布尔值 - 未勾选 - 可操作
- 申请单号:文本 - 字符串 - “CS20250812001” - 不可操作
- 病人姓名:文本 - 字符串 - “陈明” - 不可操作
- 会诊时间:日期 - 日期时间 - “2025-08-12 17:48” - 不可操作
- 邀请对象:文本 - 字符串 - “演示测试” - 不可操作
- 申请科室:文本 - 字符串 - “内科” - 不可操作
- 申请医师:文本 - 字符串 - “徐斌” - 不可操作
- 申请时间:日期 - 日期时间 - “2025-08-12 17:48” - 不可操作
- 确认:复选框 - 布尔值 - 勾选框 不可操作
- 签名:复选框 - 布尔值 - 勾选框 不可操作
- 操作功能:点击行选中查看会诊申请详情
- 样式特征:斑马纹交替背景,悬停高亮
#### 4. 会诊记录单表单区域
**区域位置**:列表区域下方
**区域尺寸**高度自适应宽度100%
**区域功能**:展示和编辑会诊详细信息
**包含元素**
- **基础信息区**
- 布局方式8列网格
- 包含字段:
- 病人姓名/性别/年龄/就诊卡号
- 申请单号/申请科室
- 会诊时间/紧急标志
- 会诊邀请对象
- 提交医生/提交时间
- **病史及目的区**
- 元素类型:文本区域
- 显示内容:患者主诉和会诊目的
- 交互行为:只读展示
- **会诊确认参加医师**
- **会诊意见区**
- 元素类型:可编辑文本域
- 显示内容:会诊意见文本
- 交互行为:支持多行编辑
- 样式特征浅灰色背景120px最小高度
- **确认/签名信息区**
- 包含字段:
- 所属医生/代表科室(确认后自动填充当前医生和科室)
- 签名医生/签名时间(自动填充签名医生和签名时间(系统当前时间))
### 四、交互功能详细说明
#### 1. 会诊申请选择功能
**触发方式**:点击表格行
**执行流程**
1. 高亮选中行(浅蓝色背景)
2. 同步该行数据到下方表单
3. 根据确认状态更新按钮文字
4. 加载存储的会诊意见到文本域
**异常处理**
- 无选中行时禁用确认/签名按钮
- 已签名行禁止取消确认
#### 2. 会诊确认功能
**触发方式**:点击确认按钮
**执行流程**
1. 校验必填字段(会诊意见、会诊确认参加医师)
2. 保存会诊意见等相关内容到行数据写入门诊会诊申请确认表ConsultationConfirmation
3. 勾选确认复选框
4. 更新按钮为"取消确认"状态
5. 所属医生和代表科室(自动填充当前医生和科室)
**异常处理**
- 未填写会诊意见时提示"请先填写会诊意见"
- 保存失败时保持原状态并提示错误
#### 2. 电子签名功能
**功能描述**:医生对确认的会诊进行电子签名
**触发条件**:已确认的会诊申请点击"签名"按钮
**操作流程**
1. 医生确认会诊申请
2. 点击"签名"按钮
3. 校验确认状态
4. 表格中"签名"列复选框被勾选
5. 自动记录签名医生(当前用户)
6. 自动填充签名时间为系统时间
7. 禁用取消确认功能
**成功反馈**:表单区显示签名信息
**失败处理**:提示"请先确认会诊申请"
#### 3. 打印会诊记录单
**功能描述**:打印格式化的会诊记录
**触发条件**:点击"打印"按钮
**操作流程**
1. 点击"打印"按钮
2. 系统生成打印优化视图
3. 调用浏览器打印功能
**特殊处理**:隐藏交互元素,优化打印布局
### 五、数据结构说明
**门诊会诊申请确认表(**ConsultationConfirmation****
| **字段名称** | **数据类型** | **长度** | **描述** | **约束/说明** |
|-----------------------------|--------------|----------|--------------------|------------------------------------------------------------------------------------|
| **ConsultationID** | INTEGER | 20 | 会诊申请单唯一标识 | FOREIGN KEY REFERENCES ConsultationRequest(ConsultationID) |
| **ConfirmingPhysicianID** | TEXT | -20 | 确认会诊的医生ID | 操作【确认】按钮的当前医生ID |
| **ConfirmingPhysicianName** | TEXT | -20 | 确认会诊的医生姓名 | 操作【确认】按钮的当前医生姓名 |
| **ConfirmingDeptName** | TEXT | 20 | 代表科室 | 操作【确认】按钮的当前开单科室 |
| **ConfirmingDate** | DateTime | - | 确认会诊的日期 | 操作【确认】按钮当前系统时间 |
| **ConsultationStatus** | TEXT | 20 | 会诊状态 | CHECK (ConsultationStatus IN ('已确认', '取消确认', '已签名', '已完成')), NOT NULL |
| **ConsultationOpinion** | TEXT | 500 | 会诊意见 | |
| **ConfirmingPhysician** | TEXT | 100 | 会诊确认参加医师 | |
| **Signature** | TEXT | 20 | 签名医生 | |
| **SignatureDate** | DateTime | - | 签名时间 | - |
ConsultationConfirmation.ConsultationStatu会诊状态
| **状态值** | **状态名** | **描述** |
|------------|------------|-----------------------------------|
| **0** | 取消确认 | 作废 |
| **20** | 已确认 | 会诊医生已查看/同意,可写初步意见 |
| **30** | 已签名 | 已电子签名,意见最终生效 |
| **40** | 已完成 | 会诊报告已回写,流程关闭 |
**按钮涉及的事务**
| **按钮** | **涉及表** | **执行事务** | **锁/并发** | **成功状态** | **失败处理** |
|--------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|------------------|--------------------------|
| **确认** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =20<br>2、医嘱 状态='已执行'<br>3、写入ConsultationConfirmation表相关的数据 | SELECT ... FOR UPDATE | 已提交 → 已确认 | 任何异常 → 整体 ROLLBACK |
| **取消确认** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =10<br>2、医嘱 状态='已提交'<br>3、ConsultationConfirmation. ConsultationStatus = 0 | 同上 | 已确认→ 取消确认 | 同上回滚 |
| **签名** | 1、ConsultationRequest<br>2、门诊医嘱<br>3、ConsultationConfirmation | 1、ConsultationRequest.ConsultationStatus =30<br>2、医嘱 Status='已完成'<br>3、写入ConsultationConfirmation. Signature, SignatureDate,ConsultationStatus =30 | 同上 | 已确认 → 已签名 | 同上回滚 |
### 六、开发实现要点
**样式规范**
- **主色调**\#4A89DC按钮蓝色
- **辅助色**\#4CAF50成功绿色
- **字体规范**14px/1.5 常规16px 标题
- **间距系统**8px基础间距24px区块间距
- **组件样式**
- 按钮6px圆角1px边框
- 输入框4px圆角1px \#E0E0E0边框
**技术要求**
- **浏览器兼容**Chrome/Firefox/Edge最新版
- **性能要求**:列表加载时间\<1s
**注意事项**
1. 确认和签名状态需要联动控制
2. 打印功能需要特殊样式处理
3. 时间字段需统一使用YYYY-MM-DD HH:mm:ss格式
4. 移动端需优化表单布局

View File

@@ -0,0 +1,267 @@
## 门诊会诊申请管理界面PRD文档
### 一、页面概述
**页面名称**:门诊会诊申请管理界面
**页面目标**:提供会诊申请的全流程管理功能,包括申请记录查询、编辑申请、查看详情、状态变更等核心操作
**适用场景**:门诊医生需要查看会诊申请或管理已有申请记录时使用
**页面类型**:列表页+表单弹窗复合型页面
**核心功能**
1. 多条件组合筛选会诊申请记录
2. 会诊申请表格展示与操作(编辑/查看/删除)
3. 会诊申请单的填写与提交
4. 会诊状态标记(提交/结束)
**用户价值**:规范会诊申请流程,减少纸质单据流转,提高多科室协作效率
原型图地址https://static.pm-ai.cn/prototype/20260116/aed1f102d614677f100c0d1fe3104999/index.html
**流程图:**
```mermaid
flowchart TD
Start([Start]) --> A[进入门诊会诊申请管理界面]
A --> B{用户操作类型}
B -->|筛选查询| C[设置筛选条件]
B -->|编辑申请| D[点击编辑按钮]
B -->|查看详情| E[点击查看按钮]
B -->|删除申请| G[点击删除按钮]
C --> H{验证筛选条件}
H -->|有效| I[展示筛选结果]
H -->|无效| J[显示错误提示]
J --> C
I --> K[用户浏览列表]
D --> L{检查会诊状态}
L -->|未结束| M[打开编辑弹窗]
L -->|已结束| N[提示不可编辑]
N --> O[关闭弹窗]
M --> P[修改表单内容]
P --> Q{表单验证}
Q -->|通过| R[保存修改]
Q -->|不通过| S[标红错误字段]
S --> P
E --> T{检查会诊状态}
T -->|未结束| U[打开只读弹窗]
T -->|已结束| U
G --> Z{删除验证}
Z -->|可删除| AA[确认删除]
Z -->|不可删除| AB[提示删除失败]
AB --> K
AA --> AC[更新状态为已取消]
AC --> AD[更新列表显示]
R --> AD
AD --> K
K --> AE([End])
```
### 二、整体布局分析
**页面宽度**:自适应布局
**主要区域划分**
1. 顶部筛选区高度自适应约80px
2. 表格展示区(高度自适应,占主要空间)
3. 底部页码区固定高度56px
**布局特点**:上下布局+弹性布局,采用左右对齐方式
### 三、页面区域详细描述
#### 1. 顶部筛选区
**区域位置**:页面顶部
**区域尺寸**100%宽度,高度自适应
**区域功能**:提供多维度筛选和快速搜索功能
**包含元素**
- **时间类型选择器**
- 元素类型:下拉选择框
- 显示内容:默认"会诊时间",可选"申请时间"
- 交互行为:点击展开下拉选项
- 样式特征宽度120px高度32px圆角4px
- **时间范围选择器**(开始/结束时间)
- 元素类型:日期时间输入框
- 显示内容placeholder提示"开始时间"/“结束时间”
- 交互行为:点击弹出日期选择面板
- 样式特征宽度180px高度32px
- **申请科室/申请医生选择器**
- 元素类型带datalist的输入框
- 显示内容placeholder提示"选择或输入科室/医生"
- 交互行为:输入时显示匹配选项
- 数据来源:动态生成申请科室/医生候选列表取值于门诊会诊申请单表ConsultationRequest.Department/ RequestingPhysician
- **会诊状态筛选器**
- 元素类型:下拉选择框
- 可选值:全部/未提交/提交/结束
- 默认值:全部
- **病人姓名搜索框**
- 元素类型:文本输入框
- 显示内容placeholder提示"病人姓名"
- 交互行为:支持模糊搜索
- **操作按钮组**
- 查询按钮
- 样式:蓝色背景,带搜索图标
- 交互:触发筛选条件应用
- 重置按钮
- 样式:灰色背景,带刷新图标
- 交互:清空所有筛选条件
- 打印按钮
- 样式:深灰色背景,带打印图标
- 交互:调起浏览器打印功能
#### 2. 表格展示区
**区域位置**:页面中部
**区域尺寸**100%宽度,高度自适应
**区域功能**:展示会诊申请列表数据,支持行内操作
**包含元素**
取值于门诊会诊申请单表ConsultationRequest和门诊会诊申请单表ConsultationRequest)
- **数据表格**
- 展示方式11列固定表头表格
- 数据字段:
- ID文本 - 15 -申请单号
- 急:复选框 - 布尔值 - 示例false 不可编辑
- 病人姓名:文本 - 朱某某 - 红色高亮
- 会诊时间:日期 - 2026-01-05 15:08
- 申请科室:文本 - 内科
- 邀请对象:文本 - 吴院长
- 申请时间:日期 - 2026-01-05 15:08
- 申请医师:文本 - 演示测试
- 提交:复选框 - 布尔值 - 示例false
- 结束:复选框 - 布尔值 - 示例false
- 操作功能:
- 编辑按钮(✏️):点击打开编辑弹窗
- 查看按钮(👁️):点击打开只读弹窗
- 删除按钮(🗑️):点击确认删除
- 【删除】将状态改为“已取消”
- UPDATE ConsultationRequest
- SET ConsultationStatus = 50,cancelnatureDate = \<作废会诊时间\>
- WHERE ConsultationID = \<会诊申请单ID\> and ConsultationStatus \<\> 40 ;
- 交互行为:
- 行悬停效果:浅蓝色背景
- 复选框点击:即时更新状态(需防抖处理)
#### 3. 底部页码区
**区域位置**:页面底部
**区域尺寸**100%宽度固定高度56px
**区域功能**:分页控制和数据统计
**包含元素**
- **总数统计**总数统计文本“总数15”
- **分页控制器**
- 上一页按钮(\<
- 当前页按钮1active状态
- 下一页按钮(\>
- 交互反馈hover时边框变蓝
#### 4. 会诊申请弹窗(模态框)
**触发方式**:点击表格行操作列的编辑/查看按钮
**区域功能**:展示/编辑会诊申请详细信息
**包含元素**
- 头部区域
- 标题:“会诊申请单”
- 关闭按钮(×图标)
- 表单区域(分两栏布局)
- 基础信息区:
- 申请单号(只读)
- 申请时间(不可编辑)
- 病人姓名(不可编辑)
- 性别/年龄(不可编辑)
- 就诊卡号(不可编辑)
- 会诊信息区:
- 会诊时间(日期时间选择器)
- 申请医师(不可编辑)
- 紧急程度(复选框)
- 申请科室(不可编辑)
- 门诊诊断(不可编辑)
- 会诊邀请对象
- 会诊确认参加医师
- 所属医生
- 代表科室
- 签名医生
- 签名时间
- 文本域:
- 病史及会诊目的(多行文本)
- 会诊意见(多行文本)
- 底部按钮区:
- 取消按钮(左对齐)
- 保存按钮(右对齐,蓝色)
### 四、交互功能详细说明
#### 1. 会诊申请编辑功能
**功能描述**:修改已有会诊申请信息
**触发条件**:点击表格行中的"✏️"按钮
**操作流程**
1. 检查会诊状态是否为"结束",若已结束则提示不可编辑
2. 弹出会诊申请编辑弹窗,填充当前行数据的会诊申请和确认相关的数据
3. 用户修改表单内容(必填字段校验)
4. 点击"保存"按钮提交修改
**异常处理**
- 必填字段为空时,标红提示
- 保存失败时显示toast提示"保存失败,请重试"
#### 2. 会诊申请查看功能
**功能描述**:查看会诊申请详细信息
**触发条件**:点击表格行中的"👁️"按钮
**操作流程**
1. 弹出只读弹窗,显示完整申请信息
2. 所有字段禁用编辑
3. 仅显示"取消"按钮用于关闭弹窗
#### 3. 数据筛选功能
**功能描述**:多条件组合查询会诊记录
**触发条件**:点击"查询"按钮
**数据过滤逻辑**
- 时间范围:根据选择的时间类型(会诊/申请)进行筛选
- 申请科室/申请医生:支持模糊匹配
- 会诊状态筛选:支持多选逻辑(未提交/提交/结束)
1. 收集所有筛选条件值
2. 发起异步请求(示例中为前端过滤)
3. 更新表格数据展示
**异常处理**
- 时间范围不合法:提示"结束时间不能早于开始时间"
- 无查询结果:显示空白表格+提示文字
**性能优化**:前端本地缓存数据,减少服务器请求
### 五、数据结构说明
门诊会诊申请单表ConsultationRequest和门诊会诊申请单表ConsultationRequest)
### 六、开发实现要点
**样式规范**
- **主色调**\#5D9CEC按钮/交互元素)
- **辅助色**\#8E8E8E次要按钮
- **字体规范**14px/1.5主要内容16px/1.5(标题)
- **间距系统**16px元素间距24px区块间距
- **组件样式**
- 按钮圆角6px内边距0 16px
- 输入框1px实线边框\#D9D9D9圆角4px
**技术要求**
- **浏览器兼容**支持Chrome/Firefox/Edge最新版
- **性能要求**:列表数据筛选响应时间\<200ms
**注意事项**
1. 状态变更逻辑:已结束的记录不可编辑
2. 时间字段需要做时区转换处理
3. 申请科室/申请医生选择器需要支持拼音首字母检索

View File

@@ -0,0 +1,62 @@
**门诊手术中计费PRD文档**
**目标:**
支持手术中追加计费(耗材、药品等)、退费等场景使用
术后一站式结算(发票、清单、医保等)
**流程图:**
```mermaid
flowchart TD
A["医生开立手术申请单"] --> B{"系统生成计费包"}
B --> C["患者缴费"]
C --> D["手术室确认"]
D --> E{"术中追加/退费?"}
E -- "是" --> F{"术中计费"}
F -- "耗材" --> F2["护士扫码追加耗材\n实时计价 更新库存"]
F -- "药品" --> F3["麻醉师追加药品\n实时计价 更新库存"]
F -- "诊疗项目" --> F4["追加麻醉时长/项目\n实时计价"]
F2 --> F6["生成术中追加计费单"]
F3 --> F6
F4 --> F6
F6 --> G{"患者支付?"}
G -- "是" --> P["提示支付成功"]--> J
G -- "否" --> H["提示支付失败\n保持待支付"]
H --> D
E -- "否" --> I["手术完成"]
I --> J["术后统一结算"]
J --> K["发票/清单/分割单"]
K --> L["财务对账"]
```
**注意:**待门诊手术安排界面禅道需求编号93完成后再执行
![](media/4fa3fca6b8362de7b938ded77d6e4982.png)
图1门诊手术安排界面禅道需求编号93
![](media/2756f39fb624c7f686d56b675b4d4d10.png)
图2门诊管理-》门诊划价:手术计费界面复制《门诊划价》界面红色框内容
1、如上图1、2所示在门诊手术安排界面增加【计费】按钮实现对门诊手术中追加的费用进行记账手术计费界面如图2所示复制《门诊划价》界面红色框内容进行个性化改造患者信息取值于手术安排界面选中行的患者信息计费账号为当前系统登录的账号。
\*比如在手术计费界面给患者1计费成功后重新从手术按钮界面选中患者1点击【计费】打开界面时显示当前患者已计费成功的手术费用。
写入事务注意:
adm_charge_item费用项管理表
①、术中费用仍走“门诊就诊管理”的就诊IDadm_encounter.id = adm_charge_item.encounter_id
2\. 为了事后能追溯“这些费用是术中发生的”,在费用项管理表明细上加一个 “来源业务单据SourceBillNo” 字段adm_charge_item.generate_source_enum = 2帐单生成来源为手术计费SourceBillNo = 手术申请单号)。
3\. 其他内容按照《门诊划价》的业务数据流程走。

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -1,2 +0,0 @@
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">OpenHis v0.0.1</h1>

View File

@@ -33,7 +33,9 @@ public class SysMenuController extends BaseController {
@GetMapping("/list")
public AjaxResult list(SysMenu menu) {
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menus);
// 构建带完整路径的菜单树
List<SysMenu> menuTreeWithFullPath = menuService.buildMenuTreeWithFullPath(menus);
return success(menuTreeWithFullPath);
}
/**
@@ -115,4 +117,25 @@ public class SysMenuController extends BaseController {
}
return toAjax(menuService.deleteMenuById(menuId));
}
/**
* 获取菜单完整路径
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@GetMapping("/fullPath/{menuId}")
public AjaxResult getFullPath(@PathVariable("menuId") Long menuId) {
String fullPath = menuService.getMenuFullPath(menuId);
return success(fullPath);
}
/**
* 生成完整路径
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@PostMapping("/generateFullPath")
public AjaxResult generateFullPath(@RequestParam(required = false) Long parentId,
@RequestParam String currentPath) {
String fullPath = menuService.generateFullPath(parentId, currentPath);
return success(fullPath);
}
}

View File

@@ -21,13 +21,15 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
<arg>--add-modules</arg>
<arg>java.base</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>

View File

@@ -3,6 +3,7 @@ package com.core.common.core.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@@ -12,7 +13,7 @@ import java.util.Date;
/**
* Entity基类
*
*
* @author system
*/
@Data
@@ -27,6 +28,7 @@ public class HisBaseEntity implements Serializable {
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 */
@@ -35,6 +37,7 @@ public class HisBaseEntity implements Serializable {
/** 更新时间 */
@TableField(fill = FieldFill.UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 租户ID */

View File

@@ -69,6 +69,9 @@ public class SysMenu extends BaseEntity {
/** 子菜单 */
private List<SysMenu> children = new ArrayList<SysMenu>();
/** 完整路径 */
private String fullPath;
public Long getMenuId() {
return menuId;
}
@@ -212,6 +215,14 @@ public class SysMenu extends BaseEntity {
this.children = children;
}
public String getFullPath() {
return fullPath;
}
public void setFullPath(String fullPath) {
this.fullPath = fullPath;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("menuId", getMenuId())
@@ -219,8 +230,8 @@ public class SysMenu extends BaseEntity {
.append("path", getPath()).append("component", getComponent()).append("query", getQuery())
.append("routeName", getRouteName()).append("isFrame", getIsFrame()).append("IsCache", getIsCache())
.append("menuType", getMenuType()).append("visible", getVisible()).append("status ", getStatus())
.append("perms", getPerms()).append("icon", getIcon()).append("createBy", getCreateBy())
.append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
.append("perms", getPerms()).append("icon", getIcon()).append("fullPath", getFullPath())
.append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
}
}

View File

@@ -21,7 +21,7 @@ public final class ServiceException extends RuntimeException {
/**
* 错误明细,内部调试错误
*
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
* 和
*/
private String detailMessage;

View File

@@ -0,0 +1,50 @@
package com.core.common.service;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.core.common.utils.AuditFieldUtil;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
/**
* 包含审计字段自动设置功能的基础服务类
*
* @param <M> Mapper 类型,必须继承 BaseMapper<T>
* @param <T> 实体类型
*/
public abstract class BaseService<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
/**
* 重写保存方法,自动设置创建审计字段
*/
@Override
@Transactional
public boolean save(T entity) {
// 在保存前设置创建审计字段
AuditFieldUtil.setCreateInfo(entity);
return super.save(entity);
}
/**
* 重写批量保存方法,自动设置创建审计字段
*/
@Override
@Transactional
public boolean saveBatch(Collection<T> entityList) {
// 为每个实体设置创建审计字段
entityList.forEach(AuditFieldUtil::setCreateInfo);
return super.saveBatch(entityList);
}
/**
* 重写更新方法,自动设置更新审计字段
*/
@Override
@Transactional
public boolean updateById(T entity) {
// 在更新前设置更新审计字段
AuditFieldUtil.setUpdateInfo(entity);
return super.updateById(entity);
}
}

View File

@@ -0,0 +1,135 @@
package com.core.common.utils;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Date;
/**
* 审计字段工具类
* 用于手动设置创建人、创建时间、更新人、更新时间等审计字段
*/
@Component
public class AuditFieldUtil {
/**
* 为实体设置创建相关的审计字段
* @param entity 实体对象
*/
public static void setCreateInfo(Object entity) {
if (entity == null) return;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser != null ? loginUser.getUsername() : "system";
Date currentTime = new Date();
// 使用反射设置字段值
Field createByField = getField(entity.getClass(), "createBy");
if (createByField != null) {
createByField.setAccessible(true);
// 只有当字段值为 null 或空字符串时才设置
if (createByField.get(entity) == null || "".equals(createByField.get(entity))) {
createByField.set(entity, username);
}
}
Field createTimeField = getField(entity.getClass(), "createTime");
if (createTimeField != null) {
createTimeField.setAccessible(true);
// 只有当字段值为 null 时才设置
if (createTimeField.get(entity) == null) {
createTimeField.set(entity, currentTime);
}
}
// 处理下划线命名的字段
Field createByFieldUnderscore = getField(entity.getClass(), "create_by");
if (createByFieldUnderscore != null) {
createByFieldUnderscore.setAccessible(true);
// 只有当字段值为 null 或空字符串时才设置
if (createByFieldUnderscore.get(entity) == null || "".equals(createByFieldUnderscore.get(entity))) {
createByFieldUnderscore.set(entity, username);
}
}
Field createTimeFieldUnderscore = getField(entity.getClass(), "create_time");
if (createTimeFieldUnderscore != null) {
createTimeFieldUnderscore.setAccessible(true);
// 只有当字段值为 null 时才设置
if (createTimeFieldUnderscore.get(entity) == null) {
createTimeFieldUnderscore.set(entity, currentTime);
}
}
} catch (Exception e) {
System.err.println("设置创建审计字段时发生异常: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 为实体设置更新相关的审计字段
* @param entity 实体对象
*/
public static void setUpdateInfo(Object entity) {
if (entity == null) return;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser != null ? loginUser.getUsername() : "system";
Date currentTime = new Date();
// 设置更新人字段
Field updateByField = getField(entity.getClass(), "updateBy");
if (updateByField != null) {
updateByField.setAccessible(true);
updateByField.set(entity, username);
}
// 设置更新时间字段
Field updateTimeField = getField(entity.getClass(), "updateTime");
if (updateTimeField != null) {
updateTimeField.setAccessible(true);
updateTimeField.set(entity, currentTime);
}
// 处理下划线命名的字段
Field updateByFieldUnderscore = getField(entity.getClass(), "update_by");
if (updateByFieldUnderscore != null) {
updateByFieldUnderscore.setAccessible(true);
updateByFieldUnderscore.set(entity, username);
}
Field updateTimeFieldUnderscore = getField(entity.getClass(), "update_time");
if (updateTimeFieldUnderscore != null) {
updateTimeFieldUnderscore.setAccessible(true);
updateTimeFieldUnderscore.set(entity, currentTime);
}
} catch (Exception e) {
System.err.println("设置更新审计字段时发生异常: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 使用反射获取字段,支持父类字段
* @param clazz 类
* @param fieldName 字段名
* @return 字段对象
*/
private static Field getField(Class<?> clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
// 如果在当前类中找不到,尝试在父类中查找
if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
return getField(clazz.getSuperclass(), fieldName);
}
return null;
}
}
}

View File

@@ -54,6 +54,12 @@
<artifactId>oshi-core</artifactId>
</dependency>
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 系统模块-->
<dependency>
<groupId>com.core</groupId>
@@ -65,6 +71,12 @@
<artifactId>core-common</artifactId>
</dependency>
<!-- MyBatis-Plus 支持 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- JSQLParser - 用于MyBatis Plus -->
<dependency>
<groupId>com.github.jsqlparser</groupId>

View File

@@ -1,22 +1,32 @@
package com.core.framework.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.core.common.utils.SecurityUtils;
import com.core.framework.handler.MybastisColumnsHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -149,4 +159,55 @@ public class MybatisPlusConfig {
return result != null ? result : 1; // 默认租户ID
}
/**
* 配置 SqlSessionFactory
* 由于排除了 DataSourceAutoConfiguration需要手动配置
*/
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dataSource,
MybatisPlusInterceptor mybatisPlusInterceptor,
MetaObjectHandler metaObjectHandler) throws Exception { // 注入 MetaObjectHandler
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置 mapper 文件位置
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/**/*Mapper.xml"));
// 设置 typeAliases 包路径
sessionFactory.setTypeAliasesPackage("com.core.**.domain,com.openhis.**.domain");
// 配置 MyBatis-Plus
MybatisConfiguration configuration = new MybatisConfiguration();
// 使用驼峰命名法转换字段
configuration.setMapUnderscoreToCamelCase(true);
// 开启缓存
configuration.setCacheEnabled(true);
// 允许JDBC支持自动生成主键
configuration.setUseGeneratedKeys(true);
// 配置默认的执行器
configuration.setDefaultExecutorType(org.apache.ibatis.session.ExecutorType.SIMPLE);
// 配置日志实现
configuration.setLogImpl(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
sessionFactory.setConfiguration(configuration);
// 设置 MyBatis-Plus 的全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(metaObjectHandler);
sessionFactory.setGlobalConfig(globalConfig);
// 设置拦截器(通过参数注入避免循环依赖)
sessionFactory.setPlugins(mybatisPlusInterceptor);
return sessionFactory.getObject();
}
/**
* 注册自动填充处理器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MybastisColumnsHandler();
}
}

View File

@@ -27,8 +27,9 @@ public class MybastisColumnsHandler implements MetaObjectHandler {
}
} catch (Exception ignored) {
}
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "tenantId", Integer.class, getCurrentTenantId());
// 使用 fillStrategy 而不是 strictInsertFill确保即使字段已设置也能填充如果为null
this.fillStrategy(metaObject, "createBy", username != null ? username : "system");
this.fillStrategy(metaObject, "tenantId", getCurrentTenantId());
}
// 设置数据修改update时候的字段自动赋值规则

View File

@@ -18,6 +18,12 @@
<dependencies>
<!-- MyBatis-Plus 支持 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
@@ -36,6 +42,24 @@
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Lombok 支持 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- JSON工具类 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Jackson 注解支持 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -23,6 +23,12 @@
<dependencies>
<!-- MyBatis-Plus 支持 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.core</groupId>

View File

@@ -0,0 +1,113 @@
package com.core.system.controller;
import com.core.common.annotation.Log;
import com.core.common.core.controller.BaseController;
import com.core.common.core.domain.AjaxResult;
import com.core.common.core.page.TableDataInfo;
import com.core.common.enums.BusinessType;
import com.core.system.domain.SysUserConfig;
import com.core.system.service.ISysUserConfigService;
import com.core.common.utils.SecurityUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户配置Controller
*
* @author
* @date 2026-01-30
*/
@Api(tags = "用户配置")
@RestController
@RequestMapping("/system/userConfig")
public class SysUserConfigController extends BaseController
{
@Autowired
private ISysUserConfigService sysUserConfigService;
/**
* 查询用户配置列表
*/
@PreAuthorize("@ss.hasPermi('system:userConfig:list')")
@GetMapping("/list")
public TableDataInfo list(SysUserConfig sysUserConfig)
{
startPage();
List<SysUserConfig> list = sysUserConfigService.selectSysUserConfigList(sysUserConfig);
return getDataTable(list);
}
/**
* 获取用户配置详细信息
*/
@PreAuthorize("@ss.hasPermi('system:userConfig:query')")
@GetMapping(value = "/{configId}")
public AjaxResult getInfo(@PathVariable("configId") Long configId)
{
return AjaxResult.success(sysUserConfigService.selectSysUserConfigById(configId));
}
/**
* 新增用户配置
*/
@PreAuthorize("@ss.hasPermi('system:userConfig:add')")
@Log(title = "用户配置", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysUserConfig sysUserConfig)
{
return AjaxResult.success(sysUserConfigService.insertSysUserConfig(sysUserConfig));
}
/**
* 修改用户配置
*/
@PreAuthorize("@ss.hasPermi('system:userConfig:edit')")
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysUserConfig sysUserConfig)
{
return toAjax(sysUserConfigService.updateSysUserConfig(sysUserConfig));
}
/**
* 删除用户配置
*/
@PreAuthorize("@ss.hasPermi('system:userConfig:remove')")
@Log(title = "用户配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{configIds}")
public AjaxResult remove(@PathVariable Long[] configIds)
{
return toAjax(sysUserConfigService.deleteSysUserConfigByIds(configIds));
}
/**
* 获取当前用户的指定配置
*/
@ApiOperation("获取当前用户的指定配置")
@GetMapping("/currentUserConfig")
public AjaxResult getCurrentUserConfig(@RequestParam String configKey)
{
Long userId = SecurityUtils.getUserId();
String configValue = sysUserConfigService.selectConfigValueByUserIdAndKey(userId, configKey);
// 返回原始配置值,不需要额外编码,由前端处理
return AjaxResult.success(configValue);
}
/**
* 保存当前用户的配置
*/
@ApiOperation("保存当前用户的配置")
@Log(title = "用户配置", businessType = BusinessType.UPDATE)
@PostMapping("/saveCurrentUserConfig")
public AjaxResult saveCurrentUserConfig(@RequestParam String configKey, @RequestParam String configValue)
{
Long userId = SecurityUtils.getUserId();
int result = sysUserConfigService.saveConfigValueByUserIdAndKey(userId, configKey, configValue);
return result > 0 ? AjaxResult.success() : AjaxResult.error("保存失败");
}
}

View File

@@ -0,0 +1,60 @@
package com.core.system.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 用户配置对象 sys_user_config
*
* @author
* @date 2026-01-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user_config")
public class SysUserConfig extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 配置ID */
@TableId
private Long configId;
/** 用户ID */
private Long userId;
/** 配置键名 */
private String configKey;
/** 配置值 */
private String configValue;
/** 备注 */
private String remark;
/** 创建者 - 标记为非数据库字段 */
@TableField(exist = false)
private String createBy;
/** 创建时间 - 使用自动填充 */
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 - 标记为非数据库字段 */
@TableField(exist = false)
private String updateBy;
/** 更新时间 - 使用自动填充 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}

View File

@@ -28,6 +28,11 @@ public class MetaVo {
*/
private String link;
/**
* 菜单是否可见(用于前端侧边栏显示控制)
*/
private String visible;
public MetaVo() {}
public MetaVo(String title, String icon) {
@@ -56,6 +61,16 @@ public class MetaVo {
}
}
public MetaVo(String title, String icon, boolean noCache, String link, String visible) {
this.title = title;
this.icon = icon;
this.noCache = noCache;
if (StringUtils.ishttp(link)) {
this.link = link;
}
this.visible = visible;
}
public boolean isNoCache() {
return noCache;
}
@@ -87,4 +102,12 @@ public class MetaVo {
public void setLink(String link) {
this.link = link;
}
public String getVisible() {
return visible;
}
public void setVisible(String visible) {
this.visible = visible;
}
}

View File

@@ -0,0 +1,15 @@
package com.core.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.core.system.domain.SysUserConfig;
/**
* 用户配置Mapper接口
*
* @author
* @date 2026-01-30
*/
public interface SysUserConfigMapper extends BaseMapper<SysUserConfig>
{
}

View File

@@ -3,7 +3,6 @@ package com.core.system.service;
import com.core.common.core.domain.TreeSelect;
import com.core.common.core.domain.entity.SysMenu;
import com.core.system.domain.vo.RouterVo;
import java.util.List;
import java.util.Set;
@@ -22,7 +21,7 @@ public interface ISysMenuService {
public List<SysMenu> selectMenuList(Long userId);
/**
* 根据用户查询系统菜单列表
* 查询系统菜单列表
*
* @param menu 菜单信息
* @param userId 用户ID
@@ -50,7 +49,7 @@ public interface ISysMenuService {
* 根据用户ID查询菜单树信息
*
* @param userId 用户ID
* @return 菜单列表
* @return 菜单树信息
*/
public List<SysMenu> selectMenuTreeByUserId(Long userId);
@@ -72,15 +71,23 @@ public interface ISysMenuService {
/**
* 构建前端所需要树结构
*
*
* @param menus 菜单列表
* @return 树结构列表
*/
public List<SysMenu> buildMenuTree(List<SysMenu> menus);
/**
* 构建前端所需要树结构(包含完整路径)
*
* @param menus 菜单列表
* @return 树结构列表
*/
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus);
/**
* 构建前端所需要下拉树结构
*
*
* @param menus 菜单列表
* @return 下拉树结构列表
*/
@@ -98,15 +105,15 @@ public interface ISysMenuService {
* 是否存在菜单子节点
*
* @param menuId 菜单ID
* @return 结果 true 存在 false 不存在
* @return 结果
*/
public boolean hasChildByMenuId(Long menuId);
/**
* 查询菜单是否存在角色
* 查询菜单使用数量
*
* @param menuId 菜单ID
* @return 结果 true 存在 false 不存在
* @return 结果
*/
public boolean checkMenuExistRole(Long menuId);
@@ -141,4 +148,21 @@ public interface ISysMenuService {
* @return 结果
*/
public boolean checkMenuNameUnique(SysMenu menu);
}
/**
* 根据菜单ID获取完整路径
*
* @param menuId 菜单ID
* @return 完整路径
*/
public String getMenuFullPath(Long menuId);
/**
* 根据路径参数生成完整路径
*
* @param parentId 父级菜单ID
* @param currentPath 当前路径
* @return 完整路径
*/
public String generateFullPath(Long parentId, String currentPath);
}

View File

@@ -0,0 +1,81 @@
package com.core.system.service;
import com.core.system.domain.SysUserConfig;
import java.util.List;
/**
* 用户配置Service接口
*
* @author
* @date 2026-01-30
*/
public interface ISysUserConfigService
{
/**
* 查询用户配置
*
* @param configId 用户配置ID
* @return 用户配置
*/
public SysUserConfig selectSysUserConfigById(Long configId);
/**
* 查询用户配置列表
*
* @param sysUserConfig 用户配置
* @return 用户配置集合
*/
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig);
/**
* 新增用户配置
*
* @param sysUserConfig 用户配置
* @return 结果
*/
public int insertSysUserConfig(SysUserConfig sysUserConfig);
/**
* 修改用户配置
*
* @param sysUserConfig 用户配置
* @return 结果
*/
public int updateSysUserConfig(SysUserConfig sysUserConfig);
/**
* 批量删除用户配置
*
* @param configIds 需要删除的用户配置ID
* @return 结果
*/
public int deleteSysUserConfigByIds(Long[] configIds);
/**
* 删除用户配置信息
*
* @param configId 用户配置ID
* @return 结果
*/
public int deleteSysUserConfigById(Long configId);
/**
* 根据用户ID和配置键获取配置值
*
* @param userId 用户ID
* @param configKey 配置键
* @return 配置值
*/
public String selectConfigValueByUserIdAndKey(Long userId, String configKey);
/**
* 根据用户ID和配置键保存配置
*
* @param userId 用户ID
* @param configKey 配置键
* @param configValue 配置值
* @return 结果
*/
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue);
}

View File

@@ -147,13 +147,15 @@ public class SysMenuServiceImpl implements ISysMenuService {
List<RouterVo> routers = new LinkedList<RouterVo>();
for (SysMenu menu : menus) {
RouterVo router = new RouterVo();
router.setHidden("1".equals(menu.getVisible()));
// 不再根据 visible 字段设置 hidden确保所有有权限的路由都可用
// router.setHidden("1".equals(menu.getVisible()));
router.setHidden(false);
router.setName(getRouteName(menu));
router.setPath(getRouterPath(menu));
router.setComponent(getComponent(menu));
router.setQuery(menu.getQuery());
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()),
menu.getPath()));
menu.getPath(), menu.getVisible()));
List<SysMenu> cMenus = menu.getChildren();
if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
router.setAlwaysShow(true);
@@ -167,12 +169,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
children.setComponent(menu.getComponent());
children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(),
StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getVisible()));
children.setQuery(menu.getQuery());
childrenList.add(children);
router.setChildren(childrenList);
} else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible()));
router.setPath("/");
List<RouterVo> childrenList = new ArrayList<RouterVo>();
RouterVo children = new RouterVo();
@@ -180,7 +182,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
children.setPath(routerPath);
children.setComponent(UserConstants.INNER_LINK);
children.setName(getRouteName(menu.getRouteName(), routerPath));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, menu.getPath(), menu.getVisible()));
childrenList.add(children);
router.setChildren(childrenList);
}
@@ -191,7 +193,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 构建前端所需要树结构
*
*
* @param menus 菜单列表
* @return 树结构列表
*/
@@ -213,6 +215,36 @@ public class SysMenuServiceImpl implements ISysMenuService {
return returnList;
}
/**
* 构建前端所需要树结构(包含完整路径)
*
* @param menus 菜单列表
* @return 树结构列表
*/
@Override
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) {
List<SysMenu> menuTree = buildMenuTree(menus);
// 为每个菜单项添加完整路径
addFullPathToMenuTree(menuTree);
return menuTree;
}
/**
* 为菜单树添加完整路径
*
* @param menus 菜单树
*/
private void addFullPathToMenuTree(List<SysMenu> menus) {
for (SysMenu menu : menus) {
// 计算当前菜单的完整路径
menu.setFullPath(getMenuFullPath(menu.getMenuId()));
// 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
addFullPathToMenuTree(menu.getChildren());
}
}
}
/**
* 构建前端所需要下拉树结构
*
@@ -486,11 +518,139 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 内链域名特殊字符替换
*
*
* @return 替换后的内链域名
*/
public String innerLinkReplaceEach(String path) {
return StringUtils.replaceEach(path, new String[] {Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":"},
new String[] {"", "", "", "/", "/"});
}
/**
* 根据菜单ID获取完整路径
*
* @param menuId 菜单ID
* @return 完整路径
*/
@Override
public String getMenuFullPath(Long menuId) {
SysMenu menu = menuMapper.selectMenuById(menuId);
if (menu == null) {
return "";
}
StringBuilder fullPath = new StringBuilder();
buildMenuPath(menu, fullPath);
// 标准化完整路径,确保没有多余的斜杠
return normalizePath(fullPath.toString());
}
/**
* 递归构建菜单路径
*
* @param menu 菜单信息
* @param path 路径构建器
*/
private void buildMenuPath(SysMenu menu, StringBuilder path) {
if (menu == null || menu.getMenuId() == null) {
return;
}
// 如果不是根节点,则递归查找父节点
if (menu.getParentId() != null && menu.getParentId() > 0) {
SysMenu parentMenu = menuMapper.selectMenuById(menu.getParentId());
if (parentMenu != null) {
buildMenuPath(parentMenu, path);
}
}
// 添加当前菜单的路径,避免双斜杠
String currentPath = normalizePathSegment(menu.getPath());
if (currentPath != null && !currentPath.isEmpty()) {
if (path.length() > 0) {
// 确保路径之间只有一个斜杠分隔符
// 如果当前路径不为空,且当前路径不以斜杠结尾,则添加斜杠并追加路径
if (path.charAt(path.length() - 1) != '/') {
path.append("/").append(currentPath);
} else {
path.append(currentPath);
}
} else {
// 对于第一个路径,直接追加
path.append(currentPath);
}
}
}
/**
* 标准化路径片段,移除开头的斜杠
*
* @param path 原始路径
* @return 标准化后的路径片段
*/
private String normalizePathSegment(String path) {
if (path == null) {
return null;
}
// 移除开头的斜杠
if (path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
/**
* 标准化完整路径,移除多余的斜杠
*
* @param path 原始路径
* @return 标准化后的完整路径
*/
private String normalizePath(String path) {
if (path == null) {
return null;
}
// 处理多个连续斜杠,将其替换为单个斜杠
while (path.contains("//")) {
path = path.replace("//", "/");
}
return path;
}
/**
* 根据路径参数生成完整路径
*
* @param parentId 父级菜单ID
* @param currentPath 当前路径
* @return 完整路径
*/
@Override
public String generateFullPath(Long parentId, String currentPath) {
StringBuilder fullPath = new StringBuilder();
// 如果有父级菜单,则先获取父级菜单的完整路径
if (parentId != null && parentId > 0) {
SysMenu parentMenu = menuMapper.selectMenuById(parentId);
if (parentMenu != null) {
String parentFullPath = getMenuFullPath(parentId);
if (StringUtils.isNotEmpty(parentFullPath)) {
fullPath.append(parentFullPath);
}
}
}
// 添加当前路径
if (StringUtils.isNotEmpty(currentPath)) {
if (fullPath.length() > 0) {
fullPath.append("/").append(currentPath);
} else {
fullPath.append(currentPath);
}
}
return normalizePath(fullPath.toString());
}
}

View File

@@ -0,0 +1,178 @@
package com.core.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.core.system.domain.SysUserConfig;
import com.core.common.utils.StringUtils;
import com.core.system.mapper.SysUserConfigMapper;
import com.core.system.service.ISysUserConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 用户配置Service业务层处理
*
* @author
* @date 2026-01-30
*/
@Service
public class SysUserConfigServiceImpl extends ServiceImpl<SysUserConfigMapper, SysUserConfig> implements ISysUserConfigService
{
@Autowired
private SysUserConfigMapper sysUserConfigMapper;
/**
* 查询用户配置
*
* @param configId 用户配置ID
* @return 用户配置
*/
@Override
public SysUserConfig selectSysUserConfigById(Long configId)
{
return sysUserConfigMapper.selectById(configId);
}
/**
* 查询用户配置列表
*
* @param sysUserConfig 用户配置
* @return 用户配置集合
*/
@Override
public List<SysUserConfig> selectSysUserConfigList(SysUserConfig sysUserConfig)
{
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
lqw.eq(StringUtils.isNotEmpty(sysUserConfig.getConfigKey()), SysUserConfig::getConfigKey, sysUserConfig.getConfigKey())
.eq(sysUserConfig.getUserId() != null, SysUserConfig::getUserId, sysUserConfig.getUserId());
return sysUserConfigMapper.selectList(lqw);
}
/**
* 新增用户配置
*
* @param sysUserConfig 用户配置
* @return 结果
*/
@Override
public int insertSysUserConfig(SysUserConfig sysUserConfig)
{
return sysUserConfigMapper.insert(sysUserConfig);
}
/**
* 修改用户配置
*
* @param sysUserConfig 用户配置
* @return 结果
*/
@Override
public int updateSysUserConfig(SysUserConfig sysUserConfig)
{
return sysUserConfigMapper.updateById(sysUserConfig);
}
/**
* 根据用户ID和配置键更新配置值
*
* @param userId 用户ID
* @param configKey 配置键
* @param configValue 配置值
* @return 结果
*/
private int updateConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
{
SysUserConfig config = new SysUserConfig();
config.setConfigValue(configValue);
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
lqw.eq(SysUserConfig::getUserId, userId)
.eq(SysUserConfig::getConfigKey, configKey);
return sysUserConfigMapper.update(config, lqw);
}
/**
* 批量删除用户配置
*
* @param configIds 需要删除的用户配置ID
* @return 结果
*/
@Override
public int deleteSysUserConfigByIds(Long[] configIds)
{
return sysUserConfigMapper.deleteBatchIds(java.util.Arrays.asList(configIds));
}
/**
* 删除用户配置信息
*
* @param configId 用户配置ID
* @return 结果
*/
@Override
public int deleteSysUserConfigById(Long configId)
{
return sysUserConfigMapper.deleteById(configId);
}
/**
* 根据用户ID和配置键获取配置值
*
* @param userId 用户ID
* @param configKey 配置键
* @return 配置值
*/
@Override
public String selectConfigValueByUserIdAndKey(Long userId, String configKey)
{
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
lqw.eq(SysUserConfig::getUserId, userId)
.eq(SysUserConfig::getConfigKey, configKey);
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
return config != null ? config.getConfigValue() : null;
}
/**
* 根据用户ID和配置键保存配置
*
* @param userId 用户ID
* @param configKey 配置键
* @param configValue 配置值
* @return 结果
*/
@Override
public int saveConfigValueByUserIdAndKey(Long userId, String configKey, String configValue)
{
// 参数验证
if (userId == null || configKey == null) {
throw new IllegalArgumentException("用户ID和配置键不能为空");
}
try {
LambdaQueryWrapper<SysUserConfig> lqw = new LambdaQueryWrapper<>();
lqw.eq(SysUserConfig::getUserId, userId)
.eq(SysUserConfig::getConfigKey, configKey);
SysUserConfig config = sysUserConfigMapper.selectOne(lqw);
if (config != null) {
// 更新现有配置,只更新配置值,避免更新审计字段
return updateConfigValueByUserIdAndKey(userId, configKey, configValue);
} else {
// 插入新配置
SysUserConfig newConfig = new SysUserConfig();
newConfig.setUserId(userId);
newConfig.setConfigKey(configKey);
newConfig.setConfigValue(configValue);
return sysUserConfigMapper.insert(newConfig);
}
} catch (Exception e) {
// 记录错误日志以便调试
System.err.println("保存用户配置时发生错误: " + e.getMessage());
e.printStackTrace();
throw e; // 重新抛出异常让上层处理
}
}
}

View File

@@ -10,6 +10,7 @@ import com.core.common.utils.SecurityUtils;
import com.core.common.utils.StringUtils;
import com.core.common.utils.bean.BeanValidators;
import com.core.common.utils.spring.SpringUtils;
import com.core.common.utils.AuditFieldUtil; // 引入我们创建的工具类
import com.core.system.domain.SysPost;
import com.core.system.domain.SysUserPost;
import com.core.system.domain.SysUserRole;
@@ -55,6 +56,132 @@ public class SysUserServiceImpl implements ISysUserService {
@Autowired
private ISysDeptService deptService;
/**
* 新增保存用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional
public int insertUser(SysUser user) {
// 在保存前设置审计字段
AuditFieldUtil.setCreateInfo(user);
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}
/**
* 修改保存用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional
public int updateUser(SysUser user) {
// 在更新前设置审计字段
AuditFieldUtil.setUpdateInfo(user);
Long userId = user.getUserId();
// 删除用户与角色关联
userRoleMapper.deleteUserRoleByUserId(userId);
// 新增用户与角色管理
insertUserRole(user);
// 删除用户与岗位关联
userPostMapper.deleteUserPostByUserId(userId);
// 新增用户与岗位管理
insertUserPost(user);
return userMapper.updateUser(user);
}
/**
* 注册用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
public boolean registerUser(SysUser user) {
// 在保存前设置审计字段
AuditFieldUtil.setCreateInfo(user);
return userMapper.insertUser(user) > 0;
}
/**
* 导入用户数据
*
* @param userList 用户数据列表
* @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
* @param operName 操作用户
* @return 结果
*/
@Override
public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
if (StringUtils.isNull(userList) || userList.size() == 0) {
throw new ServiceException("导入用户数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (SysUser user : userList) {
try {
// 验证是否存在这个用户
SysUser u = userMapper.selectUserByUserName(user.getUserName());
if (StringUtils.isNull(u)) {
BeanValidators.validateWithException(validator, user);
deptService.checkDeptDataScope(user.getDeptId());
String password = configService.selectConfigByKey("sys.user.initPassword");
user.setPassword(SecurityUtils.encryptPassword(password));
// 在导入用户时设置审计字段
AuditFieldUtil.setCreateInfo(user);
userMapper.insertUser(user);
successNum++;
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
} else if (isUpdateSupport) {
BeanValidators.validateWithException(validator, user);
checkUserAllowed(u);
checkUserDataScope(u.getUserId());
deptService.checkDeptDataScope(user.getDeptId());
user.setUserId(u.getUserId());
// 在更新用户时设置审计字段
AuditFieldUtil.setUpdateInfo(user);
userMapper.updateUser(user);
successNum++;
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
failureMsg.append(msg + e.getMessage());
log.error(msg, e);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
// 以下是原有方法,保持不变
/**
* 根据条件分页查询用户列表
*
@@ -161,7 +288,7 @@ public class SysUserServiceImpl implements ISysUserService {
/**
* 校验手机号码是否唯一
*
*
* @param user 用户信息
* @return
*/
@@ -177,7 +304,7 @@ public class SysUserServiceImpl implements ISysUserService {
/**
* 校验email是否唯一
*
*
* @param user 用户信息
* @return
*/
@@ -220,56 +347,6 @@ public class SysUserServiceImpl implements ISysUserService {
}
}
/**
* 新增保存用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional
public int insertUser(SysUser user) {
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}
/**
* 注册用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
public boolean registerUser(SysUser user) {
return userMapper.insertUser(user) > 0;
}
/**
* 修改保存用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional
public int updateUser(SysUser user) {
Long userId = user.getUserId();
// 删除用户与角色关联
userRoleMapper.deleteUserRoleByUserId(userId);
// 新增用户与角色管理
insertUserRole(user);
// 删除用户与岗位关联
userPostMapper.deleteUserPostByUserId(userId);
// 新增用户与岗位管理
insertUserPost(user);
return userMapper.updateUser(user);
}
/**
* 用户授权角色
*
@@ -425,69 +502,9 @@ public class SysUserServiceImpl implements ISysUserService {
return userMapper.deleteUserByIds(userIds);
}
/**
* 导入用户数据
*
* @param userList 用户数据列表
* @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
* @param operName 操作用户
* @return 结果
*/
@Override
public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
if (StringUtils.isNull(userList) || userList.size() == 0) {
throw new ServiceException("导入用户数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (SysUser user : userList) {
try {
// 验证是否存在这个用户
SysUser u = userMapper.selectUserByUserName(user.getUserName());
if (StringUtils.isNull(u)) {
BeanValidators.validateWithException(validator, user);
deptService.checkDeptDataScope(user.getDeptId());
String password = configService.selectConfigByKey("sys.user.initPassword");
user.setPassword(SecurityUtils.encryptPassword(password));
user.setCreateBy(operName);
userMapper.insertUser(user);
successNum++;
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
} else if (isUpdateSupport) {
BeanValidators.validateWithException(validator, user);
checkUserAllowed(u);
checkUserDataScope(u.getUserId());
deptService.checkDeptDataScope(user.getDeptId());
user.setUserId(u.getUserId());
user.setUpdateBy(operName);
userMapper.updateUser(user);
successNum++;
successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
failureMsg.append(msg + e.getMessage());
log.error(msg, e);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
/**
* 扩展属性
*
*
* @param userId 系统用户id
* @return 扩展属性
*/
@@ -498,7 +515,7 @@ public class SysUserServiceImpl implements ISysUserService {
/**
* 通过科室id获取医院id
*
*
* @param orgId 科室id
* @return 医院id
*/
@@ -509,7 +526,7 @@ public class SysUserServiceImpl implements ISysUserService {
/**
* 查询 option集合
*
*
* @param tenantId 租户id
* @return option集合
*/
@@ -520,7 +537,7 @@ public class SysUserServiceImpl implements ISysUserService {
/**
* 查询当前登录账号角色集合
*
*
* @param userId 系统用户id
* @return 当前登录账号角色集合
*/
@@ -529,4 +546,4 @@ public class SysUserServiceImpl implements ISysUserService {
return userMapper.getRoleList(userId);
}
}
}

View File

@@ -21,6 +21,7 @@
<result property="status" column="status"/>
<result property="perms" column="perms"/>
<result property="icon" column="icon"/>
<result property="fullPath" column="full_path"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.core.system.mapper.SysUserConfigMapper">
<resultMap type="com.core.system.domain.SysUserConfig" id="SysUserConfigResult">
<result property="configId" column="config_id" />
<result property="userId" column="user_id" />
<result property="configKey" column="config_key" />
<result property="configValue" column="config_value" />
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<!-- 根据用户ID和配置键名查询配置值 -->
<select id="selectConfigValueByUserIdAndKey" resultType="String">
SELECT config_value
FROM sys_user_config
WHERE user_id = #{userId} AND config_key = #{configKey}
</select>
<!-- 根据用户ID和配置键名查询完整配置 -->
<select id="selectByUserIdAndKey" resultMap="SysUserConfigResult">
SELECT config_id, user_id, config_key, config_value, remark, create_time, update_time
FROM sys_user_config
WHERE user_id = #{userId} AND config_key = #{configKey}
</select>
<!-- 根据用户ID和配置键更新配置值 -->
<update id="updateConfigValueByUserIdAndKey">
UPDATE sys_user_config
SET config_value = #{configValue}, update_time = CURRENT_TIMESTAMP
WHERE user_id = #{userId} AND config_key = #{configKey}
</update>
</mapper>

View File

@@ -195,6 +195,7 @@
<if test="status != null and status != ''">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null and remark != ''">remark,</if>
tenant_id,
create_time
)values(
<if test="userId != null and userId != ''">#{userId},</if>
@@ -209,6 +210,7 @@
<if test="status != null and status != ''">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null and remark != ''">#{remark},</if>
#{tenantId},
now()
)
</insert>

View File

@@ -69,6 +69,7 @@
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!-- rabbitMQ -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
@@ -111,6 +112,13 @@
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>

View File

@@ -28,7 +28,10 @@ import com.openhis.web.datadictionary.dto.DiagnosisTreatmentDto;
import com.openhis.web.datadictionary.dto.DiagnosisTreatmentSelParam;
import com.openhis.web.datadictionary.mapper.ActivityDefinitionManageMapper;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@@ -45,6 +48,8 @@ import java.util.List;
@Service
public class LisConfigManageAppServiceImpl implements ILisConfigManageAppService {
private static final Logger log = LoggerFactory.getLogger(LisConfigManageAppServiceImpl.class);
@Resource
private ActivityDefinitionManageMapper activityDefinitionManageMapper;
@Resource
@@ -120,31 +125,73 @@ public class LisConfigManageAppServiceImpl implements ILisConfigManageAppServic
}
@Override
@Transactional
public R<?> saveAll(LisConfigManageDto manageDto) {
//先全部删除项目下详情
activityDefDeviceDefMapper.delete(new QueryWrapper<ActivityDefDeviceDef>().eq("activity_definition_id", manageDto.getId()));
activityDefObservationDefMapper.delete(new QueryWrapper<ActivityDefObservationDef>().eq("activity_definition_id", manageDto.getId()));
activityDefSpecimenDefMapper.delete(new QueryWrapper<ActivityDefSpecimenDef>().eq("activity_definition_id", manageDto.getId()));
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 根据ID查询【诊疗目录】详情
DiagnosisTreatmentDto diseaseTreatmentOne = activityDefinitionManageMapper.getDiseaseTreatmentOne(manageDto.getId(), tenantId);
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
activityDefDeviceDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
});
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
activityDefObservationDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefObservationDefMapper.insert(activityDefObservationDef);
});
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
activityDefSpecimenDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
});
return R.ok();
try {
// 先全部删除项目下详情
activityDefDeviceDefMapper.delete(new QueryWrapper<ActivityDefDeviceDef>().eq("activity_definition_id", manageDto.getId()));
activityDefObservationDefMapper.delete(new QueryWrapper<ActivityDefObservationDef>().eq("activity_definition_id", manageDto.getId()));
activityDefSpecimenDefMapper.delete(new QueryWrapper<ActivityDefSpecimenDef>().eq("activity_definition_id", manageDto.getId()));
// 获取租户ID并验证
Integer tenantId = null;
try {
tenantId = SecurityUtils.getLoginUser().getTenantId();
} catch (Exception e) {
log.warn("获取租户ID失败使用默认值", e);
}
// 根据ID查询【诊疗目录】详情
DiagnosisTreatmentDto diseaseTreatmentOne = activityDefinitionManageMapper.getDiseaseTreatmentOne(manageDto.getId(), tenantId);
if (diseaseTreatmentOne == null) {
log.warn("未找到诊疗目录id={}, tenantId={}", manageDto.getId(), tenantId);
// 即使未找到诊疗目录也继续保存使用ID作为名称
String activityDefinitionName = String.valueOf(manageDto.getId());
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
activityDefDeviceDef.setActivityDefinitionName(activityDefinitionName);
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
});
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
activityDefObservationDef.setActivityDefinitionName(activityDefinitionName);
activityDefObservationDefMapper.insert(activityDefObservationDef);
});
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
activityDefSpecimenDef.setActivityDefinitionName(activityDefinitionName);
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
});
} else {
// 正常保存
manageDto.getActivityDefDeviceDefs().forEach(activityDefDeviceDef -> {
activityDefDeviceDef.setActivityDefinitionId(manageDto.getId());
activityDefDeviceDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefDeviceDefMapper.insert(activityDefDeviceDef);
});
manageDto.getActivityDefObservationDefs().forEach(activityDefObservationDef -> {
activityDefObservationDef.setActivityDefinitionId(manageDto.getId());
activityDefObservationDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefObservationDefMapper.insert(activityDefObservationDef);
});
manageDto.getActivityDefSpecimenDefs().forEach(activityDefSpecimenDef -> {
activityDefSpecimenDef.setActivityDefinitionId(manageDto.getId());
activityDefSpecimenDef.setActivityDefinitionName(diseaseTreatmentOne.getName());
activityDefSpecimenDefMapper.insert(activityDefSpecimenDef);
});
}
log.info("保存检验项目设置成功id={}, deviceCount={}, observationCount={}, specimenCount={}",
manageDto.getId(),
manageDto.getActivityDefDeviceDefs().size(),
manageDto.getActivityDefObservationDefs().size(),
manageDto.getActivityDefSpecimenDefs().size());
return R.ok("保存成功");
} catch (Exception e) {
log.error("保存检验项目设置失败id={}, error={}", manageDto.getId(), e.getMessage(), e);
return R.fail("保存失败:" + e.getMessage());
}
}
@Override

View File

@@ -22,6 +22,8 @@ import com.openhis.web.Inspection.dto.ObservationDefManageDto;
import com.openhis.web.Inspection.dto.ObservationDefManageInitDto;
import com.openhis.web.Inspection.dto.ObservationDefSelParam;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
@@ -42,6 +44,8 @@ import java.util.stream.Stream;
@RequiredArgsConstructor
public class ObservationManageAppServiceImpl implements IObservationManageAppService
{
private static final Logger log = LoggerFactory.getLogger(ObservationManageAppServiceImpl.class);
private final ObservationDefinitionMapper observationDefinitionMapper;
private final IObservationDefinitionService observationDefinitionService;
@@ -88,9 +92,23 @@ public class ObservationManageAppServiceImpl implements IObservationManageAppSer
@Override
public R<?> updateOrAddObservationDef(ObservationDefinition Observation) {
Observation.setDeleteFlag(DelFlag.NO.getCode());
observationDefinitionService.saveOrUpdate(Observation);
return R.ok(" 添加成功");
try {
Observation.setDeleteFlag(DelFlag.NO.getCode());
boolean result = observationDefinitionService.saveOrUpdate(Observation);
if (result) {
log.info("保存检验项目成功name={}, code={}, id={}",
Observation.getName(), Observation.getCode(), Observation.getId());
return R.ok("添加成功");
} else {
log.warn("保存检验项目失败name={}, code={}",
Observation.getName(), Observation.getCode());
return R.fail("添加失败:保存操作未成功");
}
} catch (Exception e) {
log.error("保存检验项目异常name={}, code={}, error={}",
Observation.getName(), Observation.getCode(), e.getMessage(), e);
return R.fail("添加失败:" + e.getMessage());
}
}
@Override

View File

@@ -13,14 +13,6 @@ import java.util.Map;
*/
public interface ITicketAppService {
/**
* 查询号源列表
*
* @param params 查询参数
* @return 号源列表
*/
R<?> listTicket(Map<String, Object> params);
/**
* 预约号源
*

View File

@@ -4,20 +4,28 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.openhis.administration.domain.Patient;
import com.openhis.administration.service.IPatientService;
import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper;
import com.openhis.appointmentmanage.service.IDoctorScheduleService;
import com.openhis.clinical.domain.Order;
import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.OrderMapper;
import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 号源管理应用服务实现类
*
@@ -31,145 +39,14 @@ public class TicketAppServiceImpl implements ITicketAppService {
@Resource
private IPatientService patientService;
@Resource
private IDoctorScheduleAppService doctorScheduleAppService;
@Resource
private DoctorScheduleMapper doctorScheduleMapper;
@Resource
private OrderMapper orderMapper;
/**
* 查询号源列表
*
* @param params 查询参数
* @return 号源列表
*/
@Override
public R<?> listTicket(Map<String, Object> params) {
// 调试日志:打印所有参数
System.out.println("=== listTicket方法收到的所有参数===");
for (Map.Entry<String, Object> entry : params.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
System.out.println("=================================");
// 构建查询条件
Ticket ticket = new Ticket();
// 设置查询参数
// 处理日期参数
if (params.containsKey("date")) {
String date = (String) params.get("date");
try {
// 将日期字符串转换为Date类型设置到appointmentDate字段
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date appointmentDate = sdf.parse(date);
ticket.setAppointmentDate(appointmentDate);
System.out.println("设置的appointmentDate" + appointmentDate);
} catch (Exception e) {
// 日期格式错误,忽略该参数
System.out.println("日期格式错误,忽略该参数:" + date + ",错误信息:" + e.getMessage());
}
}
// 处理状态参数
if (params.containsKey("status")) {
String status = (String) params.get("status");
System.out.println("接收到的status参数" + status);
if (!"all".equals(status) && !"全部".equals(status)) {
// 将中文状态转换为英文状态
if ("未预约".equals(status)) {
ticket.setStatus("unbooked");
} else if ("已预约".equals(status)) {
ticket.setStatus("booked");
} else if ("已取号".equals(status)) {
ticket.setStatus("checked");
} else if ("已取消".equals(status)) {
ticket.setStatus("cancelled");
} else if ("已锁定".equals(status)) {
ticket.setStatus("locked");
} else {
ticket.setStatus(status);
}
System.out.println("设置的status" + ticket.getStatus());
}
}
if (params.containsKey("name")) {
String name = (String) params.get("name");
ticket.setPatientName(name);
}
if (params.containsKey("card")) {
String card = (String) params.get("card");
ticket.setMedicalCard(card);
}
if (params.containsKey("phone")) {
String phone = (String) params.get("phone");
ticket.setPhone(phone);
}
if (params.containsKey("type")) {
String type = (String) params.get("type");
System.out.println("前端传递的type参数值" + type);
if (!"all".equals(type)) {
// 类型映射转换:前端传递英文类型,数据库存储中文类型
if ("general".equals(type)) {
ticket.setTicketType("普通");
} else if ("expert".equals(type)) {
ticket.setTicketType("专家");
} else if ("普通".equals(type)) {
ticket.setTicketType("普通");
} else if ("专家".equals(type)) {
ticket.setTicketType("专家");
} else {
ticket.setTicketType(type);
}
System.out.println("转换后的ticketType值" + ticket.getTicketType());
}
}
// 手动实现分页查询避免MyBatis-Plus自动COUNT查询的问题
int pageNum = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
int pageSize = params.get("limit") != null ? Integer.valueOf(params.get("limit").toString()) : 10;
// 调试:输出构建的查询条件
System.out.println("构建的查询条件ticketType=" + ticket.getTicketType() + ", status=" + ticket.getStatus() + ", appointmentDate=" + ticket.getAppointmentDate());
// 1. 获取所有符合条件的记录
List<Ticket> allTickets = ticketService.selectTicketList(ticket);
// 调试:输出查询到的所有记录
System.out.println("查询到的所有记录:" + allTickets);
if (!allTickets.isEmpty()) {
for (Ticket t : allTickets) {
System.out.println("记录详情id=" + t.getId() + ", ticketType=" + t.getTicketType() + ", status=" + t.getStatus() + ", appointmentDate=" + t.getAppointmentDate() + ", deleteFlag=" + t.getDeleteFlag());
}
}
// 2. 计算总记录数
long total = allTickets.size();
System.out.println("手动计算的总记录数:" + total);
// 3. 手动分页
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, allTickets.size());
List<Ticket> pageTickets;
if (start >= end) {
pageTickets = new ArrayList<>();
} else {
pageTickets = allTickets.subList(start, end);
}
// 4. 转换为DTO
List<TicketDto> dtoList = pageTickets.stream().map(this::convertToDto).toList();
// 5. 构建响应数据,符合前端预期格式
Map<String, Object> result = new HashMap<>();
result.put("list", dtoList);
result.put("records", dtoList); // 兼容前端框架如Element UI可能使用的records字段
result.put("total", total);
result.put("page", pageNum);
result.put("current", pageNum); // 兼容前端框架可能使用的current字段
result.put("limit", pageSize);
result.put("pageSize", pageSize); // 兼容前端框架可能使用的pageSize字段
result.put("size", pageSize); // 兼容前端框架可能使用的size字段
result.put("pageNum", pageNum); // 兼容前端框架可能使用的pageNum字段
result.put("pages", (int) Math.ceil((double) total / pageSize)); // 计算总页数
// 调试:输出响应数据
System.out.println("返回的响应数据:" + result);
return R.ok(result);
}
private static final Logger log = LoggerFactory.getLogger(TicketAppServiceImpl.class);
/**
* 预约号源
@@ -179,35 +56,73 @@ public class TicketAppServiceImpl implements ITicketAppService {
*/
@Override
public R<?> bookTicket(Map<String, Object> params) {
// 1. 获取 ticketId 和 slotId
Long ticketId = null;
Long slotId = null;
if (params.get("ticketId") != null) {
ticketId = Long.valueOf(params.get("ticketId").toString());
}
if (ticketId == null) {
return R.fail("参数错误");
if (params.get("slotId") != null) {
slotId = Long.valueOf(params.get("slotId").toString());
}
// 2. 参数校验
if (ticketId == null || slotId == null) {
return R.fail("参数错误ticketId 或 slotId 不能为空");
}
try {
// 3. 执行原有的预约逻辑
int result = ticketService.bookTicket(params);
return R.ok(result > 0 ? "预约成功" : "预约失败");
if (result > 0) {
// 4. 预约成功后,更新排班表状态
DoctorSchedule schedule = new DoctorSchedule();
schedule.setId(Math.toIntExact(slotId)); // 对应 XML 中的 WHERE id = #{id}
schedule.setIsStopped(true); // 设置为已预约
schedule.setStopReason("booked"); // 设置停用原因
// 执行更新
int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule);
if (updateCount > 0) {
return R.ok("预约成功并已更新排班状态");
} else {
// 如果更新失败,可能需要根据业务逻辑决定是否回滚预约
return R.ok("预约成功,但排班状态更新失败");
}
} else {
return R.fail("预约失败");
}
} catch (Exception e) {
return R.fail(e.getMessage());
// e.printStackTrace();
log.error(e.getMessage());
return R.fail("系统异常:" + e.getMessage());
}
}
/**
* 取消预约
*
* @param ticketId 号源ID
* @param slotId 医生排班ID
* @return 结果
*/
@Override
public R<?> cancelTicket(Long ticketId) {
if (ticketId == null) {
public R<?> cancelTicket(Long slotId) {
if (slotId == null) {
return R.fail("参数错误");
}
try {
int result = ticketService.cancelTicket(ticketId);
return R.ok(result > 0 ? "取消成功" : "取消失败");
ticketService.cancelTicket(slotId);
DoctorSchedule schedule = new DoctorSchedule();
schedule.setId(Math.toIntExact(slotId)); // 对应 WHERE id = #{id}
schedule.setIsStopped(false); // 设置为 false (数据库对应 0)
schedule.setStopReason(""); // 将原因清空 (设为空字符串)
// 3. 调用自定义更新方法
int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule);
if (updateCount > 0) {
return R.ok("取消成功");
} else {
return R.ok("取消成功");
}
} catch (Exception e) {
return R.fail(e.getMessage());
}
@@ -253,31 +168,87 @@ public class TicketAppServiceImpl implements ITicketAppService {
@Override
public R<?> listAllTickets() {
// 创建固定的测试数据,用于验证前端是否能展示数据
List<TicketDto> testTickets = new ArrayList<>();
// 创建5条测试数据
for (int i = 1; i <= 5; i++) {
TicketDto dto = new TicketDto();
dto.setSlot_id((long) i);
dto.setBusNo("TEST0000" + i);
dto.setDepartment("内科");
dto.setDoctor("张三");
dto.setTicketType("expert");
dto.setDateTime("08:00-08:50");
dto.setStatus("未预约");
dto.setFee("150");
dto.setAppointmentDate(new Date());
testTickets.add(dto);
// 1. 从 AppService 获取排班数据
R<?> response = doctorScheduleAppService.getDoctorScheduleList();
// 获取返回的 List 数据 (假设 R.ok 里的数据是 List<DoctorSchedule>)
List<DoctorSchedule> scheduleList = (List<DoctorSchedule>) response.getData();
// 2. 转换数据为 TicketDto
List<TicketDto> tickets = new ArrayList<>();
if (scheduleList != null) {
for (DoctorSchedule schedule : scheduleList) {
TicketDto dto = new TicketDto();
// 基础信息映射
dto.setSlot_id(Long.valueOf(schedule.getId())); // Integer 转 Long
dto.setBusNo(String.valueOf(schedule.getId())); // 生成一个业务编号
dto.setDepartment(String.valueOf(schedule.getDeptId())); // 如果有科室名建议关联查询这里暂填ID
dto.setDoctor(schedule.getDoctor());
// 号源类型处理:根据挂号项目判断是普通号还是专家号
String registerItem = schedule.getRegisterItem();
if (registerItem != null && registerItem.contains("专家")) {
dto.setTicketType("expert");
} else {
dto.setTicketType("general");
}
// 时间处理:格式化为日期+时间范围,如 "2025-12-01 08:00-12:00"
String currentDate = LocalDate.now().toString(); // 或者从schedule中获取具体日期
String timeRange = schedule.getStartTime() + "-" + schedule.getEndTime();
dto.setDateTime(currentDate + " " + timeRange);
LocalTime nowTime = LocalTime.now();
LocalTime endTime = schedule.getEndTime();
String stopReason1 = schedule.getStopReason();
if ("cancelled".equals(stopReason1)||(endTime != null && nowTime.isAfter(endTime))) {
dto.setStatus("已停诊");
}else if (Boolean.TRUE.equals(schedule.getIsStopped())) {
// 获取原因并处理可能的空值
String stopReason = schedule.getStopReason();
// 使用 .equals() 比较内容,并将常量放在前面防止空指针
if ("booked".equals(stopReason)) {
dto.setStatus("已预约");
// --- 新增:获取患者信息 ---
List<Order> Order = orderMapper.selectOrderBySlotId(Long.valueOf(schedule.getId()));
Order latestOrder=Order.get(0);
if (latestOrder != null) {
dto.setPatientName(latestOrder.getPatientName());
dto.setPatientId(String.valueOf(latestOrder.getPatientId()));
dto.setPhone(latestOrder.getPhone());
}
// -----------------------
} else if ("checked".equals(stopReason)) {
dto.setStatus("已取号");
} else {
// 兜底逻辑:如果 is_stopped 为 true 但没有匹配到原因
dto.setStatus("不可预约");
}
} else {
// is_stopped 为 false 或 null 时
dto.setStatus("未预约");
}
// 费用处理 (挂号费 + 诊疗费)
int totalFee = schedule.getRegisterFee() + schedule.getDiagnosisFee();
dto.setFee(String.valueOf(totalFee));
// 日期处理LocalDateTime 转 Date
if (schedule.getCreateTime() != null) {
ZonedDateTime zdt = schedule.getCreateTime().atZone(ZoneId.systemDefault());
dto.setAppointmentDate(Date.from(zdt.toInstant()));
}
tickets.add(dto);
}
}
// 构建响应数据
// 3. 封装分页响应结
Map<String, Object> result = new HashMap<>();
result.put("list", testTickets);
result.put("total", testTickets.size());
result.put("list", tickets);
result.put("total", tickets.size());
result.put("page", 1);
result.put("limit", 20);
return R.ok(result);
}
@@ -322,9 +293,6 @@ public class TicketAppServiceImpl implements ITicketAppService {
case "cancelled":
dto.setStatus("已取消");
break;
case "locked":
dto.setStatus("已锁定");
break;
default:
dto.setStatus(status);
}

View File

@@ -21,28 +21,6 @@ public class TicketController {
@Resource
private ITicketAppService ticketAppService;
/**
* 查询号源列表
*
* @param params 查询参数
* @return 号源列表
*/
@PostMapping("/list")
public R<?> listTicket(@RequestBody Map<String, Object> params) {
return ticketAppService.listTicket(params);
}
/**
* 查询号源列表支持GET请求兼容旧版本
*
* @param params 查询参数
* @return 号源列表
*/
@GetMapping("/list")
public R<?> listTicketByGet(@RequestParam Map<String, Object> params) {
return ticketAppService.listTicket(params);
}
/**
* 查询所有号源(用于测试)

View File

@@ -5,6 +5,7 @@ import com.core.common.core.domain.R;
import com.openhis.web.basedatamanage.dto.OrganizationDto;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* Organization 应该服务类
@@ -17,13 +18,13 @@ public interface IOrganizationAppService {
* @param pageSize 查询条数
* @param name 科室名称
* @param typeEnum 科室类型
* @param classEnum 科室分类
* @param classEnumList 科室分类列表(逗号分隔的值)
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param request 请求数据
* @return 机构树分页列表
*/
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, Integer classEnum,
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, List<String> classEnumList,
String sortField, String sortOrder, HttpServletRequest request);
/**

View File

@@ -39,34 +39,69 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
private AssignSeqUtil assignSeqUtil;
@Override
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, Integer classEnum,
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, List<String> classEnumList,
String sortField, String sortOrder, HttpServletRequest request) {
// 使用Page对象进行分页查询
Page<Organization> page = new Page<>(pageNo, pageSize);
// 创建查询条件
LambdaQueryWrapper<Organization> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Organization::getDeleteFlag, "0"); // 只查询未删除的记录
// 添加查询条件
if (StringUtils.isNotEmpty(name)) {
queryWrapper.like(Organization::getName, name);
}
if (typeEnum != null) {
queryWrapper.eq(Organization::getTypeEnum, typeEnum);
}
if (classEnum != null) {
queryWrapper.eq(Organization::getClassEnum, classEnum);
if (classEnumList != null && !classEnumList.isEmpty()) {
// 使用OR条件来匹配class_enum字段中包含任一值的记录
queryWrapper.and(wrapper -> {
for (int i = 0; i < classEnumList.size(); i++) {
String classEnum = classEnumList.get(i);
if (i == 0) {
// 第一个条件
wrapper.and(subWrapper -> {
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
.or() // 或者
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
.or() // 或者
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
.or() // 或者
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
});
} else {
// 后续条件使用OR连接
wrapper.or(subWrapper -> {
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
.or() // 或者
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
.or() // 或者
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
.or() // 或者
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
});
}
}
});
}
// 创建Page对象
Page<Organization> page = new Page<>(pageNo, pageSize);
// 执行分页查询
page = organizationService.page(page, queryWrapper);
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
List<Organization> organizationList = page.getRecords();
// 将机构列表转为树结构
// 将查询结果转为DTO并构建树结构
List<Organization> organizationList = resultPage.getRecords();
List<OrganizationDto> orgTree = buildTree(organizationList);
Page<OrganizationDto> orgQueryDtoPage = new Page<>(pageNo, pageSize, page.getTotal());
orgQueryDtoPage.setRecords(orgTree);
return orgQueryDtoPage;
// 创建结果分页对象
Page<OrganizationDto> result = new Page<>();
result.setRecords(orgTree);
result.setTotal(resultPage.getTotal());
result.setSize(resultPage.getSize());
result.setCurrent(resultPage.getCurrent());
return result;
}
/**
@@ -89,7 +124,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
OrganizationDto node = new OrganizationDto();
BeanUtils.copyProperties(record, node);
node.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, node.getTypeEnum()));
node.setClassEnum_dictText(EnumUtils.getInfoByValue(OrganizationClass.class, node.getClassEnum()));
node.setClassEnum_dictText(formatClassEnumDictText(node.getClassEnum()));
node.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, node.getActiveFlag()));
// 将当前节点加入映射
nodeMap.put(bNo, node);
@@ -130,7 +165,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
OrganizationDto organizationDto = new OrganizationDto();
BeanUtils.copyProperties(organization, organizationDto);
organizationDto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, organizationDto.getTypeEnum()));
organizationDto.setClassEnum_dictText(EnumUtils.getInfoByValue(OrganizationClass.class, organizationDto.getClassEnum()));
organizationDto.setClassEnum_dictText(formatClassEnumDictText(organizationDto.getClassEnum()));
organizationDto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, organizationDto.getActiveFlag()));
return R.ok(organizationDto, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息查询"}));
@@ -221,6 +256,35 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息停用"}));
}
/**
* 格式化多选的分类字典文本
*/
private String formatClassEnumDictText(String classEnum) {
if (StringUtils.isEmpty(classEnum)) {
return "";
}
String[] classEnums = classEnum.split(",");
List<String> dictTexts = new ArrayList<>();
for (String cls : classEnums) {
String trimmedCls = cls.trim();
if (StringUtils.isNotEmpty(trimmedCls)) {
try {
Integer enumValue = Integer.parseInt(trimmedCls);
String dictText = EnumUtils.getInfoByValue(OrganizationClass.class, enumValue);
if (dictText != null) {
dictTexts.add(dictText);
}
} catch (NumberFormatException e) {
// 如果转换失败,跳过该值
}
}
}
return String.join(",", dictTexts);
}
/**
* 校验字段是否为指定类中的有效属性
*/

View File

@@ -92,6 +92,20 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
bizUser.setPassword(SecurityUtils.encryptPassword(userAndPractitionerDto.getPassword())); // 密码
bizUser.setStatus(userAndPractitionerDto.getStatus()); // 状态
bizUser.setRemark(userAndPractitionerDto.getRemark()); // 备注
// 确保tenantId被设置如果自动填充机制未能正确填充
if (bizUser.getTenantId() == null) {
try {
bizUser.setTenantId(SecurityUtils.getLoginUser().getTenantId());
} catch (Exception e) {
// 如果无法获取当前登录用户则使用默认租户ID
bizUser.setTenantId(1); // 默认租户ID
}
}
// 确保审计字段被设置
com.core.common.utils.AuditFieldUtil.setCreateInfo(bizUser);
iBizUserService.save(bizUser);
Long userId =
iBizUserService.getOne(new LambdaQueryWrapper<BizUser>().eq(BizUser::getUserName, userName)).getUserId(); // 用户id
@@ -119,6 +133,17 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
practitioner.setAddress(userAndPractitionerDto.getAddress()); // 地址
practitioner.setYbNo(userAndPractitionerDto.getYbNo()); // 医保码
practitioner.setUserId(userId); // 系统用户id
// 确保tenantId被设置如果自动填充机制未能正确填充
if (practitioner.getTenantId() == null) {
try {
practitioner.setTenantId(SecurityUtils.getLoginUser().getTenantId());
} catch (Exception e) {
// 如果无法获取当前登录用户则使用默认租户ID
practitioner.setTenantId(1); // 默认租户ID
}
}
// 责任科室
List<PractitionerOrgAndLocationDto> responsibilityOrgDtoList =
userAndPractitionerDto.getResponsibilityOrgDtoList();
@@ -295,6 +320,20 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
bizUser.setSex(sex); // 性别
bizUser.setStatus(userAndPractitionerDto.getStatus()); // 状态
bizUser.setRemark(userAndPractitionerDto.getRemark()); // 备注
// 确保tenantId被设置如果自动填充机制未能正确填充
if (bizUser.getTenantId() == null) {
try {
bizUser.setTenantId(SecurityUtils.getLoginUser().getTenantId());
} catch (Exception e) {
// 如果无法获取当前登录用户则使用默认租户ID
bizUser.setTenantId(1); // 默认租户ID
}
}
// 确保审计字段被设置
com.core.common.utils.AuditFieldUtil.setUpdateInfo(bizUser);
iBizUserService.update(bizUser, new LambdaQueryWrapper<BizUser>().eq(BizUser::getUserId, userId));
// 先删除,再新增 sys_user_role
practitionerAppAppMapper.delUserRole(userId);
@@ -316,6 +355,17 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
practitioner.setAddress(userAndPractitionerDto.getAddress()); // 地址
practitioner.setYbNo(userAndPractitionerDto.getYbNo()); // 医保码
practitioner.setUserId(userId); // 系统用户id
// 确保tenantId被设置如果自动填充机制未能正确填充
if (practitioner.getTenantId() == null) {
try {
practitioner.setTenantId(SecurityUtils.getLoginUser().getTenantId());
} catch (Exception e) {
// 如果无法获取当前登录用户则使用默认租户ID
practitioner.setTenantId(1); // 默认租户ID
}
}
// 责任科室
List<PractitionerOrgAndLocationDto> responsibilityOrgDtoList =
userAndPractitionerDto.getResponsibilityOrgDtoList();
@@ -446,6 +496,17 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
Practitioner practitioner = new Practitioner();
practitioner.setId(practitionerId);
practitioner.setOrgId(orgId);
// 确保tenantId被设置如果自动填充机制未能正确填充
if (practitioner.getTenantId() == null) {
try {
practitioner.setTenantId(SecurityUtils.getLoginUser().getTenantId());
} catch (Exception e) {
// 如果无法获取当前登录用户则使用默认租户ID
practitioner.setTenantId(1); // 默认租户ID
}
}
iPractitionerService.updateById(practitioner);
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"切换科室"}));
}

View File

@@ -5,6 +5,7 @@ package com.openhis.web.basedatamanage.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.utils.StringUtils;
import com.core.common.utils.MessageUtils;
import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.web.basedatamanage.appservice.IOrganizationAppService;
@@ -16,6 +17,8 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
/**
* 机构管理controller
@@ -43,7 +46,7 @@ public class OrganizationController {
* @param pageSize 查询条数
* @param name 科室名称
* @param typeEnum 科室类型
* @param classEnum 科室分类
* @param classEnum 科室分类(支持多选,逗号分隔)
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param request 请求对象
@@ -54,11 +57,18 @@ public class OrganizationController {
@RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "typeEnum", required = false) Integer typeEnum,
@RequestParam(value = "classEnum", required = false) Integer classEnum,
@RequestParam(value = "classEnum", required = false) String classEnum,
@RequestParam(value = "sortField", required = false) String sortField,
@RequestParam(value = "sortOrder", required = false) String sortOrder, HttpServletRequest request) {
// 解析classEnum参数支持逗号分隔的多个值
List<String> classEnumList = null;
if (StringUtils.isNotBlank(classEnum)) {
classEnumList = Arrays.asList(classEnum.split(","));
}
Page<OrganizationDto> organizationTree =
iOrganizationAppService.getOrganizationTree(pageNo, pageSize, name, typeEnum, classEnum, sortField, sortOrder, request);
iOrganizationAppService.getOrganizationTree(pageNo, pageSize, name, typeEnum, classEnumList, sortField, sortOrder, request);
return R.ok(organizationTree,
MessageUtils.createMessage(PromptMsgConstant.Common.M00009, new Object[] {"机构信息"}));
}

View File

@@ -3,6 +3,7 @@
*/
package com.openhis.web.basedatamanage.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@@ -39,7 +40,8 @@ public class OrganizationDto {
private String typeEnum_dictText;
/** 机构分类枚举 */
private Integer classEnum;
@JsonFormat(shape = JsonFormat.Shape.STRING)
private String classEnum;
private String classEnum_dictText;
/** 拼音码 */

View File

@@ -112,6 +112,12 @@ public class UserAndPractitionerDto {
private Long orgId;
private String orgId_dictText;
/**
* 创建时间
*/
@com.fasterxml.jackson.annotation.JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 责任科室
*/

View File

@@ -73,8 +73,10 @@ public class OutpatientPricingAppServiceImpl implements IOutpatientPricingAppSer
} else {
adviceTypes = List.of(1, 2, 3);
}
// 门诊划价:不要强制 pricingFlag=1 参与过滤wor_activity_definition.pricing_flag 可能为 0
// 否则会导致诊疗项目(adviceType=3)查询结果为空 records=[]
return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null,
organizationId, pageNo, pageSize, Whether.YES.getValue(), adviceTypes, null);
organizationId, pageNo, pageSize, null, adviceTypes, null);
}
}

View File

@@ -383,6 +383,7 @@ public class OutpatientRefundAppServiceImpl implements IOutpatientRefundAppServi
newRefundRequest.setStatusEnum(RequestStatus.CANCELLED.getValue());
newRefundRequest.setRefundDeviceId(deviceRequest.getId()); // 关联原ID
newRefundRequest.setPrescriptionNo("T" + deviceRequest.getPrescriptionNo());
newRefundRequest.setTenantId(deviceRequest.getTenantId()); // 显式设置租户ID
deviceRequestService.save(newRefundRequest);
Long newRequestId = newRefundRequest.getId();

View File

@@ -29,6 +29,8 @@ import com.openhis.web.chargemanage.dto.PatientMetadata;
import com.openhis.web.chargemanage.dto.PractitionerMetadata;
import com.openhis.web.chargemanage.dto.ReprintRegistrationDto;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
import com.openhis.web.paymentmanage.appservice.IPaymentRecService;
import com.openhis.web.paymentmanage.dto.CancelPaymentDto;
import com.openhis.web.paymentmanage.dto.CancelRegPaymentDto;
@@ -38,12 +40,15 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* 门诊挂号 应用实现类
*/
@Slf4j
@Service
public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistrationAppService {
@@ -76,6 +81,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
@Resource
IPatientIdentifierService patientIdentifierService;
@Resource
TriageCandidateExclusionService triageCandidateExclusionService;
/**
* 门诊挂号 - 查询患者信息
@@ -157,7 +165,7 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
@Override
public List<OrgMetadata> getOrgMetadata() {
List<Organization> list =
iOrganizationService.getList(OrganizationType.DEPARTMENT.getValue(), OrganizationClass.CLINIC.getValue());
iOrganizationService.getList(OrganizationType.DEPARTMENT.getValue(), String.valueOf(OrganizationClass.CLINIC.getValue()));
List<OrgMetadata> orgMetadataList = new ArrayList<>();
OrgMetadata orgMetadata;
for (Organization organization : list) {
@@ -308,6 +316,47 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue());
// 过滤候选池排除列表(如果是从智能候选池查询,排除已加入队列的患者)
// 检查请求参数 excludeFromCandidatePool如果为 true 或未设置,则过滤排除列表
String excludeParam = request.getParameter("excludeFromCandidatePool");
boolean shouldExclude = excludeParam == null || "true".equalsIgnoreCase(excludeParam);
if (shouldExclude && currentDayEncounter != null && !currentDayEncounter.getRecords().isEmpty()) {
try {
// 获取当前租户和日期
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
LocalDate today = LocalDate.now();
// 查询排除列表
List<TriageCandidateExclusion> exclusions = triageCandidateExclusionService.list(
new LambdaQueryWrapper<TriageCandidateExclusion>()
.eq(TriageCandidateExclusion::getTenantId, tenantId)
.eq(TriageCandidateExclusion::getExclusionDate, today)
.eq(TriageCandidateExclusion::getDeleteFlag, "0")
);
if (exclusions != null && !exclusions.isEmpty()) {
// 构建排除的 encounterId 集合
Set<Long> excludedEncounterIds = exclusions.stream()
.map(TriageCandidateExclusion::getEncounterId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 过滤结果
List<CurrentDayEncounterDto> filteredRecords = currentDayEncounter.getRecords().stream()
.filter(e -> e.getEncounterId() == null || !excludedEncounterIds.contains(e.getEncounterId()))
.collect(Collectors.toList());
// 更新分页结果
currentDayEncounter.setRecords(filteredRecords);
currentDayEncounter.setTotal(filteredRecords.size());
}
} catch (Exception e) {
// 如果过滤失败,记录日志但不影响正常查询
log.warn("过滤候选池排除列表失败", e);
}
}
currentDayEncounter.getRecords().forEach(e -> {
// 性别
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));

View File

@@ -118,7 +118,24 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
return R.ok(checkPackage.getId(), "保存成功");
} catch (Exception e) {
log.error("新增检查套餐失败", e);
return R.fail("新增检查套餐失败: " + e.getMessage());
// 捕获PostgreSQL唯一约束冲突异常
String errorMessage = e.getMessage();
if (errorMessage != null) {
// PostgreSQL唯一约束错误通常包含 "duplicate key value" 或约束名称
if (errorMessage.contains("duplicate key value") ||
errorMessage.contains("违反唯一约束") ||
errorMessage.contains("unique constraint")) {
// 提取约束名称或字段信息
String constraintInfo = "";
if (errorMessage.contains("check_package")) {
constraintInfo = "套餐名称或编码";
}
return R.fail("保存失败:数据重复," + constraintInfo + "已存在。详细错误:" + errorMessage);
}
}
return R.fail("新增检查套餐失败: " + errorMessage);
}
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.core.domain.entity.SysDictData;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.*;
import com.core.common.utils.bean.BeanUtils;
import com.core.common.utils.poi.ExcelUtil;
@@ -27,6 +28,7 @@ import com.openhis.web.datadictionary.dto.*;
import com.openhis.web.datadictionary.mapper.ActivityDefinitionManageMapper;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.mapper.ActivityDefinitionMapper;
import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.service.IServiceRequestService;
import com.openhis.yb.service.YbManager;
@@ -63,6 +65,8 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
@Resource
private ActivityDefinitionManageMapper activityDefinitionManageMapper;
@Resource
private ActivityDefinitionMapper activityDefinitionMapper;
@Resource
private IItemDefinitionService itemDefinitionService;
@Resource
private ISysDictTypeService sysDictTypeService;
@@ -234,7 +238,13 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
ActivityDefinition activityDefinition = new ActivityDefinition();
BeanUtils.copyProperties(diagnosisTreatmentUpDto, activityDefinition);
// 显式设置新增的字段
activityDefinition.setSortOrder(diagnosisTreatmentUpDto.getSortOrder());
activityDefinition.setServiceRange(diagnosisTreatmentUpDto.getServiceRange());
// 显式设置划价标记(避免前端字段/类型差异导致 copyProperties 后仍为默认值)
activityDefinition.setPricingFlag(diagnosisTreatmentUpDto.getPricingFlag());
// 拼音码
activityDefinition.setPyStr(ChineseConvertUtils.toPinyinFirstLetter(activityDefinition.getName()));
// 五笔码
@@ -252,12 +262,31 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
}
}
// 查询现有的价格定义
LambdaQueryWrapper<ChargeItemDefinition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ChargeItemDefinition::getInstanceId, diagnosisTreatmentUpDto.getId())
.eq(ChargeItemDefinition::getInstanceTable, CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
ChargeItemDefinition existingItem = chargeItemDefinitionService.getOne(queryWrapper);
ChargeItemDefinition chargeItemDefinition = new ChargeItemDefinition();
chargeItemDefinition.setYbType(diagnosisTreatmentUpDto.getYbType())
.setTypeCode(diagnosisTreatmentUpDto.getItemTypeCode())
.setInstanceTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION)
chargeItemDefinition.setInstanceTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION)
.setInstanceId(diagnosisTreatmentUpDto.getId()).setPrice(diagnosisTreatmentUpDto.getRetailPrice())
.setPriceCode(diagnosisTreatmentUpDto.getPriceCode()).setChargeName(diagnosisTreatmentUpDto.getName());
// 如果前端没有提交财务类别,则保留原有的值
if (StringUtils.isEmpty(diagnosisTreatmentUpDto.getItemTypeCode()) && existingItem != null) {
chargeItemDefinition.setTypeCode(existingItem.getTypeCode());
} else {
chargeItemDefinition.setTypeCode(diagnosisTreatmentUpDto.getItemTypeCode());
}
// 如果前端没有提交医保类别,则保留原有的值
if (StringUtils.isEmpty(diagnosisTreatmentUpDto.getYbType()) && existingItem != null) {
chargeItemDefinition.setYbType(existingItem.getYbType());
} else {
chargeItemDefinition.setYbType(diagnosisTreatmentUpDto.getYbType());
}
// 插入操作记录
operationRecordService.addEntityOperationRecord(DbOpType.UPDATE.getCode(),
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, activityDefinition);
@@ -267,9 +296,12 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
// 更新子表,修改零售价,条件:单位
boolean upItemDetail1 = itemDefinitionService.updateItemDetail(chargeItemDefinition,
diagnosisTreatmentUpDto.getRetailPrice(), ConditionCode.UNIT.getCode());
// 更新子表,修改最高零售价,条件:限制
boolean upItemDetail2 = itemDefinitionService.updateItemDetail(chargeItemDefinition,
diagnosisTreatmentUpDto.getMaximumRetailPrice(), ConditionCode.LIMIT.getCode());
// 更新子表,修改最高零售价,条件:限制只有当最高零售价不为null时才更新
boolean upItemDetail2 = true;
if (diagnosisTreatmentUpDto.getMaximumRetailPrice() != null) {
upItemDetail2 = itemDefinitionService.updateItemDetail(chargeItemDefinition,
diagnosisTreatmentUpDto.getMaximumRetailPrice(), ConditionCode.LIMIT.getCode());
}
// 更新价格表
return upItemDef && upItemDetail1 && upItemDetail2
@@ -353,9 +385,16 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
ActivityDefinition activityDefinition = new ActivityDefinition();
BeanUtils.copyProperties(diagnosisTreatmentUpDto, activityDefinition);
// 使用10位数基础采番
String code = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_DEFINITION_NUM.getPrefix(), 10);
activityDefinition.setBusNo(code);
// 显式设置新增的字段
activityDefinition.setSortOrder(diagnosisTreatmentUpDto.getSortOrder());
activityDefinition.setServiceRange(diagnosisTreatmentUpDto.getServiceRange());
// 如果前端没有传入编码则使用10位数基础采番
if (StringUtils.isEmpty(activityDefinition.getBusNo())) {
String code = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_DEFINITION_NUM.getPrefix(), 10);
activityDefinition.setBusNo(code);
}
// 拼音码
activityDefinition.setPyStr(ChineseConvertUtils.toPinyinFirstLetter(activityDefinition.getName()));
// 五笔码
@@ -363,6 +402,36 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
// 新增外来诊疗目录
activityDefinition.setStatusEnum(PublicationStatus.ACTIVE.getValue());
// 显式设置创建者和租户ID确保插入时不为null
String createBy = "system";
Integer tenantId = null;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
createBy = loginUser.getUsername();
tenantId = loginUser.getTenantId();
}
} catch (Exception e) {
// 如果获取失败,使用默认值
}
activityDefinition.setCreateBy(createBy);
activityDefinition.setTenantId(tenantId != null ? tenantId : 1); // 默认租户ID为1
// 确保创建时间不为null
if (activityDefinition.getCreateTime() == null) {
activityDefinition.setCreateTime(new java.util.Date());
}
// 检查编码是否已存在
List<ActivityDefinition> existingDefinitions = activityDefinitionMapper.selectList(
new LambdaQueryWrapper<ActivityDefinition>()
.eq(ActivityDefinition::getBusNo, activityDefinition.getBusNo())
);
if (!existingDefinitions.isEmpty()) {
return R.fail(null, "诊疗编码已存在:" + activityDefinition.getBusNo());
}
if (activityDefinitionService.addDiagnosisTreatment(activityDefinition)) {
// 调用医保目录对照接口
String ybSwitch = SecurityUtils.getLoginUser().getOptionJson().getString(CommonConstants.Option.YB_SWITCH); // 医保开关
@@ -422,7 +491,8 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
for (DiagnosisTreatmentImportDto importDto : importDtoList) {
// 创建诊疗定义
ActivityDefinition activityDefinition = createActivityDefinitionEntity(importDto, orgId);
activityDefinitionService.save(activityDefinition);
// 使用 addDiagnosisTreatment 方法,确保字段完整性
activityDefinitionService.addDiagnosisTreatment(activityDefinition);
// 创建费用定价和详情
chargeItemDefinitionService.addChargeItemDefinitionAndDetail(importDto.getName(), importDto.getTypeCode(),
importDto.getYbType(), importDto.getPermittedUnitCode(), null, importDto.getRetailPrice(),
@@ -578,6 +648,24 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
.setYbMatchFlag(CommonUtil.tryParseInt(importDto.getYbMatchFlag()))
.setStatusEnum(PublicationStatus.ACTIVE.getValue())
.setChrgitmLv(CommonUtil.tryParseInt(importDto.getChrgitmLv()));
// 显式设置创建者和租户ID确保插入时不为null
String createBy = "system";
Integer tenantId = null;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
createBy = loginUser.getUsername();
tenantId = loginUser.getTenantId();
}
} catch (Exception e) {
// 如果获取失败,使用默认值
}
activityDefinition.setCreateBy(createBy);
activityDefinition.setTenantId(tenantId != null ? tenantId : 1); // 默认租户ID为1
// 确保创建时间不为null
if (activityDefinition.getCreateTime() == null) {
activityDefinition.setCreateTime(new java.util.Date());
}
return activityDefinition;
}
}

View File

@@ -2,6 +2,8 @@ package com.openhis.web.datadictionary.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils;
import com.core.common.utils.bean.BeanUtils;
import com.openhis.administration.domain.ChargeItemDefDetail;
import com.openhis.administration.domain.ChargeItemDefinition;
@@ -17,6 +19,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
@@ -47,6 +50,9 @@ public class ItemDefinitionServiceImpl implements IItemDefinitionService {
ChargeItemDefinition chargeItemDefinition = new ChargeItemDefinition();
BeanUtils.copyProperties(itemUpFromDirectoryDto, chargeItemDefinition);
// 显式设置创建者、创建时间和租户ID确保插入时不为null
setRequiredFields(chargeItemDefinition);
boolean insertCIDSuccess = chargeItemDefinitionService.save(chargeItemDefinition);
@@ -86,6 +92,9 @@ public class ItemDefinitionServiceImpl implements IItemDefinitionService {
shargeItemDefDetails.add(chargeItemDefDetail3);
// 批量设置必需字段tenant_id、create_by、create_time
setRequiredFieldsForDetailList(shargeItemDefDetails);
return chargeItemDefDetailService.saveBatch(shargeItemDefDetails);
}
@@ -139,4 +148,55 @@ public class ItemDefinitionServiceImpl implements IItemDefinitionService {
}
/**
* 设置必需的字段tenant_id、create_by、create_time确保插入时不为null
*
* @param chargeItemDefinition 费用定价对象
*/
private void setRequiredFields(ChargeItemDefinition chargeItemDefinition) {
String createBy = "system";
Integer tenantId = null;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
createBy = loginUser.getUsername();
tenantId = loginUser.getTenantId();
}
} catch (Exception e) {
// 如果获取失败,使用默认值
}
chargeItemDefinition.setCreateBy(createBy != null ? createBy : "system");
chargeItemDefinition.setTenantId(tenantId != null ? tenantId : 1);
if (chargeItemDefinition.getCreateTime() == null) {
chargeItemDefinition.setCreateTime(new Date());
}
}
/**
* 批量设置费用定价详情列表的必需字段tenant_id、create_by、create_time
*
* @param chargeItemDefDetailList 费用定价详情对象列表
*/
private void setRequiredFieldsForDetailList(List<ChargeItemDefDetail> chargeItemDefDetailList) {
String createBy = "system";
Integer tenantId = null;
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
createBy = loginUser.getUsername();
tenantId = loginUser.getTenantId();
}
} catch (Exception e) {
// 如果获取失败,使用默认值
}
Date now = new Date();
for (ChargeItemDefDetail detail : chargeItemDefDetailList) {
detail.setCreateBy(createBy != null ? createBy : "system");
detail.setTenantId(tenantId != null ? tenantId : 1);
if (detail.getCreateTime() == null) {
detail.setCreateTime(now);
}
}
}
}

View File

@@ -125,4 +125,9 @@ public class DiagnosisTreatmentDto {
*/
private String priceCode;
/** 序号 */
private Integer sortOrder;
/** 服务范围 */
private String serviceRange;
}

View File

@@ -1,5 +1,6 @@
package com.openhis.web.datadictionary.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.openhis.common.annotation.Dict;
@@ -105,11 +106,26 @@ public class DiagnosisTreatmentUpDto {
private String childrenJson;
/** 划价标记 */
@JsonAlias({"pricing_flag"})
private Integer pricingFlag;
/**
* 兼容前端把勾选框按 boolean 传参true/false的场景
* - true -> 1
* - false -> 0
*/
public void setPricingFlag(Boolean pricingFlag) {
this.pricingFlag = pricingFlag == null ? null : (pricingFlag ? 1 : 0);
}
/**
* 物价编码
*/
private String priceCode;
/** 序号 */
private Integer sortOrder;
/** 服务范围 */
private String serviceRange;
}

View File

@@ -0,0 +1,34 @@
package com.openhis.web.debug;
import com.core.common.core.domain.R;
import com.openhis.web.basedatamanage.appservice.IPractitionerAppService;
import com.openhis.web.basedatamanage.dto.UserAndPractitionerDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 调试控制器 - 用于检查API返回数据
*/
@RestController
@RequestMapping("/debug")
public class DebugController {
@Resource
private IPractitionerAppService practitionerAppService;
/**
* 获取用户及参与者数据用于调试
*/
@GetMapping("/user-practitioner-debug")
public R<UserAndPractitionerDto> getUserPractitionerDebug() {
// 获取第一页第一条数据用于调试
var page = practitionerAppService.getUserPractitionerPage(new UserAndPractitionerDto(), "", 1, 1);
if (page.getRecords() != null && !page.getRecords().isEmpty()) {
return R.ok(page.getRecords().get(0));
}
return R.fail("没有找到数据");
}
}

View File

@@ -1,16 +1,14 @@
package com.openhis.web.doctorstation.appservice;
import com.core.common.core.domain.R;
import com.openhis.template.domain.DoctorPhrase;
import java.util.List;
public interface IDoctorPhraseAppService {
R<?> getDoctorPhraseList();
List<DoctorPhrase> getDoctorPhraseList();
List<DoctorPhrase> searchDoctorPhraseList(String phraseName, Integer phraseType);
Boolean addDoctorPhrase(DoctorPhrase doctorPhrase);
Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase);
Boolean deleteDoctorPhrase(Integer doctorPhraseId);
R<?> searchDoctorPhraseList(String phraseName ,Integer phraseType);
R<?> addDoctorPhrase(DoctorPhrase doctorPhrase);
R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase);
R<?> deleteDoctorPhrase(Integer doctorPhraseId);
}

View File

@@ -58,4 +58,28 @@ public interface IDoctorStationEmrAppService {
* @return 病历详情
*/
R<?> getEmrDetail(Long encounterId);
/**
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
*/
R<?> getPendingEmrList(Long doctorId);
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @return 待写病历数量
*/
R<?> getPendingEmrCount(Long doctorId);
/**
* 检查患者是否需要写病历
*
* @param encounterId 就诊ID
* @return 患者是否需要写病历
*/
R<?> checkNeedWriteEmr(Long encounterId);
}

View File

@@ -50,7 +50,7 @@ public interface IDoctorStationMainAppService {
* @param encounterId 就诊id
* @return 结果
*/
R<?> completeEncounter(Long encounterId);
R<?> completeEncounter(Long encounterId, Integer firstEnum);
/**
* 取消完成
@@ -90,4 +90,10 @@ public interface IDoctorStationMainAppService {
*/
List<ReceptionStatisticsDto> getReceptionStatistics(String startTime,String endTime,Long practitionerId);
/**
* 过号重排
* @param encounterId 就诊ID
* @return 操作结果
*/
R<?> rearrangeMissedEncounter(Long encounterId);
}

View File

@@ -1,83 +0,0 @@
package com.openhis.web.doctorstation.appservice.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.core.common.core.domain.R;
import com.openhis.template.domain.DoctorPhrase;
import com.openhis.template.service.IDoctorPhraseService;
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class DoctorPhraesAppServiceImpl implements IDoctorPhraseAppService {
@Resource
private IDoctorPhraseService doctorPhraseService;
@Override
public R<?> getDoctorPhraseList() {
List<DoctorPhrase> list = doctorPhraseService.list();
return R.ok(list);
}
@Override
public R<?> searchDoctorPhraseList(String phraseName,Integer phraseType) {
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
if (phraseName !=null && ObjectUtil.isNotEmpty(phraseName)) {
wrapper.like(DoctorPhrase::getPhraseName, phraseName);
}
if (phraseType !=null && ObjectUtil.isNotEmpty(phraseType)) {
wrapper.eq(DoctorPhrase::getPhraseType, phraseType);
}
//2.查询
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
return R.ok(list);
}
@Override
public R<?> addDoctorPhrase(DoctorPhrase doctorPhrase) {
//1.数据校验
if(ObjectUtil.isEmpty(doctorPhrase)){
return R.fail("医生常用语不能为空");
}
//2.名称唯一性校验
LambdaUpdateWrapper<DoctorPhrase> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName());
DoctorPhrase one = doctorPhraseService.getOne(wrapper);
if(ObjectUtil.isNotEmpty(one)){
return R.fail("该名称已经存在");
}
//3.新增
boolean save = doctorPhraseService.save(doctorPhrase);
System.out.println(save);
return R.ok(save);
}
@Override
public R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase) {
//1.数据校验
if(ObjectUtil.isEmpty(doctorPhrase)){
return R.fail("医生常用语不能为空");
}
//2.更新
boolean updateById = doctorPhraseService.updateById(doctorPhrase);
return R.ok(updateById);
}
@Override
public R<?> deleteDoctorPhrase(Integer doctorPhraseId) {
//1.数据校验
if(doctorPhraseId == null){
return R.fail("ID不能为空");
}
//2.删除
boolean removeById = doctorPhraseService.removeById(doctorPhraseId);
return R.ok(removeById);
}
}

View File

@@ -0,0 +1,209 @@
package com.openhis.web.doctorstation.appservice.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.utils.SecurityUtils;
import com.openhis.common.enums.BindingType;
import com.openhis.template.domain.DoctorPhrase;
import com.openhis.template.service.IDoctorPhraseService;
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@Service
public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
@Resource
private IDoctorPhraseService doctorPhraseService;
@Override
public List<DoctorPhrase> getDoctorPhraseList() {
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (log.isDebugEnabled()) {
log.debug("orgId: {}", orgId);
}
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
// 1. 获取当前登录用户信息
Long userId = SecurityUtils.getUserId();
// 2. 权限判定:非管理员才需要过滤
// 如果是超级管理员,默认可以看到所有
if (!SecurityUtils.isAdmin(userId)) {
// 3. 获取当前医生的科室编码
String deptCode = "";
if (orgId != null) {
deptCode = String.valueOf(orgId);
}
// final 变量用于 Lambda 表达式
String finalDeptCode = deptCode;
// 4. 核心逻辑:三级数据共享
wrapper.and(w -> w
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
);
}
// 5. 按排序号排序(可选优化,让常用语显示更整齐)
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
return list;
}
@Override
public List<DoctorPhrase> searchDoctorPhraseList(String phraseName,Integer phraseType) {
// 1. 获取当前登录用户信息
Long userId = SecurityUtils.getUserId();
//2.获取到当前医生当前科室的id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (log.isDebugEnabled()) {
log.debug("Search phrase - orgId: {}, phraseName: {}, phraseType: {}", orgId, phraseName, phraseType);
}
String deptCode = "";
if (orgId != null) {
deptCode = String.valueOf(orgId);
}
String finalDeptCode = deptCode;
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
if (phraseName !=null && ObjectUtil.isNotEmpty(phraseName)) {
wrapper.like(DoctorPhrase::getPhraseName, phraseName);
}
if (phraseType !=null && ObjectUtil.isNotEmpty(phraseType)) {
wrapper.eq(DoctorPhrase::getPhraseType, phraseType);
}
Long currentUserId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(currentUserId)) {
// 建议统一使用 staffId 进行业务隔离
wrapper.and(w -> w
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
);
}
//2.查询
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
return list;
}
@Override
public Boolean addDoctorPhrase(DoctorPhrase doctorPhrase) {
// 1. 基础校验
if (ObjectUtil.isEmpty(doctorPhrase) || ObjectUtil.isEmpty(doctorPhrase.getPhraseName())) {
throw new IllegalArgumentException("新增失败:常用语名称不能为空");
}
Long currentUserId = SecurityUtils.getUserId();
/*
* 如果前端没传类型,必须给个默认值(比如 1-个人)
* 否则存成 NULL查询列表时会被过滤掉导致"新增了却看不见"
*/
if (doctorPhrase.getPhraseType() == null) {
doctorPhrase.setPhraseType(BindingType.PERSONAL.getValue());
}
// 2. 注入归属信息
doctorPhrase.setStaffId(currentUserId.intValue());
doctorPhrase.setCreatorId(currentUserId.intValue());
// 注入科室 (处理 null 情况)
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (orgId != null) {
// 检查dept_code字段长度避免数据库错误
String deptCode = String.valueOf(orgId);
if (deptCode.length() > 50) { // 假设字段长度为50根据实际情况调整
// 如果超过字段长度限制,可以考虑截断或抛出有意义的错误
throw new IllegalArgumentException("科室ID过长无法保存");
}
doctorPhrase.setDeptCode(deptCode);
}
// =========== 【修复点 2】查重范围限制在"个人" ===========
// 使用 QueryWrapper 而不是 UpdateWrapper
LambdaQueryWrapper<DoctorPhrase> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName())
.eq(DoctorPhrase::getStaffId, currentUserId.intValue()); // 重点:只查自己名下的!
if (doctorPhraseService.count(queryWrapper) > 0) {
throw new IllegalArgumentException("新增失败:您已存在同名的常用语");
}
return doctorPhraseService.save(doctorPhrase);
}
@Override
public Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase) {
// 1. 基础校验
if (ObjectUtil.isEmpty(doctorPhrase) || doctorPhrase.getId() == null) {
throw new IllegalArgumentException("修改失败ID不能为空");
}
// 2. 查旧数据
DoctorPhrase original = doctorPhraseService.getById(doctorPhrase.getId());
if (original == null) {
throw new IllegalArgumentException("修改失败:该常用语不存在或已被删除");
}
Long currentUserId = SecurityUtils.getUserId();
// 3. 【权限校验优化】
if (!SecurityUtils.isAdmin(currentUserId)) {
// 规则 A严禁修改全院公共模板 (Type=3)
// 这一步是关键!之前你的代码漏了这里,所以普通医生可能改动全院模板
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
throw new SecurityException("无权操作:全院公共常用语仅限管理员修改");
}
// 规则 B严禁修改他人的模板
if (!original.getStaffId().equals(currentUserId.intValue())) {
throw new SecurityException("无权操作:您只能修改自己创建的常用语");
}
}
// 4. 数据保护:防止篡改归属
doctorPhrase.setStaffId(original.getStaffId());
doctorPhrase.setCreatorId(original.getCreatorId());
Long orgId = SecurityUtils.getLoginUser().getOrgId();
if (orgId != null) {
// 检查dept_code字段长度避免数据库错误
String deptCode = String.valueOf(orgId);
if (deptCode.length() > 50) { // 假设字段长度为50根据实际情况调整
throw new IllegalArgumentException("科室ID过长无法保存");
}
doctorPhrase.setDeptCode(deptCode);
}
return doctorPhraseService.updateById(doctorPhrase);
}
@Override
public Boolean deleteDoctorPhrase(Integer doctorPhraseId) {
if (doctorPhraseId == null) {
throw new IllegalArgumentException("删除失败ID不能为空");
}
DoctorPhrase original = doctorPhraseService.getById(doctorPhraseId);
if (original == null) {
throw new IllegalArgumentException("删除失败:数据不存在");
}
Long currentUserId = SecurityUtils.getUserId();
// 权限校验
if (!SecurityUtils.isAdmin(currentUserId)) {
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
throw new SecurityException("无权操作:全院公共常用语仅限管理员删除");
}
if (!original.getStaffId().equals(currentUserId.intValue())) {
throw new SecurityException("无权操作:您只能删除自己创建的常用语");
}
}
return doctorPhraseService.removeById(doctorPhraseId);
}
}

View File

@@ -38,7 +38,9 @@ import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IServiceRequestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
@@ -92,6 +94,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
@Resource
DoctorStationSendApplyUtil doctorStationSendApplyUtil;
/**
* 查询医嘱信息
*
@@ -111,8 +114,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
public IPage<AdviceBaseDto> getAdviceBaseInfo(AdviceBaseDto adviceBaseDto, String searchKey, Long locationId,
List<Long> adviceDefinitionIdParamList, Long organizationId, Integer pageNo, Integer pageSize,
Integer pricingFlag, List<Integer> adviceTypes, String orderPricing) {
// 设置默认科室 (不取前端传的了)
organizationId = SecurityUtils.getLoginUser().getOrgId();
// 生成缓存键处理可能的null值
String safeSearchKey = searchKey != null ? searchKey : "";
String safeAdviceTypesStr = "";
if (adviceTypes != null && !adviceTypes.isEmpty()) {
safeAdviceTypesStr = String.join(",",
adviceTypes.stream().map(String::valueOf).collect(Collectors.toList()));
}
String safeOrganizationId = organizationId != null ? organizationId.toString() : "";
String safePricingFlag = pricingFlag != null ? pricingFlag.toString() : "";
String safePageNo = pageNo != null ? pageNo.toString() : "";
String safePageSize = pageSize != null ? pageSize.toString() : "";
log.info("从数据库查询医嘱基础信息");
// 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室
// 否则会导致门诊划价等场景(按患者挂号科室查询)返回空
if (organizationId == null) {
organizationId = SecurityUtils.getLoginUser().getOrgId();
}
// 医嘱定价来源
String orderPricingSource = TenantOptionUtil.getOptionContent(TenantOptionDict.ORDER_PRICING_SOURCE);
@@ -122,8 +143,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
orderPricingSource = orderPricing;
}
// 开药时药房允许多选开关
String pharmacyMultipleChoiceSwitch
= TenantOptionUtil.getOptionContent(TenantOptionDict.PHARMACY_MULTIPLE_CHOICE_SWITCH);
String pharmacyMultipleChoiceSwitch = TenantOptionUtil
.getOptionContent(TenantOptionDict.PHARMACY_MULTIPLE_CHOICE_SWITCH);
// 药房允许多选
boolean pharmacyMultipleChoice = Whether.YES.getCode().equals(pharmacyMultipleChoiceSwitch);
@@ -133,7 +154,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
IPage<AdviceBaseDto> adviceBaseInfo = doctorStationAdviceAppMapper.getAdviceBaseInfo(
new Page<>(pageNo, pageSize), PublicationStatus.ACTIVE.getValue(), organizationId,
CommonConstants.TableName.MED_MEDICATION_DEFINITION, CommonConstants.TableName.ADM_DEVICE_DEFINITION,
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, pricingFlag, adviceDefinitionIdParamList, adviceTypes,
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, pricingFlag, adviceDefinitionIdParamList,
adviceTypes,
queryWrapper);
List<AdviceBaseDto> adviceBaseDtoList = adviceBaseInfo.getRecords();
@@ -143,22 +165,22 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
// 医嘱定义ID集合
List<Long> adviceDefinitionIdList
= adviceBaseDtoList.stream().map(AdviceBaseDto::getAdviceDefinitionId).collect(Collectors.toList());
List<Long> adviceDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getAdviceDefinitionId)
.collect(Collectors.toList());
// 费用定价主表ID集合
List<Long> chargeItemDefinitionIdList
= adviceBaseDtoList.stream().map(AdviceBaseDto::getChargeItemDefinitionId).collect(Collectors.toList());
List<Long> chargeItemDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getChargeItemDefinitionId)
.collect(Collectors.toList());
// 医嘱库存集合
List<AdviceInventoryDto> adviceInventoryList
= doctorStationAdviceAppMapper.getAdviceInventory(locationId, adviceDefinitionIdList,
CommonConstants.SqlCondition.ABOUT_INVENTORY_TABLE_STR, PublicationStatus.ACTIVE.getValue());
List<AdviceInventoryDto> adviceInventoryList = doctorStationAdviceAppMapper.getAdviceInventory(locationId,
adviceDefinitionIdList,
CommonConstants.SqlCondition.ABOUT_INVENTORY_TABLE_STR, PublicationStatus.ACTIVE.getValue());
// 待发放个数信息
List<AdviceInventoryDto> adviceDraftInventoryList = doctorStationAdviceAppMapper.getAdviceDraftInventory(
CommonConstants.TableName.MED_MEDICATION_DEFINITION, CommonConstants.TableName.ADM_DEVICE_DEFINITION,
DispenseStatus.DRAFT.getValue(), DispenseStatus.PREPARATION.getValue());
// 预减库存
List<AdviceInventoryDto> adviceInventory
= adviceUtils.subtractInventory(adviceInventoryList, adviceDraftInventoryList);
List<AdviceInventoryDto> adviceInventory = adviceUtils.subtractInventory(adviceInventoryList,
adviceDraftInventoryList);
// 查询取药科室配置
List<AdviceInventoryDto> medLocationConfig = doctorStationAdviceAppMapper.getMedLocationConfig(organizationId);
// 将配置转为 {categoryCode -> 允许的locationId集合}
@@ -172,98 +194,197 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.add(cfg.getLocationId());
}
}
// 费用定价子表信息
List<AdvicePriceDto> childCharge = doctorStationAdviceAppMapper
.getChildCharge(ConditionCode.LOT_NUMBER_PRICE.getCode(), chargeItemDefinitionIdList);
// 费用定价主表信息
List<AdvicePriceDto> mainCharge
= doctorStationAdviceAppMapper.getMainCharge(chargeItemDefinitionIdList, PublicationStatus.ACTIVE.getValue());
// 费用定价子表信息 - 使用分批处理避免大量参数问题
List<AdvicePriceDto> childCharge = new ArrayList<>();
if (chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
// 分批处理每批最多1000个ID增加批次大小以减少查询次数
int batchSize = 1000;
for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, chargeItemDefinitionIdList.size());
List<Long> batch = chargeItemDefinitionIdList.subList(i, endIndex);
childCharge.addAll(doctorStationAdviceAppMapper
.getChildCharge(ConditionCode.LOT_NUMBER_PRICE.getCode(), batch));
}
}
// 费用定价主表信息 - 使用分批处理避免大量参数问题
List<AdvicePriceDto> mainCharge = new ArrayList<>();
if (chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
// 分批处理每批最多500个ID
int batchSize = 500;
for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, chargeItemDefinitionIdList.size());
List<Long> batch = chargeItemDefinitionIdList.subList(i, endIndex);
mainCharge
.addAll(doctorStationAdviceAppMapper.getMainCharge(batch, PublicationStatus.ACTIVE.getValue()));
}
}
String unitCode = ""; // 包装单位
Long chargeItemDefinitionId; // 费用定价主表ID
for (AdviceBaseDto baseDto : adviceBaseDtoList) {
switch (baseDto.getAdviceTableName()) {
case CommonConstants.TableName.MED_MEDICATION_DEFINITION: // 药品
// 是否皮试
baseDto
.setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getSkinTestFlag()));
// 是否为注射药物
baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag()));
case CommonConstants.TableName.ADM_DEVICE_DEFINITION: // 耗材
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
List<AdviceInventoryDto> inventoryList = adviceInventory.stream().filter(e -> baseDto
.getAdviceDefinitionId().equals(e.getItemId())
&& baseDto.getAdviceTableName().equals(e.getItemTable())
&& (pharmacyMultipleChoice
|| (baseDto.getPositionId() == null || baseDto.getPositionId().equals(e.getLocationId()))))
.collect(Collectors.toList());
// 库存信息
baseDto.setInventoryList(inventoryList);
// 设置默认产品批号
if (!inventoryList.isEmpty()) {
// 库存大于0
List<AdviceInventoryDto> hasInventoryList = inventoryList.stream()
.filter(e -> e.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList());
if (!hasInventoryList.isEmpty()) {
baseDto.setDefaultLotNumber(hasInventoryList.get(0).getLotNumber());
}
}
if (!inventoryList.isEmpty() && !medLocationConfig.isEmpty()) {
// 第一步在medLocationConfig中匹配categoryCode
AdviceInventoryDto result1 = medLocationConfig.stream()
.filter(dto -> baseDto.getCategoryCode().equals(dto.getCategoryCode())).findFirst()
.orElse(null);
if (result1 != null) {
// 第二步在inventoryList中匹配locationId
AdviceInventoryDto result2 = inventoryList.stream()
.filter(dto -> result1.getLocationId().equals(dto.getLocationId())).findFirst()
.orElse(null);
if (result2 != null && result2.getLotNumber() != null) {
baseDto.setDefaultLotNumber(result2.getLotNumber());
}
}
}
String tableName = baseDto.getAdviceTableName();
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(tableName)) { // 药品
// 是否皮试
baseDto
.setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getSkinTestFlag()));
// 是否为注射药物
baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag()));
unitCode = baseDto.getUnitCode();
chargeItemDefinitionId = baseDto.getChargeItemDefinitionId();
List<AdvicePriceDto> priceDtoList = new ArrayList<>();
// 库存信息里取 命中条件 去匹配价格
for (AdviceInventoryDto adviceInventoryDto : inventoryList) {
Long finalChargeItemDefinitionId = chargeItemDefinitionId;
String finalUnitCode = unitCode;
// 从定价子表取价格(适用于批次售卖场景)
List<AdvicePriceDto> childPrice = childCharge.stream()
.filter(e -> e.getDefinitionId().equals(finalChargeItemDefinitionId)
&& e.getConditionValue().equals(adviceInventoryDto.getLotNumber()))
.peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode
.collect(Collectors.toList());
// 从定价主表取价格(适用于统一零售价场景)
List<AdvicePriceDto> mainPrice = mainCharge.stream()
.filter(e -> baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId()))
.collect(Collectors.toList());
// 按批次售价
if (OrderPricingSource.BATCH_SELLING_PRICE.getCode().equals(orderPricingSource)) {
priceDtoList.addAll(childPrice);
} else {
priceDtoList.addAll(mainPrice);
// fallthrough to 耗材处理逻辑(保持原有逻辑)
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
List<AdviceInventoryDto> inventoryList = adviceInventory
.stream().filter(e -> baseDto.getAdviceDefinitionId() != null && e.getItemId() != null
&& baseDto.getAdviceDefinitionId().equals(e.getItemId())
&& baseDto.getAdviceTableName() != null && e.getItemTable() != null
&& baseDto.getAdviceTableName().equals(e.getItemTable())
&& (pharmacyMultipleChoice
|| (baseDto.getPositionId() == null || (e.getLocationId() != null
&& baseDto.getPositionId().equals(e.getLocationId())))))
.collect(Collectors.toList());
// 库存信息
baseDto.setInventoryList(inventoryList);
// 设置默认产品批号
if (!inventoryList.isEmpty()) {
// 库存大于0
List<AdviceInventoryDto> hasInventoryList = inventoryList.stream()
.filter(e -> e.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList());
if (!hasInventoryList.isEmpty()) {
baseDto.setDefaultLotNumber(hasInventoryList.get(0).getLotNumber());
}
}
if (!inventoryList.isEmpty() && !medLocationConfig.isEmpty()) {
// 第一步在medLocationConfig中匹配categoryCode
AdviceInventoryDto result1 = medLocationConfig.stream()
.filter(dto -> baseDto.getCategoryCode() != null && dto.getCategoryCode() != null
&& baseDto.getCategoryCode().equals(dto.getCategoryCode()))
.findFirst()
.orElse(null);
if (result1 != null) {
// 第二步在inventoryList中匹配locationId
AdviceInventoryDto result2 = inventoryList.stream()
.filter(dto -> result1.getLocationId() != null && dto.getLocationId() != null
&& result1.getLocationId().equals(dto.getLocationId()))
.findFirst()
.orElse(null);
if (result2 != null && result2.getLotNumber() != null) {
baseDto.setDefaultLotNumber(result2.getLotNumber());
}
}
// 价格信息
baseDto.setPriceList(priceDtoList);
break;
case CommonConstants.TableName.WOR_ACTIVITY_DEFINITION: // 诊疗
List<AdvicePriceDto> priceList
= mainCharge.stream().filter(e -> baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId()))
.collect(Collectors.toList());
// 价格信息
baseDto.setPriceList(priceList);
// 活动类型
baseDto.setActivityType_enumText(
EnumUtils.getInfoByValue(ActivityType.class, baseDto.getActivityType()));
break;
default:
break;
}
unitCode = baseDto.getUnitCode();
chargeItemDefinitionId = baseDto.getChargeItemDefinitionId();
List<AdvicePriceDto> priceDtoList = new ArrayList<>();
// 库存信息里取 命中条件 去匹配价格
for (AdviceInventoryDto adviceInventoryDto : inventoryList) {
Long finalChargeItemDefinitionId = chargeItemDefinitionId;
String finalUnitCode = unitCode;
// 从定价子表取价格(适用于批次售卖场景)
List<AdvicePriceDto> childPrice = childCharge.stream()
.filter(e -> e.getDefinitionId() != null && finalChargeItemDefinitionId != null
&& e.getDefinitionId().equals(finalChargeItemDefinitionId)
&& e.getConditionValue() != null && adviceInventoryDto.getLotNumber() != null
&& e.getConditionValue().equals(adviceInventoryDto.getLotNumber()))
.peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode
.collect(Collectors.toList());
// 从定价主表取价格(适用于统一零售价场景)
List<AdvicePriceDto> mainPrice = mainCharge.stream()
.filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null
&& baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId()))
.collect(Collectors.toList());
// 按批次售价
if (OrderPricingSource.BATCH_SELLING_PRICE.getCode().equals(orderPricingSource)) {
priceDtoList.addAll(childPrice);
} else {
priceDtoList.addAll(mainPrice);
}
}
// 价格信息
baseDto.setPriceList(priceDtoList);
} else if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(tableName)) { // 耗材
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
List<AdviceInventoryDto> inventoryList = adviceInventory
.stream().filter(e -> baseDto.getAdviceDefinitionId() != null && e.getItemId() != null
&& baseDto.getAdviceDefinitionId().equals(e.getItemId())
&& baseDto.getAdviceTableName() != null && e.getItemTable() != null
&& baseDto.getAdviceTableName().equals(e.getItemTable())
&& (pharmacyMultipleChoice
|| (baseDto.getPositionId() == null || (e.getLocationId() != null
&& baseDto.getPositionId().equals(e.getLocationId())))))
.collect(Collectors.toList());
// 库存信息
baseDto.setInventoryList(inventoryList);
// 设置默认产品批号
if (!inventoryList.isEmpty()) {
// 库存大于0
List<AdviceInventoryDto> hasInventoryList = inventoryList.stream()
.filter(e -> e.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList());
if (!hasInventoryList.isEmpty()) {
baseDto.setDefaultLotNumber(hasInventoryList.get(0).getLotNumber());
}
}
if (!inventoryList.isEmpty() && !medLocationConfig.isEmpty()) {
// 第一步在medLocationConfig中匹配categoryCode
AdviceInventoryDto result1 = medLocationConfig.stream()
.filter(dto -> baseDto.getCategoryCode() != null && dto.getCategoryCode() != null
&& baseDto.getCategoryCode().equals(dto.getCategoryCode()))
.findFirst()
.orElse(null);
if (result1 != null) {
// 第二步在inventoryList中匹配locationId
AdviceInventoryDto result2 = inventoryList.stream()
.filter(dto -> result1.getLocationId() != null && dto.getLocationId() != null
&& result1.getLocationId().equals(dto.getLocationId()))
.findFirst()
.orElse(null);
if (result2 != null && result2.getLotNumber() != null) {
baseDto.setDefaultLotNumber(result2.getLotNumber());
}
}
}
unitCode = baseDto.getUnitCode();
chargeItemDefinitionId = baseDto.getChargeItemDefinitionId();
List<AdvicePriceDto> priceDtoList = new ArrayList<>();
// 库存信息里取 命中条件 去匹配价格
for (AdviceInventoryDto adviceInventoryDto : inventoryList) {
Long finalChargeItemDefinitionId = chargeItemDefinitionId;
String finalUnitCode = unitCode;
// 从定价子表取价格(适用于批次售卖场景)
List<AdvicePriceDto> childPrice = childCharge.stream()
.filter(e -> e.getDefinitionId() != null && finalChargeItemDefinitionId != null
&& e.getDefinitionId().equals(finalChargeItemDefinitionId)
&& e.getConditionValue() != null && adviceInventoryDto.getLotNumber() != null
&& e.getConditionValue().equals(adviceInventoryDto.getLotNumber()))
.peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode
.collect(Collectors.toList());
// 从定价主表取价格(适用于统一零售价场景)
List<AdvicePriceDto> mainPrice = mainCharge.stream()
.filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null
&& baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId()))
.collect(Collectors.toList());
// 按批次售价
if (OrderPricingSource.BATCH_SELLING_PRICE.getCode().equals(orderPricingSource)) {
priceDtoList.addAll(childPrice);
} else {
priceDtoList.addAll(mainPrice);
}
}
// 价格信息
baseDto.setPriceList(priceDtoList);
} else if (CommonConstants.TableName.WOR_ACTIVITY_DEFINITION.equals(tableName)) { // 诊疗
List<AdvicePriceDto> priceList = mainCharge.stream()
.filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null
&& baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId()))
.collect(Collectors.toList());
// 价格信息
baseDto.setPriceList(priceList);
// 活动类型
baseDto.setActivityType_enumText(
EnumUtils.getInfoByValue(ActivityType.class, baseDto.getActivityType()));
}
}
return adviceBaseInfo;
}
@@ -271,7 +392,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
* 查询医嘱绑定信息
*
* @param typeCode 1:用法绑东西 2:诊疗绑东西
* @param itemNo 用法的code 或者 诊疗定义id
* @param itemNo 用法的code 或者 诊疗定义id
* @return 医嘱绑定信息
*/
@Override
@@ -283,88 +404,107 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
* 门诊保存/签发医嘱
*
* @param adviceSaveParam 医嘱表单信息
* @param adviceOpType 医嘱操作类型
* @param adviceOpType 医嘱操作类型
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveAdvice(AdviceSaveParam adviceSaveParam, String adviceOpType) {
// 患者挂号对应的科室id
Long organizationId = adviceSaveParam.getOrganizationId();
// 医嘱分类信息
List<AdviceSaveDto> adviceSaveList = adviceSaveParam.getAdviceSaveList();
// 药品
List<AdviceSaveDto> medicineList = adviceSaveList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 耗材
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 诊疗活动
List<AdviceSaveDto> activityList = adviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
try {
// 患者挂号对应的科室id
Long organizationId = adviceSaveParam.getOrganizationId();
// 医嘱分类信息
List<AdviceSaveDto> adviceSaveList = adviceSaveParam.getAdviceSaveList();
// 药品
List<AdviceSaveDto> medicineList = adviceSaveList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 耗材
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 诊疗活动
List<AdviceSaveDto> activityList = adviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
/**
* 保存时,校验库存
*/
if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) {
List<AdviceSaveDto> medUpdateList
= medicineList.stream().filter(e -> e.getRequestId() != null).collect(Collectors.toList());
List<AdviceSaveDto> devUpdateList
= deviceList.stream().filter(e -> e.getRequestId() != null).collect(Collectors.toList());
// 编辑时,释放本身占用的库存发放
for (AdviceSaveDto adviceSaveDto : medUpdateList) {
iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId());
/**
* 保存时,校验库存
*/
if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) {
List<AdviceSaveDto> medUpdateList = medicineList.stream().filter(e -> e.getRequestId() != null)
.collect(Collectors.toList());
List<AdviceSaveDto> devUpdateList = deviceList.stream().filter(e -> e.getRequestId() != null)
.collect(Collectors.toList());
// 编辑时,释放本身占用的库存发放
for (AdviceSaveDto adviceSaveDto : medUpdateList) {
iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId());
}
for (AdviceSaveDto adviceSaveDto : devUpdateList) {
iDeviceDispenseService.deleteDeviceDispense(adviceSaveDto.getRequestId());
}
List<AdviceSaveDto> needCheckList = adviceSaveList.stream()
.filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType())
&& !ItemType.ACTIVITY.getValue().equals(e.getAdviceType()))
.collect(Collectors.toList());
// 校验库存
String tipRes = adviceUtils.checkInventory(needCheckList);
if (tipRes != null) {
return R.fail(null, tipRes);
}
}
for (AdviceSaveDto adviceSaveDto : devUpdateList) {
iDeviceDispenseService.deleteDeviceDispense(adviceSaveDto.getRequestId());
// 当前时间
Date curDate = new Date();
// 医嘱签发编码
String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), 10);
/**
* 处理药品请求
*/
List<String> medRequestIdList = this.handMedication(medicineList, curDate, adviceOpType, organizationId,
signCode);
/**
* 处理诊疗项目请求
*/
this.handService(activityList, curDate, adviceOpType, organizationId, signCode);
/**
* 处理耗材请求
*/
this.handDevice(deviceList, curDate, adviceOpType);
// 签发时,把草稿状态的账单更新为待收费
if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !adviceSaveList.isEmpty()) {
// 签发的医嘱id集合
List<Long> requestIds = adviceSaveList.stream()
.filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null)
.collect(Collectors.toList()).stream().map(AdviceSaveDto::getRequestId)
.collect(Collectors.toList());
// 就诊id
Long encounterId = adviceSaveList.get(0).getEncounterId();
iChargeItemService.update(new LambdaUpdateWrapper<ChargeItem>()
.set(ChargeItem::getStatusEnum, ChargeItemStatus.PLANNED.getValue())
.eq(ChargeItem::getEncounterId, encounterId)
.eq(ChargeItem::getStatusEnum, ChargeItemStatus.DRAFT.getValue())
.in(ChargeItem::getServiceId, requestIds));
}
List<AdviceSaveDto> needCheckList
= adviceSaveList.stream().filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType())
&& !ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 校验库存
String tipRes = adviceUtils.checkInventory(needCheckList);
if (tipRes != null) {
return R.fail(null, tipRes);
}
// 数据变更后清理相关缓存
clearRelatedCache();
return R.ok(medRequestIdList,
MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] { "门诊医嘱" }));
} catch (Exception e) {
// 异常处理
throw e;
}
// 当前时间
Date curDate = new Date();
// 医嘱签发编码
String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), 10);
}
/**
* 处理药品请求
*/
List<String> medRequestIdList
= this.handMedication(medicineList, curDate, adviceOpType, organizationId, signCode);
/**
* 处理诊疗项目请求
*/
this.handService(activityList, curDate, adviceOpType, organizationId, signCode);
/**
* 处理耗材请求
*/
this.handDevice(deviceList, curDate, adviceOpType);
// 签发时,把草稿状态的账单更新为待收费
if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !adviceSaveList.isEmpty()) {
// 签发的医嘱id集合
List<Long> requestIds = adviceSaveList.stream()
.filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null)
.collect(Collectors.toList()).stream().map(AdviceSaveDto::getRequestId).collect(Collectors.toList());
// 就诊id
Long encounterId = adviceSaveList.get(0).getEncounterId();
iChargeItemService.update(new LambdaUpdateWrapper<ChargeItem>()
.set(ChargeItem::getStatusEnum, ChargeItemStatus.PLANNED.getValue())
.eq(ChargeItem::getEncounterId, encounterId)
.eq(ChargeItem::getStatusEnum, ChargeItemStatus.DRAFT.getValue())
.in(ChargeItem::getServiceId, requestIds));
}
return R.ok(medRequestIdList,
MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"门诊医嘱"}));
/**
* 清理相关缓存
*/
private void clearRelatedCache() {
// 目前不使用缓存,此方法为空实现
// 如果将来启用缓存,可以在这里实现缓存清理逻辑
}
/**
@@ -374,6 +514,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
Long organizationId, String signCode) {
// 当前登录账号的科室id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
// 获取当前登录用户的tenantId
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 获取当前登录用户名
String currentUsername = SecurityUtils.getUsername();
// 保存操作
boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType);
// 签发操作
@@ -383,9 +527,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 声明费用项
ChargeItem chargeItem;
// 新增 + 修改
List<AdviceSaveDto> insertOrUpdateList
= medicineList.stream().filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType()))).collect(Collectors.toList());
List<AdviceSaveDto> insertOrUpdateList = medicineList.stream()
.filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 删除
List<AdviceSaveDto> deleteList = medicineList.stream()
.filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList());
@@ -410,7 +555,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
adviceSaveDto.getRequestId());
// 删除基于这个药品生成的需要执行的诊疗请求
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getBasedOnId, adviceSaveDto.getRequestId())
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getBasedOnId, adviceSaveDto.getRequestId())
.isNotNull(ServiceRequest::getBasedOnTable)
.eq(ServiceRequest::getStatusEnum, RequestStatus.COMPLETED.getValue()));
}
@@ -428,6 +574,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
medicationRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态
medicationRequest.setPrescriptionNo(adviceSaveDto.getPrescriptionNo()); // 处方号
medicationRequest.setGroupId(adviceSaveDto.getGroupId()); // 组号
medicationRequest.setTenantId(tenantId); // 设置租户id
medicationRequest.setCreateBy(currentUsername); // 设置创建人
medicationRequest.setCreateTime(curDate); // 设置创建时间
if (is_sign) {
medicationRequest.setSignCode(signCode);
}
@@ -472,8 +621,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
if (is_save) {
// 处理药品发放
Long dispenseId
= iMedicationDispenseService.handleMedicationDispense(medicationRequest, adviceSaveDto.getDbOpType());
Long dispenseId = iMedicationDispenseService.handleMedicationDispense(medicationRequest,
adviceSaveDto.getDbOpType());
// 保存药品费用项
chargeItem = new ChargeItem();
@@ -498,12 +647,20 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setConditionId(adviceSaveDto.getConditionId()); // 诊断id
chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
chargeItem.setDispenseId(dispenseId); // 发放ID
chargeItem.setTenantId(tenantId); // 设置租户ID (修复本次报错)
chargeItem.setCreateBy(currentUsername); // 设置创建人
chargeItem.setCreateTime(curDate); // 设置创建时间
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
// 显式设置tenantId、createBy和createTime字段防止自动填充机制失效
chargeItem.setTenantId(SecurityUtils.getLoginUser().getTenantId());
chargeItem.setCreateBy(SecurityUtils.getLoginUser().getUsername());
chargeItem.setCreateTime(new Date());
iChargeItemService.saveOrUpdate(chargeItem);
}
@@ -517,6 +674,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
private void handDevice(List<AdviceSaveDto> deviceList, Date curDate, String adviceOpType) {
// 当前登录账号的科室id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
// 获取当前登录用户的tenantId
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 获取当前登录用户名
String currentUsername = SecurityUtils.getUsername();
// 保存操作
boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType);
// 签发操作
@@ -525,9 +686,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 声明费用项
ChargeItem chargeItem;
// 新增 + 修改
List<AdviceSaveDto> insertOrUpdateList
= deviceList.stream().filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType()))).collect(Collectors.toList());
List<AdviceSaveDto> insertOrUpdateList = deviceList.stream()
.filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 删除
List<AdviceSaveDto> deleteList = deviceList.stream()
.filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList());
@@ -556,6 +718,13 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest = new DeviceRequest();
deviceRequest.setId(adviceSaveDto.getRequestId()); // 主键id
deviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态
deviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
// 显式设置审计字段,防止自动填充机制失效
deviceRequest.setCreateBy(SecurityUtils.getLoginUser().getUsername());
deviceRequest.setCreateTime(new Date());
deviceRequest.setTenantId(tenantId); // 设置租户id
deviceRequest.setCreateBy(currentUsername); // 设置创建人
deviceRequest.setCreateTime(curDate); // 设置创建时间
// 保存时,处理数据(请求,发放,账单)
if (is_save) {
@@ -585,12 +754,15 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
iDeviceRequestService.saveOrUpdate(deviceRequest);
if (is_save) {
// 处理耗材发放
Long dispenseId
= iDeviceDispenseService.handleDeviceDispense(deviceRequest, adviceSaveDto.getDbOpType());
Long dispenseId = iDeviceDispenseService.handleDeviceDispense(deviceRequest,
adviceSaveDto.getDbOpType());
// 保存耗材费用项
chargeItem = new ChargeItem();
chargeItem.setId(adviceSaveDto.getChargeItemId()); // 费用项id
chargeItem.setTenantId(tenantId); // 补全租户ID
chargeItem.setCreateBy(currentUsername); // 补全创建人
chargeItem.setCreateTime(curDate); // 补全创建时间
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(deviceRequest.getBusNo()));
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
@@ -616,6 +788,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
// 显式设置审计字段,防止自动填充机制失效
chargeItem.setTenantId(SecurityUtils.getLoginUser().getTenantId());
chargeItem.setCreateBy(SecurityUtils.getLoginUser().getUsername());
chargeItem.setCreateTime(new Date());
iChargeItemService.saveOrUpdate(chargeItem);
}
}
@@ -628,6 +805,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
String signCode) {
// 当前登录账号的科室id
Long orgId = SecurityUtils.getLoginUser().getOrgId();
// 获取当前登录用户的tenantId
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
// 获取当前登录用户名
String currentUsername = SecurityUtils.getUsername();
// 保存操作
boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType);
// 签发操作
@@ -636,9 +817,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 声明费用项
ChargeItem chargeItem;
// 新增 + 修改
List<AdviceSaveDto> insertOrUpdateList
= activityList.stream().filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType()))).collect(Collectors.toList());
List<AdviceSaveDto> insertOrUpdateList = activityList.stream()
.filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType())
|| DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 删除
List<AdviceSaveDto> deleteList = activityList.stream()
.filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList());
@@ -657,7 +839,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
for (AdviceSaveDto adviceSaveDto : deleteList) {
iServiceRequestService.removeById(adviceSaveDto.getRequestId());// 删除诊疗
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId, adviceSaveDto.getRequestId()));// 删除诊疗套餐对应的子项
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId,
adviceSaveDto.getRequestId()));// 删除诊疗套餐对应的子项
// 删除费用项
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_SERVICE_REQUEST,
adviceSaveDto.getRequestId());
@@ -667,6 +850,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest = new ServiceRequest();
serviceRequest.setId(adviceSaveDto.getRequestId()); // 主键id
serviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue());// 请求状态
serviceRequest.setTenantId(tenantId); // 设置租户id
serviceRequest.setCreateBy(currentUsername); // 设置创建人
serviceRequest.setCreateTime(curDate); // 设置创建时间
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
if (is_sign) {
serviceRequest.setSignCode(signCode);
}
@@ -704,6 +891,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
if (is_save) {
chargeItem = new ChargeItem();
chargeItem.setId(adviceSaveDto.getChargeItemId()); // 费用项id
chargeItem.setTenantId(tenantId); // 补全租户ID
chargeItem.setCreateBy(currentUsername); // 补全创建人
chargeItem.setCreateTime(curDate); // 补全创建时间
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(serviceRequest.getBusNo()));
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
@@ -956,6 +1146,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
*/
@Override
public R<?> getProofResult(Long encounterId) {
// 检查参数
if (encounterId == null) {
log.warn("获取检验结果时就诊ID为空");
return R.ok(new ArrayList<>());
}
// LIS查看报告地址
String lisReportUrl = TenantOptionUtil.getOptionContent(TenantOptionDict.LIS_REPORT_URL);
if (StringUtils.isEmpty(lisReportUrl)) {
@@ -980,6 +1176,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
*/
@Override
public R<?> getTestResult(Long encounterId) {
// 检查参数
if (encounterId == null) {
log.warn("获取检查结果时就诊ID为空");
return R.ok(new ArrayList<>());
}
// PACS查看报告地址
String pacsReportUrl = TenantOptionUtil.getOptionContent(TenantOptionDict.PACS_REPORT_URL);
if (StringUtils.isEmpty(pacsReportUrl)) {

View File

@@ -187,6 +187,12 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
condition.setTcmFlag(Whether.YES.getValue());// 中医标识
condition.setRecordedDatetime(new Date());
condition.setRecorderId(SecurityUtils.getLoginUser().getPractitionerId());// 记录人
// 设置租户ID避免数据库约束错误
condition.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
condition.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
condition.setCreateTime(new Date());
iConditionService.saveOrUpdate(condition);
saveDiagnosisChildParam.setConditionId(condition.getId());
}
@@ -225,6 +231,12 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
encounterDiagnosis.setIptDiseTypeCode(saveDiagnosisChildParam.getIptDiseTypeCode()); // 患者疾病诊断类型代码
encounterDiagnosis.setTcmFlag(Whether.YES.getValue());// 中医标识
encounterDiagnosis.setSyndromeGroupNo(saveDiagnosisChildParam.getSyndromeGroupNo());// 中医证候组号
// 设置租户ID避免数据库约束错误
encounterDiagnosis.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
encounterDiagnosis.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
if (saveDiagnosisChildParam.getDiagSrtNo() == null) {
if (i == 1) {
@@ -268,6 +280,12 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
condition.setTcmFlag(Whether.YES.getValue());// 中医标识
condition.setRecordedDatetime(new Date());
condition.setRecorderId(SecurityUtils.getLoginUser().getPractitionerId());// 记录人
// 设置租户ID避免数据库约束错误
condition.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
condition.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
condition.setCreateTime(new Date());
iConditionService.saveOrUpdate(condition);
saveDiagnosisChildParam.setConditionId(condition.getId());
}
@@ -289,6 +307,12 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
encounterDiagnosis.setIptDiseTypeCode(saveDiagnosisChildParam.getIptDiseTypeCode()); // 患者疾病诊断类型代码
encounterDiagnosis.setTcmFlag(Whether.YES.getValue());// 中医标识
encounterDiagnosis.setSyndromeGroupNo(saveDiagnosisChildParam.getSyndromeGroupNo());// 中医证候组号
// 设置租户ID避免数据库约束错误
encounterDiagnosis.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
encounterDiagnosis.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"中医诊断"}));
@@ -493,6 +517,7 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
medicationRequest.setDosageInstruction(adviceSaveDto.getDosageInstruction()); // 用药说明 , 即煎法
}
iMedicationRequestService.saveOrUpdate(medicationRequest);
adviceSaveDto.setRequestId(medicationRequest.getId());
if (is_save) {
// 处理药品发放
Long dispenseId
@@ -608,11 +633,13 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
List<Long> requestIds = medicineList.stream()
.filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null)
.collect(Collectors.toList()).stream().map(AdviceSaveDto::getRequestId).collect(Collectors.toList());
iChargeItemService.update(new LambdaUpdateWrapper<ChargeItem>()
.set(ChargeItem::getStatusEnum, ChargeItemStatus.PLANNED.getValue())
.eq(ChargeItem::getEncounterId, encounterId)
.eq(ChargeItem::getStatusEnum, ChargeItemStatus.DRAFT.getValue())
.in(ChargeItem::getServiceId, requestIds));
if (!requestIds.isEmpty()) {
iChargeItemService.update(new LambdaUpdateWrapper<ChargeItem>()
.set(ChargeItem::getStatusEnum, ChargeItemStatus.PLANNED.getValue())
.eq(ChargeItem::getEncounterId, encounterId)
.eq(ChargeItem::getStatusEnum, ChargeItemStatus.DRAFT.getValue())
.in(ChargeItem::getServiceId, requestIds));
}
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"门诊中医医嘱"}));

View File

@@ -215,6 +215,12 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
condition.setYbNo(saveDiagnosisChildParam.getYbNo());
condition.setRecordedDatetime(new Date());
condition.setRecorderId(SecurityUtils.getLoginUser().getPractitionerId());// 记录人
// 设置租户ID避免数据库约束错误
condition.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
condition.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
condition.setCreateTime(new Date());
iConditionService.saveOrUpdate(condition);
saveDiagnosisChildParam.setConditionId(condition.getId());
}
@@ -230,6 +236,12 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
encounterDiagnosis.setMedTypeCode(saveDiagnosisChildParam.getMedTypeCode());// 医疗类型
encounterDiagnosis.setDiagnosisDesc(saveDiagnosisChildParam.getDiagnosisDesc()); // 诊断描述
encounterDiagnosis.setIptDiseTypeCode(saveDiagnosisChildParam.getIptDiseTypeCode()); // 患者疾病诊断类型代码
// 设置租户ID避免数据库约束错误
encounterDiagnosis.setTenantId(SecurityUtils.getLoginUser().getTenantId());
// 设置创建人,避免数据库约束错误
encounterDiagnosis.setCreateBy(SecurityUtils.getLoginUser().getUsername());
// 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
@@ -260,7 +272,8 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
.eq(EncounterDiagnosis::getEncounterId, encounterId)
.set(EncounterDiagnosis::getMaindiseFlag, 0));
}
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
String username = SecurityUtils.getUsername();
// 保存诊断管理
Condition condition;
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
@@ -277,13 +290,24 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
condition.setYbNo(saveDiagnosisChildParam.getYbNo());
condition.setRecordedDatetime(new Date());
condition.setRecorderId(SecurityUtils.getLoginUser().getPractitionerId());// 记录人
if(condition.getCreateBy() == null){
condition.setCreateBy(username);
}
condition.setUpdateBy(username);
condition.setTenantId(tenantId);
if(condition.getCreateTime() == null){
condition.setCreateTime(new Date());
}
condition.setUpdateTime(new Date());
iConditionService.saveOrUpdate(condition);
saveDiagnosisChildParam.setConditionId(condition.getId());
}
// 保存就诊诊断
EncounterDiagnosis encounterDiagnosis;
EncounterDiagnosis encounterDiagnosis = null;
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
if (saveDiagnosisChildParam.getUpdateId() != null) {
String updateId = saveDiagnosisChildParam.getUpdateId();
@@ -306,6 +330,19 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
encounterDiagnosis.setDiagnosisDesc(saveDiagnosisChildParam.getDiagnosisDesc()); // 诊断描述
encounterDiagnosis.setIptDiseTypeCode(saveDiagnosisChildParam.getIptDiseTypeCode()); // 患者疾病诊断类型代码
encounterDiagnosis.setTcmFlag(Whether.YES.getValue());// 中医标识
encounterDiagnosis.setDoctor(saveDiagnosisChildParam.getDiagnosisDoctor());
encounterDiagnosis.setClassification(saveDiagnosisChildParam.getClassification());
encounterDiagnosis.setName(saveDiagnosisChildParam.getName());
encounterDiagnosis.setTenantId(tenantId);
encounterDiagnosis.setLongTermFlag(saveDiagnosisChildParam.getLongTermFlag());
if(encounterDiagnosis.getCreateBy() == null){
encounterDiagnosis.setCreateBy(username);
}
encounterDiagnosis.setUpdateBy(username);
if(encounterDiagnosis.getCreateTime() == null){
encounterDiagnosis.setCreateTime(new Date());
}
encounterDiagnosis.setUpdateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
i++;
}
@@ -319,11 +356,24 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
encounterDiagnosis.setMedTypeCode(saveDiagnosisChildParam.getMedTypeCode());// 医疗类型
encounterDiagnosis.setDiagnosisDesc(saveDiagnosisChildParam.getDiagnosisDesc()); // 诊断描述
encounterDiagnosis.setIptDiseTypeCode(saveDiagnosisChildParam.getIptDiseTypeCode()); // 患者疾病诊断类型代码
encounterDiagnosis.setDoctor(saveDiagnosisChildParam.getDiagnosisDoctor());
encounterDiagnosis.setClassification(saveDiagnosisChildParam.getClassification());
encounterDiagnosis.setName(saveDiagnosisChildParam.getName());
encounterDiagnosis.setTenantId(tenantId);
encounterDiagnosis.setLongTermFlag(saveDiagnosisChildParam.getLongTermFlag());
if(encounterDiagnosis.getCreateBy() == null){
encounterDiagnosis.setCreateBy(username);
}
encounterDiagnosis.setUpdateBy(username);
if(encounterDiagnosis.getCreateTime() == null){
encounterDiagnosis.setCreateTime(new Date());
}
encounterDiagnosis.setUpdateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
}
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
return R.ok(encounterDiagnosis, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
}
/**

View File

@@ -7,7 +7,15 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.administration.domain.Encounter;
import com.openhis.administration.domain.Patient;
import com.openhis.administration.mapper.EncounterMapper;
import com.openhis.administration.mapper.PatientMapper;
import java.util.Date;
import java.sql.Timestamp;
import com.openhis.common.enums.BindingType;
import com.openhis.common.enums.EncounterStatus;
import com.openhis.document.domain.Emr;
import com.openhis.document.domain.EmrDetail;
import com.openhis.document.domain.EmrDict;
@@ -46,6 +54,15 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
@Resource
IEmrDictService emrDictService;
@Resource
private EncounterMapper encounterMapper;
@Resource
private PatientMapper patientMapper;
@Resource
private com.openhis.administration.mapper.EncounterParticipantMapper encounterParticipantMapper;
/**
* 添加病人病历信息
*
@@ -175,4 +192,131 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
return emrTemplateService.removeById(id) ? R.ok() : R.fail();
}
/**
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
*/
@Override
public R<?> getPendingEmrList(Long doctorId) {
// 由于Encounter实体中没有jzPractitionerUserId字段我们需要通过关联查询来获取相关信息
// 使用医生工作站的mapper来查询相关数据
// 这里我们直接使用医生工作站的查询逻辑
// 查询当前医生负责的、状态为"就诊中"但还没有写病历的患者
// 需要通过EncounterParticipant表来关联医生信息
List<Encounter> encounters = encounterMapper.selectList(
new LambdaQueryWrapper<Encounter>()
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
);
// 过滤出由指定医生负责且还没有写病历的就诊记录
List<Map<String, Object>> pendingEmrs = new ArrayList<>();
for (Encounter encounter : encounters) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId())
);
// 检查该就诊是否由指定医生负责
boolean isAssignedToDoctor = isEncounterAssignedToDoctor(encounter.getId(), doctorId);
if (existingEmr == null && isAssignedToDoctor) {
// 如果没有病历且由该医生负责,则添加到待写病历列表
Map<String, Object> pendingEmr = new java.util.HashMap<>();
// 获取患者信息
Patient patient = patientMapper.selectById(encounter.getPatientId());
pendingEmr.put("encounterId", encounter.getId());
pendingEmr.put("patientId", encounter.getPatientId());
pendingEmr.put("patientName", patient != null ? patient.getName() : "未知");
pendingEmr.put("gender", patient != null ? patient.getGenderEnum() : null);
// 使用出生日期计算年龄
pendingEmr.put("age", patient != null && patient.getBirthDate() != null ?
calculateAge(patient.getBirthDate()) : null);
// 使用创建时间作为挂号时间
pendingEmr.put("registerTime", encounter.getCreateTime());
pendingEmr.put("busNo", encounter.getBusNo()); // 病历号
pendingEmrs.add(pendingEmr);
}
}
return R.ok(pendingEmrs);
}
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @return 待写病历数量
*/
@Override
public R<?> getPendingEmrCount(Long doctorId) {
// 获取待写病历列表,然后返回数量
R<?> result = getPendingEmrList(doctorId);
if (result.getCode() == 200) {
List<?> pendingEmrs = (List<?>) result.getData();
return R.ok(pendingEmrs.size());
}
return R.ok(0);
}
/**
* 检查患者是否需要写病历
*
* @param encounterId 就诊ID
* @return 患者是否需要写病历
*/
@Override
public R<?> checkNeedWriteEmr(Long encounterId) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounterId)
);
// 如果没有病历,则需要写病历
boolean needWrite = existingEmr == null;
return R.ok(needWrite);
}
/**
* 检查就诊是否分配给指定医生
*
* @param encounterId 就诊ID
* @param doctorId 医生ID
* @return 是否分配给指定医生
*/
private boolean isEncounterAssignedToDoctor(Long encounterId, Long doctorId) {
// 查询就诊参与者表,检查是否有指定医生的接诊记录
com.openhis.administration.domain.EncounterParticipant participant =
encounterParticipantMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.openhis.administration.domain.EncounterParticipant>()
.eq(com.openhis.administration.domain.EncounterParticipant::getEncounterId, encounterId)
.eq(com.openhis.administration.domain.EncounterParticipant::getPractitionerId, doctorId)
);
return participant != null;
}
/**
* 根据出生日期计算年龄
*
* @param birthDate 出生日期
* @return 年龄
*/
private String calculateAge(Date birthDate) {
if (birthDate == null) {
return null;
}
// 将java.util.Date转换为java.time.LocalDate
java.time.LocalDate birthLocalDate = new java.sql.Timestamp(birthDate.getTime()).toLocalDateTime().toLocalDate();
java.time.LocalDate currentDate = java.time.LocalDate.now();
int age = java.time.Period.between(birthLocalDate, currentDate).getYears();
return String.valueOf(age);
}
}

View File

@@ -22,19 +22,23 @@ import com.openhis.web.doctorstation.dto.PrescriptionInfoBaseDto;
import com.openhis.web.doctorstation.dto.PrescriptionInfoDetailDto;
import com.openhis.web.doctorstation.dto.ReceptionStatisticsDto;
import com.openhis.web.doctorstation.mapper.DoctorStationMainAppMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 医生站-主页面 应用实现类
*/
//@Slf4j
@Service
@Slf4j
public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppService {
@Resource
@@ -57,6 +61,9 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
@Resource
IDoctorStationChineseMedicalAppService iDoctorStationChineseMedicalAppService;
@Resource
private JdbcTemplate jdbcTemplate;
/**
* 查询就诊患者信息
*
@@ -97,9 +104,6 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
ParticipantType.REGISTRATION_DOCTOR.getCode(), ParticipantType.ADMITTER.getCode(), userId,
currentUserOrganizationId, pricingFlag, EncounterStatus.PLANNED.getValue(),
EncounterActivityStatus.ACTIVE.getValue(), queryWrapper);
//日志输出就诊患者信息,patientInfo
// log.debug("就诊患者信息: 总数={}, 记录数={}, 数据={}",
// patientInfo.getTotal(), patientInfo.getRecords().size(), patientInfo.getRecords());
patientInfo.getRecords().forEach(e -> {
// 性别
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
@@ -107,6 +111,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
e.setAge(e.getBirthDate() != null ? AgeCalculatorUtil.getAge(e.getBirthDate()) : "");
// 就诊状态
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(EncounterStatus.class, e.getStatusEnum()));
// 初复诊
e.setFirstEnum_enumText(EnumUtils.getInfoByValue(EncounterType.class, e.getFirstEnum()));
});
return patientInfo;
}
@@ -119,6 +125,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
*/
@Override
public R<?> receiveEncounter(Long encounterId) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
String currentUsername = SecurityUtils.getUsername();
int update = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>().eq(Encounter::getId, encounterId)
.set(Encounter::getReceptionTime, new Date())
@@ -136,6 +144,9 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
encounterParticipant.setTypeCode(ParticipantType.ADMITTER.getCode());// 接诊医生
encounterParticipant.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
encounterParticipant.setStatusEnum(EncounterActivityStatus.ACTIVE.getValue()); // 状态
encounterParticipant.setTenantId(tenantId);
encounterParticipant.setCreateBy(currentUsername);
encounterParticipant.setCreateTime(new Date());
iEncounterParticipantService.save(encounterParticipant);
return update > 0 ? R.ok() : R.fail();
}
@@ -162,12 +173,53 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
* @return 结果
*/
@Override
public R<?> completeEncounter(Long encounterId) {
@Transactional(rollbackFor = Exception.class)
public R<?> completeEncounter(Long encounterId, Integer firstEnum) {
// 1. 检查当前患者状态
Encounter encounter = encounterMapper.selectById(encounterId);
if (encounter == null) {
return R.fail("就诊记录不存在");
}
if (!EncounterStatus.IN_PROGRESS.getValue().equals(encounter.getStatusEnum())) {
return R.fail("当前患者不在就诊中状态");
}
// 2. 更新状态、完成时间以及初复诊标识
Date now = new Date();
int update = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>().eq(Encounter::getId, encounterId)
.set(Encounter::getStatusEnum, EncounterStatus.DISCHARGED.getValue())
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.DEPARTED.getValue()));
return update > 0 ? R.ok() : R.fail();
new LambdaUpdateWrapper<Encounter>()
.eq(Encounter::getId, encounterId)
.set(Encounter::getStatusEnum, EncounterStatus.DISCHARGED.getValue())
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.DEPARTED.getValue())
.set(Encounter::getEndTime, now)
.set(Encounter::getFirstEnum, firstEnum) // 直接在此处更新字段
.set(Encounter::getUpdateTime, now));
if (update <= 0) return R.fail("完诊失败");
// 3. 审计日志
try {
String username = SecurityUtils.getUsernameSafe();
String sql = "INSERT INTO sys_oper_log "
+ "(title,oper_time,method,request_method,oper_name,oper_url,oper_param,json_result) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
"完诊操作",
now,
"DoctorStationMainAppServiceImpl.completeEncounter()",
"POST",
username,
"/doctorstation/main/complete-encounter",
"{\"encounterId\": " + encounterId + "}",
"{\"code\": 200, \"msg\": \"就诊完成\", \"data\": null}");
} catch (Exception e) {
log.error("写入完诊审计日志失败", e);
// 审计日志失败不影响主流程
}
return R.ok("就诊完成");
}
/**
@@ -281,4 +333,45 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
practitionerId);
}
/**
* 过号重排核心实现
*/
@Override
@Transactional(rollbackFor = Exception.class) // 事务保证原子性
public R<?> rearrangeMissedEncounter(Long encounterId) {
// 1. 校验就诊记录是否存在
Encounter encounter = encounterMapper.selectById(encounterId);
if (encounter == null) {
return R.fail("就诊记录不存在");
}
// 2. 校验状态仅「在诊IN_PROGRESS=2」可重排
if (!EncounterStatus.IN_PROGRESS.getValue().equals(encounter.getStatusEnum())) {
return R.fail("仅「在诊」状态的患者可执行过号重排");
}
// 3. 核心更新:改回待诊+更新missed_time
Date now = new Date();
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
int updateCount = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>()
.eq(Encounter::getId, encounterId)
.set(Encounter::getStatusEnum, EncounterStatus.PLANNED.getValue()) // 改回1-待诊
.set(Encounter::getMissedTime, now) // 新增:设置过号时间为当前时间
.set(Encounter::getUpdateBy, practitionerId.toString()) // 操作医生ID
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())); // 防并发
if (updateCount == 0) {
return R.fail("过号重排失败:状态更新异常");
}
// 4. 同步更新接诊参与记录
iEncounterParticipantService.update(new LambdaUpdateWrapper<EncounterParticipant>()
.eq(EncounterParticipant::getEncounterId, encounterId)
.eq(EncounterParticipant::getTypeCode, ParticipantType.ADMITTER.getCode())
.set(EncounterParticipant::getStatusEnum, EncounterActivityStatus.COMPLETED.getValue()));
return R.ok("过号重排成功");
}
}

View File

@@ -34,6 +34,10 @@ public class DoctorStationPtDetailsAppServiceImpl implements IDoctorStationPtDet
*/
@Override
public R<?> getPtDetails(Long encounterId) {
// 检查参数
if (encounterId == null) {
return R.fail("就诊ID不能为空");
}
// 收费状态List(1:待收费,2:待结算,5:已结算)
List<Integer> statusList = new ArrayList<>();
@@ -49,6 +53,10 @@ public class DoctorStationPtDetailsAppServiceImpl implements IDoctorStationPtDet
ChargeItemContext.ACTIVITY.getValue(), ClinicalStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
ParticipantType.ADMITTER.getCode(), statusList);
if (patientDetailsDto == null) {
return R.fail("未找到患者详情信息");
}
// 住院的场合,获取现在时间,计算住院天数
if (patientDetailsDto.getClassEnum() == EncounterClass.IMP.getValue()) {
// 截至时间,用于计算当前时刻下显示的住院天数

View File

@@ -1,6 +1,7 @@
package com.openhis.web.doctorstation.controller;
import com.core.common.core.domain.R;
import com.openhis.common.enums.BindingType;
import com.openhis.template.domain.DoctorPhrase;
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
import lombok.extern.slf4j.Slf4j;
@@ -23,7 +24,13 @@ public class DoctorPhraseController {
*/
@GetMapping("/list")
public R<?> getDoctorPhraseList(){
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
try {
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("获取医生常用语列表系统异常", e);
return R.fail("获取医生常用语列表失败: " + e.getMessage());
}
}
/**
@@ -37,7 +44,13 @@ public class DoctorPhraseController {
@RequestParam(required = false) Integer phraseType,
@RequestParam(required = false) String phraseName
){
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName,phraseType));
try {
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName, phraseType));
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("查询医生常用语系统异常", e);
return R.fail("查询医生常用语失败: " + e.getMessage());
}
}
/**
@@ -48,7 +61,22 @@ public class DoctorPhraseController {
*/
@PostMapping("/add")
public R<?> addDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
return R.ok(doctorPhraseAppService.addDoctorPhrase(doctorPhrase));
try {
Boolean result = doctorPhraseAppService.addDoctorPhrase(doctorPhrase);
if (result != null && result) {
return R.ok("新增成功");
} else {
return R.fail("新增失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("新增医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("新增医生常用语系统异常", e);
return R.fail("新增失败: " + e.getMessage());
}
}
/**
@@ -59,7 +87,26 @@ public class DoctorPhraseController {
*/
@PutMapping("/update")
public R<?> updateDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
return R.ok(doctorPhraseAppService.updateDoctorPhrase(doctorPhrase));
try {
Boolean result = doctorPhraseAppService.updateDoctorPhrase(doctorPhrase);
if (result != null && result) {
return R.ok("更新成功");
} else {
return R.fail("更新失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("更新医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (SecurityException e) {
// 权限相关异常使用warn级别日志
log.warn("更新医生常用语权限异常: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("更新医生常用语系统异常", e);
return R.fail("更新失败: " + e.getMessage());
}
}
/**
@@ -70,7 +117,26 @@ public class DoctorPhraseController {
*/
@DeleteMapping("/delete/{DoctorPhraseId}")
public R<?> deleteDoctorPhrase(@PathVariable Integer DoctorPhraseId){
return R.ok(doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId));
try {
Boolean result = doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId);
if (result != null && result) {
return R.ok("删除成功");
} else {
return R.fail("删除失败");
}
} catch (IllegalArgumentException e) {
// 参数错误异常使用warn级别日志
log.warn("删除医生常用语参数错误: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (SecurityException e) {
// 权限相关异常使用warn级别日志
log.warn("删除医生常用语权限异常: {}", e.getMessage());
return R.fail(e.getMessage());
} catch (Exception e) {
// 系统异常使用error级别日志
log.error("删除医生常用语系统异常", e);
return R.fail("删除失败: " + e.getMessage());
}
}
}

View File

@@ -106,7 +106,7 @@ public class DoctorStationAdviceController {
* @return 医嘱请求数据
*/
@GetMapping(value = "/request-base-info")
public R<?> getRequestBaseInfo(@RequestParam Long encounterId) {
public R<?> getRequestBaseInfo(@RequestParam(required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getRequestBaseInfo(encounterId);
}
@@ -114,10 +114,11 @@ public class DoctorStationAdviceController {
* 查询历史医嘱请求数据
*
* @param patientId 病人id
* @param encounterId 就诊id
* @return 历史医嘱请求数据
*/
@GetMapping(value = "/request-history-info")
public R<?> getRequestHistoryInfo(@RequestParam Long patientId, Long encounterId) {
public R<?> getRequestHistoryInfo(@RequestParam Long patientId, @RequestParam(required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getRequestHistoryInfo(patientId, encounterId);
}
@@ -138,7 +139,7 @@ public class DoctorStationAdviceController {
* @return 就诊费用性质
*/
@GetMapping(value = "/get-encounter-contract")
public R<?> getEncounterContract(@RequestParam Long encounterId) {
public R<?> getEncounterContract(@RequestParam(required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getEncounterContract(encounterId);
}
@@ -162,7 +163,7 @@ public class DoctorStationAdviceController {
* @return 检验url相关参数
*/
@GetMapping(value = "/proof-result")
public R<?> getProofResult(@RequestParam(value = "encounterId") Long encounterId) {
public R<?> getProofResult(@RequestParam(value = "encounterId", required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getProofResult(encounterId);
}
@@ -173,7 +174,7 @@ public class DoctorStationAdviceController {
* @return 检查url相关参数
*/
@GetMapping(value = "/test-result")
public R<?> getTestResult(@RequestParam(value = "encounterId") Long encounterId) {
public R<?> getTestResult(@RequestParam(value = "encounterId", required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getTestResult(encounterId);
}

View File

@@ -88,7 +88,7 @@ public class DoctorStationChineseMedicalController {
* @return 中医就诊诊断信息
*/
@GetMapping(value = "/get-tcm-encounter-diagnosis")
public R<?> getTcmEncounterDiagnosis(@RequestParam Long encounterId) {
public R<?> getTcmEncounterDiagnosis(@RequestParam(required = false) Long encounterId) {
return iDoctorStationChineseMedicalAppService.getTcmEncounterDiagnosis(encounterId);
}
@@ -158,7 +158,7 @@ public class DoctorStationChineseMedicalController {
* @return 医嘱请求数据
*/
@GetMapping(value = "/tcm-request-base-info")
public R<?> getTcmRequestBaseInfo(@RequestParam Long encounterId) {
public R<?> getTcmRequestBaseInfo(@RequestParam(required = false) Long encounterId) {
return iDoctorStationChineseMedicalAppService.getTcmRequestBaseInfo(encounterId);
}
@@ -170,7 +170,7 @@ public class DoctorStationChineseMedicalController {
* @return 中医历史医嘱请求数据
*/
@GetMapping(value = "/tcm-request-history-info")
public R<?> getTcmRequestHistoryInfo(@RequestParam Long patientId, Long encounterId) {
public R<?> getTcmRequestHistoryInfo(@RequestParam Long patientId, @RequestParam(required = false) Long encounterId) {
return iDoctorStationChineseMedicalAppService.getTcmRequestHistoryInfo(patientId, encounterId);
}

View File

@@ -162,12 +162,12 @@ public class DoctorStationDiagnosisController {
/**
* 查询就诊诊断信息
*
*
* @param encounterId 就诊id
* @return 就诊诊断信息
*/
@GetMapping(value = "/get-encounter-diagnosis")
public R<?> getEncounterDiagnosis(@RequestParam Long encounterId) {
public R<?> getEncounterDiagnosis(@RequestParam(required = false) Long encounterId) {
return iDoctorStationDiagnosisAppService.getEncounterDiagnosis(encounterId);
}
@@ -178,7 +178,7 @@ public class DoctorStationDiagnosisController {
* @return 就诊诊断信息
*/
@GetMapping(value = "/get-encounter-diagnosis-ele")
public R<?> getEncounterDiagnosisByEncounterId(@RequestParam Long encounterId,@RequestParam String searchKey) {
public R<?> getEncounterDiagnosisByEncounterId(@RequestParam(required = false) Long encounterId,@RequestParam String searchKey) {
return iDoctorStationDiagnosisAppService.getEncounterDiagnosisByEncounterId(encounterId,searchKey);
}

View File

@@ -56,7 +56,7 @@ public class DoctorStationEmrController {
* @return 病历详情
*/
@GetMapping("/emr-detail")
public R<?> getEmrDetail(@RequestParam(value = "encounterId") Long encounterId) {
public R<?> getEmrDetail(@RequestParam(value = "encounterId", required = false) Long encounterId) {
return iDoctorStationEmrAppService.getEmrDetail(encounterId);
}

View File

@@ -4,6 +4,7 @@
package com.openhis.web.doctorstation.controller;
import com.core.common.core.domain.R;
import com.core.common.utils.StringUtils;
import com.openhis.common.enums.Whether;
import com.openhis.web.doctorstation.appservice.IDoctorStationMainAppService;
import com.openhis.web.doctorstation.dto.DoctorStationInitDto;
@@ -11,12 +12,10 @@ import com.openhis.web.doctorstation.dto.PatientInfoDto;
import com.openhis.web.doctorstation.dto.PrescriptionInfoBaseDto;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* 医生站-主页面 controller
@@ -64,8 +63,16 @@ public class DoctorStationMainController {
* @return 结果
*/
@GetMapping(value = "/receive-encounter")
public R<?> receiveEncounter(@RequestParam Long encounterId) {
return iDoctorStationMainAppService.receiveEncounter(encounterId);
public R<?> receiveEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
Long id = Long.parseLong(encounterId);
return iDoctorStationMainAppService.receiveEncounter(id);
} catch (NumberFormatException e) {
return R.fail("就诊ID格式错误");
}
}
/**
@@ -75,19 +82,45 @@ public class DoctorStationMainController {
* @return 结果
*/
@GetMapping(value = "/leave-encounter")
public R<?> leaveEncounter(@RequestParam Long encounterId) {
return iDoctorStationMainAppService.leaveEncounter(encounterId);
public R<?> leaveEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
Long id = Long.parseLong(encounterId);
return iDoctorStationMainAppService.leaveEncounter(id);
} catch (NumberFormatException e) {
return R.fail("就诊ID格式错误");
}
}
/**
* 就诊完成
*
* @param encounterId 就诊id
* @param params 包含 encounterId 和 firstEnum 的键值对
* @return 结果
*/
@GetMapping(value = "/complete-encounter")
public R<?> completeEncounter(@RequestParam Long encounterId) {
return iDoctorStationMainAppService.completeEncounter(encounterId);
@PostMapping(value = "/complete-encounter") // JSON提交必须用POST
public R<?> completeEncounter(@RequestBody Map<String, Object> params) {
// 从 map 中提取数据
Object encounterIdObj = params.get("encounterId");
Object firstEnumObj = params.get("firstEnum");
if (encounterIdObj == null || StringUtils.isEmpty(encounterIdObj.toString())) {
return R.fail("就诊ID不能为空");
}
if (firstEnumObj == null) {
return R.fail("初复诊状态不能为空");
}
try {
Long id = Long.parseLong(encounterIdObj.toString());
Integer firstEnum = Integer.parseInt(firstEnumObj.toString());
// 调用 Service
return iDoctorStationMainAppService.completeEncounter(id, firstEnum);
} catch (NumberFormatException e) {
return R.fail("数据格式错误ID或初复诊状态非数字");
}
}
/**
@@ -97,8 +130,39 @@ public class DoctorStationMainController {
* @return 结果
*/
@GetMapping(value = "/cancel-encounter")
public R<?> cancelEncounter(@RequestParam Long encounterId) {
return iDoctorStationMainAppService.cancelEncounter(encounterId);
public R<?> cancelEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
Long id = Long.parseLong(encounterId);
return iDoctorStationMainAppService.cancelEncounter(id);
} catch (NumberFormatException e) {
return R.fail("就诊ID格式错误");
}
}
/**
* 过号重排
*
* @param encounterId 就诊id
* @return 结果
*/
@GetMapping(value = "/rearrange-missed-encounter")
public R<?> rearrangeMissedEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
// 1. 空值校验(和现有接口保持一致)
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
// 2. 字符串转Long和现有接口保持一致
Long id = Long.parseLong(encounterId);
// 3. 调用AppService的过号重排方法
return iDoctorStationMainAppService.rearrangeMissedEncounter(id);
} catch (NumberFormatException e) {
// 4. 格式错误处理(和现有接口保持一致)
return R.fail("就诊ID格式错误");
}
}
/**

View File

@@ -32,7 +32,7 @@ public class DoctorStationPtDetailsController {
* @return 患者详情
*/
@GetMapping(value = "/patient-details")
public R<?> getPtDetails(@RequestParam Long encounterId) {
public R<?> getPtDetails(@RequestParam(required = false) Long encounterId) {
return doctorStationPtDetailsAppService.getPtDetails(encounterId);
}

View File

@@ -0,0 +1,69 @@
package com.openhis.web.doctorstation.controller;
import com.core.common.core.domain.R;
import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService;
import com.openhis.web.doctorstation.dto.PatientEmrDto;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 待写病历控制器
* 用于处理医生待写病历的相关操作
*/
@RestController
@RequestMapping("/doctor-station/pending-emr")
@Slf4j
@AllArgsConstructor
public class PendingEmrController {
private final IDoctorStationEmrAppService iDoctorStationEmrAppService;
/**
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
*/
@GetMapping("/pending-list")
public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId) {
// 如果没有传递医生ID则使用当前登录用户ID
if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
}
// 调用服务获取待写病历列表
return iDoctorStationEmrAppService.getPendingEmrList(doctorId);
}
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @return 待写病历数量
*/
@GetMapping("/pending-count")
public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId) {
// 如果没有传递医生ID则使用当前登录用户ID
if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
}
// 调用服务获取待写病历数量
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId);
}
/**
* 检查患者是否需要写病历
*
* @param encounterId 就诊ID
* @return 患者是否需要写病历
*/
@GetMapping("/need-write-emr")
public R<?> checkNeedWriteEmr(@RequestParam Long encounterId) {
return iDoctorStationEmrAppService.checkNeedWriteEmr(encounterId);
}
}

View File

@@ -89,15 +89,18 @@ public class TodayOutpatientController {
/**
* 获取患者就诊详情
*
*
* @param encounterId 就诊记录ID
* @param request HTTP请求
* @return 患者就诊详情
*/
@GetMapping("/patients/{encounterId}")
public R<TodayOutpatientPatientDto> getPatientDetail(
@PathVariable("encounterId") Long encounterId,
@PathVariable("encounterId") Long encounterId,
HttpServletRequest request) {
if (encounterId == null) {
return R.fail("就诊记录ID不能为空");
}
TodayOutpatientPatientDto patient = todayOutpatientService.getPatientDetail(encounterId, request);
return R.ok(patient);
}

View File

@@ -29,7 +29,9 @@ public class AdviceBaseDto {
/**
* 医嘱详细分类
*/
@Dict(dictCode = "activity_category_code")
private String categoryCode;
private String categoryCode_dictText;
/**
* 药品性质
@@ -104,9 +106,14 @@ public class AdviceBaseDto {
private String productName;
/**
* 活动类型
* 活动类型(诊疗项目使用目录类别)
*/
@Dict(dictCode = "activity_category_code")
private Integer activityType;
private String activityType_dictText;
/**
* 活动类型枚举文本 (手动赋值用)
*/
private String activityType_enumText;
/**

View File

@@ -1,5 +1,6 @@
package com.openhis.web.doctorstation.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.openhis.common.annotation.Dict;
@@ -127,4 +128,15 @@ public class PatientInfoDto {
* 就诊卡号
*/
private String identifierNo;
/**
* 过号时间
*/
private Date missedTime;
/**
* 初复诊标识
*/
private Integer firstEnum;
private String firstEnum_enumText;
}

View File

@@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 保存诊断 子参数类
*
@@ -33,6 +35,9 @@ public class SaveDiagnosisChildParam {
@JsonSerialize(using = ToStringSerializer.class)
private Long definitionId;
private String classification;
private String name;
/**
* 医保编码
*/
@@ -64,6 +69,18 @@ public class SaveDiagnosisChildParam {
*/
private String diagnosisDesc;
private String diagnosisDoctor;
/**
* 诊断时间
*/
private Date diagnosisTime;
/**
* 发病时间
*/
private Date onsetDate;
/** 患者疾病诊断类型代码 */
private Integer iptDiseTypeCode;
@@ -77,4 +94,6 @@ public class SaveDiagnosisChildParam {
*/
private String updateConditionId;
private Integer longTermFlag;
}

View File

@@ -293,11 +293,8 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
if (admissionPatientInfoDto.getPriorityEnum() != null) {
// 更新患者病情
encounterService.updatePriorityEnumById(encounterId, admissionPatientInfoDto.getPriorityEnum());
// 将之前的住院参与者更新为已完成
Integer result = encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
if (result == 0) {
return R.fail("患者信息更新失败,请联系管理员");
}
// 将之前的住院参与者更新为已完成(如果存在的话)
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
// 更新住院参与者
// 住院医生
encounterParticipantService.creatEncounterParticipants(encounterId, startTime,

View File

@@ -462,6 +462,7 @@ public class NurseBillingAppService implements INurseBillingAppService {
// 基础配置主键新增为null修改为已有ID、状态、业务编号
deviceRequest.setId(adviceDto.getRequestId());
deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID
// 业务编号:按日生成,前缀+4位序列号确保每日唯一
deviceRequest
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH));
@@ -533,6 +534,7 @@ public class NurseBillingAppService implements INurseBillingAppService {
// 基础配置:主键、状态、业务编号、签发编码
serviceRequest.setId(activityDto.getRequestId()); // 主键ID新增为null修改为已有ID
serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效)
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间
serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱)
serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间

View File

@@ -187,7 +187,7 @@ public class PatientHomeAppServiceImpl implements IPatientHomeAppService {
@Override
public List<OrgMetadata> getCaty() {
List<Organization> list = iOrganizationService.getList(OrganizationType.DEPARTMENT.getValue(),
OrganizationClass.INPATIENT.getValue());
OrganizationClass.INPATIENT.getCode());
List<OrgMetadata> orgMetadataList = new ArrayList<>();
OrgMetadata orgMetadata;
for (Organization organization : list) {
@@ -265,16 +265,19 @@ public class PatientHomeAppServiceImpl implements IPatientHomeAppService {
encounterService.saveOrUpdateEncounter(encounter);
// 2.就诊位置表变更
// 就诊位置ID变更
// 直接更新指定ID的就诊位置记录
EncounterLocation encounterLocation = new EncounterLocation();
encounterLocation.setId(encounterLocationId)
// 设置就诊ID
.setEncounterId(encounterId)
// 设置位置ID
.setLocationId(locationId)
// 设置状态枚举
.setStatusEnum(EncounterActivityStatus.COMPLETED.getValue())
// 设置物理枚举为 8:病床
.setFormEnum(LocationForm.BED.getValue());
encounterLocationService.saveOrUpdateEncounterLocation(encounterLocation);
// 直接更新指定ID的记录
encounterSuccess = encounterLocationService.updateById(encounterLocation);
// 3.位置表
// 旧病床状态变更(空闲)

View File

@@ -200,7 +200,29 @@ public class IInventoryAdjustPriceServiceImpl implements IInventoryAdjustPriceSe
itemDefDetailPurchaseList.add(chargeItemDefDetail);
}
}
// 批量插入价格子表
// 批量插入价格子表(在保存前设置必需字段)
// 只对新插入的记录id为null设置字段
List<ChargeItemDefDetail> newRetailList = itemDefDetailRetailList.stream()
.filter(detail -> detail.getId() == null)
.collect(Collectors.toList());
if (!newRetailList.isEmpty()) {
this.chargeItemDefDetailService.setRequiredFieldsBatch(newRetailList);
}
List<ChargeItemDefDetail> newBuyingList = itemDefDetailBuyingList.stream()
.filter(detail -> detail.getId() == null)
.collect(Collectors.toList());
if (!newBuyingList.isEmpty()) {
this.chargeItemDefDetailService.setRequiredFieldsBatch(newBuyingList);
}
List<ChargeItemDefDetail> newPurchaseList = itemDefDetailPurchaseList.stream()
.filter(detail -> detail.getId() == null)
.collect(Collectors.toList());
if (!newPurchaseList.isEmpty()) {
this.chargeItemDefDetailService.setRequiredFieldsBatch(newPurchaseList);
}
this.chargeItemDefDetailService.saveOrUpdateBatch(itemDefDetailRetailList);
this.chargeItemDefDetailService.saveOrUpdateBatch(itemDefDetailBuyingList);
this.chargeItemDefDetailService.saveOrUpdateBatch(itemDefDetailPurchaseList);

View File

@@ -25,6 +25,7 @@ import com.openhis.web.inventorymanage.dto.*;
import com.openhis.web.inventorymanage.mapper.PurchaseInventoryMapper;
import com.openhis.workflow.domain.SupplyRequest;
import com.openhis.workflow.service.ISupplyRequestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -40,6 +41,7 @@ import java.util.stream.Stream;
* @author zwh
* @date 2025-03-08
*/
@Slf4j
@Service
public class PurchaseInventoryAppServiceImpl implements IPurchaseInventoryAppService {
@@ -159,6 +161,7 @@ public class PurchaseInventoryAppServiceImpl implements IPurchaseInventoryAppSer
@Override
public R<List<ReceiptDetailDto>> getDetail(String busNo) {
List<ReceiptDetailDto> receiptDetailList = purchaseInventoryMapper.selectDetail(busNo);
log.debug("返回查询结果,receiptDetailList:{}", receiptDetailList);
if (receiptDetailList.isEmpty()) {
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, null));
}
@@ -182,7 +185,7 @@ public class PurchaseInventoryAppServiceImpl implements IPurchaseInventoryAppSer
}
});
}
log.debug("返回查询结果,receiptDetailList:{}", receiptDetailList);
return R.ok(receiptDetailList);
}
@@ -194,7 +197,6 @@ public class PurchaseInventoryAppServiceImpl implements IPurchaseInventoryAppSer
*/
@Override
public R<?> addOrEditInventoryReceipt(List<PurchaseInventoryDto> purchaseInventoryDtoList) {
// 校验(已经审批通过的单号(请求状态是同意),不能再重复编辑请求)
boolean validation = supplyRequestService.supplyRequestValidation(purchaseInventoryDtoList.get(0).getBusNo());
if (validation) {
@@ -232,11 +234,14 @@ public class PurchaseInventoryAppServiceImpl implements IPurchaseInventoryAppSer
// 制单人
.setApplicantId(SecurityUtils.getLoginUser().getPractitionerId())
// 申请时间
.setApplyTime(DateUtils.getNowDate());
.setApplyTime(DateUtils.getNowDate())
.setCreateBy(SecurityUtils.getLoginUser().getUsername())
.setCreateTime(DateUtils.getNowDate())
.setTenantId(SecurityUtils.getLoginUser().getTenantId());
supplyRequestList.add(supplyRequest);
}
// 保存
supplyRequestService.saveOrUpdateBatch(supplyRequestList);

View File

@@ -51,10 +51,11 @@ public class InspectionPackageController extends BaseController {
if (result) {
log.info("新增检验套餐成功packageName={}, basicInformationId={}",
inspectionPackage.getPackageName(), inspectionPackage.getBasicInformationId());
String idStr = inspectionPackage.getBasicInformationId() == null ? null : String.valueOf(inspectionPackage.getBasicInformationId());
return AjaxResult.success()
.put("packageId", inspectionPackage.getBasicInformationId()) // 保持向后兼容
.put("basicInformationId", inspectionPackage.getBasicInformationId())
.put("id", inspectionPackage.getBasicInformationId());
.put("packageId", idStr) // 保持向后兼容(前端按字符串处理,避免精度丢失)
.put("basicInformationId", idStr)
.put("id", idStr);
} else {
return AjaxResult.error("新增失败");
}
@@ -102,7 +103,7 @@ public class InspectionPackageController extends BaseController {
* 查询检验套餐详情
*/
@GetMapping("/{basicInformationId}")
public AjaxResult getInfo(@PathVariable String basicInformationId) {
public AjaxResult getInfo(@PathVariable Long basicInformationId) {
InspectionPackage inspectionPackage = inspectionPackageService.selectPackageById(basicInformationId);
if (inspectionPackage == null) {
return AjaxResult.error("套餐不存在");
@@ -121,15 +122,57 @@ public class InspectionPackageController extends BaseController {
if (pageNum == null) pageNum = 1;
if (pageSize == null) pageSize = 10;
List<InspectionPackage> list = inspectionPackageService.selectPackageList(inspectionPackage, pageNum, pageSize);
return getDataTable(list);
// 使用MyBatis Plus分页查询
com.baomidou.mybatisplus.extension.plugins.pagination.Page<InspectionPackage> page =
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize);
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<InspectionPackage> queryWrapper =
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
// 构建查询条件
if (inspectionPackage != null) {
if (inspectionPackage.getPackageName() != null && !inspectionPackage.getPackageName().isEmpty()) {
queryWrapper.like("package_name", inspectionPackage.getPackageName());
}
if (inspectionPackage.getPackageLevel() != null && !inspectionPackage.getPackageLevel().isEmpty()) {
queryWrapper.eq("package_level", inspectionPackage.getPackageLevel());
}
if (inspectionPackage.getDepartment() != null && !inspectionPackage.getDepartment().isEmpty()) {
queryWrapper.eq("department", inspectionPackage.getDepartment());
}
if (inspectionPackage.getPackageCategory() != null && !inspectionPackage.getPackageCategory().isEmpty()) {
queryWrapper.eq("package_category", inspectionPackage.getPackageCategory());
}
if (inspectionPackage.getIsDisabled() != null) {
queryWrapper.eq("is_disabled", inspectionPackage.getIsDisabled());
}
}
// 默认只查询未删除的记录
queryWrapper.eq("del_flag", false);
// 排序
queryWrapper.orderByDesc("create_time");
// 执行分页查询
com.baomidou.mybatisplus.extension.plugins.pagination.Page<InspectionPackage> resultPage =
inspectionPackageService.page(page, queryWrapper);
// 构建返回结果
TableDataInfo dataTable = new TableDataInfo();
dataTable.setCode(com.core.common.constant.HttpStatus.SUCCESS);
dataTable.setMsg("查询成功");
dataTable.setRows(resultPage.getRecords());
dataTable.setTotal(resultPage.getTotal());
return dataTable;
}
/**
* 删除检验套餐
*/
@DeleteMapping("/{basicInformationId}")
public AjaxResult remove(@PathVariable String basicInformationId) {
public AjaxResult remove(@PathVariable Long basicInformationId) {
// 校验套餐是否存在
InspectionPackage existing = inspectionPackageService.selectPackageById(basicInformationId);
if (existing == null) {
@@ -178,7 +221,7 @@ public class InspectionPackageController extends BaseController {
* 查询检验套餐明细列表
*/
@GetMapping("/details/{basicInformationId}")
public AjaxResult getDetails(@PathVariable String basicInformationId) {
public AjaxResult getDetails(@PathVariable Long basicInformationId) {
// 校验套餐是否存在
InspectionPackage inspectionPackage = inspectionPackageService.selectPackageById(basicInformationId);
if (inspectionPackage == null) {
@@ -235,11 +278,11 @@ public class InspectionPackageController extends BaseController {
// 请求DTO类
public static class BatchSaveDetailRequest {
private String basicInformationId;
private Long basicInformationId;
private List<InspectionPackageDetail> details;
public String getBasicInformationId() { return basicInformationId; }
public void setBasicInformationId(String basicInformationId) { this.basicInformationId = basicInformationId; }
public Long getBasicInformationId() { return basicInformationId; }
public void setBasicInformationId(Long basicInformationId) { this.basicInformationId = basicInformationId; }
public List<InspectionPackageDetail> getDetails() { return details; }
public void setDetails(List<InspectionPackageDetail> details) { this.details = details; }
}

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