297 Commits

Author SHA1 Message Date
2eec988c56 Merge remote-tracking branch 'origin/develop' into develop 2026-03-13 08:59:35 +08:00
8820048d55 feat(emr): 添加住院病历菜单类型枚举
- 新增 primaryMenuEnum 字段用于标识住院病历类型
- 设置默认值为 1 对应住院病历文档类型枚举
2026-03-13 08:59:10 +08:00
6af7720470 feat(diagnosis): 完善诊断模块功能并优化病历数据获取
- 添加isSaving状态控制保存过程
- 监听患者信息变化自动获取病历详情和诊断列表
- 增强getDetail方法添加错误处理和日志输出
- 重构handleAddDiagnosis方法分离验证逻辑到独立函数
- 优化病历详情获取接口同时查询门诊和住院病历数据
- 添加文档定义树形列表按使用范围筛选功能
- 修复历史病历数据加载错误处理机制
2026-03-12 23:21:34 +08:00
wangjian963
5f134945ab 1. 在ActivityDefinition实体类及相关DTO中添加inspectionTypeId字段
2. 新增检验类型分页查询接口及前端API调用
3. 优化检验申请模块的前后端交互逻辑
4.完成修改78 增加门诊医生开立检验申请单立检验申请单的检验项目写死的问题
5.对检验目录设置的查询,更新和保存进行修改完善。
6.对检验项目设置的页面使用vue3+elementui进行修改。
2026-03-12 18:58:54 +08:00
bc12cc1b08 fix(charge): 修复用法绑定耗材门诊收费未显示问题 (Bug #145) 2026-03-12 18:05:17 +08:00
17b8ea7192 fix(nurse-station): 修复住院护士站门户护理级别筛选功能失效问题 (Bug #172) 2026-03-12 17:38:31 +08:00
Ranyunqiao
2bfdd686c7 栈溢出 2026-03-12 17:31:50 +08:00
Ranyunqiao
066cfaba46 168 入科分配床位填写的住院医生、主治医生、责任护士字段的内容双击查看未显示。 2026-03-12 16:58:39 +08:00
e8850e85fc feat(transfer): add warehouse type support and lab specimen tables 2026-03-12 16:31:31 +08:00
d083a3123a fix: Bug #177 修复新增医嘱报错 - category_code 类型转换错误
问题原因:
SQL查询中尝试将 wor_activity_definition.category_code(中文值如'检验'、'检查')
直接转换为 INTEGER 类型,导致 PostgreSQL 类型转换错误。

修复方案:
使用 CASE WHEN 语句将中文 category_code 映射为对应的整数值:
- 检验 -> 1
- 检查 -> 2
- 护理 -> 3
- 手术 -> 4
- 其他 -> 5

这与 ActivityType 枚举定义保持一致。
2026-03-12 15:53:06 +08:00
96c1927f8d fix: Bug #177 门诊医生站耗材医嘱保存提示未匹配库存信息
问题原因:
1. 前端查询耗材列表时未设置 adviceTableName 字段
2. 后端库存校验时严格要求 adviceTableName 匹配,导致耗材无法匹配库存

修复方案:
1. 前端(adviceBaseList.vue): 添加 adviceTableName = 'adm_device_definition' 字段
2. 后端(AdviceUtils.java): 添加容错处理,当 adviceTableName 为空时跳过该项匹配

双保险策略确保问题彻底解决。
2026-03-12 15:44:03 +08:00
bdac9d0709 feat: 护理记录患者列表恢复按科室过滤 (Bug #175)
- 恢复orgId过滤,只显示当前用户科室的在院患者
- 配合后端SQL修复,从就诊表获取科室ID
- 李光明等同科室患者现在可以正常显示
2026-03-12 15:18:42 +08:00
8faba1ea21 fix: 护理记录患者列表科室ID从就诊表获取 (Bug #175)
- 将org_id来源从adm_patient改为adm_encounter
- adm_patient.organization_id通常为空
- adm_encounter.organization_id才是入院科室
- 修复按科室过滤时查不到患者的问题
2026-03-12 15:13:37 +08:00
dd9b77f6bb fix: 护理记录患者列表显示所有在院患者 (Bug #175)
- 移除orgId过滤,不再按科室限制患者显示
- 修复李光明等患者无法显示的问题
- 现在显示所有在院患者,不按科室/病区过滤
2026-03-12 14:57:27 +08:00
d45955f6de Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-03-12 14:56:13 +08:00
f905915f34 fix: 护理记录患者列表改为按病区过滤 (Bug #175)
- 将orgId过滤改为wardLocationId过滤
- 显示当前护士负责病区的所有患者
- 修复李光明等患者无法显示的问题
2026-03-12 14:55:58 +08:00
Ranyunqiao
52951d7296 167 住院管理-》住院护士站-》入出转管理:护士登录的科室能接收查看到其他科室的入科患者 入院病区字段下拉内容限制只能显示当前登录科室对应的病区,如该护士还有其他科室的权限需要做切换科室操作。 2026-03-12 14:30:19 +08:00
3c47979913 fix: 修复护理记录患者列表不显示在院患者的问题 (Bug #175)
- 将INNER JOIN改为LEFT JOIN,允许患者未分配床位时也能显示在列表中
- 修复getPatientPage和getNursingPatientPage两个查询
- 解决患者已入院但无床位信息时查询不到数据的问题
2026-03-12 14:15:56 +08:00
9aad809322 Merge remote-tracking branch 'origin/develop' into develop 2026-03-12 13:49:07 +08:00
b7850e5b8a fix: 修复耗材目录添加项目时地点字段缺少耗材库数据问题
- 问题:formList参数只包含'11,16'(药库和药房),缺少'17'(耗材库)
- 解决:将formList参数从'11,16'修改为'11,16,17'
- 影响:器材目录新增对话框的地点下拉选项现在可以显示耗材库数据

Closes #174
2026-03-12 13:47:36 +08:00
liuliu
effcdfbbe6 166 收费工作站-》住院登记:待登记入院TAB页点击【登记】按钮无响应。 2026-03-12 13:17:18 +08:00
4277a369d2 fix(order): 解决医嘱类型字段处理问题
- 优化了JSON解析逻辑,避免重复解析contentJson字段
- 确保therapyEnum字段正确传递,默认值设置为长期医嘱('1')
- 修复了医嘱保存和签发过程中类型字段丢失的问题
- 统一了前后端therapyEnum字段的默认值处理逻辑
- 添加了必要的注释说明字段处理规则
2026-03-12 12:42:17 +08:00
cf3f971741 feat(checkType): 添加检查类型下拉选项功能
- 新增 getAllCheckTypes 接口用于获取所有检查类型列表
- 在前端组件中使用检查类型选项替换原有字典数据
- 实现套餐名称字段的下拉选择和模糊过滤功能
- 统一检查类型相关的标签显示逻辑
- 优化检查项目设置界面的表单交互体验
2026-03-11 18:21:36 +08:00
75737cf95c feat(doctorstation): 添加取消接诊功能
- 在医生工作站界面添加取消接诊按钮
- 实现取消接诊的前端处理逻辑和确认对话框
- 添加计算属性控制取消接诊按钮的禁用状态
- 完善后端取消接诊服务的安全性检查和异常处理
- 优化取消接诊时的业务数据验证流程
- 添加详细的错误提示和用户反馈机制
2026-03-11 16:21:49 +08:00
4b544dc214 fix(diagnosis): 添加缺失的 Date 导入并验证构建成功 2026-03-11 15:25:43 +08:00
597e621b69 fix(diagnosis): 修复发病日期和诊断日期保存问题
根本原因: 数据库表 adm_encounter_diagnosis 缺少 onset_date 和 diagnosis_time 字段

修复内容:
1. 新增数据库字段: onset_date, diagnosis_time
2. 后端实体类 EncounterDiagnosis 添加字段
3. 后端保存逻辑添加日期字段映射
4. 后端DTO DiagnosisQueryDto 添加字段
5. 查询SQL添加日期字段查询
2026-03-11 14:49:46 +08:00
725ac4b76a feat(diagnosis): 优化门诊医生站诊断模块
- 诊断字段显示框改为div样式,支持完整显示诊断名称
- 新增诊断选择器弹窗,带标题栏和关闭按钮
- 诊断选择器支持搜索功能(按诊断名称/ICD代码)
- 优化字段对齐,统一列宽和间距
- 修复数据保存问题(longTermFlag字段、日期格式)
2026-03-11 14:27:31 +08:00
8e8b35faa4 fix(diagnosis): 修复诊断组件表格结构错误
- 移除多余的 el-select 和 el-form-item 标签闭合
- 修正日期选择器的嵌套结构问题
- 清理重复的表格列定义代码
- 优化医生和长效诊断标识字段的显示逻辑
2026-03-11 13:49:37 +08:00
664ee0312c style(diagnosis): 优化诊断组件表格样式和功能
- 添加表格边框样式
- 调整各列宽度以适应内容显示
- 优化表单元素底部间距设置
- 添加超出文本提示功能
- 为输入框和选择框统一设置合适的宽度
- 添加诊断日期字段支持
- 添加长效诊断标识字段
- 添加医生字段显示
- 优化日期格式化处理逻辑
- 修复数据保存时的日期类型转换问题
- 设置默认非长效诊断标识
- 统一表单验证规则的底部间距处理
2026-03-11 13:40:42 +08:00
cceaf7fb07 debug(invoice): 添加调试日志并优化员工账号获取逻辑
- 在用户列表获取后添加调试日志输出
- 实现通过employeeId从用户列表中获取员工账号的逻辑
- 添加getUserAccountById方法的调试日志
- 优化发票管理中的员工账号字段赋值逻辑
- 添加空值检查和调试信息输出
2026-03-11 13:06:15 +08:00
d5d638b60b Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-server-new/openhis-domain/src/main/java/com/openhis/administration/domain/InvoiceSegment.java
2026-03-11 12:13:01 +08:00
8de5ae3a4f feat(domain): 添加员工账号字段到发票段实体
- 在 InvoiceSegment 实体中新增 employeeAccount 字段
- 添加 employeeAccount 字段的 getter 和 setter 方法
- 更新 toString 方法包含 employeeAccount 信息
2026-03-11 12:09:34 +08:00
8f20c48baa docs(skills): 删除 UI/UX Pro Max 和 Web Design Guidelines 技能文档
- 移除 ui-ux-pro-max 技能的完整文档文件 SKILL.md
- 移除 web-design-guidelines 技能的完整文档文件 SKILL.md
- 删除 xlsx 技能相关的多个 XSD 模式定义文件
- 清理 office 文档格式的架构验证相关文件
2026-03-11 12:09:20 +08:00
Ranyunqiao
547cccbeb7 栈溢出 2026-03-11 11:08:25 +08:00
86e665bcae Merge remote-tracking branch 'origin/develop' into develop 2026-03-11 11:06:39 +08:00
Ranyunqiao
d1aa91f727 167 住院管理-》住院护士站-》入出转管理:护士登录的科室能接收查看到其他科室的入科患者 2026-03-11 10:26:00 +08:00
bc021924e4 docs(guide): 移除AI编码助手指南和OpenCode配置文件
- 删除了.github/copilot-instructions.md中的AI编码助手指南文档
- 移除了.opencode目录下的所有命令、技能和插件配置文件
- 清理了.opencode/openwork.json工作区配置
- 删除了.trae/documents中的多个问题修复计划文档
- 移除了数据库迁移记录文件migration_history.sql
2026-03-11 09:19:26 +08:00
6179a89b6c fix: 删除有依赖冲突的文件(处方审核、门诊报表)
- PrescriptionReviewAppServiceImpl 依赖 doctorstation DTO 变更
- OutpatientManageReportAppServiceImpl 依赖 encounterService 新方法
- 这些功能需要与 doctorstation 模块一起合并

暂时删除以保持编译通过,后续可单独评估合并
2026-03-10 18:49:54 +08:00
7c12028f63 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-03-10 18:39:28 +08:00
befb4739ee feat: 合并 upstream/v1.3 新增功能模块 2026-03-10 18:39:07 +08:00
fe07cee58c feat: 合并 upstream/v1.3 新增功能模块(安全合并策略)
新增功能模块:
- 药房管理:住院退药、处方审核功能
- 报表管理:门诊管理报表、药房结算报表、医嘱统计报表
- 支付管理:三方对账功能
- 新增枚举类:电子处方类型、频次类型、病历状态等10个
- 新增实体类:处方审核记录、第三方支付请求、中医结算目录
- 工具类增强:年龄计算、Excel工具

合并策略:仅合并低风险新增文件,保留现有业务功能
上游版本:v1.3 (2025-03-06发版)
合并分支:merge-upstream-v1.3-0310

🤖 Auto-generated by Claude Code
2026-03-10 18:30:35 +08:00
liuliu
066c457d90 169 库房管理-》采购管理-》采购管理:选中采购管理中的数据,点击删除报错,已解决,并新增批量删除功能 2026-03-10 18:07:07 +08:00
39b608dfd0 ```
fix(invoice): 禁用员工工号字段编辑功能

- 移除员工工号输入框,改为只读显示
- 添加注释说明员工工号从用户账号自动获取
- 统一使用 span 标签显示员工工号信息
```
2026-03-10 17:57:34 +08:00
6b600b44ca Merge remote-tracking branch 'origin/develop' into develop 2026-03-10 16:46:04 +08:00
sindir
b26ad75299 151 门诊医生站的诊断TAB页通过维护的个人/科室诊断内容双击开单诊断类型字段显示数字11 2026-03-10 16:33:47 +08:00
b69f312611 refactor(router): 优化路由配置和打印功能实现
- 统一路由配置中的代码风格,移除多余空格
- 移除医生个人报卡管理菜单项
- 移除检查管理中的医生报告快捷访问路径
- 替换浏览器打印为hiprint打印方案
- 添加vue-plugin-hiprint依赖和相关配置
- 实现门诊挂号单的hiprint打印功能
- 优化WebView环境检测逻辑和错误处理
2026-03-10 16:28:41 +08:00
c65db9abc3 chore(release): bump version to 3.8.8 2026-03-10 15:40:17 +08:00
1b4ad5e710 feat(print): 修改门诊挂号保存打印模板与补打挂号一致
- 在 printUtils.js 中添加 printRegistrationReceipt 函数
- 使用与补打挂号相同的 HTML 打印模板 (去掉"补打"字样)
- 修改 chargeDialog.vue 中的 printReceipt 函数调用新的打印方法
- 打印内容包括:患者信息、挂号信息、费用信息、流水号、二维码

Ref: 门诊挂号界面保存挂号打印功能
2026-03-10 15:40:02 +08:00
e46e2be830 refactor(doctorstation): 优化传染病报卡管理功能
- 将前端表单字段 diseaseCategory 统一改为 diseaseType
- 修复统计数据获取失败时的错误处理逻辑
- 完善数据列表查询的错误提示和调试日志
- 优化后端日期时间格式化处理方式
- 增强统计数据返回的安全性检查
- 移除冗余的报卡状态验证代码并修复更新时间格式
2026-03-10 10:28:13 +08:00
f515b90c43 fix: 添加 doctorreport 路由并修复 inspection/report 组件错误
- 添加 /inspection 路由组,包含 7 个子路由
- 添加 /doctorreport 快捷访问路由指向检查报告页面
- 修复 inspection/report/index.vue 组件错误:
  - 添加缺失的 getCurrentInstance 和 toRefs 导入
  - 修复 resetQuery 函数中未定义的 dateRange 引用
  - 清理未使用的 cancel 函数

修复后用户可通过 /doctorreport 或 /inspection/report 访问检查报告页面
2026-03-09 23:26:49 +08:00
6aff10e240 Merge remote-tracking branch 'origin/develop' into develop 2026-03-09 18:16:27 +08:00
wangjian963
5d02da03b4 101 系统管理-》目录管理-》诊断目录:增加报卡类型字段的bug--删除报卡类型字段的内容点击【确认】提示“修改成功”,重新点击【编辑】进入修改界面报卡类型字段内容未删除成功。 2026-03-09 17:58:05 +08:00
d99188bfb9 feat(card): 实现医生个人报卡管理系统
- 添加医生个人报卡统计、分页查询、提交、撤回、删除功能
- 实现批量提交和删除报卡操作
- 添加报卡导出为Word文档功能
- 新增DoctorCardStatisticsDto、DoctorCardListDto等数据传输对象
- 在InfectiousCardDto中添加状态文本字段
- 优化报卡状态显示,将"待审核"改为"已提交"并新增"作废"状态
- 添加多个DTO类的getter/setter方法以确保序列化正常工作
- 实现医生权限验证确保只能操作自己的报卡
- 完善报卡状态流转控制和业务逻辑验证
2026-03-09 14:57:45 +08:00
c3776c642b feat(doctor): 添加医生站报卡管理功能
- 新增医生报卡统计、列表查询、详情查看等API接口
- 实现报卡的提交、撤回、删除、批量操作等功能
- 添加报卡编辑和Word文档导出功能
- 构建完整的医生报卡管理界面,包含筛选、分页、状态显示等
- 实现报卡状态管理(待提交、已提交、已审核、已上报、失败、作废)
- 添加前端表格展示、弹窗详情、表单验证等交互功能
- 创建医生报卡更新DTO数据传输对象
2026-03-09 14:52:00 +08:00
46a99ecd55 feat(doctorstation): 新增传染病报告卡功能并优化患者登记组件
- 新增传染病报告卡完整实现,包含甲乙丙类传染病选择和报告信息录入
- 在患者登记组件中修复性别字典数据去重问题
- 添加编辑模式支持和原始数据存储功能
- 实现姓名和身份证唯一性校验的编辑模式跳过逻辑
- 添加年龄自动计算功能基于出生日期
- 确保性别值为字符串类型以便与下拉框选项匹配
- 更新患者登记组件的标题和状态管理逻辑
2026-03-09 13:47:56 +08:00
81744b9b9e feat(diagnosisTreatment): 添加划价标记默认值处理逻辑
- 编辑时若原值为null或undefined则默认设置pricingFlag为1(允许划价)
- 新增时默认将pricingFlag设为1(允许划价)
- 确保划价标记字段始终有明确的默认值
2026-03-06 23:53:35 +08:00
469b325f0e feat(card): 新增传染病报卡管理系统
- 实现报卡管理服务接口和具体实现类
- 添加报卡统计、分页查询、详情查看功能
- 实现批量审核、批量退回、单条审核功能
- 添加审核记录查询和科室树获取功能
- 实现报卡数据导出Excel功能
- 创建报卡查询参数和统计数据显示对象
- 添加审核记录、传染病卡片等数据传输对象
- 实现报卡和审核记录的数据访问层
- 定义传染病卡片和审核记录领域实体模型
- 提供REST API控制器接口供前端调用
2026-03-06 22:33:36 +08:00
8a3fe5461e fix(common): 统一异常处理并迁移打印功能到hiprint
- 替换所有System.out.println和printStackTrace为slf4j日志记录
- 在BeanUtils、AuditFieldUtil、DateUtils、ServletUtils等工具类中添加Logger实例
- 在Flowable相关控制器和服务中统一错误日志记录格式
- 在代码生成器中添加日志记录功能
- 将前端打印组件从Lodop迁移到hiprint打印方案
- 更新体温单打印功能使用hiprint预览打印
- 移除调试用的console.log语句
- 修复打印模板中线条元素类型定义
2026-03-06 22:32:56 +08:00
b65841c0cc fix(common): 统一异常处理并迁移打印功能到hiprint
- 替换所有System.out.println和printStackTrace为slf4j日志记录
- 在BeanUtils、AuditFieldUtil、DateUtils、ServletUtils等工具类中添加Logger实例
- 在Flowable相关控制器和服务中统一错误日志记录格式
- 在代码生成器中添加日志记录功能
- 将前端打印组件从Lodop迁移到hiprint打印方案
- 更新体温单打印功能使用hiprint预览打印
- 移除调试用的console.log语句
- 修复打印模板中线条元素类型定义
2026-03-06 22:16:44 +08:00
8ef334ba1b fix(print): 修复打印模板创建失败问题
1. OutpatientSurgeryCharge.json: 添加缺失的 paperList 属性
   - 在 panels 中添加 paperList 配置,指定自定义纸张尺寸 (80x297)
   - 与成功模板 OutpatientBilling.json 保持一致的结构

2. printUtils.js: 移除重复的 hiprint.init() 调用
   - 注释掉 previewPrint 函数中的 hiprint.init() 调用
   - 避免覆盖 main.js 中带有 providers 的初始化配置
   - 防止元素类型未正确注册导致的问题

🤖 Generated with [Qoder][https://qoder.com]
2026-03-06 17:03:21 +08:00
wangjian963
2492daa0ad 完成:102 门诊医生站-》诊断TAB页:增加报卡弹框登记界面
疾病报告卡新增功能。
修改诊断疾病的sql查询语句
2026-03-06 16:49:21 +08:00
8af06f6916 perf(database): 优化数据库查询性能和前端请求处理
- 优化ActivityDefinitionManageMapper.xml中的分页查询,减少JOIN操作并使用索引友好的写法
- 修复purchaseinventory组件中API调用的数据传递格式问题
- 将前端请求超时时间从60秒增加到120秒以配合后端超时设置
- 在手术申请页面添加远程搜索防抖功能,避免频繁API调用
- 重构SurgeryAppServiceImpl中的名称字段填充逻辑,使用批量查询减少数据库访问次数
- 优化SurgeryMapper.xml中的分页查询,使用子查询预加载关联数据并减少不必要的JOIN
2026-03-04 18:32:06 +08:00
7008fb007f Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-ui-vue3/src/views/doctorstation/components/diagnosis/diagnosis.vue
2026-03-04 15:34:12 +08:00
dc039fcced fix(diagnosis): 修复诊断类型字段处理逻辑
- 移除medTypeCode的默认值'11',改为undefined以避免强制设置默认类型
- 在新增诊断时不再预设诊断类型,要求用户主动选择
- 从已保存的数据中获取medTypeCode值而不是使用固定默认值
- 添加诊断类型选择验证,在保存时检查是否所有诊断都选择了类型
- 在完诊前验证诊断信息完整性,包括诊断存在性、类型选择和主诊断设置
- 优化UI显示逻辑,当诊断类型选项未加载完成时显示加载状态提示
- 调整删除按钮显示逻辑,改进弹窗确认交互体验
2026-03-04 15:33:25 +08:00
HuangXinQuan
fcb1d771f4 151 门诊医生站的用户信息展示重叠 2026-03-04 15:27:10 +08:00
30ca81090a fix(doctorstation): 修复诊断和手术模块的数据类型及权限过滤问题
- 修复诊断模块中medTypeCode字段使用数字类型字典值而非字符串
- 在报损管理中添加权限过滤后的数据回退机制,当权限过滤无数据时获取全部数据
- 为报损管理新增getPharmacyListAll和getDispensaryListAll接口函数
- 在手术申请模块中添加患者选中校验,防止未选择患者时的操作异常
- 修复手术模块的编辑、查看、删除操作缺少患者校验的问题
2026-03-04 13:16:37 +08:00
e722841e60 feat(print): 添加门诊手术收费凭证打印模板
- 新增 OutpatientSurgeryCharge.json 打印配置文件
- 配置门诊收费凭证的页面布局和样式设置
- 添加患者基本信息显示区域(姓名、性别、年龄等)
- 实现收费项目明细表格展示功能
- 集成支付方式统计(现金、微信、支付宝、医保等)
- 添加手术计费流程图展示
- 设置页眉页脚和水印选项
- 配置打印元素的位置和格式参数
2026-03-04 11:13:40 +08:00
b4ab67aed9 Merge remote-tracking branch 'origin/develop' into develop 2026-03-04 11:05:18 +08:00
6a8f82bb2e refactor(print): 更新打印功能实现并优化药品管理查询
- 替换旧的hiprint直接调用为统一的printUtils工具类
- 新增门诊手术计费打印模板支持(含流程图)
- 修改门诊收费结算单打印逻辑,使用新的打印工具类
- 修复门诊挂号打印模板注释说明
- 优化药品库房查询,过滤已删除的记录
- 更新药品管理接口URL路径配置
- 添加打印客户端连接状态检查,支持浏览器打印预览备选方案
- 改进打印错误处理和用户提示机制
2026-03-04 11:05:12 +08:00
09761c8ce8 删除 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册/03.his使用说明书/HIS使用说明书.md 2026-03-03 16:38:47 +08:00
wangjian963
5e3affcf3a Merge remote-tracking branch 'origin/develop' into develop 2026-03-03 16:29:26 +08:00
wangjian963
455f7938be 完成-101-系统管理-》目录管理-》诊断目录:增加报卡类型字段
完成-78-增加门诊医生开立检验申请单的检验项目选择区的查询。
2026-03-03 16:29:08 +08:00
HuangXinQuan
9525b1d927 80 门诊医生站检查申请单开单界面 2026-03-03 16:16:52 +08:00
8810c678c9 Merge remote-tracking branch 'origin/develop' into develop 2026-03-03 14:17:40 +08:00
cd3155e63c feat(inventory): 扩展库存筛选功能支持多范围条件
- 将原有的库存是否为零筛选扩展为更灵活的库存范围筛选
- 新增多种库存数量筛选选项:无限制、等于0、大于0、小于等于20、小于等于50
- 使用switch语句重构筛选逻辑提高代码可读性
- 更新注释文档说明新的筛选参数含义
- 修改查询方法支持返回全部记录不分页的功能
2026-03-03 14:16:52 +08:00
45fdca65a7 fix(print): 修复处方打印功能并优化路由查询解析
- 使用全局用户存储替代代理访问医院名称
- 更改打印方法为浏览器打印预览方式,无需客户端连接
- 添加打印样式处理和回调函数
- 在主入口文件初始化hiprint并配置本地客户端连接
- 移除重复的路由查询解析逻辑,简化代码结构
2026-03-03 11:17:53 +08:00
491db8bc03 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册/03.his使用说明书 2026-03-03 10:45:22 +08:00
c735bc3a78 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/.vuepress/public/img/png/HISOperationManual 2026-03-03 10:33:25 +08:00
e2db4bd3a5 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册/03.his使用说明书 2026-03-03 10:00:25 +08:00
fc0f5a11be fix(search): 修复头部搜索组件点击内部时不关闭的问题
- 为close函数添加事件参数支持
- 添加点击目标检测逻辑,避免搜索框内部点击触发关闭
- 检查事件目标是否包含在搜索元素内
- 仅当点击外部区域时才执行关闭操作
- 保持原有的blur和重置功能不变
2026-03-03 09:37:57 +08:00
c5528ce1b7 fix(ui): 修复多个功能模块的验证和数据处理问题
- 在医生工作站退费功能中添加患者选择验证
- 统一药品管理中的仓库类型选择逻辑,移除重复代码
- 修复统计管理页面清空按钮的数据重置问题
- 修正西药管理页面处方打印按钮的功能绑定
- 完善库存报表查询的SQL过滤条件实现
- 更新多个控制器接口参数类型以支持业务流程
- 优化退费列表对话框的数据加载和错误处理
2026-03-02 23:27:22 +08:00
9116ea4a84 fix(ui): 修复多个功能模块的验证和数据处理问题
- 在医生工作站退费功能中添加患者选择验证
- 统一药品管理中的仓库类型选择逻辑,移除重复代码
- 修复统计管理页面清空按钮的数据重置问题
- 修正西药管理页面处方打印按钮的功能绑定
- 完善库存报表查询的SQL过滤条件实现
- 更新多个控制器接口参数类型以支持业务流程
- 优化退费列表对话框的数据加载和错误处理
2026-03-02 23:27:11 +08:00
ce8b0b16b1 feat(inventory): add BusNoRequest DTO for inventory management 2026-03-02 18:39:41 +08:00
259c62b6b4 Merge remote-tracking branch 'origin/develop' into develop 2026-03-02 18:36:40 +08:00
208b8fc41d fix(ui): 修复多个功能模块的验证和数据处理问题
- 在医生工作站退费功能中添加患者选择验证
- 统一药品管理中的仓库类型选择逻辑,移除重复代码
- 修复统计管理页面清空按钮的数据重置问题
- 修正西药管理页面处方打印按钮的功能绑定
- 完善库存报表查询的SQL过滤条件实现
- 更新多个控制器接口参数类型以支持业务流程
- 优化退费列表对话框的数据加载和错误处理
2026-03-02 18:36:22 +08:00
c611c0ce6f Fix:148 门诊管理-》门诊处置:选上处置项目点击【打印处置单】按钮报错 2026-03-02 15:37:33 +08:00
25c266babb fix(report): 修复库存报表查询SQL格式化问题并优化处方组套处理逻辑
- 修复InventoryProductReportMapper.xml中SQL查询的缩进格式问题
- 在prescriptionlist.vue中添加组套数据验证防止空值异常
- 优化组套处理逻辑确保医嘱详情数据正确获取
- 修复最小单位数量计算避免partPercent为空时的错误
2026-03-02 14:14:20 +08:00
04ad139eae feat(advice): 添加药品库存过滤功能
- 在搜索结果中过滤掉无库存的药品项目
- 仅对药品类型(adviceType === 1)进行库存检查
- 计算库存总数量并过滤库存大于0的项目
- 保持非药品类型的原有搜索逻辑不变
- 优化代码缩进格式以提高可读性
2026-03-02 11:38:34 +08:00
wangjian963
6add091a7b 1:枚举替换,2:新增格式化函数来优化金额显示,3:新增格式化函数,4:表格状态标识优化 2026-02-28 17:48:28 +08:00
wangjian963
a05b3a8d3c 需求-78-增加门诊医生开立检验申请单的开立与删除功能以及页面的调整。 2026-02-28 14:59:21 +08:00
35bc735ecc 153 系统管理-》基础数据-》库房/药房管理:点击【删除】按钮提示“操作失败,该数据已被他人删除,请刷新后重试” 2026-02-27 19:04:04 +08:00
HuangXinQuan
fcd2d03424 80 门诊医生站检查申请单开单界面,排班的回显问题 2026-02-27 14:25:17 +08:00
87c7981ad9 Merge remote-tracking branch 'origin/develop' into develop 2026-02-26 17:10:27 +08:00
9edf8936ba fix(infusion): 修复输液记录功能中的参数传递和服务状态查询问题
- 修改前端API调用,将encounterId作为params对象传递而非直接参数
- 移除表格行样式设置功能并调整相关代码结构
- 更新服务状态默认值从10改为3(已完成状态)
- 修复后端查询逻辑,当serviceStatus为null时不添加状态过滤条件
- 调整控制器参数注解,使serviceStatus和serviceReqId参数可选
- 在门诊输液应用服务实现中优化查询条件构建逻辑
- 更新Mapper XML文件,添加输液标志过滤条件
- 优化费用管理服务中的诊断ID列表处理,过滤空值并去重
2026-02-26 17:09:57 +08:00
HuangXinQuan
753bd8bb4b 耗材入口采购不显示耗材库 2026-02-25 14:52:43 +08:00
3e09b4cc10 fix(chargemanage): 修复门诊挂号退号状态检查逻辑
- 添加待诊状态检查,只允许待诊状态的患者退号
- 防止已接诊患者进行退号操作
- 完善退号业务流程的状态验证机制
2026-02-25 13:39:52 +08:00
55970622f1 Merge remote-tracking branch 'origin/develop' into develop 2026-02-25 12:58:45 +08:00
98164c65a2 fix(router): 修复药品管理模块审核页面路由路径
- 修正 chkstockBatch 页面审核路由路径
- 修正 lossReporting 页面审核路由路径
- 修正 purchaseDocument 页面审核路由路径
- 修正 requisitionManagement 页面审核路由路径
- 修正 returningInventory 页面审核路由路径
- 修正 returnedPurchase 页面审核路由路径
- 修正 batchTransfer 页面审核路由路径
- 修正 transferManagent 页面审核路由路径
- 在采购单据拒绝流程中添加关闭当前页签功能
- 在采购单据通过流程中添加关闭当前页签功能
2026-02-25 12:58:32 +08:00
HuangXinQuan
d63a34f4e6 医生排班的分页查询 2026-02-25 11:39:50 +08:00
20ab9f890f bug142 库房管理-》统计管理-》库存明细记录-》库存明细页面显示空白 2026-02-25 10:29:15 +08:00
038213a26c Merge remote-tracking branch 'origin/develop' into develop 2026-02-24 17:30:36 +08:00
ff41aa9c04 feat(dict): 新增字典注解删除标记字段支持并修复库存计算空指针异常
- 在Dict注解中新增deleteFlag字段用于指定删除标记字段名
- 修改DictAspect切面逻辑支持删除标记字段的过滤查询
- 更新ProductDetailAppMapper.xml中的关联查询条件排序
- 修复ProductDetailAppServiceImpl中partPercent为空时的空指针异常
- 为ReceiptPageDto中的字典字段添加删除标记过滤配置
- 新增药物统计管理门户页面提供各类统计报表入口
2026-02-24 17:30:23 +08:00
f60e070984 帮助文档调整为弹出新页面 2026-02-24 16:50:56 +08:00
3f7169844c 修复Bug132 进入【病区/床位管理】→点击新增关联科室时必填项,需要加* 2026-02-24 15:33:13 +08:00
chenjinyang
8b993d5ddd 修改base路径 2026-02-11 17:55:49 +08:00
chenjinyang
9cfa9a3417 修改图片位置 2026-02-11 17:44:47 +08:00
chenjinyang
b200f80d88 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-02-11 17:19:38 +08:00
chenjinyang
e57baaead6 修改配置base 2026-02-11 17:18:18 +08:00
chenjinyang
2576f62f88 统一图片路径前缀 2026-02-11 17:03:28 +08:00
chenjinyang
cd24fe007f 修改图片路径 2026-02-11 16:33:54 +08:00
chenjinyang
3898880665 修改门诊挂号文件里图片路径 2026-02-11 16:21:31 +08:00
562e618aaa 更新 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册/02.门诊挂号/01.门诊挂号操作.md 2026-02-11 16:09:46 +08:00
ec4d57ea69 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/.vuepress/public/img/png/outpatientregistrationoperation 2026-02-11 15:42:15 +08:00
HuangXinQuan
7a5b26607e Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-02-11 14:56:34 +08:00
HuangXinQuan
50ceb98e83 87 门诊医生站-》增加医生常用语维护界面
医生常用语这个实体类继承了基类HisBaseEntity,使用里面的delete_flag的字段进行逻辑删除,保留了enable_flag字段,方便后续实现启用和禁用的功能
2026-02-11 14:56:13 +08:00
chenjinyang
2065395bd2 修改门诊挂号文件里图片路径 2026-02-11 14:52:43 +08:00
HuangShun
517dc41f01 修复mapper路径问题 2026-02-11 14:29:27 +08:00
weixin_45799331
6f8e677045 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-02-11 14:18:52 +08:00
weixin_45799331
1747291f41 96-门诊医生站会诊申请确认界面和97-门诊会诊申请管理界面全部功能。 2026-02-11 14:16:30 +08:00
chenjinyang
cff1e0145b 修改门诊挂号操作文件命名和位置 2026-02-11 14:07:56 +08:00
weixin_45799331
3ab7ea1898 Merge remote-tracking branch 'origin/develop' into develop 2026-02-11 13:41:38 +08:00
weixin_45799331
c5d75f053b 会诊管理中 门诊会诊申请确认和门诊会诊申请管理模块全部功能的实现。包括数据库设计,前端UI设计,后端接口开发。 2026-02-11 13:40:32 +08:00
chenjinyang
730476e927 修改adviceType值为药品,手术安排md文档命名更改 2026-02-11 13:02:10 +08:00
d8a4487b2b 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册/02.门诊挂号 2026-02-11 12:37:46 +08:00
c93a5f69d8 上传文件至 openhis-ui-vue3/public/help-center/vuepress-theme-vdoing-doc/docs/01.HIS操作手册 2026-02-11 12:04:20 +08:00
HuangShun
b826afb17c 需求76 门诊预约挂号
1.就诊卡号与性别显示
2.选中患者优化
3.解决确认报错
2026-02-11 11:44:42 +08:00
HuangXinQuan
2a5a157c57 获取当前用户的今日日程 2026-02-10 17:25:32 +08:00
chenjinyang
ca9b145d3e 修复手术删除临时医嘱删除问题 2026-02-10 17:10:34 +08:00
HuangShun
7676f03c96 需求102 门诊医生站-》诊断TAB页:增加报卡弹框登记界面;
1.建立对应数据库表infectious_card
2.实现前端表单样式
3.完成相关表单数据查询以及数据传递与保存
2026-02-10 16:56:59 +08:00
chenjinyang
b5b91d8971 完成100需求,补充99需求追溯术中产生的费用新增费用项时向表中插入SourceBillNo和generate_source_enum字段值 2026-02-10 16:49:28 +08:00
HuangXinQuan
f1bddf3fbe 75修改还是提示“当前卫生机构下已存在该科室名称的诊室”。 2026-02-10 15:08:38 +08:00
b1c966f69f fix(sidebar): 解决路由查询参数解析错误问题
- 添加 try-catch 语句处理 JSON.parse 异常
- 当路由查询参数解析失败时返回默认路径
- 记录解析失败的错误信息到控制台
- 确保应用在无效查询参数情况下正常运行
2026-02-10 11:28:07 +08:00
dba6350493 fix(doctorstation): 修复会诊模块数据加载异常处理
- 添加了接口响应空值检查,防止因空响应导致的页面崩溃
- 完善了错误处理逻辑,统一返回空数组避免组件渲染异常
- 增强了网络错误捕获,提供更准确的错误信息提示
- 优化了后端服务异常处理,确保查询失败时返回安全的默认值
- 修复了手术收费模块中的用户卡信息引用错误
- 改进了学生合同号处理逻辑,增加了前置条件验证
2026-02-09 23:47:19 +08:00
HuangXinQuan
6fb5b5993a 75 门诊出诊医生诊室设置的完善 2026-02-09 16:38:54 +08:00
chenjinyang
4c2b015210 修改帮助中心readme静态资源说明规范图片存放便于管理 2026-02-09 13:34:54 +08:00
chenjinyang
9f9f193287 修改门诊手术计费按钮位置更新readme新增静态资源放置及引用说明 2026-02-09 13:03:25 +08:00
chenjinyang
d34a314f02 完成99需求 2026-02-06 17:12:29 +08:00
8d45cfe9db 上传文件至 md/需求 2026-02-06 16:07:22 +08:00
f9d897c081 refactor(surgicalschedule): 更新手术排程页面中的组件导入路径
- 将 SurgeryCharge 组件的导入路径从相对路径 ../../charge/surgerycharge/index.vue 修改为 ../charge/surgerycharge/index.vue
- 修正了因目录结构调整导致的模块解析错误
- 优化了组件间的依赖关系,确保项目结构的一致性
2026-02-06 13:51:41 +08:00
d2616ac2f9 chore(config): 更新日志配置并添加OpenCode技能和命令
- 将MyBatis日志实现从STDOUT更改为SLF4J
- 调整开发环境日志级别从debug降级为info
- 添加OpenCode命令配置文件包括快速开始、文件操作、插件和技能相关命令
- 创建OpenCode技能框架包含代理创建器、命令创建器、插件创建器和技能创建器
- 初始化OpenWork工作区配置文件
- 添加工作区引导和Chrome DevTools演示技能
2026-02-06 13:41:36 +08:00
d2a6780c23 refactor(consultation): 重构会诊模块包结构和日期时间处理
- 将ConsultationRequest相关类从web包移动到consultation包
- 替换java.util.Date为java.time.LocalDateTime进行日期时间处理
- 统一日期时间格式化方式,使用DateTimeFormatter替代SimpleDateFormat
- 优化年龄计算逻辑,使用Java 8时间API替代Calendar
- 在ConsultationRequest实体上添加MyBatis别名注解
- 更新MyBatis映射文件中的命名空间和类型引用
- 调整数据库字段映射,移除无效字段并添加新字段
2026-02-06 13:26:31 +08:00
fc32b83980 refactor(consultation): 重构会诊模块包结构和日期时间处理
- 将ConsultationRequest相关类从web包移动到consultation包
- 替换java.util.Date为java.time.LocalDateTime进行日期时间处理
- 统一日期时间格式化方式,使用DateTimeFormatter替代SimpleDateFormat
- 优化年龄计算逻辑,使用Java 8时间API替代Calendar
- 在ConsultationRequest实体上添加MyBatis别名注解
- 更新MyBatis映射文件中的命名空间和类型引用
- 调整数据库字段映射,移除无效字段并添加新字段
2026-02-06 13:26:28 +08:00
chenjinyang
b936654a11 提交帮助中心模块手册模板及图片 2026-02-06 13:05:38 +08:00
2a525f95b9 refactor(surgicalschedule): 修正手术费用组件导入路径
- 更新 SurgeryCharge 组件的相对路径引用
- 从当前目录调整为上两级目录的正确路径
2026-02-06 12:45:09 +08:00
585b9bd720 Merge remote-tracking branch 'origin/develop' into develop 2026-02-06 12:35:43 +08:00
weixin_45799331
faf73a5ac4 95-门诊医生站开立会诊申请单界面PRD_2026-01-15,全部功能。 2026-02-06 11:24:08 +08:00
89bf85fd97 feat: 门诊手术中计费功能
- 数据库:在adm_charge_item表添加SourceBillNo字段
- 后端实体类:更新ChargeItem.java添加SourceBillNo字段
- 前端组件:创建手术计费界面(基于门诊划价界面)
- 后端API:扩展PrePrePaymentDto支持手术计费标识
- 后端Service:扩展getChargeItems方法支持手术计费过滤
- 门诊手术安排界面:添加【计费】按钮

注意事项:
- 需要手动执行SQL脚本:openhis-server-new/sql/add_source_bill_no_to_adm_charge_item.sql
- 术后一站式结算功能待后续开发
2026-02-05 23:47:02 +08:00
f3d56bff45 feat(menu): 添加菜单缓存刷新功能和拖拽排序支持
- 在SysMenuController中添加refreshCache和refreshCurrentUserMenuCache接口
- 实现菜单缓存的按需刷新和用户级别缓存清理功能
- 优化菜单列表查询的缓存key策略,支持更精确的缓存命中
- 为菜单树查询添加缓存注解提升性能
- 在菜单增删改操作中完善缓存清理逻辑
- 添加allocateMenuToRole方法实现菜单角色分配功能
- 在前端DictTag组件中修复标签类型验证逻辑
- 为首页配置页面添加拖拽排序功能,支持快捷功能重新排列
- 集成Sortable.js实现拖拽交互和排序保存
- 优化菜单管理页面的缓存刷新机制和数据展示
- 完善配置更新事件处理,支持实时配置同步
2026-02-05 23:07:31 +08:00
chenjinyang
cd6c015d8f 提交OpScheduleDto 2026-02-05 16:39:41 +08:00
chenjinyang
dfdab41c00 完成93需求 2026-02-05 16:30:25 +08:00
HuangXinQuan
f69de5e78f 医生排班日期的正确插入 2026-02-05 16:00:56 +08:00
HuangShun
74892ea80f 修复Bug138 系统管理-》基础数据-》病区/床位管理:关联科室字段的下拉选项 2026-02-05 11:24:43 +08:00
HuangShun
4bf74a1ff0 修复Bug131 【入出转管理】-》入院病区下拉框无数据,后面的相应联动框应该也有数据 2026-02-05 10:22:09 +08:00
3a53837e50 项目修改插入时不按照序号排列异常 2026-02-04 17:25:13 +08:00
HuangShun
2c2bb1adb0 实现需求56 检查项目设置-》检查类型维护中的分页功能。 2026-02-04 16:03:41 +08:00
HuangXinQuan
a434dfdfff 73 门诊医生排班管理 2026-02-03 17:36:29 +08:00
4c14d802c4 取药科室下项目显示异常,没有耗材库的修复 2026-02-03 16:03:32 +08:00
a7602057e2 Revert "套餐设置套餐管理完善"
This reverts commit 6c15f0d4d5.
2026-02-03 15:30:47 +08:00
97af9d5eee Merge branch 'develop' of https://gitea.gentronhealth.com/py/his into develop 2026-02-03 15:29:32 +08:00
wangjian963
3cabb5f803 Merge remote-tracking branch 'origin/develop' into develop 2026-02-03 15:16:03 +08:00
wangjian963
a55884a710 维护多个挂号类型的医生,只能检索出一个医生的问题。 2026-02-03 15:15:55 +08:00
dfac362c37 Merge branch 'develop' of https://gitea.gentronhealth.com/py/his into develop 2026-02-03 15:11:03 +08:00
HuangShun
897afd4da2 修复Bug130 【门诊挂号】-》【新增患者】性别设置有2个,多了一个;将页面回退至需求37 门诊挂号-》新增患者 /页面,并实现患者新建患者档案年龄校验 2026-02-03 14:55:13 +08:00
3d31b3482a 套餐设置套餐管理完善 2026-02-03 14:03:07 +08:00
3acf8ad50a Merge branch 'develop' of https://gitea.gentronhealth.com/py/his into develop 2026-02-03 14:00:59 +08:00
HuangShun
fa06a52d71 需求85 门诊挂号-》预约号源已缴费签到未看诊进行退号;建立退费表,在退诊时将相应数据插入到表中。 2026-02-03 10:31:58 +08:00
d1b290881f 上传文件至 md/需求 2026-02-02 16:51:23 +08:00
5781e39c20 Merge remote-tracking branch 'origin/develop' into develop 2026-02-02 16:28:39 +08:00
9ed43c9413 feat(home): 添加医生专属患者统计和菜单跳转功能
- 在HomeStatisticsDto中新增我的患者数量和待写病历数量字段
- 实现医生患者查询功能,支持按租户隔离数据
- 更新首页统计服务,为医生用户提供专属患者统计数据
- 添加菜单名称点击跳转功能,支持路由导航和外部链接打开
- 修复首页统计数据显示,确保医生看到正确的患者数量
- 添加医保日结结算相关实体、服务和前端页面
- 配置前端路由控制器,支持Vue Router History模式
2026-02-02 16:28:31 +08:00
270004afee Merge branch 'develop' of https://gitea.gentronhealth.com/py/his into develop 2026-02-02 15:02:06 +08:00
HuangShun
96941cb4e0 修复需求56 检查项目设置-》检查类型维护中去掉选择部位勾选框点击【保存】按钮未实现数据存储。 2026-02-02 11:50:15 +08:00
6c15f0d4d5 套餐设置套餐管理完善 2026-02-02 11:08:19 +08:00
HuangShun
063be326eb 修复Bug125 【住院登记】的入院科室无法选择,入院病区也无法选择;修改科室数据来源 2026-02-02 10:55:04 +08:00
5534a71c7d feat(menu): 优化菜单服务性能并新增医生排班功能
- 添加菜单缓存注解以提升查询性能
- 实现菜单完整路径计算优化,解决 N+1 查询问题
- 新增 selectAllMenus 方法供路径计算使用
- 添加今日医生排班查询功能
- 重构前端图标显示逻辑,使用 SVG 图标替代 Element 图标
- 添加前端菜单数据本地缓存机制
- 更新菜单管理界面的表单组件绑定方式
- 新增预约管理、门诊管理和药房管理路由配置
2026-02-02 08:46:33 +08:00
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
1031 changed files with 74581 additions and 15657 deletions

View File

@@ -1,76 +0,0 @@
# OpenHIS — AI 编码助手 指南
目的:帮助自动化/AI 编码代理快速上手本仓库,包含架构要点、关键文件、常用构建/运行命令以及项目约定。请只按照仓库内真实可见的内容提出修改建议或补充说明。
- **代码组织**: 本项目是一个 Java 后端(多模块 Maven+ Vue3 前端Vite的大型应用。
- 后端主模块目录:`openhis-server-new/`(顶层为 `pom`,包含多个子模块)。关键子模块示例:`openhis-application`, `openhis-domain`, `openhis-common`, `core-*` 系列。
- 前端目录:`openhis-ui-vue3/`Vite + Vue 3使用 Pinia、Element Plus 等)。
- **大局观Big Picture**: 后端以 Spring BootJava 17实现使用多模块 Maven 管理公共库与业务模块;前端由单独仓库目录通过 Vite 构建并以环境变量(`VITE_APP_BASE_API`)与后端交互。后端扫描 `com.core``com.openhis` 包(见 `OpenHisApplication.java`),启动类位于:`openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
- **运行/构建Windows PowerShell 示例)**:
- 构建后端(从仓库根执行):
```powershell
cd openhis-server-new
mvn clean package -DskipTests
```
- 仅运行后端模块(开发时常用):
```powershell
cd openhis-server-new/openhis-application
mvn spring-boot:run
# 或在 IDE 中运行 `com.openhis.OpenHisApplication` 的 main()
```
- 前端启动与构建(需要 Node.js v16.x仓库 README 建议 v16.15
```powershell
cd openhis-ui-vue3
npm install
npm run dev # 本地开发(热重载)
npm run build:prod # 生产构建
```
- **环境与配置**:
- 后端配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`数据库、端口、profile 等。README 还提及 `application-druid.yml`(若存在请优先查看)。
- 前端配置:多个 `.env.*` 文件(例如 `.env.development`, `.env.staging`, `.env.production`),关键变量:`VITE_APP_BASE_API`(例如 `/dev-api`),前端通过 `import.meta.env.VITE_APP_BASE_API` 拼接后端 URL`src/utils/request.js`、多个视图与组件)。
- **重要约定 / 模式**:
- 后端采用 Java 17、Spring Boot 2.5.x 家族,父 POM在 `openhis-server-new/pom.xml` 定义。常用依赖版本在该 POM 的 `<properties>` 中集中维护。
- 模块间以 Maven 模块依赖与 `com.core` / `com.openhis` 包名分层(见 `pom.xml``<modules>``dependencyManagement`)。
- 前端通过 Vite 插件配置(`openhis-ui-vue3/vite/plugins`)管理 svg、自动导入等。UI 框架为 Element Plus状态管理为 Pinia。
- **集成点 & 外部依赖**:
- 数据库PostgreSQLREADME 建议 v16.2),仓库根含一个大型初始化 SQL`数据库初始话脚本请使用navicat16版本导入.sql`,用于初始化表与演示数据。
- 缓存/会话Redis需自行配置
- 其他Flowable工作流Druid连接池监控第三方服务通过特定配置类例如 `YbServiceConfig``OpenHisApplication` 中启用)。
- **调试与常见位置**:
- 启动类:`openhis-server-new/openhis-application/src/main/java/com/openhis/OpenHisApplication.java`
- 全局配置:`openhis-server-new/openhis-application/src/main/resources/``application.yml`、profile 文件等)。
- 前端入口:`openhis-ui-vue3/src/main.js`、路由在 `openhis-ui-vue3/src/router/index.js`
- API 文档与监控路径(通常由后端暴露并被前端访问):
- Swagger UI: `<VITE_APP_BASE_API>/swagger-ui/index.html`(前端视图在 `src/views/tool/swagger/index.vue`)。
- Druid: `<VITE_APP_BASE_API>/druid/login.html`(见前端相关视图引用)。
- **为 AI 代理的具体建议(如何安全、有效地修改代码)**:
- 修改后端时:优先在子模块(例如 `openhis-application`)本地运行 `mvn spring-boot:run` 验证启动与基础 API大量改动前先执行 `mvn -T1C -DskipTests clean package` 在 CI 环境上验证构建(本地机器也可用)。
- 修改前端时:检查/调整对应 `.env.*` 文件中的 `VITE_APP_BASE_API`,使用 `npm run dev` 本地联调后端接口(可通过代理或将 `VITE_APP_BASE_API` 指向后端地址)。
- 修改数据库结构或 seed请参考仓库根的 SQL 初始化脚本,任何 DDL/数据变更需同步该脚本并通知数据库管理员/运维。
- **举例(常见任务示例)**:
- 本地联调前端 + 后端PowerShell:
```powershell
# 启动后端
cd openhis-server-new/openhis-application
mvn spring-boot:run
# 启动前端(另开终端)
cd openhis-ui-vue3
npm run dev
```
如需我把这些内容合并为更短或更详细的版本,或把其中某部分(例如后端模块依赖关系图、关键 Java 包说明)展开,请告诉我要增强哪一节。

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
}

View File

@@ -1,26 +0,0 @@
# 修复门诊预约界面专家号查询结果显示问题
## 问题分析
1. 前端传递的参数正确:`type=expert`,后端正确转换为`ticketType=专家`
2. 实际查询返回了5条记录但COUNT查询只返回了1条记录
3. 这导致前端只显示了1条记录而不是全部5条
4. 原因MyBatis-Plus自动生成的COUNT查询和实际查询使用了不同的条件特别是逻辑删除条件
## 解决方案
1. 修改TicketMapper.xml中的自定义COUNT查询显式添加`delete_flag = '0'`条件
2. 在selectTicketPage和selectTicketPage_mpCount查询中都添加逻辑删除条件
3. 确保两个查询使用完全相同的WHERE条件
## 修复步骤
1. 修改`selectTicketPage`查询,添加逻辑删除条件`and delete_flag = '0'`
2. 修改`selectTicketPage_mpCount`查询,添加逻辑删除条件`and delete_flag = '0'`
3. 确保两个查询的WHERE条件完全一致
4. 测试修复后的功能确保专家号能正确显示全部5条记录
## 代码修改点
- 文件:`d:/work/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/TicketMapper.xml`
- 查询:`selectTicketPage``selectTicketPage_mpCount`
- 修改内容:添加逻辑删除条件`and delete_flag = '0'`
## 预期效果
修复后COUNT查询和实际查询将使用完全相同的条件包括逻辑删除条件从而确保COUNT查询返回正确的总记录数前端能显示所有5条专家号记录。

View File

@@ -1,30 +0,0 @@
# 修复门诊预约界面专家号查询COUNT结果不正确问题
## 问题分析
1. 前端传递的参数正确:`type=expert`,后端正确转换为`ticketType=专家`
2. COUNT查询和实际查询的WHERE条件完全相同`WHERE delete_flag = '0' AND ticket_type = '专家'`
3. 但COUNT查询只返回1条记录而实际查询返回5条记录
4. 原因MyBatis-Plus的分页插件在处理自定义COUNT查询时存在bug导致COUNT查询结果不正确
## 解决方案
修改`TicketAppServiceImpl.java`中的`listTicket`方法不使用MyBatis-Plus的自动分页功能而是手动实现分页查询
1. 直接调用`ticketService.countTickets`方法获取总记录数
2. 手动构建查询条件
3. 确保COUNT查询和实际查询使用完全相同的条件
## 修复步骤
1. 修改`TicketAppServiceImpl.java`中的`listTicket`方法
2. 手动实现分页查询,包括:
- 构建查询条件
- 调用`countTickets`获取总记录数
- 调用`selectTicketList`获取分页数据
- 手动组装分页结果
3. 测试修复后的功能确保专家号能正确显示全部5条记录
## 代码修改点
- 文件:`d:/work/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
- 方法:`listTicket`
- 修改内容替换MyBatis-Plus的自动分页改为手动分页实现
## 预期效果
修复后COUNT查询和实际查询将使用完全相同的条件COUNT查询将返回正确的总记录数5条前端能显示所有5条专家号记录。

View File

@@ -1,32 +0,0 @@
## 问题分析
根据日志和代码分析,发现号源列表显示"没有更多数据了"的问题原因:
1. **后端查询正常**成功查询到5条符合条件的专家号源记录
2. **数据转换失败**:在`convertToDto`方法中,`fee`字段类型转换错误
3. **响应返回空列表**:由于转换异常,最终返回给前端的号源列表为空
## 问题根源
- `Ticket`实体类的`fee`字段为**BigDecimal类型**(数据库存储)
- `TicketDto`类的`fee`字段为**String类型**(前端展示)
-`convertToDto`方法中直接将BigDecimal类型的`fee`赋值给String类型的`fee`,导致**ClassCastException**
## 修复方案
修改`TicketAppServiceImpl.java`文件中的`convertToDto`方法将BigDecimal类型的`fee`转换为String类型
```java
// 原代码
dto.setFee(ticket.getFee());
// 修复后代码
dto.setFee(ticket.getFee().toString());
```
## 预期效果
1. 修复后,后端能成功将`Ticket`实体转换为`TicketDto`
2. 前端能接收到包含5条专家号源的完整列表
3. 页面显示正常,不再出现"没有更多数据了"的提示
## 验证方法
1. 重新启动项目,访问号源管理页面
2. 选择"专家号"类型查看是否能正确显示5条号源记录
3. 检查日志,确认没有类型转换异常

View File

@@ -1,23 +0,0 @@
# 修复门诊预约界面专家号查询问题
## 问题分析
从日志中发现关键问题:
- 前端传递的ticket_type值是英文`general` (普通号) 和 `expert` (专家号)
- 数据库中存储的ticket_type值是中文`普通``专家`
- 导致查询条件不匹配,无法查询到数据
## 解决方案
需要在后端添加类型映射转换,将前端传递的英文类型转换为数据库中存储的中文类型。
## 修复步骤
1. 修改 `TicketAppServiceImpl.java` 文件在处理type参数时添加映射转换逻辑
2. 添加从英文类型到中文类型的映射关系
3. 测试修复后的功能,确保普通号和专家号都能正确查询
## 代码修改点
- 文件:`d:/work/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
- 方法:`listTicket` 中的type参数处理部分
- 修改内容:添加类型映射转换,将 "general" 转换为 "普通""expert" 转换为 "专家"
## 预期效果
修复后,前端选择"普通号"或"专家号"时,系统能正确查询到对应的号源数据,不再出现"没有更多数据了"的提示。

View File

@@ -1,23 +0,0 @@
**问题分析**
后端返回的响应格式是`{code: 200, msg: "操作成功", data: {total: 5, limit: 20, page: 1, list: [5条记录]}}`,而前端可能期望直接访问`list`属性导致只能显示1条数据。
**修复方案**
1. 修改`TicketAppServiceImpl.java``listTicket`方法,确保返回的分页数据格式正确
2. 调整响应结构,使其更符合前端期望
3. 保持与现有代码的兼容性
**修改点**
* `TicketAppServiceImpl.java`:优化`listTicket`方法的响应格式
* 确保分页信息和列表数据都能正确返回给前端
**预期效果**
* 后端返回正确格式的响应数据
* 前端能够正确显示所有5条专家号数据
* 保持与现有代码的兼容性

188
AGENTS.md Normal file
View File

@@ -0,0 +1,188 @@
# OpenHIS - AI Agent Development Guide
## 项目概览
OpenHIS 是一个医院管理系统,采用 Java 17 + Spring Boot 后端和 Vue 3 + Vite 前端架构。
## 构建和运行命令
### 后端Java/Spring Boot
```bash
# 构建整个项目
cd openhis-server-new
mvn clean package -DskipTests
# 运行后端(开发模式)
cd openhis-server-new/openhis-application
mvn spring-boot:run
# 运行特定模块
cd openhis-server-new/[module-name]
mvn spring-boot:run
```
### 前端Vue 3 + Vite
```bash
# 安装依赖
cd openhis-ui-vue3
npm install
# 开发服务器
npm run dev
# 生产构建
npm run build:prod
# 测试环境构建
npm run build:test
# 预览构建结果
npm run preview
```
### 测试
项目当前没有配置正式的测试框架。如需添加测试:
- 后端:考虑使用 JUnit 5 + Mockito
- 前端:考虑使用 Vitest + Vue Test Utils
## 代码风格规范
### Java 后端规范
- **Java 版本**: 17
- **框架**: Spring Boot 2.5.15
- **ORM**: MyBatis Plus 3.5.5
- **数据库**: PostgreSQL
- **包结构**:
- `com.openhis` - 业务逻辑
- `com.core` - 核心框架
- **命名约定**:
- 类名PascalCase`UserController`
- 方法名camelCase`getUserList`
- 常量SCREAMING_SNAKE_CASE
- 配置文件kebab-case
- **注解使用**:
- 使用 `@Slf4j` 替代手动声明 logger
- 使用 `@Data` 在实体类中
- 使用 `@Service/@Controller/@Repository` 等 Spring 注解
- **异常处理**:
- 使用统一的异常处理机制
- 自定义业务异常继承 `RuntimeException`
### Vue 前端规范
- **框架**: Vue 3 + Composition API
- **UI 库**: Element Plus
- **状态管理**: Pinia
- **路由**: Vue Router 4
- **构建工具**: Vite 5
- **组件命名**: PascalCase
- **文件命名**: kebab-case
- **变量命名**: camelCase
- **常量命名**: SCREAMING_SNAKE_CASE
- **函数命名**:
- 事件处理:`handle` 前缀
- 数据获取:`get`/`load` 前缀
- 提交操作:`submit` 前缀
### 导入顺序
#### Java
1. `java.*`
2. `javax.*`
3. 第三方库
4. `com.core.*`
5. `com.openhis.*`
6. `*.*`(其他包)
#### JavaScript/Vue
1. `vue` 相关
2. 第三方库
3. `@/` 别名导入
4. 相对路径导入
### 代码格式
#### Java
- 缩进4个空格
- 行长度120字符
- 左大括号不换行
#### Vue/JavaScript
- 缩进2个空格
- 字符串:优先使用单引号
- 行长度100字符
## 关键配置文件
### 后端配置
- 主配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`
- 环境配置:`application-{profile}.yml`
- Maven 父 POM`openhis-server-new/pom.xml`
### 前端配置
- Vite 配置:`openhis-ui-vue3/vite.config.js`
- 环境变量:`.env.*` 文件
- 路由配置:`openhis-ui-vue3/src/router/index.js`
## 开发约定
### API 设计
- RESTful API 风格
- 统一响应格式
- 使用 Swagger 文档
- 错误码统一管理
### 数据库
- 表名snake_case
- 字段名snake_case
- 主键:使用 `id`
- 软删除:使用 `valid_flag` 字段
### 前端组件
- 单一职责原则
- Props 使用 camelCase
- Events 使用 kebab-case
- 使用 Composition API
- 组件文档使用 JSDoc
### 状态管理
- 模块化设计
- 异步操作使用 actions
- 避免在组件中直接修改状态
## 环境变量
### 前端
- `VITE_APP_BASE_API`: API 基础路径
- `VITE_APP_ENV`: 环境标识
### 后端
- `spring.profiles.active`: 激活的配置文件
- `core.name`: 应用名称
- `core.version`: 应用版本
## 安全规范
- 所有 API 接口需要权限验证
- 敏感信息使用环境变量
- SQL 注入防护
- XSS 攻击防护
## 性能优化
- 后端使用连接池Druid
- 前端使用路由懒加载
- 图片使用 WebP 格式
- 大列表使用虚拟滚动
## 常用工具类
- 后端:`com.core.common.utils.*`
- 前端:`@/utils/*`
## 注意事项
1. 修改数据库结构需要同步 SQL 脚本
2. 新增功能需要添加权限配置
3. 前端路由需要在权限系统中注册
4. 接口变更需要更新 Swagger 文档
5. 遵循现有代码风格,避免不必要的变化
## 故障排除
- 后端端口18080
- 前端端口81
- API 前缀:`/openhis`
- Swagger UI`/openhis/swagger-ui/index.html`
- Druid 监控:`/openhis/druid/login.html`

674
LICENSE
View File

@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright 2022-2025 湖北天天数链技术有限公司
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
OpenHis Copyright (C) 2022-2025 湖北天天数链技术有限公司
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,104 +0,0 @@
-- 检查流水号display_order是否按“科室+医生+当天”正确递增
--
-- 说明:
-- 1. display_order 存的是纯数字1, 2, 3...),不带时间戳前缀
-- 2. 时间戳前缀(如 20260109是在前端显示时加上的
-- 3. 后端用 Redis key "ORG-{科室ID}-DOC-{医生ID}" 按天自增
--
-- 如何判断逻辑是否正确:
-- 同一科室、同一医生、同一天的记录display_order 应该递增1, 2, 3...
-- 不同科室、不同医生、不同天的记录,可能都是 1这是正常的
-- ========================================
-- 查询1按“科室+医生+日期”分组,看每组内的 display_order 是否递增
-- ========================================
SELECT
DATE(start_time) AS ,
organization_id AS ID,
registrar_id AS ID,
COUNT(*) AS ,
MIN(display_order) AS ,
MAX(display_order) AS ,
STRING_AGG(display_order::text, ', ' ORDER BY start_time) AS ,
STRING_AGG(id::text, ', ' ORDER BY start_time) AS ID列表
FROM adm_encounter
WHERE delete_flag = '0'
AND start_time >= CURRENT_DATE - INTERVAL '7 days' -- 只看最近7天
AND display_order IS NOT NULL
GROUP BY DATE(start_time), organization_id, registrar_id
ORDER BY DESC, ID, ID;
-- ========================================
-- 查询2详细查看每条记录看同组内的序号是否连续
-- ========================================
SELECT
id AS ID,
DATE(start_time) AS ,
organization_id AS ID,
registrar_id AS ID,
start_time AS ,
display_order AS ,
-- 计算:同组内的序号应该是 1, 2, 3...,看是否有重复或跳号
ROW_NUMBER() OVER (
PARTITION BY DATE(start_time), organization_id, registrar_id
ORDER BY start_time
) AS ,
CASE
WHEN display_order = ROW_NUMBER() OVER (
PARTITION BY DATE(start_time), organization_id, registrar_id
ORDER BY start_time
) THEN '✓ 正常'
ELSE '✗ 异常'
END AS
FROM adm_encounter
WHERE delete_flag = '0'
AND start_time >= CURRENT_DATE - INTERVAL '7 days'
AND display_order IS NOT NULL
ORDER BY DATE(start_time) DESC, organization_id, registrar_id, start_time;
-- ========================================
-- 查询3只看今天的数据最直观
-- ========================================
SELECT
id AS ID,
organization_id AS ID,
registrar_id AS ID,
start_time AS ,
display_order AS
FROM adm_encounter
WHERE delete_flag = '0'
AND DATE(start_time) = CURRENT_DATE
AND display_order IS NOT NULL
ORDER BY organization_id, registrar_id, start_time;
-- ========================================
-- 查询4发现问题 - 找出同组内 display_order 重复的记录
-- ========================================
WITH ranked AS (
SELECT
id,
DATE(start_time) AS reg_date,
organization_id,
registrar_id,
start_time,
display_order,
ROW_NUMBER() OVER (
PARTITION BY DATE(start_time), organization_id, registrar_id
ORDER BY start_time
) AS should_be_order
FROM adm_encounter
WHERE delete_flag = '0'
AND start_time >= CURRENT_DATE - INTERVAL '7 days'
AND display_order IS NOT NULL
)
SELECT
reg_date AS ,
organization_id AS ID,
registrar_id AS ID,
COUNT(*) AS ,
STRING_AGG(id::text || '->' || display_order::text, ', ') AS
FROM ranked
WHERE display_order != should_be_order
GROUP BY reg_date, organization_id, registrar_id
ORDER BY reg_date DESC;

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试合并11111</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,349 @@
## 医生个人报卡管理界面PRD文档
### 一、页面概述
**页面名称**:医生个人报卡管理界面
**页面目标**:为医生提供传染病报告卡的管理功能,包括查看、编辑、提交、撤回、导出等操作,并提供数据统计和筛选功能。
**适用场景**:医生需要查看/编辑或管理已填写的传染病报告卡,进行批量操作或筛选特定状态的报告卡
**页面类型**:列表页(含筛选功能) + 表单页(编辑/查看模态框)。
**核心功能**
1. 报卡数据统计展示(总报卡数/待处理失败/已成功上报)
2. 报卡列表筛选与查询(按日期/状态/关键词)
3. 报卡详情查看与编辑
4. 批量操作(全选/批量提交/批量删除)
5. 报卡导出为Word格式
**用户价值**
- 快速掌握个人报卡工作整体情况
- 高效管理不同状态的报卡记录
- 规范疾病报告卡流程,确保数据及时准确上报
**原型图地址**https://static.pm-ai.cn/prototype/20260129/865d147e5650ff42c054b38244ed8239/index.html
**流程图**
```mermaid
flowchart TD
Start([医生进入个人报卡管理界面]) --> Load[展示数据统计卡片]
Load --> Stats[展示统计卡片\n总报卡数待处理已上报]
Stats --> Filter[渲染筛选区日期状态名称]
Filter --> Table[展示报卡列表]
Table --> Op{用户操作选择}
Op -->|点击查看| View1[弹出只读模态框]
View1 --> View2[展示完整报卡信息]
View2 --> View3[关闭模态框]
View3 --> Table
Op -->|点击编辑| Edit1{状态是待提交?}
Edit1 -->|否| Table
Edit1 -->|是| Edit2[弹出编辑模态框]
Edit2 --> Edit3[修改表单字段]
Edit3 --> Edit4[点击保存]
Edit4 --> Edit5{验证必填项}
Edit5 -->|失败| Edit6[显示错误原因]
Edit6 --> Edit2
Edit5 -->|通过| Edit7[保存数据]
Edit7 --> Edit8[提示成功]
Edit8 --> Table
Op -->|点击提交| Sub1{状态是待提交?}
Sub1 -->|是| Sub2[确认对话框]
Sub2 --> Sub3[变更为已提交]
Sub3 --> Sub4[刷新表格统计]
Sub4 --> Table
Sub1 -->|否| Table
Op -->|点击撤回| Back1{状态是已提交?}
Back1 -->|是| Back2[变更为待提交]
Back2 --> Table
Back1 -->|否| Table
Op -->|点击导出| Exp1{状态是已上报?}
Exp1 -->|是| Exp2[报告卡导出预览]
Exp2 --> Exp3[导出Word文档]
Exp3 --> Table
Exp1 -->|否| Table
Op -->|应用筛选| Filt1[触发筛选条件]
Filt1 --> Filt2[重新加载列表]
Filt2 --> Table
Op -->|批量操作| Batch1{已选记录?}
Batch1 -->|否| Batch2[提示请选择记录]
Batch2 --> Table
Batch1 -->|是| Batch3{操作类型}
Batch3 -->|批量提交| Batch4{全是待提交?}
Batch4 -->|否| Batch5[提示只能提交待提交]
Batch5 --> Table
Batch4 -->|是| Batch6[确认数量]
Batch6 --> Batch7[更新为已提交]
Batch7 --> Batch8[刷新统计数据]
Batch8 --> Table
Batch3 -->|批量删除| Batch9{全是待提交?}
Batch9 -->|否| Batch10[提示只能删除待提交]
Batch10 --> Table
Batch9 -->|是| Batch11[确认删除]
Batch11 --> Batch12[状态变为作废]
Batch12 --> Batch13[刷新数据]
Batch13 --> Table
```
### 二、整体布局分析
**页面宽度**:自适应布局
**主要区域划分**
1. **顶部标题区**5%):页面标题。
2. **数据统计区**15%):展示总报卡数、待处理失败数、已成功上报数。
3. **高级筛选区**15%):提供日期范围、状态、报卡名称等筛选条件。
4. **数据表格区**55%):展示报卡列表,支持多选和操作按钮。
5. **底部批量操作区**10%):全选、批量删除、批量提交功能。
**布局特点**:上下布局,采用卡片式设计,主内容区为表格展示
### 三、页面区域详细描述
#### 1. 顶部标题区
**区域位置**:页面最上方
**区域尺寸**高度60px宽度100%。
**区域功能**:展示页面标题和主要操作入口
**包含元素**
- **页面标题**
- - 元素类型:文本
- 显示内容:“我的报卡”
- 样式特征20px/600深灰色(#1e293b)
#### 2. 数据统计卡片区
**区域位置**:标题区下方
**区域尺寸**高度150px宽度100%。
**区域功能**:展示关键统计数据,帮助医生快速了解报卡状态分布。
**包含元素**
- **总报卡数卡片**
- - 元素类型:统计卡片
- 显示内容:图标+数值+“总报卡数”
- 样式特征紫色渐变背景圆角12px
- **待处理失败卡片**
- - 同总报卡数卡片,红色系配色
- **已成功上报卡片**
- - 同总报卡数卡片,绿色系配色
#### 3. 高级筛选区
**区域位置**:统计卡片下方
**区域尺寸**高度150px宽度100%
**区域功能**:提供多维筛选条件,支持快速定位目标报卡。
**包含元素**
- **日期范围选择器**
- - 元素类型表单控件两个date输入框
- 交互行为:选择日期后触发筛选。
- 限制条件:结束日期不能早于开始日期。
- **状态筛选下拉框**
- - 元素类型:下拉选择
- 可选值:全部状态/待提交/已提交/已审核/已上报/失败/作废
- **报卡名称搜索框**
- - 元素类型:文本输入框
- 占位文本:“输入报卡名称…”
- **应用筛选按钮**
- - 元素类型:主要操作按钮
- 交互行为:点击后触发表格数据刷新
- **重置条件按钮**
- - 元素类型:次要操作按钮
- 交互行为:清空所有筛选条件
#### 4. 数据表格区
**区域位置**:页面中部
**区域尺寸**高度自适应宽度100%。
**区域功能**:展示报卡列表及提供行级操作
**包含元素**
- **表格头部**
**数据主要取值于传染病报卡表infectious_card**
- - 包含字段选择框、卡片ID、患者姓名、身份证号、联系电话、就诊卡号、报卡名称、提交时间、状态、操作
- 样式特征灰色背景13px字号大写字母
- **表格内容行**
- - 展示方式:每行显示一条报卡记录
- 数据字段:
- - 卡片ID文本 - HOSP202601150001 - 不可操作
- 患者姓名:文本 - 张三 - 不可操作
- 身份证号:不脱敏文本 - 110101199001011234 - 不可操作
- 联系电话:文本 - 13800138000 - 不可操作
- 就诊卡号:文本 - M12345678 - 不可操作
- 报卡名称:文本 - 中华人民共和国传染病报告卡 - 不可操作
- 提交时间:时间 - 2026-01-15 14:30 - 不可操作
- 状态标签:根据状态值显示不同颜色
- 操作功能:
- - 查看按钮:所有状态可见
- 编辑按钮:仅"待提交"状态可见
- 提交按钮:仅"待提交"状态可见
- 撤回按钮:仅"已提交"状态可见
- 导出按钮:仅"已上报"状态可见
#### 5. 底部批量操作区
**区域位置**:页面底部
**区域尺寸**高度60px宽度100%。
**区域功能**:支持批量操作选中报卡。
**包含元素**
- **全选复选框**
- - 交互行为:勾选后选中当前页所有记录
- **批量删除按钮**
- - 元素类型:文本按钮
- 限制条件:仅对"待提交"状态记录有效
- 交互行为:提交后状态变为"作废"。
- **批量提交按钮**
- - 元素类型:主要操作按钮
- 限制条件:仅对"待提交"状态记录有效
- 交互行为:提交后状态变为"已提交"。
#### 6. 报卡详情弹窗界面内容功能与需求编号102界面保持一致建议用同一个界面
**区域位置**:页面居中模态框
**区域功能**:查看/编辑完整报卡信息
**包含元素**
- 表单字段(按模块分组):
- 1. 患者基本信息(姓名/身份证号/联系方式*
2. 临床信息(发病日期/诊断日期*
3. 传染病分类(甲/乙/丙类多选*
4. 报告信息(报告单位/医生*
- 操作按钮:
o 取消:关闭弹窗不保存
o 保存:验证必填项后保持数据
### 四、交互功能详细说明
#### 1. 报卡查看功能
**功能描述**:查看报卡详细信息
**触发条件**:点击任意行的"查看"按钮
**操作流程**
1. 弹出模态框展示完整报卡信息
2. 所有字段为只读状态
3. 点击关闭按钮或蒙层关闭模态框
#### 2. 报卡编辑功能
**功能描述**:修改待提交的报卡信息
**触发条件**:点击"待提交"状态的"编辑"按钮
**操作流程**
1. 弹出可编辑的报卡表单模态框
2. 修改必要字段(带*号为必填项)
3. 点击"保存"按钮提交修改
4. 成功提示后关闭模态框
#### 3. 批量提交功能
**功能描述**:批量提交选中的待提交报卡
**触发条件**:勾选记录后点击"批量提交"按钮
**操作流程**
1. 校验是否选中有效记录(状态为待提交)
2. 弹出确认对话框显示待提交数量
3. 确认后更新记录状态为"已提交"
4. 刷新表格数据和统计卡片
**异常处理**
- 未选中记录:提示"请选择待提交的记录"
- 包含不可提交记录:提示"只能提交待提交状态的记录"
#### 4. 报卡状态流转
**触发方式**:点击操作列按钮
**执行流程**
1. 待提交 → 已提交:点击"提交"按钮 → 弹窗确认 → 状态变更
2. 已提交 → 待提交:点击"撤回"按钮 → 状态回滚
3. 已上报 → 导出生成标准疾病报告卡Word文档含医院红头格式
**异常处理**
- 提交失败:显示具体错误原因(如:必填项未完成)
### 五、数据结构说明
**关键数据字段**
**传染病报卡表infectious_card**
### 六、开发实现要点
**样式规范**
- **主色调**#6366f1(紫色)
- **状态色**
- - 待提交:#f59e0b(橙色)
- 已提交:#2563eb(蓝色)
- 已上报:#16a34a(绿色)
- 失败:#dc2626(红色)
- **字体规范**
- - 标题20px/600
- 正文14px/400
- **间距系统**
- - 卡片内边距24px
- 元素间距16px
**技术要求**
- **表格组件**:需支持虚拟滚动(大数据量场景)
- **导出功能**实现Word导出
**注意事项**
1. 身份证号等敏感信息不需做脱敏处理
2. 批量操作需考虑性能优化(分页处理)
3. 状态变更需同步更新统计卡片数据
4. 移动端需特别处理表格的横向滚动体验
5. ‘待提交’状态就是‘暂存’状态
6. 只能查询医生本人报卡的数据

View File

@@ -0,0 +1,434 @@
## 报卡管理界面PRD文档
### 一、页面概述
**页面名称**:报卡管理界面
**页面目标**:提供传染病报卡的审核、管理、筛选及批量操作功能,帮助疾控人员高效完成报卡审核工作
**适用场景**:疾控中心工作人员日常审核医疗机构上报的传染病病例
- 医院CDC管理员日常审核传染病报卡
- 批量处理待审核/退回的报卡
- 按条件筛选统计报卡数据
**页面类型**:数据管理列表页(含详情抽屉)
**核心功能**
1. 报卡数据概览统计(今日待审/本月失败/本月成功/本月上报)
2. 多维度筛选报卡数据(时间/状态/科室/来源等)
3. 报卡列表展示与批量操作(审核/退回/导出)
4. 报卡详情查看与单条审核
5. 审核记录追溯与意见填写
**用户价值**
- 疾控人员可快速处理待审报卡
- 支持批量审核提升工作效率
- 完整记录审核过程便于追溯
- 多维度筛选快速定位目标报卡
**原型图地址**https://static.pm-ai.cn/prototype/20260206/cc9991b716df0303fa3459042e33a1ea/index.html
**流程图**
```mermaid
flowchart TD
A["进入报卡管理界面"] --> B["查看报卡概览统计"]
B --> C["点击统计卡片"]
C --> D["悬停统计卡片"]
D --> E["自动设置筛选项"]
B --> F["鼠标悬停"]
F --> G["筛选报卡数据"]
G --> H["卡片上浮+阴影"]
E --> I["设置筛选条件"]
I --> K["点击重置按钮"]
K --> J["清空条件"]
I --> L["点击查询按钮"]
L --> M["触发筛选"]
M --> N["操作报卡列表"]
N --> O["勾选报卡"]
N --> P["处理单条报卡"]
O --> Q["批量操作报卡"]
P --> R["点击单条审核"]
P --> S["点击单条查看"]
Q --> T["点击批量审核"]
Q --> U["点击批量退回"]
R --> V{"报卡状态"}
S --> W{"报卡状态"}
V -- 已审核 --> X["打开查看抽屉"]
V -- 待审核/失败 --> Y["校验选择状态"]
W -- 任意 --> X
T --> Z["校验选择状态"]
U --> AA["校验选择状态"]
Y -- 选择有效 --> AB["打开审核抽屉"]
AB-->AN["展示审核记录"]
AB-->AO["展示审核记录"]
Y -- 未选择 --> AC["提示请选择报卡"]
Z -- 选择有效 --> AD{"包含已审核项"}
Z -- 未选择 --> AE["提示请选择报卡"]
AD -- 是 --> AF["提示只能选择待审核报卡"]
AD -- 否 --> AG["弹出审核弹窗"]
AA -- 选择有效 --> AH{"包含已审核项"}
AA -- 未选择 --> AI["提示请选择报卡"]
AH -- 是 --> AF
AH -- 否 --> AJ["弹出退回弹窗"]
AG --> AK["加载报卡详情"]
AJ --> AL["加载报卡详情"]
AK -- 加载失败 --> AM["提示数据加载失败"]
AL -- 加载失败 --> AM
AK -- 成功 --> AN["展示审核记录"]
AL -- 成功 --> AO["展示审核记录"]
AN --> AP["填写审核意见"]
AO --> AQ["填写退回原因"]
AP --> AR{"意见是否为空"}
AQ --> AS{"原因是否为空"}
AR -- 是 --> AT["红字提示必填"]
AS -- 是 --> AU["红字提示必填"]
AR -- 否 --> AV["点击确认审核"]
AS -- 否 --> AW["点击确认退回"]
AV --> AX["点击审核通过"]
AW --> AY["点击退回修改"]
AX --> AZ["更新报卡状态"]
AY --> BA["更新报卡状态"]
AZ --> BB["生成审核记录"]
BA --> BC["生成审核记录"]
BB --> BD["按钮置灰"]
BC --> BD["按钮置灰"]
AX --> BE["提示操作失败,请检查网络"]
AY --> BE
AZ --> BF["刷新表格数据"]
BA --> BF
BF --> BG["刷新行状态"]
BG --> BH["结束"]
BE --> BH
```
### 二、整体布局分析
**页面宽度**:自适应布局
**主要区域划分**
1. **顶部导航栏**固定高度60px
2. **报卡管理概览区**(统计卡片+快捷操作,高度自适应)
3. **筛选控制区**(多条件组合筛选,折叠式布局)
4. **报卡列表区**表格展示占主体60%高度)
**布局特点**:上下层级结构,筛选区支持折叠/展开
5. **抽屉详情区**
### 三、页面区域详细描述
#### 1. 顶部导航栏
**区域位置**:页面顶部固定
**区域尺寸**高度60px100%宽度
**区域功能**:展示系统标识和用户信息
**包含元素**
- **系统Logo**
- - 元素类型:图标+文字组合
- 显示内容:"CDC"图标+"报卡管理"文字
- 样式特征:蓝色主色调(#4a6fa5),左侧对齐
#### 2. 报卡管理概览
**区域位置**:导航栏下方
**区域尺寸**100%宽度,自适应高度
**区域功能**:关键数据统计与快速操作入口
**包含元素**
- **统计卡片组**4个
- - 展示方式网格布局4列
- 数据字段:
| **字段名** | **类型** | **示例值** | **可操作** | **计算逻辑** |
| ------------ | -------- | ---------- | ---------- | ------------------------- |
| 今日待审核 | 数字 | 12 | 可点击 | 当天created_at+待审状态 |
| 本月审核失败 | 数字 | 3 | 可点击 | 当月created_at+失败状态 |
| 本月审核成功 | 数字 | 2 | 可点击 | 当月created_at+成功状态 |
| 本月已上报 | 数字 | 156 | 可点击 | 当月created_at+已上报状态 |
o 交互行为:
- - - 悬停卡片上浮5px+阴影加深
- 点击:自动设置对应筛选项
- 样式特征:左侧状态色条(蓝/橙/红/绿)
#### 3. 筛选控制区
**区域功能**:多维度组合筛选报卡数据
**区域尺寸**100%宽度,高度自适应(展开状态)
**包含元素**
- **筛选条件组**(横向排列→移动端垂直堆叠)
- - 登记来源(下拉单选):全部/门诊/住院/急诊/体检
- 上报时间范围(双日期选择器)--默认值:最近一个月
- 患者姓名(文本输入)
- 审核状态(下拉单选):全部/待审核/审核通过/审核失败/已上报
- 上报科室(树形下拉多选)--全部科室/取值于《科室管理》adm_organization表
- **操作按钮**
- - “查询”(主按钮,触发筛选)
- “重置”(次要按钮,清空条件)
#### 4. 报卡列表区
**区域功能**:展示报卡数据及操作入口
**包含元素**
- **表格头部**
- - 全选复选框(联动所有行选择状态)
- 列标题:报卡名称/病种名称/患者信息等11列
- **快捷操作按钮组**
- - 包含按钮:
- - “批量审核”蓝色按钮,(需选择条目)点击弹出填写审核备注弹窗
- “批量退回修改”橙色警示按钮,(需选择条目)点击弹出退回原因填写弹窗
- “导出当前”(按筛选条件)
- **表格行**
- - 数据字段:
取值于传染病报卡表infectious_card
| **列名** | **数据类型** | **示例值** | **说明** |
| -------- | ------------ | ---------------- | --------------- |
| 选择框 | boolean | - | 带全选功能 |
| 报卡名称 | string | 传染病报告卡 | - |
| 病种名称 | string | 病毒性肝炎 | - |
| 报卡编号 | string | HOSP202601150001 | 唯一标识 |
| 患者姓名 | string | 张某某 | 脱敏显示 |
| 性别 | enum | 男 | 男/女/未知 |
| 年龄 | number | 32 | - |
| 上报科室 | string | 儿科 | - |
| 登记来源 | string | 门诊 | - |
| 上报时间 | datetime | 2026-01-31 09:23 | - |
| 状态 | badge | 待审核 | 待审核<->已提交 |
| 操作 | button | 审核/查看 | 根据状态禁用 |
- - 行操作按钮:
- - “审核”(主按钮,打开可编辑抽屉)
- “查看”(次要按钮,打开只读抽屉)
- 交互规则:
- - 已审核通过的报卡禁用"审核"按钮
- 行hover时显示浅蓝色背景
- 分页功能:
- - 实现分页功能可以设置每页5/10/20条
#### 5. 抽屉详情区
**区域位置**:右侧滑出
**区域尺寸**:自适应布局
**包含元素**
· **报卡表单(****根据具体报卡登记的界面内容比如《中华人民共和国传染病报告卡》内容和功能与需求编号102界面保持一致建议用同一个界面**
- - 字段分组:
- 1. 患者基本信息(姓名、身份证等)
2. 临床信息(发病日期、诊断分类等)
3. 疾病选择(甲/乙/丙类传染病复选框)
4. 上报信息(报告单位、医生等)
· **审核记录**
- - 展示方式:时间轴列表
- 数据字段:时间、操作人、操作类型、意见
· **操作按钮组**
- - 主按钮:审核通过(绿色)
- 次按钮:退回修改(橙色)
### 四、交互功能详细说明
#### 1. 批量审核流程
**触发条件**:勾选多行后点击"批量审核"
**操作流程**
1. 系统校验至少选择1条非"已通过"状态的报卡
2. 弹出审核弹窗500px居中模态框
3. 填写审核意见(必填)
4. 点击"确认审核"
5. 批量更新状态为"审核通过",(自动批量写入每一条审核记录(插入infectious_audit 字段详表②、更改表infectious_card. Statu= 2和infectious_card. update_time= now()
6. 刷新表格数据
7. - 成功:更新状态为"审核通过",添加审核记录
- 失败:提示"审核失败,请检查网络"
- 包含已审核项:提示"只能选择待审核报卡"
- 未填写意见:阻止提交并红字提示
#### 2. 批量退回修改操作
**触发方式**:勾选多选框后点击"批量退回"
**前置校验**
- 至少勾选一条非"已审核"状态报卡
- 选中已审核报卡时提示"只能操作待审核报卡"
**执行流程**
1. 弹出模态框要求填写退回原因(必填)填写退回原因:阻止提交并红字提示
2. 提交后批量更新选中报卡状态
3. 每条生成审核记录①、插入infectious_audit 字段详表②、更改表infectious_card. Statu=5和infectious_card. update_time= now()
#### 3.单卡审核流程
**触发方式**:点击操作列"审核"按钮
**状态控制**
- 待审核/审核失败:可操作
- 审核通过:按钮禁用
**执行流程**
1. 右侧滑出审核抽屉
2. 从行数据获取报卡ID自动填充患者报卡信息异步加载报卡详情数据
3. 展示历史审核记录(如有)
4. 填写审核意见/退回原因
5. 点击"审核通过"或"退回修改"
6. 更新表格行状态生成审核记录①、插入infectious_audit 字段详表②、更改表infectious_card. Statu=2/5和infectious_card. update_time= now()
**异常处理**
· 数据加载失败:提示"数据加载失败,请重试"
**·** 重复提交:按钮置灰防止重复点击
**状态变化**
- 审核通过:表格行变绿色,按钮禁用,状态变成“审核通过”
- 退回修改:表格行变橙色,生成退回记录,状态变成“审核失败”审核失败<->退回
**数据校验**
- 必填字段红框提示
- 身份证号格式校验
- 日期逻辑校验(发病日期≤诊断日期)
#### 4. 筛选查询功能
**触发方式**:点击"查询"按钮
**查询逻辑**
- 登记来源:精确匹配
- 患者姓名:模糊匹配
- 时间范围:闭区间查询
- 状态筛选:精确匹配
- 上报科室:多选
**性能优化**
- 500ms防抖处理
- 分页加载可以设置每页5/10/20条实现分页功能
#### 5. 报卡详情查看
**触发条件**:点击行"查看"按钮
**抽屉内容**
- 只读表单(包含所有报卡字段)
- 审核记录时间轴(倒序展示)
- 关闭按钮(右上角×图标)
**数据加载**根据行数据获取报卡ID自动填充患者报卡信息异步加载报卡详情数据
#### 6. 筛选联动逻辑
| **操作** | **系统响应** |
| ---------------------- | ------------------------------------------ |
| 点击"今日待审核"统计卡 | 自动设置: - 时间=当天 - 状态=待审核 |
| 本月审核失败 | 自动设置: - 时间=本月 - 状态=审核失败 |
| 本月审核成功 | 自动设置: - 时间=本月 - 状态=审核成功 |
| 本月已上报 | 自动设置: - 时间=本月 - 状态=已上报 |
### 五、数据结构说明
**关键数据表**
```
infectious_card 与报卡审核记录表infectious_audit采用 一对多 关联:
① 、一张报卡可经历多次审核(初审、复审、退回、重审等)。
② 、关联键infectious_card.card_no → infectious_audit.card_idFK
*infectious_card. Statu增加状态5退回=审核失败(当批量退回修改/退回修改时更改表infectious_card. Statu=5 and infectious_card. update_time= now()
*infectious_audit 字段详表
```
| **字段名** | **中文名称** | **取值说明** | **类型****(PG)** | **约束** |
| ----------------- | ------------ | ------------------------------------------------------------ | ---------------- | ---------------------------------------- |
| audit_id | 审核记录ID | 主键,自增 | BIGINT | PRIMARY KEY |
| card_id | 报卡ID | 关联infectious_card.card_no | BIGINT | NOT NULL, FK |
| audit_seq | 审核序号 | 第几次审核从1开始 | SMALLINT | NOT NULL, ≥1 |
| audit_type | 审核类型 | 1批量审核/2单审核通过/3批量退回修改/4单退回修改 /5其他 | CHAR(1) | NOT NULL, IN('1','2','3','4','5') |
| audit_status_from | 审核前状态 | 同infectious_card.status(0暂存/1已提交=待审核/2已审核=审核通过/3已上报/4失败/5退回=审核失败) | CHAR(1) | NOT NULL, IN('0','1','2','3','4','5') |
| audit_status_to | 审核后状态 | 同上,审核后的新状态 | CHAR(1) | NOT NULL, IN('0','1','2','3','4') |
| audit_time | 审核时间 | 精确到秒,当前时间戳 | TIMESTAMP | NOT NULL, DEFAULT now() |
| auditor_id | 审核人账号 | 登录账号 | VARCHAR(20) | NOT NULL |
| auditor_name | 审核人姓名 | 登录账号的姓名 | VARCHAR(50) | NOT NULL |
| audit_opinion | 审核意见 | 审核意见简述 | TEXT | |
| Reason_for_return | 退回原因 | 退回原因简述 | TEXT | |
| fail_reason_code | 失败原因码 | 字典001必填项/002逻辑错误/003网络超时等 | VARCHAR(20) | |
| fail_reason_desc | 失败详情 | 详细描述,可空 | TEXT | |
| is_batch | 是否批量 | 0单条审核/1批量审核 | BOOLEAN | NOT NULL, DEFAULT false |
| batch_size | 批量数量 | 批量时涉及报卡数 | INTEGER | NOT NULL, DEFAULT 1, ≥1 |
| created_time | 记录创建时间 | 自动生成 | TIMESTAMP | NOT NULL, DEFAULT now() |
| updated_time | 记录更新时间 | 自动更新 | TIMESTAMP | NOT NULL, DEFAULT now(), ON UPDATE now() |
```
```
### 六、开发实现要点
**样式规范**
- **主色调**`#4a6fa5`(导航栏/主按钮)
- **状态色**
- - 待审核:`rgba(74, 111, 165, 0.1)`
- 审核失败:`rgba(231, 76, 60, 0.1)`
- **字体**
- - 标题:`16px/1.5 #333`
- 表格内容:`14px/1.5 #666`
**注意事项**
- 审核记录需永久保存不可删除
### 七、补充说明
1. **日期格式**:统一使用`YYYY/MM/DD`(符合医疗系统惯例)
2. **地址组件**:四级联动(省→市→区→街道)
3. **职业选项**:使用国家标准职业分类
4. **病种名称**:严格遵循《传染病报告卡》规范用词

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

@@ -0,0 +1,46 @@
package com.core.web.controller.common;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 前端路由 fallback 控制器
* 处理 Vue Router History 模式下的路由
*
* @author
*/
@Controller
public class FrontRouterController {
/**
* 处理前端路由,将所有前端路由请求转发到 index.html
*/
@RequestMapping(value = {
"/ybmanagement/**",
"/system/**",
"/monitor/**",
"/tool/**",
"/doctorstation/**",
"/features/**",
"/todo/**",
"/appoinmentmanage/**",
"/clinicmanagement/**",
"/medicationmanagement/**",
"/yb/**",
"/patient/**",
"/charge/**",
"/nurse/**",
"/pharmacy/**",
"/report/**",
"/document/**",
"/triage/**",
"/check/**",
"/lab/**",
"/financial/**",
"/crosssystem/**",
"/workflow/**"
})
public String index() {
return "forward:/index.html";
}
}

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,47 @@ 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);
}
/**
* 刷新菜单缓存
*/
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
@PostMapping("/refreshCache")
public AjaxResult refreshCache() {
menuService.refreshMenuCache();
return success("菜单缓存已刷新");
}
/**
* 强制刷新当前用户菜单缓存
*/
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
@PostMapping("/refreshCurrentUserMenuCache")
public AjaxResult refreshCurrentUserMenuCache() {
menuService.clearMenuCacheByUserId(getUserId());
return success("当前用户菜单缓存已刷新");
}
}

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

@@ -1,6 +1,7 @@
package com.core.common.annotation;
import java.lang.annotation.*;
import java.math.BigDecimal;
/**
* Excel额外表头信息注解
@@ -14,7 +15,7 @@ public @interface ExcelExtra {
/**
* 表头名称
*/
String name();
String name() default "";
/**
* 日期格式yyyy-MM-dd HH:mm:ss
@@ -35,4 +36,15 @@ public @interface ExcelExtra {
* 是否导出
*/
boolean isExport() default true;
/**
* 精度 默认:-1(默认不开启BigDecimal格式化)
*/
int scale() default -1;
/**
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
*/
int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
}

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

@@ -0,0 +1,34 @@
package com.core.common.core.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果类
*
* @author
* @date 2026-02-02
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 数据列表
*/
private List<T> rows;
/**
* 总数
*/
private long total;
}

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

@@ -0,0 +1,28 @@
package com.core.common.enums;
/**
* 角色枚举
*
* @author swb
* @date 2026-01-29
*/
public enum RoleEnum {
DOCTOR("doctor", "医生"),
NURSE("nurse", "护士"),
ADMIN("admin", "管理员");
private final String code;
private final String info;
RoleEnum(String code, String info) {
this.code = code;
this.info = info;
}
public String getCode() {
return code;
}
public String getInfo() {
return info;
}
}

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

@@ -33,67 +33,125 @@ public final class AgeCalculatorUtil {
return period.getYears();
}
// /**
// * 当前年龄取得(床位列表表示年龄用)
// */
// public static String getAge(Date date) {
// // 将 Date 转换为 LocalDateTime
// LocalDateTime dateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
// LocalDateTime now = LocalDateTime.now();
// int years = now.getYear() - dateTime.getYear();
// if (years > 2) {
// return String.format("%d岁", years);
// }
//
// Period period = Period.between(dateTime.toLocalDate(), now.toLocalDate());
// int months = period.getMonths();
// int days = period.getDays();
// long hours = ChronoUnit.HOURS.between(dateTime, now) - (days * 24L);
//
// if (hours < 0) {
// hours += 24;
// days--;
// }
// if (days < 0) {
// months--;
// days = getLastDayOfMonth(dateTime) - dateTime.getDayOfMonth() + now.getDayOfMonth();
// }
// if (months < 0) {
// months += 12;
// years--;
// }
// if (years < 0) {
// return "1小时";
// }
//
// if (years > 0 && months > 0) {
// return String.format("%d岁%d月", years, months);
// }
// if (years > 0) {
// return String.format("%d岁", years);
// }
// if (months > 0 && days > 0) {
// return String.format("%d月%d天", months, days);
// }
// if (months > 0) {
// return String.format("%d月", months);
// }
// if (days > 0 && hours > 0) {
// return String.format("%d天%d小时", days, hours);
// }
// if (days > 0) {
// return String.format("%d天", days);
// }
// if (hours > 0) {
// return String.format("%d小时", hours);
// }
// return "1小时";
// }
/**
* 当前年龄取得(床位列表表示年龄用)
* 复刻Oracle函数FUN_GET_AGE的核心逻辑返回年龄字符串
*
* @param birthDate 出生日期
* @return 年龄字符串29岁、3岁5月、2月15天、18天出生日期晚于当前日期返回空字符串
*/
public static String getAge(Date date) {
// 添加空值检查
if (date == null) {
public static String getAge(Date birthDate) {
// 入参校验
if (birthDate == null) {
return "";
}
// 将 Date 转换为 LocalDateTime
LocalDateTime dateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime now = LocalDateTime.now();
int years = now.getYear() - dateTime.getYear();
if (years > 2) {
return String.format("%d岁", years);
// 将Date转换为LocalDate使用系统默认时区
LocalDate birthLocalDate = birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate currentDate = LocalDate.now();
// 计算总天数对应Oracle中的IDAY
long totalDays = ChronoUnit.DAYS.between(birthLocalDate, currentDate);
// 若出生日期晚于当前日期,返回空字符串
if (totalDays < 0) {
return "";
}
Period period = Period.between(dateTime.toLocalDate(), now.toLocalDate());
int months = period.getMonths();
int days = period.getDays();
long hours = ChronoUnit.HOURS.between(dateTime, now) - (days * 24L);
// 计算年份复刻Oracle的闰年补偿逻辑(当前年-出生年)/4 补偿闰年天数)
int birthYear = birthLocalDate.getYear();
int currentYear = currentDate.getYear();
long leapYearCompensation = (currentYear - birthYear) / 4;
long adjustedDays = totalDays - leapYearCompensation;
if (hours < 0) {
hours += 24;
days--;
}
if (days < 0) {
months--;
days = getLastDayOfMonth(dateTime) - dateTime.getDayOfMonth() + now.getDayOfMonth();
}
if (months < 0) {
months += 12;
years--;
}
if (years < 0) {
return "1小时";
}
// 计算年、月、天按365天/年、30天/月粗略折算与Oracle逻辑一致
int iYear = (int) (adjustedDays / 365);
long remainingDaysAfterYear = adjustedDays - iYear * 365;
int iMonth = (int) (remainingDaysAfterYear / 30);
int iDay = (int) (remainingDaysAfterYear - iMonth * 30);
if (years > 0 && months > 0) {
return String.format("%d岁%d月", years, months);
// 按原函数规则拼接返回字符串
if (iYear <= 0) {
// 小于1岁
if (iMonth <= 0) {
// 小于1个月返回X天
return iDay + "";
} else {
// 1个月及以上返回X月X天
return iMonth + "" + iDay + "";
}
} else {
// 1岁及以上
if (iYear < 5) {
// 1-4岁
if (iMonth <= 0) {
// 无整月返回X岁X天
return iYear + "" + iDay + "";
} else {
// 有整月返回X岁X月
return iYear + "" + iMonth + "";
}
} else {
// 5岁及以上仅返回X岁
return iYear + "";
}
}
if (years > 0) {
return String.format("%d岁", years);
}
if (months > 0 && days > 0) {
return String.format("%d月%d天", months, days);
}
if (months > 0) {
return String.format("%d月", months);
}
if (days > 0 && hours > 0) {
return String.format("%d天%d小时", days, hours);
}
if (days > 0) {
return String.format("%d天", days);
}
if (hours > 0) {
return String.format("%d小时", hours);
}
return "1小时";
}
private static int getLastDayOfMonth(LocalDateTime dateTime) {
int[] daysInMonth = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (isLeapYear(dateTime.getYear()) && dateTime.getMonthValue() == 2) {

View File

@@ -0,0 +1,137 @@
package com.core.common.utils;
import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Date;
/**
* 审计字段工具类
* 用于手动设置创建人、创建时间、更新人、更新时间等审计字段
*/
@Component
public class AuditFieldUtil {
private static final Logger log = LoggerFactory.getLogger(AuditFieldUtil.class);
/**
* 为实体设置创建相关的审计字段
* @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) {
log.error("设置创建审计字段时发生异常", e);
}
}
/**
* 为实体设置更新相关的审计字段
* @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) {
log.error("设置更新审计字段时发生异常", e);
}
}
/**
* 使用反射获取字段,支持父类字段
* @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

@@ -1,6 +1,8 @@
package com.core.common.utils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory;
import java.text.ParseException;
@@ -13,10 +15,13 @@ import java.util.Date;
/**
* 时间工具类
*
*
* @author system
*/
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
private static final Logger log = LoggerFactory.getLogger(DateUtils.class);
public static String YYYY = "yyyy";
public static String YYYY_MM = "yyyy-MM";
@@ -227,7 +232,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
return endTime;
}
} catch (DateTimeParseException e) {
e.printStackTrace();
log.warn("日期解析失败: {}", strDate, e);
}
return null;
}
@@ -250,7 +255,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
// 检查日期是否是未来的时间
return dateToCheck.isAfter(currentDate);
} catch (Exception e) {
e.printStackTrace();
log.warn("日期解析失败: {}", dateString, e);
// 解析失败或其他异常,返回 false 或根据需要处理异常
return false;
}

View File

@@ -1,19 +1,18 @@
package com.core.common.utils;
import com.core.common.annotation.Excel;
import com.core.common.annotation.Excel.ColumnType;
import com.core.common.annotation.Excel.Type;
import com.core.common.annotation.ExcelExtra;
import com.core.common.annotation.Excels;
import com.core.common.config.CoreConfig;
import com.core.common.core.domain.AjaxResult;
import com.core.common.core.text.Convert;
import com.core.common.exception.UtilException;
import com.core.common.utils.file.FileTypeUtils;
import com.core.common.utils.file.FileUtils;
import com.core.common.utils.file.ImageUtils;
import com.core.common.utils.poi.ExcelHandlerAdapter;
import com.core.common.utils.reflect.ReflectUtils;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -30,17 +29,20 @@ import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import com.core.common.annotation.Excel;
import com.core.common.annotation.Excel.ColumnType;
import com.core.common.annotation.Excel.Type;
import com.core.common.annotation.ExcelExtra;
import com.core.common.annotation.Excels;
import com.core.common.config.CoreConfig;
import com.core.common.core.domain.AjaxResult;
import com.core.common.core.text.Convert;
import com.core.common.exception.UtilException;
import com.core.common.utils.file.FileTypeUtils;
import com.core.common.utils.file.FileUtils;
import com.core.common.utils.file.ImageUtils;
import com.core.common.utils.poi.ExcelHandlerAdapter;
import com.core.common.utils.reflect.ReflectUtils;
/**
* Excel相关处理
@@ -1164,6 +1166,11 @@ public class NewExcelUtil<T> {
ParameterizedType pt = (ParameterizedType)field.getGenericType();
Class<?> subClass = (Class<?>)pt.getActualTypeArguments()[0];
this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
if (StringUtils.isNotEmpty(includeFields)) {
this.subFields = this.subFields.stream().filter(f -> ArrayUtils.contains(includeFields, f.getName())).collect(Collectors.toList());
} else if (StringUtils.isNotEmpty(excludeFields)) {
this.subFields = this.subFields.stream().filter(f -> !ArrayUtils.contains(excludeFields, f.getName())).collect(Collectors.toList());
}
}
}
@@ -1441,7 +1448,28 @@ public class NewExcelUtil<T> {
}
try {
// 计算表格体总列数
int totalCols = 0;
for (Object[] os : fields) {
Field field = (Field)os[0];
if (Collection.class.isAssignableFrom(field.getType()) && subFields != null) {
long subCount = subFields.stream().filter(f -> f.isAnnotationPresent(Excel.class)).count();
totalCols += subCount;
} else {
totalCols++;
}
}
if (totalCols == 0) totalCols = 1;
int currentRowNum = rownum;
int colIndex = 0;
Row row = null;
boolean hasVisible = false;
// 布局配置Label占用1列Value占用2列共3列
int labelCols = 1;
int valueCols = 2;
int itemCols = labelCols + valueCols;
for (Object[] os : extraFields) {
Field field = (Field)os[0];
@@ -1451,43 +1479,50 @@ public class NewExcelUtil<T> {
if (isExtraFieldHidden(field.getName())) {
continue;
}
hasVisible = true;
Row row = sheet.createRow(currentRowNum++);
// 自动换行:如果不是行首,且剩余空间不足,则换行
if (row == null) {
row = sheet.createRow(currentRowNum);
} else if (colIndex > 0 && colIndex + itemCols > totalCols) {
currentRowNum++;
row = sheet.createRow(currentRowNum);
colIndex = 0;
}
// 创建标签单元格第0列
Cell labelCell = row.createCell(0);
// 1. 创建 Label 单元格
Cell labelCell = row.createCell(colIndex);
labelCell.setCellValue(attr.name());
labelCell.setCellStyle(styles.get("extraLabel"));
// 创建值单元格第1列
Cell valueCell = row.createCell(1);
// 2. 创建 Value 单元格
int valueStartCol = colIndex + labelCols;
Cell valueCell = row.createCell(valueStartCol);
Object value = field.get(entity);
String cellValue = formatExtraCellValue(value, attr);
valueCell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue);
valueCell.setCellStyle(styles.get("extraValue"));
// 创建合并区域第1列到第2列
CellRangeAddress mergedRegion = new CellRangeAddress(row.getRowNum(), row.getRowNum(), 1, 2);
sheet.addMergedRegion(mergedRegion);
// 3. 合并 Value 单元格
if (valueCols > 1) {
int valueEndCol = valueStartCol + valueCols - 1;
CellRangeAddress mergedRegion = new CellRangeAddress(row.getRowNum(), row.getRowNum(), valueStartCol, valueEndCol);
sheet.addMergedRegion(mergedRegion);
// 设置边框
RegionUtil.setBorderTop(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderBottom(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderLeft(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderRight(BorderStyle.THIN, mergedRegion, sheet);
}
// 手动设置合并区域的边框,确保完整显示
RegionUtil.setBorderTop(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderBottom(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderLeft(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setBorderRight(BorderStyle.THIN, mergedRegion, sheet);
RegionUtil.setTopBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
RegionUtil.setBottomBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
RegionUtil.setLeftBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
RegionUtil.setRightBorderColor(IndexedColors.BLACK.getIndex(), mergedRegion, sheet);
colIndex += itemCols;
}
// 设置列宽
sheet.setColumnWidth(0, 15 * 256); // 标签列宽
sheet.setColumnWidth(1, 20 * 256); // 值列宽
sheet.setColumnWidth(2, 20 * 256); // 值列宽
// 更新当前行号,在额外表头和数据表头之间空一行
rownum = currentRowNum + 1;
if (hasVisible) {
rownum = currentRowNum + 2;
}
subMergedFirstRowNum = rownum;
subMergedLastRowNum = rownum;
@@ -1508,6 +1543,10 @@ public class NewExcelUtil<T> {
return "";
}
if (value instanceof BigDecimal && attr.scale() >= 0) {
return ((BigDecimal) value).setScale(attr.scale(), attr.roundingMode()).toString();
}
if (StringUtils.isNotEmpty(attr.dateFormat())) {
return parseDateToStr(attr.dateFormat(), value);
}
@@ -1808,7 +1847,7 @@ public class NewExcelUtil<T> {
row = sheet.createRow(rowNo);
}
// 子字段也要排序
List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class);
List<Field> subFields = this.subFields;
List<Field> sortedSubFields = subFields.stream().sorted(Comparator.comparing(subField -> {
Excel subExcel = subField.getAnnotation(Excel.class);
return subExcel.sort();
@@ -1832,4 +1871,4 @@ public class NewExcelUtil<T> {
}
}
}
}
}

View File

@@ -2,6 +2,8 @@ package com.core.common.utils;
import com.core.common.constant.Constants;
import com.core.common.core.text.Convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -20,10 +22,13 @@ import java.util.Map;
/**
* 客户端工具类
*
*
* @author system
*/
public class ServletUtils {
private static final Logger log = LoggerFactory.getLogger(ServletUtils.class);
/**
* 获取String参数
*/
@@ -130,7 +135,7 @@ public class ServletUtils {
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
log.error("渲染响应字符串失败", e);
}
}

View File

@@ -1,5 +1,8 @@
package com.core.common.utils.bean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@@ -12,6 +15,9 @@ import java.util.regex.Pattern;
* @author system
*/
public class BeanUtils extends org.springframework.beans.BeanUtils {
private static final Logger log = LoggerFactory.getLogger(BeanUtils.class);
/**
* Bean方法名中属性名开始的下标
*/
@@ -37,7 +43,7 @@ public class BeanUtils extends org.springframework.beans.BeanUtils {
try {
copyProperties(src, dest);
} catch (Exception e) {
e.printStackTrace();
log.error("Bean属性复制失败", e);
}
}

View File

@@ -115,7 +115,7 @@ public class FlowDefinitionController extends BaseController {
ImageIO.write(image, "png", os);
}
} catch (Exception e) {
e.printStackTrace();
log.error("读取流程图片失败, deployId: {}", deployId, e);
} finally {
try {
if (os != null) {
@@ -123,7 +123,7 @@ public class FlowDefinitionController extends BaseController {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
log.error("关闭输出流失败", e);
}
}

View File

@@ -211,7 +211,7 @@ public class FlowTaskController extends BaseController {
ImageIO.write(image, "png", os);
}
} catch (Exception e) {
e.printStackTrace();
log.error("读取流程图片失败", e);
} finally {
try {
if (os != null) {
@@ -219,7 +219,7 @@ public class FlowTaskController extends BaseController {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
log.error("关闭输出流失败", e);
}
}
}

View File

@@ -722,7 +722,7 @@ public class FlowableUtils {
// 反射设置属性值
field.set(propertyDto, attribute.getValue());
} catch (IllegalAccessException e) {
e.printStackTrace();
log.warn("反射设置属性值失败", e);
// 如果反射设置失败则忽略该属性
}
});
@@ -730,7 +730,7 @@ public class FlowableUtils {
return propertyDto;
}).collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
log.error("解析流程属性失败", e);
return Collections.emptyList(); // 如果发生异常则返回空列表
}
}

View File

@@ -209,7 +209,7 @@ public class FlowDefinitionServiceImpl extends FlowServiceFactory implements IFl
}
return AjaxResult.success("流程启动成功");
} catch (Exception e) {
e.printStackTrace();
log.error("流程启动错误", e);
return AjaxResult.error("流程启动错误");
}
}

View File

@@ -113,7 +113,7 @@ public class FlowInstanceServiceImpl extends FlowServiceFactory implements IFlow
runtimeService.startProcessInstanceById(procDefId, variables);
return AjaxResult.success("流程启动成功");
} catch (Exception e) {
e.printStackTrace();
log.error("流程启动错误, procDefId: {}", procDefId, e);
return AjaxResult.error("流程启动错误");
}
}

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

@@ -19,6 +19,8 @@ import com.core.generator.service.IGenTableColumnService;
import com.core.generator.service.IGenTableService;
import org.apache.commons.io.IOUtils;
import org.apache.poi.ss.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -43,6 +45,8 @@ import java.util.Map;
@RestController
@RequestMapping("/tool/gen")
public class GenController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(GenController.class);
@Autowired
private IGenTableService genTableService;
@@ -436,7 +440,7 @@ public class GenController extends BaseController {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
writer.write(str);
} catch (IOException e) {
e.printStackTrace();
log.error("写入文件失败: {}", filePath, e);
} finally {
is.close();
}

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

@@ -140,4 +140,11 @@ public interface SysMenuMapper {
* @return 结果
*/
public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
/**
* 查询所有菜单信息
*
* @return 菜单列表
*/
public List<SysMenu> selectAllMenus();
}

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,40 @@ 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);
/**
* 刷新菜单缓存
*/
public void refreshMenuCache();
/**
* 根据用户ID清除菜单缓存
*/
public void clearMenuCacheByUserId(Long userId);
/**
* 将菜单分配给角色
*
* @param roleId 角色ID
* @param menuIds 菜单ID列表
* @return 结果
*/
public int allocateMenuToRole(Long roleId, List<Long> menuIds);
}

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

@@ -8,6 +8,7 @@ import com.core.common.core.domain.entity.SysRole;
import com.core.common.core.domain.entity.SysUser;
import com.core.common.utils.SecurityUtils;
import com.core.common.utils.StringUtils;
import com.core.system.domain.SysRoleMenu;
import com.core.system.domain.vo.MetaVo;
import com.core.system.domain.vo.RouterVo;
import com.core.system.mapper.SysMenuMapper;
@@ -54,11 +55,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 查询系统菜单列表
*
*
* @param menu 菜单信息
* @return 菜单列表
*/
@Override
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : (#menu.menuName != null ? #menu.menuName : 'all') + ':' + (#menu.visible != null ? #menu.visible : 'all') + ':' + (#menu.status != null ? #menu.status : 'all'))")
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
List<SysMenu> menuList = null;
// 管理员显示所有菜单信息
@@ -109,11 +111,12 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 根据用户ID查询菜单
*
*
* @param userId 用户名称
* @return 菜单列表
*/
@Override
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuTree:' + #userId")
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
List<SysMenu> menus = null;
if (SecurityUtils.isAdmin(userId)) {
@@ -147,13 +150,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 +172,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 +185,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 +196,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 构建前端所需要树结构
*
*
* @param menus 菜单列表
* @return 树结构列表
*/
@@ -213,6 +218,146 @@ public class SysMenuServiceImpl implements ISysMenuService {
return returnList;
}
/**
* 构建前端所需要树结构(包含完整路径)
*
* @param menus 菜单列表
* @return 树结构列表
*/
@Override
public List<SysMenu> buildMenuTreeWithFullPath(List<SysMenu> menus) {
List<SysMenu> menuTree = buildMenuTree(menus);
// 一次性获取所有菜单信息避免N+1查询问题
List<SysMenu> allMenus = menuMapper.selectAllMenus();
Map<Long, SysMenu> menuMap = allMenus.stream()
.collect(Collectors.toMap(SysMenu::getMenuId, menu -> menu));
// 为每个菜单项添加完整路径优化版本避免N+1查询问题
addFullPathsToMenuTreeOptimized(menuTree, menuMap);
return menuTree;
}
/**
* 为菜单树添加完整路径(优化版本)
*
* @param menus 菜单树
* @param menuMap 菜单映射
*/
private void addFullPathsToMenuTreeOptimized(List<SysMenu> menus, Map<Long, SysMenu> menuMap) {
for (SysMenu menu : menus) {
// 使用优化的路径计算方法
menu.setFullPath(computeMenuFullPathOptimized(menu, menuMap));
// 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
addFullPathsToMenuTreeOptimized(menu.getChildren(), menuMap);
}
}
}
/**
* 优化的计算菜单完整路径方法
*
* @param menu 菜单
* @param menuMap 菜单映射
* @return 完整路径
*/
private String computeMenuFullPathOptimized(SysMenu menu, Map<Long, SysMenu> menuMap) {
if (menu == null || menu.getMenuId() == null) {
return "";
}
StringBuilder fullPath = new StringBuilder();
buildMenuPathOptimized(menu, fullPath, menuMap);
return normalizePath(fullPath.toString());
}
/**
* 优化的递归构建菜单路径
*
* @param menu 菜单信息
* @param path 路径构建器
* @param menuMap 菜单映射
*/
private void buildMenuPathOptimized(SysMenu menu, StringBuilder path, Map<Long, SysMenu> menuMap) {
if (menu == null || menu.getMenuId() == null) {
return;
}
// 如果不是根节点,则递归查找父节点
if (menu.getParentId() != null && menu.getParentId() > 0) {
SysMenu parentMenu = menuMap.get(menu.getParentId());
if (parentMenu != null) {
buildMenuPathOptimized(parentMenu, path, menuMap);
}
}
// 添加当前菜单的路径,避免双斜杠
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);
}
}
}
/**
* 递归收集菜单树中的所有菜单ID
*
* @param menus 菜单树
* @return 菜单ID列表
*/
private List<Long> collectMenuIds(List<SysMenu> menus) {
List<Long> menuIds = new ArrayList<>();
for (SysMenu menu : menus) {
menuIds.add(menu.getMenuId());
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
menuIds.addAll(collectMenuIds(menu.getChildren()));
}
}
return menuIds;
}
/**
* 批量获取菜单完整路径
*
* @param menuIds 菜单ID列表
* @return 菜单ID到完整路径的映射
*/
private Map<Long, String> batchGetMenuFullPaths(List<Long> menuIds) {
Map<Long, String> fullPathMap = new HashMap<>();
for (Long menuId : menuIds) {
// 使用缓存的getMenuFullPath方法
fullPathMap.put(menuId, getMenuFullPath(menuId));
}
return fullPathMap;
}
/**
* 为菜单树设置完整路径
*
* @param menus 菜单树
* @param fullPathMap 完整路径映射
*/
private void setFullPathsToMenuTree(List<SysMenu> menus, Map<Long, String> fullPathMap) {
for (SysMenu menu : menus) {
// 设置当前菜单的完整路径
menu.setFullPath(fullPathMap.get(menu.getMenuId()));
// 递归处理子菜单
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
setFullPathsToMenuTree(menu.getChildren(), fullPathMap);
}
}
}
/**
* 构建前端所需要下拉树结构
*
@@ -262,34 +407,50 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 新增保存菜单信息
*
*
* @param menu 菜单信息
* @return 结果
*/
@Override
@org.springframework.transaction.annotation.Transactional
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
})
public int insertMenu(SysMenu menu) {
//路径Path唯一性判断
SysMenu sysMenu = menuMapper.selectMenuByPath(menu.getPath());
if (sysMenu != null){
return -1;
}
return menuMapper.insertMenu(menu);
int rows = menuMapper.insertMenu(menu);
// 如果是管理员创建菜单,自动分配给所有角色(可选逻辑)
// 或者,可以将新菜单分配给创建者所属的角色
// 这里我们暂时不自动分配,因为这可能不符合安全最佳实践
return rows;
}
/**
* 修改保存菜单信息
*
*
* @param menu 菜单信息
* @return 结果
*/
@Override
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId"),
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #menu.updateBy")
})
public int updateMenu(SysMenu menu) {
//路径Path唯一性判断排除当前菜单本身
String path = menu.getPath();
if (StringUtils.isNotBlank(path)) {
SysMenu sysMenu = menuMapper.selectMenuByPathExcludeId(menu.getPath(), menu.getMenuId());
if (sysMenu != null) {
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
menu.getMenuId(), menu.getPath(), sysMenu.getMenuId());
return -1; // 路由地址已存在
}
@@ -300,11 +461,14 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 删除菜单管理信息
*
*
* @param menuId 菜单ID
* @return 结果
*/
@Override
@org.springframework.cache.annotation.Caching(evict = {
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
})
public int deleteMenuById(Long menuId) {
return menuMapper.deleteMenuById(menuId);
}
@@ -486,11 +650,186 @@ 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
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'fullPath:' + #menuId", unless = "#result == null || #result.isEmpty()")
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());
}
/**
* 刷新菜单缓存
*/
@Override
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
public void refreshMenuCache() {
log.info("菜单缓存已刷新");
}
/**
* 根据用户ID清除菜单缓存
*/
@Override
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #userId")
public void clearMenuCacheByUserId(Long userId) {
log.info("清除用户 {} 的菜单树缓存", userId);
}
/**
* 将菜单分配给角色
*
* @param roleId 角色ID
* @param menuIds 菜单ID列表
* @return 结果
*/
@Override
@org.springframework.transaction.annotation.Transactional
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
public int allocateMenuToRole(Long roleId, List<Long> menuIds) {
// 先删除该角色现有的所有菜单权限
roleMenuMapper.deleteRoleMenuByRoleId(roleId);
// 重新分配菜单给角色
if (menuIds != null && !menuIds.isEmpty()) {
List<SysRoleMenu> roleMenuList = new ArrayList<>();
for (Long menuId : menuIds) {
SysRoleMenu rm = new SysRoleMenu();
rm.setRoleId(roleId);
rm.setMenuId(menuId);
roleMenuList.add(rm);
}
return roleMenuMapper.batchRoleMenu(roleMenuList);
}
return 0;
}
}

View File

@@ -0,0 +1,181 @@
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
{
private static final Logger log = LoggerFactory.getLogger(SysUserConfigServiceImpl.class);
@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) {
// 记录错误日志以便调试
log.error("保存用户配置时发生错误, userId: {}, configKey: {}", userId, configKey, e);
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"/>
@@ -273,4 +274,27 @@
where menu_id = #{menuId}
</delete>
<select id="selectAllMenus" resultMap="SysMenuResult">
select menu_id,
parent_id,
menu_name,
path,
component,
"query",
route_name,
is_frame,
is_cache,
menu_type,
visible,
status,
perms,
icon,
order_num,
create_time,
update_time,
remark
from sys_menu
order by parent_id, order_num
</select>
</mapper>

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

@@ -0,0 +1,38 @@
# Web Layer - API Controllers
**Module**: `openhis-application/web`
**Role**: API endpoint layer - all REST controllers for frontend communication
## OVERVIEW
46 web modules serving REST APIs for all business functionality.
## STRUCTURE
```
web/
├── [module-name]/
│ ├── controller/ # REST endpoints (@RestController)
│ ├── dto/ # Data transfer objects
│ ├── mapper/ # MyBatis mappers (if module-specific)
│ └── appservice/ # Application service layer
│ └── impl/
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| API endpoints | `*/controller/*Controller.java` |
| Request/Response schemas | `*/dto/*.java` |
| Business logic orchestration | `*/appservice/*.java` |
## CONVENTIONS
- Controllers: `@RestController`, `@RequestMapping("/module-name")`
- Standard response: `AjaxResult` from core-common
- DTO naming: `XxxRequest`, `XxxResponse`, `XxxDTO`
- Service pattern: interface in `appservice/`, impl in `appservice/impl/`
- API naming: `listXxx()`, `getXxx()`, `addXxx()`, `updateXxx()`, `deleteXxx()`
## ANTI-PATTERNS
- Never put business logic in controllers - delegate to appservice
- Never return raw entities - use DTOs
- Never bypass `AjaxResult` wrapper
- Never create module-specific mappers without justification

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

@@ -104,4 +104,8 @@ public class InstrumentManageDto {
/** 备注 */
private String remarks;
// 手动添加 getter 方法
public Integer getInstrumentTypeEnum() {
return instrumentTypeEnum;
}
}

View File

@@ -18,6 +18,19 @@ public class InstrumentManageInitDto {
private List<InstrumentType> InstrumentTypeList;
private List<InstrumentStatusEnumOption> InstrumentStatusEnumList;
// 手动添加 setter 方法
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
this.statusFlagOptions = statusFlagOptions;
}
public void setInstrumentTypeList(List<InstrumentType> InstrumentTypeList) {
this.InstrumentTypeList = InstrumentTypeList;
}
public void setInstrumentStatusEnumList(List<InstrumentStatusEnumOption> InstrumentStatusEnumList) {
this.InstrumentStatusEnumList = InstrumentStatusEnumList;
}
/**
* 状态
*/

View File

@@ -13,4 +13,13 @@ import java.util.List;
public class InstrumentStatusRequest {
private List<Long> ids;
private String type;
// 手动添加 getter 方法
public String getType() {
return type;
}
public List<Long> getIds() {
return ids;
}
}

View File

@@ -28,4 +28,36 @@ public class LisConfigManageDto {
private List<ActivityDefSpecimenDef> activityDefSpecimenDefs;
// 手动添加 getter 和 setter 方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<ActivityDefDeviceDef> getActivityDefDeviceDefs() {
return activityDefDeviceDefs;
}
public void setActivityDefDeviceDefs(List<ActivityDefDeviceDef> activityDefDeviceDefs) {
this.activityDefDeviceDefs = activityDefDeviceDefs;
}
public List<ActivityDefObservationDef> getActivityDefObservationDefs() {
return activityDefObservationDefs;
}
public void setActivityDefObservationDefs(List<ActivityDefObservationDef> activityDefObservationDefs) {
this.activityDefObservationDefs = activityDefObservationDefs;
}
public List<ActivityDefSpecimenDef> getActivityDefSpecimenDefs() {
return activityDefSpecimenDefs;
}
public void setActivityDefSpecimenDefs(List<ActivityDefSpecimenDef> activityDefSpecimenDefs) {
this.activityDefSpecimenDefs = activityDefSpecimenDefs;
}
}

View File

@@ -23,4 +23,16 @@ public class LisConfigManageInitDto {
private List<SpecimenDefinition> specimenDefs;
// 手动添加 setter 方法
public void setDeviceDefs(List<DeviceDefinition> deviceDefs) {
this.deviceDefs = deviceDefs;
}
public void setObservationDefs(List<ObservationDefinition> observationDefs) {
this.observationDefs = observationDefs;
}
public void setSpecimenDefs(List<SpecimenDefinition> specimenDefs) {
this.specimenDefs = specimenDefs;
}
}

View File

@@ -44,7 +44,16 @@ public class ObservationDefManageDto {
/** 删除状态) */
private String deleteFlag;
// 手动添加 getter 方法
public Long getInstrumentId() {
return instrumentId;
}
public Integer getStatusEnum() {
return statusEnum;
}
public Integer getObservationTypeEnum() {
return observationTypeEnum;
}
}

View File

@@ -18,6 +18,19 @@ public class ObservationDefManageInitDto {
private List<ObservationTypeEnumOption> ObservationTypeList;
private List<InstrumentEnumOption> instrumentEnumOptionList;
// 手动添加 setter 方法
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
this.statusFlagOptions = statusFlagOptions;
}
public void setObservationTypeList(List<ObservationTypeEnumOption> ObservationTypeList) {
this.ObservationTypeList = ObservationTypeList;
}
public void setInstrumentEnumOptionList(List<InstrumentEnumOption> instrumentEnumOptionList) {
this.instrumentEnumOptionList = instrumentEnumOptionList;
}
/**
* 状态
*/

View File

@@ -13,4 +13,13 @@ import java.util.List;
public class ObservationDefStatusRequest {
private List<Long> ids;
private String type;
// 手动添加 getter 方法
public String getType() {
return type;
}
public List<Long> getIds() {
return ids;
}
}

View File

@@ -32,4 +32,8 @@ public class ReportResultManageDto {
private String authoredTime; // 开单时间
// 手动添加 getter 方法
public Integer getGenderEnum() {
return genderEnum;
}
}

View File

@@ -40,4 +40,12 @@ public class SampleCollectManageDto {
private String authoredTime; // 开单时间
// 手动添加 getter 方法
public Integer getGenderEnum() {
return genderEnum;
}
public Integer getCollectionStatusEnum() {
return collectionStatusEnum;
}
}

View File

@@ -13,4 +13,13 @@ import java.util.List;
public class SampleCollectStatusRequest {
private List<Long> ids;
private String type;
// 手动添加 getter 方法
public String getType() {
return type;
}
public List<Long> getIds() {
return ids;
}
}

View File

@@ -59,4 +59,12 @@ public class SpecimenDefManageDto {
private Integer statusEnum;
private String statusEnumText;
// 手动添加 getter 方法
public Integer getSpecimenTypeEnum() {
return specimenTypeEnum;
}
public Integer getStatusEnum() {
return statusEnum;
}
}

View File

@@ -18,6 +18,15 @@ public class SpecimenDefManageInitDto {
private List<statusEnumOption> statusFlagOptions;
private List<SpecimenType> SpecimenTypeList;
// 手动添加 setter 方法
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
this.statusFlagOptions = statusFlagOptions;
}
public void setSpecimenTypeList(List<SpecimenType> SpecimenTypeList) {
this.SpecimenTypeList = SpecimenTypeList;
}
/**
* 状态
*/

View File

@@ -13,4 +13,13 @@ import java.util.List;
public class SpecimenDefStatusRequest {
private List<Long> ids;
private String type;
// 手动添加 getter 方法
public String getType() {
return type;
}
public List<Long> getIds() {
return ids;
}
}

View File

@@ -69,4 +69,13 @@ public class AdjustPriceDataVo {
private Long locationId;
private BigDecimal finalTotalQuantity;
// 手动添加 getter 方法
public Long getItemId() {
return itemId;
}
public Integer getCategoryType() {
return categoryType;
}
}

View File

@@ -7,7 +7,19 @@ public interface IDoctorScheduleAppService {
R<?> getDoctorScheduleList();
R<?> getTodayDoctorScheduleList();
R<?> getTodayMySchedule();
R<?> getDoctorScheduleListByDeptId(Long deptId);
R<?> getDoctorScheduleListByDeptIdAndDateRange(Long deptId, String startDate, String endDate);
R<?> addDoctorSchedule(DoctorSchedule doctorSchedule);
R<?> addDoctorScheduleWithDate(DoctorSchedule doctorSchedule, String scheduledDate);
R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule);
R<?> removeDoctorSchedule(Integer doctorScheduleId);
}

View File

@@ -5,4 +5,6 @@ import com.openhis.web.appointmentmanage.dto.SchedulePoolDto;
public interface ISchedulePoolAppService {
R<?> addSchedulePool(SchedulePoolDto schedulePoolDto);
R<?> list(SchedulePoolDto schedulePoolDto);
}

View File

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

View File

@@ -2,35 +2,101 @@ package com.openhis.web.appointmentmanage.appservice.impl;
import cn.hutool.core.util.ObjectUtil;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
import com.openhis.appointmentmanage.domain.SchedulePool;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper;
import com.openhis.appointmentmanage.service.IDoctorScheduleService;
import com.openhis.appointmentmanage.service.ISchedulePoolService;
import com.openhis.appointmentmanage.service.IScheduleSlotService;
import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
@Resource
private IDoctorScheduleService doctorScheduleService;
@Resource
private ISchedulePoolService schedulePoolService;
@Resource
private IScheduleSlotService scheduleSlotService;
@Resource
private DoctorScheduleMapper doctorScheduleMapper;
@Override
public R<?> getDoctorScheduleList() {
List<DoctorSchedule> list = doctorScheduleService.list();
return R.ok(list);
}
@Override
public R<?> getDoctorScheduleListByDeptId(Long deptId) {
List<DoctorSchedule> list = doctorScheduleService.list(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DoctorSchedule>()
.eq("dept_id", deptId));
return R.ok(list);
}
@Override
public R<?> getDoctorScheduleListByDeptIdAndDateRange(Long deptId, String startDate, String endDate) {
// 联表查询 adm_doctor_schedule LEFT JOIN adm_schedule_pool
// 通过 schedule_date 获取具体出诊日期,解决按星期匹配导致日期错位的问题
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectScheduleWithDateByDeptAndRange(
deptId, startDate, endDate);
return R.ok(list);
}
@Transactional(readOnly = true)
@Override
public R<?> getTodayDoctorScheduleList() {
// 联表查询 adm_schedule_pool按今日具体日期查询排班替代原来按星期匹配的方式
String todayStr = LocalDate.now().toString(); // yyyy-MM-dd
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectTodaySchedule(todayStr);
return R.ok(list);
}
@Transactional(readOnly = true)
@Override
public R<?> getTodayMySchedule() {
// 联表查询 adm_schedule_pool按今日具体日期 + 医生ID 查询个人排班
String todayStr = LocalDate.now().toString(); // yyyy-MM-dd
// 从 SecurityUtils 获取当前登录医生ID
Long currentDoctorId = SecurityUtils.getLoginUser().getPractitionerId();
if (currentDoctorId != null) {
List<DoctorScheduleWithDateDto> list = doctorScheduleMapper.selectTodayMySchedule(todayStr,
currentDoctorId);
return R.ok(list);
}
// 如果未绑定医生,则返回空列表
return R.ok(Collections.emptyList());
}
@Override
public R<?> addDoctorSchedule(DoctorSchedule doctorSchedule) {
if (ObjectUtil.isEmpty(doctorSchedule)) {
return R.fail("医生排班不能为空");
}
if (doctorSchedule.getLimitNumber() == null || doctorSchedule.getLimitNumber() <= 0) {
return R.fail("限号数量必须大于0");
}
// 创建新对象排除id字段数据库id列是GENERATED ALWAYS由数据库自动生成
DoctorSchedule newSchedule = new DoctorSchedule();
newSchedule.setWeekday(doctorSchedule.getWeekday());
@@ -41,33 +107,316 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
newSchedule.setEndTime(doctorSchedule.getEndTime());
newSchedule.setLimitNumber(doctorSchedule.getLimitNumber());
// call_sign_record 字段不能为null设置默认值为空字符串
newSchedule.setCallSignRecord(doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
newSchedule.setCallSignRecord(
doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
newSchedule.setRegisterItem(doctorSchedule.getRegisterItem() != null ? doctorSchedule.getRegisterItem() : "");
newSchedule.setRegisterFee(doctorSchedule.getRegisterFee() != null ? doctorSchedule.getRegisterFee() : 0);
newSchedule.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
newSchedule
.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
newSchedule.setDiagnosisFee(doctorSchedule.getDiagnosisFee() != null ? doctorSchedule.getDiagnosisFee() : 0);
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : false);
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : true);
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
newSchedule.setDeptId(doctorSchedule.getDeptId());
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
// 不设置id字段让数据库自动生成
// 使用自定义的insertWithoutId方法确保INSERT语句不包含id字段
int result = doctorScheduleMapper.insertWithoutId(newSchedule);
boolean save = result > 0;
if (save) {
// 返回保存后的实体对象,包含数据库生成的ID
return R.ok(newSchedule);
if (result > 0) {
// 创建号源池,并传入正确的医生ID
SchedulePool pool = createSchedulePool(newSchedule, doctorSchedule.getDoctorId());
boolean poolSaved = schedulePoolService.save(pool);
if (poolSaved) {
// 创建号源槽
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
newSchedule.getStartTime(), newSchedule.getEndTime());
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
if (slotsSaved) {
// 不更新available_num字段因为它是数据库生成列
// pool.setAvailableNum(newSchedule.getLimitNumber());
// schedulePoolService.updateById(pool);
return R.ok(newSchedule);
} else {
throw new RuntimeException("创建号源槽失败");
}
} else {
throw new RuntimeException("创建号源池失败");
}
} else {
return R.fail("保存失败");
return R.fail("保存排班信息失败");
}
}
@Override
public R<?> addDoctorScheduleWithDate(DoctorSchedule doctorSchedule, String scheduledDate) {
if (ObjectUtil.isEmpty(doctorSchedule)) {
return R.fail("医生排班不能为空");
}
if (doctorSchedule.getLimitNumber() == null || doctorSchedule.getLimitNumber() <= 0) {
return R.fail("限号数量必须大于0");
}
// 创建新对象排除id字段数据库id列是GENERATED ALWAYS由数据库自动生成
DoctorSchedule newSchedule = new DoctorSchedule();
newSchedule.setWeekday(doctorSchedule.getWeekday());
newSchedule.setTimePeriod(doctorSchedule.getTimePeriod());
newSchedule.setDoctor(doctorSchedule.getDoctor());
newSchedule.setClinic(doctorSchedule.getClinic());
newSchedule.setStartTime(doctorSchedule.getStartTime());
newSchedule.setEndTime(doctorSchedule.getEndTime());
newSchedule.setLimitNumber(doctorSchedule.getLimitNumber());
// call_sign_record 字段不能为null设置默认值为空字符串
newSchedule.setCallSignRecord(
doctorSchedule.getCallSignRecord() != null ? doctorSchedule.getCallSignRecord() : "");
newSchedule.setRegisterItem(doctorSchedule.getRegisterItem() != null ? doctorSchedule.getRegisterItem() : "");
newSchedule.setRegisterFee(doctorSchedule.getRegisterFee() != null ? doctorSchedule.getRegisterFee() : 0);
newSchedule
.setDiagnosisItem(doctorSchedule.getDiagnosisItem() != null ? doctorSchedule.getDiagnosisItem() : "");
newSchedule.setDiagnosisFee(doctorSchedule.getDiagnosisFee() != null ? doctorSchedule.getDiagnosisFee() : 0);
newSchedule.setIsOnline(doctorSchedule.getIsOnline() != null ? doctorSchedule.getIsOnline() : true);
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
newSchedule.setDeptId(doctorSchedule.getDeptId());
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
// 不设置id字段让数据库自动生成
// 使用自定义的insertWithoutId方法确保INSERT语句不包含id字段
int result = doctorScheduleMapper.insertWithoutId(newSchedule);
if (result > 0) {
// 创建号源池并传入正确的医生ID和具体日期
SchedulePool pool = createSchedulePoolWithDate(newSchedule, doctorSchedule.getDoctorId(), scheduledDate);
boolean poolSaved = schedulePoolService.save(pool);
if (poolSaved) {
// 创建号源槽
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
newSchedule.getStartTime(), newSchedule.getEndTime());
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
if (slotsSaved) {
return R.ok(newSchedule);
} else {
throw new RuntimeException("创建号源槽失败");
}
} else {
throw new RuntimeException("创建号源池失败");
}
} else {
return R.fail("保存排班信息失败");
}
}
@Override
public R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule) {
if (ObjectUtil.isEmpty(doctorSchedule) || ObjectUtil.isEmpty(doctorSchedule.getId())) {
return R.fail("医生排班ID不能为空");
}
// 注意:此为核心更新,暂未处理号源池和号源槽的同步更新
int result = doctorScheduleMapper.updateDoctorSchedule(doctorSchedule);
return result > 0 ? R.ok(result) : R.fail("更新排班信息失败");
}
/**
* 创建号源池
*/
private SchedulePool createSchedulePool(DoctorSchedule schedule, Long doctorId) {
SchedulePool pool = new SchedulePool();
// 生成唯一池编码
pool.setPoolCode("POOL_" + System.currentTimeMillis());
pool.setHospitalId(1L); // 默认医院ID实际项目中应从上下文获取
pool.setDoctorId(doctorId); // 使用正确的医生ID
pool.setDoctorName(schedule.getDoctor());
pool.setDeptId(schedule.getDeptId());
pool.setClinicRoom(schedule.getClinic());
// 设置出诊日期,这里假设是下周的对应星期
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
pool.setShift(schedule.getTimePeriod());
pool.setStartTime(schedule.getStartTime());
pool.setEndTime(schedule.getEndTime());
pool.setTotalQuota(schedule.getLimitNumber());
pool.setBookedNum(0);
pool.setLockedNum(0);
// 不设置available_num因为它是数据库生成列
// pool.setAvailableNum(0); // 初始为0稍后更新
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
// 暂时设置support_channel为空字符串避免JSON类型问题
pool.setSupportChannel("");
pool.setStatus(1); // 1表示可用
// 设置时间字段
java.util.Date now = new java.util.Date();
java.util.Date tomorrow = new java.util.Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 明天的时间
pool.setReleaseTime(now);
pool.setDeadlineTime(tomorrow); // 截止时间为明天
pool.setScheduleId(schedule.getId());
return pool;
}
/**
* 创建号源池(使用具体日期)
*/
private SchedulePool createSchedulePoolWithDate(DoctorSchedule schedule, Long doctorId, String scheduledDateStr) {
SchedulePool pool = new SchedulePool();
// 生成唯一池编码
pool.setPoolCode("POOL_" + System.currentTimeMillis());
pool.setHospitalId(1L); // 默认医院ID实际项目中应从上下文获取
pool.setDoctorId(doctorId); // 使用正确的医生ID
pool.setDoctorName(schedule.getDoctor());
pool.setDeptId(schedule.getDeptId());
pool.setClinicRoom(schedule.getClinic());
// 使用传入的具体日期
if (scheduledDateStr != null && !scheduledDateStr.isEmpty()) {
try {
LocalDate scheduledDate = LocalDate.parse(scheduledDateStr);
pool.setScheduleDate(scheduledDate);
} catch (Exception e) {
// 如果解析失败,回退到原来的计算方式
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
}
} else {
// 如果没有提供具体日期,使用原来的计算方式
pool.setScheduleDate(calculateScheduleDate(schedule.getWeekday()));
}
pool.setShift(schedule.getTimePeriod());
pool.setStartTime(schedule.getStartTime());
pool.setEndTime(schedule.getEndTime());
pool.setTotalQuota(schedule.getLimitNumber());
pool.setBookedNum(0);
pool.setLockedNum(0);
// 不设置available_num因为它是数据库生成列
// pool.setAvailableNum(0); // 初始为0稍后更新
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
// 暂时设置support_channel为空字符串避免JSON类型问题
pool.setSupportChannel("");
pool.setStatus(1); // 1表示可用
// 设置时间字段
java.util.Date now = new java.util.Date();
java.util.Date tomorrow = new java.util.Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 明天的时间
pool.setReleaseTime(now);
pool.setDeadlineTime(tomorrow); // 截止时间为明天
pool.setScheduleId(schedule.getId());
return pool;
}
/**
* 创建号源槽
*/
private List<ScheduleSlot> createScheduleSlots(Integer poolId, Integer limitNumber, LocalTime startTime,
LocalTime endTime) {
List<ScheduleSlot> slots = new ArrayList<>();
// 计算时间间隔
long totalTimeMinutes = startTime.until(endTime, java.time.temporal.ChronoUnit.MINUTES);
long interval = totalTimeMinutes / limitNumber;
for (int i = 1; i <= limitNumber; i++) {
ScheduleSlot slot = new ScheduleSlot();
slot.setPoolId(poolId);
slot.setSeqNo(i); // 序号
slot.setStatus(0); // 0表示可用
// 计算预计叫号时间,均匀分布在开始时间和结束时间之间
LocalTime expectTime = startTime.plusMinutes(interval * (i - 1));
slot.setExpectTime(expectTime);
java.util.Date now = new java.util.Date();
slot.setCreateTime(now);
slot.setUpdateTime(now);
slots.add(slot);
}
return slots;
}
/**
* 根据星期几计算具体日期(下周的对应星期)
*/
private LocalDate calculateScheduleDate(String weekday) {
// 这里简单实现,实际项目中可能需要更复杂的日期计算逻辑
LocalDate today = LocalDate.now();
int currentDayOfWeek = today.getDayOfWeek().getValue(); // 1=Monday, 7=Sunday
int targetDayOfWeek = getDayOfWeekNumber(weekday); // 假设weekday是中文如"周一"
// 计算到下周对应星期的天数差
int daysToAdd = targetDayOfWeek - currentDayOfWeek + 7; // 加7确保是下周
return today.plusDays(daysToAdd);
}
/**
* 将中文星期转换为数字1=周一7=周日)
*/
private int getDayOfWeekNumber(String weekday) {
switch (weekday) {
case "周一":
return 1;
case "周二":
return 2;
case "周三":
return 3;
case "周四":
return 4;
case "周五":
return 5;
case "周六":
return 6;
case "周日":
return 7;
default:
return 1; // 默认周一
}
}
@Transactional
@Override
public R<?> removeDoctorSchedule(Integer doctorScheduleId) {
if (doctorScheduleId == null && ObjectUtil.isEmpty(doctorScheduleId)) {
if (doctorScheduleId == null) {
return R.fail("排班id不能为空");
}
boolean remove = doctorScheduleService.removeById(doctorScheduleId);
return R.ok(remove);
// 1. 根据排班ID找到关联的号源池
List<SchedulePool> pools = schedulePoolService.list(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SchedulePool>()
.eq("schedule_id", doctorScheduleId));
if (ObjectUtil.isNotEmpty(pools)) {
List<Long> poolIds = pools.stream().map(SchedulePool::getId).collect(java.util.stream.Collectors.toList());
// 2. 根据号源池ID找到所有关联的号源槽
List<ScheduleSlot> slots = scheduleSlotService.list(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ScheduleSlot>()
.in("pool_id", poolIds));
if (ObjectUtil.isNotEmpty(slots)) {
List<Integer> slotIds = slots.stream().map(ScheduleSlot::getId)
.collect(java.util.stream.Collectors.toList());
// 3. 逻辑删除所有号源槽
scheduleSlotService.removeByIds(slotIds);
}
// 4. 逻辑删除所有号源池
schedulePoolService.removeByIds(poolIds);
}
// 5. 逻辑删除主排班记录
boolean removed = doctorScheduleService.removeById(doctorScheduleId);
return R.ok(removed);
}
}

View File

@@ -39,4 +39,15 @@ public class SchedulePoolAppServiceImpl implements ISchedulePoolAppService {
boolean save = schedulePoolService.save(schedulePool);
return R.ok(save);
}
@Override
public R<?> list(SchedulePoolDto schedulePoolDto) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SchedulePool> wrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
wrapper.like(ObjectUtil.isNotEmpty(schedulePoolDto.getDoctorName()), SchedulePool::getDoctorName, schedulePoolDto.getDoctorName());
wrapper.eq(ObjectUtil.isNotNull(schedulePoolDto.getDeptId()), SchedulePool::getDeptId, schedulePoolDto.getDeptId());
wrapper.ge(ObjectUtil.isNotEmpty(schedulePoolDto.getQueryBeginDate()), SchedulePool::getScheduleDate, schedulePoolDto.getQueryBeginDate());
wrapper.le(ObjectUtil.isNotEmpty(schedulePoolDto.getQueryEndDate()), SchedulePool::getScheduleDate, schedulePoolDto.getQueryEndDate());
return R.ok(schedulePoolService.list(wrapper));
}
}

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(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(slotId); // 对应 WHERE id = #{id}
schedule.setIsStopped(false); // 设置为 false
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,91 @@ 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) {
// 1. 先转成 Instant
Instant instant = schedule.getCreateTime().toInstant();
// 2. 结合时区转成 ZonedDateTime
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
// 3. 再转回 Date (如果 DTO 需要的是 Date)
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 +297,6 @@ public class TicketAppServiceImpl implements ITicketAppService {
case "cancelled":
dto.setStatus("已取消");
break;
case "locked":
dto.setStatus("已锁定");
break;
default:
dto.setStatus(status);
}

View File

@@ -22,13 +22,53 @@ public class DoctorScheduleController {
return R.ok(doctorScheduleAppService.getDoctorScheduleList());
}
/*
* 根据科室ID获取医生排班List
*
* */
@GetMapping("/list-by-dept/{deptId}")
public R<?> getDoctorScheduleListByDeptId(@PathVariable Long deptId) {
return R.ok(doctorScheduleAppService.getDoctorScheduleListByDeptId(deptId));
}
/*
* 根据科室ID和日期范围获取医生排班List
*
* */
@GetMapping("/list-by-dept-and-date")
public R<?> getDoctorScheduleListByDeptIdAndDateRange(@RequestParam Long deptId,
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(doctorScheduleAppService.getDoctorScheduleListByDeptIdAndDateRange(deptId, startDate, endDate));
}
/*
* 新增医生排班
*
* */
@PostMapping("/add")
public R<?> addDoctorSchedule(@RequestBody DoctorSchedule doctorSchedule) {
return R.ok(doctorScheduleAppService.addDoctorSchedule(doctorSchedule));
return doctorScheduleAppService.addDoctorSchedule(doctorSchedule);
}
/*
* 新增医生排班(带具体日期)
*
* */
@PostMapping("/add-with-date")
public R<?> addDoctorScheduleWithDate(@RequestBody DoctorSchedule doctorSchedule) {
// 从DoctorSchedule对象中获取scheduledDate字段
String scheduledDate = doctorSchedule.getScheduledDate();
return doctorScheduleAppService.addDoctorScheduleWithDate(doctorSchedule, scheduledDate);
}
/*
* 修改医生排班
*
* */
@PutMapping("/update")
public R<?> updateDoctorSchedule(@RequestBody DoctorSchedule doctorSchedule) {
return doctorScheduleAppService.updateDoctorSchedule(doctorSchedule);
}
/*
@@ -40,4 +80,22 @@ public class DoctorScheduleController {
return R.ok(doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId));
}
/*
* 获取今日医生排班List
*
* */
@GetMapping("/today")
public R<?> getTodayDoctorScheduleList() {
return R.ok(doctorScheduleAppService.getTodayDoctorScheduleList());
}
/*
* 获取当前登录医生今日排班List
*
* */
@GetMapping("/today-my-schedule")
public R<?> getTodayMySchedule() {
return doctorScheduleAppService.getTodayMySchedule();
}
}

View File

@@ -4,9 +4,7 @@ package com.openhis.web.appointmentmanage.controller;
import com.core.common.core.domain.R;
import com.openhis.web.appointmentmanage.appservice.ISchedulePoolAppService;
import com.openhis.web.appointmentmanage.dto.SchedulePoolDto;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@@ -20,8 +18,18 @@ public class SchedulePoolController {
* 新增号源
*
* */
@PostMapping("/add")
public R<?> addSchedulePool(@RequestBody SchedulePoolDto schedulePoolDto) {
return R.ok(schedulePoolAppService.addSchedulePool(schedulePoolDto));
return schedulePoolAppService.addSchedulePool(schedulePoolDto);
}
/*
* 查询号源
*
* */
@GetMapping("/list")
public R<?> list(SchedulePoolDto schedulePoolDto) {
return schedulePoolAppService.list(schedulePoolDto);
}
}

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

@@ -14,19 +14,19 @@ import java.time.LocalTime;
@Data
public class SchedulePoolDto {
/** id */
private Integer id;
private Long id;
/** 业务编号 */
private String poolCode;
/** 医院ID */
private Integer hospitalId;
private Long hospitalId;
/** 科室ID */
private Integer deptId;
private Long deptId;
/** 医生ID */
private Integer doctorId;
private Long doctorId;
/** 医生姓名 */
private String doctorName;
@@ -86,17 +86,23 @@ public class SchedulePoolDto {
private Integer version;
/** 操作人ID */
private Integer opUserId;
private Long opUserId;
/** 备注 */
private String remark;
/** 排班ID */
private Integer scheduleId;
private Long scheduleId;
/** 创建时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateTime;
/** 查询开始日期 */
private String queryBeginDate;
/** 查询结束日期 */
private String queryEndDate;
}

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 应该服务类
@@ -13,18 +14,19 @@ public interface IOrganizationAppService {
/**
* 查询机构树
*
* @param pageNo 当前页码
* @param pageSize 查询条数
* @param name 科室名称
* @param typeEnum 科室类型
* @param classEnum 科室分类
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param request 请求数据
* @param pageNo 当前页码
* @param pageSize 查询条数
* @param name 科室名称
* @param typeEnum 科室类型
* @param classEnumList 科室分类列表(逗号分隔的值)
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param request 请求数据
* @return 机构树分页列表
*/
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, Integer classEnum,
String sortField, String sortOrder, HttpServletRequest request);
Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum,
List<String> classEnumList,
String sortField, String sortOrder, HttpServletRequest request);
/**
* 机构信息详情
@@ -66,4 +68,15 @@ public interface IOrganizationAppService {
*/
R<?> inactiveOrg(Long orgId);
/**
* 获取挂号科室列表
*
* @param pageNum 当前页码
* @param pageSize 查询条数
* @param name 机构/科室名称
* @param orgName 机构名称
* @return 挂号科室列表
*/
R<?> getRegisterOrganizations(Integer pageNum, Integer pageSize, String name, String orgName);
}

View File

@@ -1,7 +1,6 @@
package com.openhis.web.basedatamanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.utils.AssignSeqUtil;
@@ -27,8 +26,6 @@ import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
import static com.baomidou.mybatisplus.core.toolkit.StringUtils.camelToUnderline;
@Service
public class OrganizationAppServiceImpl implements IOrganizationAppService {
@@ -39,34 +36,70 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
private AssignSeqUtil assignSeqUtil;
@Override
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum, Integer classEnum,
String sortField, String sortOrder, HttpServletRequest request) {
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;
}
/**
@@ -78,7 +111,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
private List<OrganizationDto> buildTree(List<Organization> records) {
// 按b_no的层级排序确保父节点先处理
List<Organization> sortedRecords = records.stream()
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
Map<String, OrganizationDto> nodeMap = new HashMap<>();
List<OrganizationDto> tree = new ArrayList<>();
@@ -89,7 +122,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);
@@ -123,17 +156,20 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
public R<?> getOrgInfo(Long orgId) {
Organization organization = organizationService.getById(orgId);
if (organization == null) {
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] {"机构信息"}));
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] { "机构信息" }));
}
// 转换为DTO对象确保数据格式一致
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.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, organizationDto.getActiveFlag()));
return R.ok(organizationDto, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息查询"}));
organizationDto
.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, organizationDto.getTypeEnum()));
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[] { "机构信息查询" }));
}
/**
@@ -160,7 +196,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
// 如果传了上级科室 把当前的code拼到后边
if (StringUtils.isNotEmpty(organization.getBusNo())) {
organization.setBusNo(String.format(CommonConstants.Common.MONTAGE_FORMAT, organization.getBusNo(),
CommonConstants.Common.POINT, code));
CommonConstants.Common.POINT, code));
} else {
organization.setBusNo(code);
}
@@ -169,7 +205,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
}
// 返回机构id
return R.ok(organization.getId(),
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息更新添加"}));
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息更新添加" }));
}
/**
@@ -189,8 +225,8 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
// 删除机构信息
boolean deleteOrgSuccess = organizationService.removeByIds(orgIdList);
return deleteOrgSuccess
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00005, new Object[] {"机构信息"}))
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息"}));
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00005, new Object[] { "机构信息" }))
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] { "机构信息" }));
}
/**
@@ -203,8 +239,9 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
public R<?> activeOrg(Long orgId) {
// 机构启用
boolean result = organizationService.activeOrg(orgId);
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息启用"}))
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息启用"}));
return result
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息启用" }))
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] { "机构信息启用" }));
}
/**
@@ -217,8 +254,94 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
public R<?> inactiveOrg(Long orgId) {
// 机构停用
boolean result = organizationService.inactiveOrg(orgId);
return result ? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"机构信息停用"}))
: R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00007, new Object[] {"机构信息停用"}));
return result
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息停用" }))
: 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);
}
/**
* 获取挂号科室列表
*
* @param pageNo 当前页码
* @param pageSize 查询条数
* @param name 机构/科室名称
* @param orgName 机构名称
* @return 挂号科室列表
*/
@Override
public R<?> getRegisterOrganizations(Integer pageNo, Integer pageSize, String name, String orgName) {
// 使用Page对象进行分页查询
Page<Organization> page = new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
// 创建查询条件只查询register_flag为1的组织机构
LambdaQueryWrapper<Organization> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Organization::getRegisterFlag, 1); // 只获取挂号科室
queryWrapper.eq(Organization::getDeleteFlag, "0"); // 确保未删除
// 添加名称过滤条件
if (StringUtils.isNotEmpty(name)) {
queryWrapper.like(Organization::getName, name);
}
// 如果有机构名称筛选
if (StringUtils.isNotEmpty(orgName)) {
// 这里假设 orgName 是父机构名称,如果需要更复杂的关联查询可在此扩展
// 当前逻辑暂保持与原逻辑一致的过滤方式或根据需求调整
}
// 按编码排序
queryWrapper.orderByAsc(Organization::getBusNo);
// 执行分页查询
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
// 转换为DTO对象并设置字典文本
List<OrganizationDto> organizationDtoList = resultPage.getRecords().stream().map(org -> {
OrganizationDto dto = new OrganizationDto();
BeanUtils.copyProperties(org, dto);
dto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, dto.getTypeEnum()));
dto.setClassEnum_dictText(formatClassEnumDictText(dto.getClassEnum()));
dto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, dto.getActiveFlag()));
return dto;
}).collect(Collectors.toList());
// 创建返回分页对象
Page<OrganizationDto> finalResult = new Page<>();
finalResult.setRecords(organizationDtoList);
finalResult.setTotal(resultPage.getTotal());
finalResult.setSize(resultPage.getSize());
finalResult.setCurrent(resultPage.getCurrent());
return R.ok(finalResult);
}
/**

View File

@@ -64,6 +64,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
.add(new OrgLocInitDto.locationFormOption(LocationForm.CABINET.getValue(), LocationForm.CABINET.getInfo()));
chargeItemStatusOptions.add(
new OrgLocInitDto.locationFormOption(LocationForm.PHARMACY.getValue(), LocationForm.PHARMACY.getInfo()));
chargeItemStatusOptions.add(
new OrgLocInitDto.locationFormOption(LocationForm.WAREHOUSE.getValue(), LocationForm.WAREHOUSE.getInfo()));
// 获取科室下拉选列表
List<Organization> organizationList = organizationService.getList(OrganizationType.DEPARTMENT.getValue(), null);
@@ -89,6 +91,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
locationList = locationService.getCabinetList();
} else if (LocationForm.PHARMACY.getValue().equals(locationForm)) {
locationList = locationService.getPharmacyList();
} else if (LocationForm.WAREHOUSE.getValue().equals(locationForm)) {
locationList = locationService.getWarehouseList();
}
List<OrgLocInitDto.locationOption> locationOptions = locationList.stream()
.map(location -> new OrgLocInitDto.locationOption(location.getId(), location.getName()))

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