Compare commits
83 Commits
0a854c9b45
...
zhaoyun
| Author | SHA1 | Date | |
|---|---|---|---|
| f77b665d08 | |||
| 15299f762c | |||
| aec3bf3e34 | |||
| 2970c3a4a2 | |||
| a2b7bc81fd | |||
| 7adf298ce7 | |||
| fdf56a33ce | |||
| 8914dca1df | |||
| 6f288f99de | |||
| 7d9da53cc4 | |||
| 6dc9aaba6c | |||
| 3bc8a85426 | |||
| 5b90a61484 | |||
| 6e514d2939 | |||
| 86f12b425a | |||
| d0289a3431 | |||
| 829fca8869 | |||
| b126612256 | |||
| 32bbda6dd4 | |||
| 560d646381 | |||
| 471eacaf52 | |||
| f6bb48c603 | |||
| f1ac7cc1fb | |||
| 0ca6b9f807 | |||
| b67725d08c | |||
| 6c89b0ae03 | |||
| 3a3f0c65e3 | |||
| c2ed6e04b0 | |||
| ab9b183b9a | |||
| 8fafa12337 | |||
| 085d51549e | |||
| 1801fc27ae | |||
| 9e6e836680 | |||
| d335520a57 | |||
| b6b8f8be71 | |||
| a9daab268b | |||
| 96a15b4dd8 | |||
| 9d486c3742 | |||
| 38bc99ee14 | |||
| 05332ce2d9 | |||
| 686fcb5692 | |||
| 99812e1bf0 | |||
| 5ab3865e04 | |||
| 5c9d2c42a7 | |||
| 0b183dacf8 | |||
| 5d7886a9a2 | |||
| 5ddfbaeb8d | |||
| 0e055fdadc | |||
| 552eff2c4f | |||
| 7b4cfeb6d5 | |||
| 844eb8b7ab | |||
| 179d8c9c97 | |||
| ed1dd56ad4 | |||
| 523a64daf0 | |||
| d9a1b188b5 | |||
| 20a69cc78d | |||
| 469e705d47 | |||
| b537749904 | |||
| a5a44cd215 | |||
| a1bc4b30c9 | |||
| 68ddee277f | |||
| be90a13cd6 | |||
| deafee0621 | |||
| 554c1fe97b | |||
| 91236c5499 | |||
| 8bc80efe2c | |||
| 8b2b47b71c | |||
| 18e3c06b1a | |||
| e117022bb6 | |||
| 50cabbeb32 | |||
| b4e605829c | |||
| ccd85f73e4 | |||
| 0adf56eee6 | |||
| 33ac51ff04 | |||
| 4c384a03fd | |||
| 19f986ad25 | |||
| 1d55523d69 | |||
| ef07eadbf9 | |||
| a3e234b6bd | |||
| 42a4631dbe | |||
| 0eeb655492 | |||
| 3fc1665ea9 | |||
| 2eca6a7f31 |
@@ -1,7 +1,7 @@
|
||||
# HealthLink-HIS 代码模块索引
|
||||
|
||||
> 供 LLM 快速定位代码。每个模块列出 Controller → Service → Mapper 关键文件。
|
||||
> 最后更新: 2026-06-19 00:00 (335 个 Controller)
|
||||
> 最后更新: 2026-06-20 00:00 (345 个 Controller)
|
||||
|
||||
## 关键词 → 模块速查
|
||||
|
||||
|
||||
451
MD/architecture/HEALTHLINK_HIS_UPGRADE_PLAN.md
Normal file
451
MD/architecture/HEALTHLINK_HIS_UPGRADE_PLAN.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# HealthLink-HIS 深度优化升级设计方案
|
||||
|
||||
> **文档类型**: 架构设计+实施计划
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-06-18
|
||||
> **目标**: 对标国内头部HIS厂商,补齐核心差距,提升竞争力
|
||||
|
||||
---
|
||||
|
||||
## 一、升级目标
|
||||
|
||||
### 1.1 总体目标
|
||||
|
||||
将HealthLink-HIS从"功能完整"升级为"行业领先",在以下5个维度达到国内一线水平:
|
||||
|
||||
| 维度 | 当前状态 | 目标状态 | 时间 |
|
||||
|------|---------|---------|:----:|
|
||||
| **移动化** | 响应式Web | APP+小程序+H5全覆盖 | 3月 |
|
||||
| **智能化** | 基础CDSS | AI辅助诊疗+智能推荐 | 6月 |
|
||||
| **平台化** | 单体架构 | 微服务+数据中台 | 6月 |
|
||||
| **云化** | 传统部署 | 云原生+SaaS多租户 | 12月 |
|
||||
| **生态化** | 闭环系统 | 互联网医院+开放API | 12月 |
|
||||
|
||||
### 1.2 核心KPI
|
||||
|
||||
| KPI | 当前 | 目标 | 衡量方式 |
|
||||
|-----|------|------|---------|
|
||||
| 三甲医院客户 | 0 | 10+ | 签约数 |
|
||||
| 并发用户支持 | 300 | 1000+ | 压测结果 |
|
||||
| 接口响应时间 | 500ms | <200ms | APM监控 |
|
||||
| 系统可用性 | 99.9% | 99.99% | 运维监控 |
|
||||
| 移动端覆盖率 | 0% | 80%+ | 功能覆盖率 |
|
||||
|
||||
---
|
||||
|
||||
## 二、架构升级方案
|
||||
|
||||
### 2.1 微服务拆分
|
||||
|
||||
#### 拆分策略
|
||||
|
||||
```
|
||||
当前单体架构
|
||||
↓
|
||||
按业务域拆分为微服务
|
||||
↓
|
||||
服务注册/发现 + API网关 + 配置中心
|
||||
↓
|
||||
容器化部署 + 编排管理
|
||||
```
|
||||
|
||||
#### 微服务划分
|
||||
|
||||
| 服务名 | 职责 | 依赖 | 优先级 |
|
||||
|--------|------|------|:------:|
|
||||
| `gateway-service` | API网关、路由、限流、鉴权 | — | P0 |
|
||||
| `auth-service` | 认证授权、SSO、OAuth2 | Redis | P0 |
|
||||
| `user-service` | 用户管理、角色权限、组织架构 | PostgreSQL | P0 |
|
||||
| `patient-service` | 患者主索引、EMPI | PostgreSQL | P0 |
|
||||
| `registration-service` | 挂号预约、分诊叫号 | Redis | P0 |
|
||||
| `doctor-service` | 门诊医生站、医嘱处方 | PostgreSQL | P0 |
|
||||
| `nurse-service` | 护士站、护理评估、生命体征 | PostgreSQL | P0 |
|
||||
| `inpatient-service` | 住院管理、入出转、床位 | PostgreSQL | P0 |
|
||||
| `pharmacy-service` | 药品管理、药房、药库 | PostgreSQL | P0 |
|
||||
| `lab-service` | LIS检验管理、质控 | PostgreSQL | P1 |
|
||||
| `pacs-service` | PACS影像管理、报告 | MinIO | P1 |
|
||||
| `surgery-service` | 手术麻醉、围术期管理 | PostgreSQL | P1 |
|
||||
| `emr-service` | 电子病历、病历质控 | PostgreSQL | P0 |
|
||||
| `mr-service` | 病案管理、DRG/DIP | PostgreSQL | P1 |
|
||||
| `finance-service` | 收费结算、医保对接 | PostgreSQL | P0 |
|
||||
| `report-service` | 统计报表、BI分析 | ClickHouse | P1 |
|
||||
| `cdss-service` | 临床决策支持、规则引擎 | PostgreSQL | P1 |
|
||||
| `message-service` | 消息通知、SSE推送 | Redis+RabbitMQ | P0 |
|
||||
| `file-service` | 文件存储、影像归档 | MinIO | P0 |
|
||||
| `audit-service` | 操作审计、日志 | Elasticsearch | P1 |
|
||||
|
||||
#### 技术选型
|
||||
|
||||
| 组件 | 选型 | 说明 |
|
||||
|------|------|------|
|
||||
| 服务网关 | Spring Cloud Gateway | 路由、限流、鉴权 |
|
||||
| 服务注册 | Nacos | 服务发现+配置中心 |
|
||||
| 服务调用 | OpenFeign | 声明式HTTP客户端 |
|
||||
| 消息队列 | RabbitMQ | 异步消息、事件驱动 |
|
||||
| 缓存 | Redis Cluster | 分布式缓存、会话管理 |
|
||||
| 搜索 | Elasticsearch | 全文搜索、日志分析 |
|
||||
| 文件存储 | MinIO | 对象存储、影像归档 |
|
||||
| 链路追踪 | SkyWalking | 分布式链路追踪 |
|
||||
| 配置中心 | Nacos | 动态配置管理 |
|
||||
|
||||
### 2.2 云原生部署
|
||||
|
||||
#### 容器化架构
|
||||
|
||||
```
|
||||
Docker容器
|
||||
↓
|
||||
Kubernetes编排
|
||||
↓
|
||||
Helm Charts部署
|
||||
↓
|
||||
CI/CD流水线 (Jenkins/GitLab CI)
|
||||
```
|
||||
|
||||
#### 基础设施
|
||||
|
||||
| 组件 | 选型 | 说明 |
|
||||
|------|------|------|
|
||||
| 容器运行时 | Docker | 容器化 |
|
||||
| 编排 | Kubernetes | 自动扩缩容 |
|
||||
| 服务网格 | Istio | 流量管理、安全 |
|
||||
| 日志 | ELK Stack | 日志收集分析 |
|
||||
| 监控 | Prometheus+Grafana | 指标监控告警 |
|
||||
| CI/CD | GitLab CI | 持续集成部署 |
|
||||
|
||||
#### 多租户架构
|
||||
|
||||
```
|
||||
租户隔离策略:
|
||||
├── 数据隔离: 按tenant_id字段隔离(共享数据库)
|
||||
├── 缓存隔离: Redis Key前缀隔离
|
||||
├── 文件隔离: MinIO Bucket隔离
|
||||
└── 配置隔离: Nacos Namespace隔离
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、移动化方案
|
||||
|
||||
### 3.1 移动护理APP
|
||||
|
||||
#### 功能清单
|
||||
|
||||
| 模块 | 功能 | 优先级 |
|
||||
|------|------|:------:|
|
||||
| **医嘱执行** | 扫码执行、医嘱查询、执行记录 | P0 |
|
||||
| **生命体征** | 体温/脉搏/血压/血氧录入、趋势图 | P0 |
|
||||
| **护理评估** | Braden/Morse/NRS2002量表、自动评分 | P0 |
|
||||
| **输液管理** | 输液巡视、速度监控、异常报警 | P0 |
|
||||
| **标本采集** | 条码扫描、采集记录、运送追踪 | P1 |
|
||||
| **护理文书** | 护理记录单、交班报告 | P1 |
|
||||
| **患者查询** | 患者列表、基本信息、费用查询 | P0 |
|
||||
| **消息通知** | 危急值提醒、医嘱提醒、交班提醒 | P0 |
|
||||
|
||||
#### 技术方案
|
||||
|
||||
| 维度 | 选型 | 说明 |
|
||||
|------|------|------|
|
||||
| 框架 | uni-app / Flutter | 跨平台一套代码 |
|
||||
| 状态管理 | Pinia / Riverpod | 响应式状态 |
|
||||
| 网络请求 | Dio / Axios | HTTP客户端 |
|
||||
| 本地存储 | SQLite / Hive | 离线缓存 |
|
||||
| 推送 | JPush / FCM | 消息推送 |
|
||||
| 扫码 | ZXing / FlutterBarcode | 条码/二维码扫描 |
|
||||
|
||||
### 3.2 互联网医院
|
||||
|
||||
#### 功能清单
|
||||
|
||||
| 模块 | 功能 | 优先级 |
|
||||
|------|------|:------:|
|
||||
| **在线问诊** | 图文/语音/视频问诊 | P0 |
|
||||
| **复诊开方** | 慢病复诊、处方流转 | P0 |
|
||||
| **药品配送** | 处方审核→药房→配送 | P0 |
|
||||
| **预约挂号** | 线上预约、支付、取消 | P0 |
|
||||
| **报告查询** | 检验检查报告在线查看 | P0 |
|
||||
| **健康档案** | 个人健康档案、体检报告 | P1 |
|
||||
| **健康管理** | 慢病管理、用药提醒 | P1 |
|
||||
| **在线支付** | 微信/支付宝/医保在线支付 | P0 |
|
||||
|
||||
### 3.3 小程序矩阵
|
||||
|
||||
| 小程序 | 用户 | 功能 |
|
||||
|--------|------|------|
|
||||
| **患者端** | 患者 | 预约挂号、报告查询、在线问诊、健康档案 |
|
||||
| **医生端** | 医生 | 患者管理、处方开具、会诊协作 |
|
||||
| **护士端** | 护士 | 医嘱执行、护理评估、交班报告 |
|
||||
| **管理端** | 管理层 | 数据看板、审批流程、消息通知 |
|
||||
|
||||
---
|
||||
|
||||
## 四、AI智能化方案
|
||||
|
||||
### 4.1 CDSS临床决策支持(增强版)
|
||||
|
||||
#### 规则引擎升级
|
||||
|
||||
```
|
||||
当前: 简单字符串匹配
|
||||
↓
|
||||
目标: 复杂表达式解析 + 知识图谱 + 机器学习
|
||||
```
|
||||
|
||||
#### AI能力矩阵
|
||||
|
||||
| 能力 | 描述 | 技术方案 | 优先级 |
|
||||
|------|------|---------|:------:|
|
||||
| **诊断辅助** | 基于症状推荐诊断 | NLP+知识图谱 | P0 |
|
||||
| **用药审查** | 药物相互作用+个体化给药 | 规则引擎+ML | P0 |
|
||||
| **检验预警** | 异常结果自动提醒 | 规则引擎 | P0 |
|
||||
| **病历质控** | 自动检查病历完整性 | NLP+规则 | P1 |
|
||||
| **编码推荐** | ICD-10自动编码 | NLP+ML | P1 |
|
||||
| **风险预测** | 入院风险评估 | ML模型 | P2 |
|
||||
| **影像AI** | CT/MRI辅助诊断 | 深度学习 | P2 |
|
||||
| **语音录入** | 语音转病历 | ASR+NLP | P2 |
|
||||
|
||||
### 4.2 智能推荐系统
|
||||
|
||||
| 推荐场景 | 描述 | 技术方案 |
|
||||
|---------|------|---------|
|
||||
| **诊断推荐** | 基于症状+病史推荐诊断 | 知识图谱+协同过滤 |
|
||||
| **处方推荐** | 基于诊断推荐用药方案 | 协同过滤+规则 |
|
||||
| **检查推荐** | 基于诊断推荐检查项目 | 规则+统计 |
|
||||
| **路径推荐** | 基于诊断推荐临床路径 | 规则+ML |
|
||||
| **费用预测** | 预估住院费用和DRG分组 | ML回归模型 |
|
||||
|
||||
### 4.3 NLP病历处理
|
||||
|
||||
| 功能 | 描述 | 技术方案 |
|
||||
|------|------|---------|
|
||||
| **病历结构化** | 自由文本→结构化数据 | NLP+规则 |
|
||||
| **关键词提取** | 提取诊断/症状/药物 | NER模型 |
|
||||
| **病历摘要** | 自动生成病历摘要 | Seq2Seq模型 |
|
||||
| **相似病例** | 查找相似病例 | 文本相似度 |
|
||||
| **病历质控** | 检查病历规范性 | 规则+NLP |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据平台方案
|
||||
|
||||
### 5.1 数据中台架构
|
||||
|
||||
```
|
||||
数据源(HIS/LIS/PACS/EMR)
|
||||
↓
|
||||
数据采集(CDC/ETL)
|
||||
↓
|
||||
数据存储(ClickHouse/Hive)
|
||||
↓
|
||||
数据治理(质量/标准/安全)
|
||||
↓
|
||||
数据服务(API/报表/大屏)
|
||||
↓
|
||||
数据应用(BI/AI/运营)
|
||||
```
|
||||
|
||||
### 5.2 数据仓库设计
|
||||
|
||||
| 数据域 | 数据表 | 说明 |
|
||||
|--------|--------|------|
|
||||
| **患者域** | 患者主索引、就诊记录、诊断记录 | 患者360°视图 |
|
||||
| **临床域** | 医嘱、处方、检验、检查、手术 | 临床数据仓库 |
|
||||
| **运营域** | 收入、成本、绩效、工作量 | 运营数据仓库 |
|
||||
| **质量域** | 质控指标、不良事件、院感数据 | 质量数据仓库 |
|
||||
| **科研域** | 病例数据、随访数据、科研数据 | 科研数据仓库 |
|
||||
|
||||
### 5.3 BI决策支持
|
||||
|
||||
| 报表类型 | 描述 | 技术方案 |
|
||||
|---------|------|---------|
|
||||
| **经营分析** | 收入/成本/利润分析 | ClickHouse+Grafana |
|
||||
| **临床分析** | 诊断/手术/用药分析 | ClickHouse+自研 |
|
||||
| **质量分析** | 质控指标/不良事件分析 | ClickHouse+自研 |
|
||||
| **绩效分析** | 医生/科室绩效分析 | ClickHouse+自研 |
|
||||
| **数据大屏** | 实时数据可视化 | ECharts+WebSocket |
|
||||
|
||||
---
|
||||
|
||||
## 六、安全加固方案
|
||||
|
||||
### 6.1 安全架构
|
||||
|
||||
```
|
||||
用户 → WAF → API网关(限流/鉴权) → 微服务 → 数据库
|
||||
↓
|
||||
审计日志(全量记录)
|
||||
↓
|
||||
安全监控(SIEM)
|
||||
```
|
||||
|
||||
### 6.2 安全能力
|
||||
|
||||
| 安全域 | 能力 | 优先级 |
|
||||
|--------|------|:------:|
|
||||
| **认证** | JWT+OAuth2+SSO+MFA | P0 |
|
||||
| **授权** | RBAC+ABAC+数据权限 | P0 |
|
||||
| **加密** | TLS+数据加密+密钥管理 | P0 |
|
||||
| **审计** | 全量操作审计+合规报告 | P0 |
|
||||
| **防护** | WAF+SQL注入防护+XSS防护 | P0 |
|
||||
| **监控** | 安全事件监控+告警 | P1 |
|
||||
| **等保** | 等保三级认证 | P1 |
|
||||
| **密评** | 密码应用安全性评估 | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 七、实施路线图
|
||||
|
||||
### Phase 1: 移动化+安全加固(1-3月)
|
||||
|
||||
```
|
||||
Month 1:
|
||||
├── Week 1-2: 移动护理APP原型设计
|
||||
├── Week 3-4: 医嘱执行+生命体征模块开发
|
||||
├── Week 5-6: 护理评估+输液管理开发
|
||||
├── Week 7-8: 测试+上线
|
||||
|
||||
Month 2:
|
||||
├── Week 1-2: 互联网医院架构设计
|
||||
├── Week 3-4: 在线问诊+复诊开方开发
|
||||
├── Week 5-6: 药品配送+预约挂号开发
|
||||
├── Week 7-8: 测试+上线
|
||||
|
||||
Month 3:
|
||||
├── Week 1-2: 安全加固(认证授权升级)
|
||||
├── Week 3-4: 审计日志+WAF部署
|
||||
├── Week 5-6: 等保三级准备
|
||||
├── Week 7-8: 安全测试+上线
|
||||
```
|
||||
|
||||
**交付物**: 移动护理APP + 互联网医院 + 安全加固
|
||||
|
||||
### Phase 2: AI+数据平台(4-6月)
|
||||
|
||||
```
|
||||
Month 4:
|
||||
├── Week 1-2: CDSS规则引擎升级
|
||||
├── Week 3-4: 诊断辅助+NLP病历
|
||||
├── Week 5-6: 用药审查增强
|
||||
├── Week 7-8: 测试+上线
|
||||
|
||||
Month 5:
|
||||
├── Week 1-2: 数据中台架构设计
|
||||
├── Week 3-4: 数据采集+ETL开发
|
||||
├── Week 5-6: 数据仓库建设
|
||||
├── Week 7-8: BI报表开发
|
||||
|
||||
Month 6:
|
||||
├── Week 1-2: 智能推荐系统
|
||||
├── Week 3-4: 影像AI集成
|
||||
├── Week 5-6: 语音录入
|
||||
├── Week 7-8: 测试+上线
|
||||
```
|
||||
|
||||
**交付物**: AI辅助诊疗 + 数据中台 + BI决策
|
||||
|
||||
### Phase 3: 微服务+云原生(7-9月)
|
||||
|
||||
```
|
||||
Month 7:
|
||||
├── Week 1-2: 微服务拆分方案设计
|
||||
├── Week 3-4: 网关+认证服务拆分
|
||||
├── Week 5-6: 核心业务服务拆分
|
||||
├── Week 7-8: 测试+灰度发布
|
||||
|
||||
Month 8:
|
||||
├── Week 1-2: 容器化+K8s部署
|
||||
├── Week 3-4: CI/CD流水线
|
||||
├── Week 5-6: 监控+日志+链路追踪
|
||||
├── Week 7-8: 压测+优化
|
||||
|
||||
Month 9:
|
||||
├── Week 1-2: 多租户架构
|
||||
├── Week 3-4: SaaS化改造
|
||||
├── Week 5-6: 云平台部署
|
||||
├── Week 7-8: 测试+上线
|
||||
```
|
||||
|
||||
**交付物**: 微服务架构 + 云原生部署 + SaaS平台
|
||||
|
||||
### Phase 4: 生态建设(10-12月)
|
||||
|
||||
```
|
||||
Month 10:
|
||||
├── Week 1-2: 开放API平台设计
|
||||
├── Week 3-4: API网关+开发者门户
|
||||
├── Week 5-6: 第三方集成SDK
|
||||
├── Week 7-8: 测试+上线
|
||||
|
||||
Month 11:
|
||||
├── Week 1-2: 区域医疗信息共享平台
|
||||
├── Week 3-4: 医联体云平台
|
||||
├── Week 5-6: 健康档案共享
|
||||
├── Week 7-8: 测试+上线
|
||||
|
||||
Month 12:
|
||||
├── Week 1-2: 智慧病房(床旁交互+智能输液)
|
||||
├── Week 3-4: 数字孪生医院
|
||||
├── Week 5-6: 全量测试+性能优化
|
||||
├── Week 7-8: 正式发布
|
||||
```
|
||||
|
||||
**交付物**: 开放API + 区域共享 + 智慧病房
|
||||
|
||||
---
|
||||
|
||||
## 八、资源需求
|
||||
|
||||
### 8.1 团队规模
|
||||
|
||||
| 角色 | 当前 | 目标 | 新增 |
|
||||
|------|:----:|:----:|:----:|
|
||||
| 后端开发 | 5 | 15 | +10 |
|
||||
| 前端开发 | 3 | 8 | +5 |
|
||||
| 移动端开发 | 0 | 4 | +4 |
|
||||
| AI工程师 | 0 | 3 | +3 |
|
||||
| 数据工程师 | 0 | 3 | +3 |
|
||||
| 测试工程师 | 1 | 4 | +3 |
|
||||
| 运维工程师 | 1 | 3 | +2 |
|
||||
| 架构师 | 1 | 2 | +1 |
|
||||
| 产品经理 | 1 | 2 | +1 |
|
||||
| **合计** | **12** | **44** | **+32** |
|
||||
|
||||
### 8.2 预算估算
|
||||
|
||||
| 类别 | 年度预算 | 说明 |
|
||||
|------|:-------:|------|
|
||||
| 人力成本 | 500万 | 32人×平均15万/年 |
|
||||
| 云资源 | 50万 | 服务器+存储+带宽 |
|
||||
| 软件授权 | 30万 | 中间件+工具+SDK |
|
||||
| 培训费用 | 10万 | 团队培训+认证 |
|
||||
| 其他 | 10万 | 差旅+办公+杂项 |
|
||||
| **合计** | **600万** | |
|
||||
|
||||
---
|
||||
|
||||
## 九、风险与应对
|
||||
|
||||
| 风险 | 概率 | 影响 | 应对措施 |
|
||||
|------|:----:|:----:|---------|
|
||||
| 微服务拆分复杂度高 | 高 | 延期 | 渐进式拆分,先拆核心服务 |
|
||||
| AI模型效果不达预期 | 中 | 功能受限 | 先用规则引擎,ML逐步引入 |
|
||||
| 移动端兼容性问题 | 中 | 用户体验差 | 多设备测试+灰度发布 |
|
||||
| 团队扩招困难 | 高 | 进度延迟 | 提前招聘+外包协作 |
|
||||
| 客户接受度低 | 低 | 推广受阻 | 先做标杆客户+案例营销 |
|
||||
|
||||
---
|
||||
|
||||
## 十、成功标准
|
||||
|
||||
| 阶段 | 成功标准 | 验证方式 |
|
||||
|------|---------|---------|
|
||||
| Phase 1 | 移动护理上线+互联网医院上线 | 用户验收测试 |
|
||||
| Phase 2 | AI辅助诊疗上线+数据中台上线 | 功能测试+性能测试 |
|
||||
| Phase 3 | 微服务架构上线+SaaS平台上线 | 压测+安全测试 |
|
||||
| Phase 4 | 开放API上线+区域共享上线 | 集成测试+客户验收 |
|
||||
|
||||
---
|
||||
|
||||
> **文档版本**: v1.0
|
||||
> **最后更新**: 2026-06-18
|
||||
> **下一步**: 确认后从Phase 1开始执行
|
||||
11
healthlink-his-mobile/.env.development
Normal file
11
healthlink-his-mobile/.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = HealthLink移动护理
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
# API地址
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
# 后端代理地址
|
||||
VITE_API_PROXY = 'http://localhost:18080/healthlink-his'
|
||||
8
healthlink-his-mobile/.env.production
Normal file
8
healthlink-his-mobile/.env.production
Normal file
@@ -0,0 +1,8 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = HealthLink移动护理
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
# API地址
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
6
healthlink-his-mobile/.gitignore
vendored
Normal file
6
healthlink-his-mobile/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.log
|
||||
package-lock.json
|
||||
14
healthlink-his-mobile/index.html
Normal file
14
healthlink-his-mobile/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>HealthLink 移动护理</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
29
healthlink-his-mobile/package.json
Normal file
29
healthlink-his-mobile/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "healthlink-his-mobile",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "HealthLink-HIS 移动护理H5工作站",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "echo 'No lint configured'"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.0",
|
||||
"axios": "^1.7.0",
|
||||
"element-plus": "^2.7.0",
|
||||
"echarts": "^5.5.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-to-regexp": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.4.0",
|
||||
"sass": "^1.77.0"
|
||||
}
|
||||
}
|
||||
3
healthlink-his-mobile/src/App.vue
Normal file
3
healthlink-his-mobile/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
57
healthlink-his-mobile/src/api/index.js
Normal file
57
healthlink-his-mobile/src/api/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
service.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('Admin-Token')
|
||||
if (token && !(config.headers && config.headers.isToken === false)) {
|
||||
config.headers.Authorization = 'Bearer ' + token
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code === 401) {
|
||||
localStorage.removeItem('Admin-Token')
|
||||
localStorage.removeItem('userInfo')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
return res
|
||||
},
|
||||
error => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('Admin-Token')
|
||||
localStorage.removeItem('userInfo')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const authApi = {
|
||||
login: (data) => service.post('/login', data, { headers: { isToken: false } }),
|
||||
getTenants: (username) => service.get('/system/tenant/user-bind/' + username, { headers: { isToken: false } }),
|
||||
getAllTenants: () => service.get('/system/tenant/page', { headers: { isToken: false }, params: { pageSize: 100 } }),
|
||||
getInfo: () => service.get('/getInfo')
|
||||
}
|
||||
|
||||
export const nursingApi = {
|
||||
getTasks: (params) => service.get('/nurse-station/advice-process/page', { params }),
|
||||
completeTask: (id, data) => service.post(`/nurse-station/advice-process/execute`, data),
|
||||
getPatientInfo: (id) => service.get('/inpatientmanage/inhospitalregister/' + id),
|
||||
getPatientList: (params) => service.get('/inpatientmanage/inhospitalregister/list', { params }),
|
||||
getOrders: (encounterId) => service.get('/nurse-station/advice-process/page', { params: { encounterId } }),
|
||||
getVitalSigns: (patientId) => service.get('/nursing/vital-signs/' + patientId),
|
||||
submitVitalSign: (data) => service.post('/nursing/vital-sign', data),
|
||||
getAssessments: (encounterId) => service.get('/nursing/assessment/encounter/' + encounterId),
|
||||
submitAssessment: (data) => service.post('/nursing/assessment', data)
|
||||
}
|
||||
|
||||
export default service
|
||||
14
healthlink-his-mobile/src/main.js
Normal file
14
healthlink-his-mobile/src/main.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/mobile.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { size: 'large', locale: zhCn })
|
||||
app.mount('#app')
|
||||
22
healthlink-his-mobile/src/router/index.js
Normal file
22
healthlink-his-mobile/src/router/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', component: () => import('../views/Login.vue'), meta: { title: '登录' } },
|
||||
{ path: '/', redirect: '/mobile/home' },
|
||||
{ path: '/mobile', component: () => import('../views/MobileLayout.vue'), meta: { requiresAuth: true }, children: [
|
||||
{ path: 'home', component: () => import('../views/Home.vue'), meta: { title: '首页' } },
|
||||
{ path: 'tasks', component: () => import('../views/TaskList.vue'), meta: { title: '任务列表' } },
|
||||
{ path: 'patients', component: () => import('../views/PatientList.vue'), meta: { title: '患者列表' } },
|
||||
{ path: 'patient-detail/:id', component: () => import('../views/PatientDetail.vue'), meta: { title: '患者详情' } },
|
||||
{ path: 'vital-entry/:patientId', component: () => import('../views/VitalSignEntry.vue'), meta: { title: '生命体征录入' } },
|
||||
{ path: 'assessment/:patientId', component: () => import('../views/AssessmentForm.vue'), meta: { title: '护理评估' } },
|
||||
{ path: 'mine', component: () => import('../views/Mine.vue'), meta: { title: '我的' } }
|
||||
]}
|
||||
]
|
||||
|
||||
const router = createRouter({ history: createWebHistory(), routes })
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth && !localStorage.getItem('Admin-Token')) { next('/login'); return }
|
||||
next()
|
||||
})
|
||||
export default router
|
||||
6
healthlink-his-mobile/src/styles/mobile.css
Normal file
6
healthlink-his-mobile/src/styles/mobile.css
Normal file
@@ -0,0 +1,6 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; color: #333; background: #f5f5f5; -webkit-font-smoothing: antialiased; }
|
||||
:root { --primary: #1890ff; --success: #52c41a; --warning: #fa8c16; --danger: #f5222d; --bg: #f5f5f5; --card: #fff; --border: #e8e8e8; }
|
||||
input, button, textarea { font-family: inherit; font-size: inherit; }
|
||||
button { cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
||||
::-webkit-scrollbar { display: none; }
|
||||
86
healthlink-his-mobile/src/views/AssessmentForm.vue
Normal file
86
healthlink-his-mobile/src/views/AssessmentForm.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="assessment-form">
|
||||
<div class="type-select">
|
||||
<div v-for="type in assessmentTypes" :key="type.key" class="type-card" :class="{ active: selectedType === type.key }" @click="selectedType = type.key">
|
||||
<div class="type-icon">{{ type.icon }}</div><div class="type-name">{{ type.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedType" class="form-content">
|
||||
<div v-for="(item, idx) in currentItems" :key="idx" class="form-item">
|
||||
<div class="item-label">{{ item.label }}</div>
|
||||
<div class="item-options">
|
||||
<span v-for="opt in item.options" :key="opt.value" class="option" :class="{ selected: formData[item.key] === opt.value }" @click="formData[item.key] = opt.value">{{ opt.label }} ({{ opt.score }}分)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-result"><div class="total-score">总分: {{ totalScore }}</div><div class="risk-level" :class="riskLevel">{{ riskLevelText }}</div></div>
|
||||
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '提交评估' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const selectedType = ref('')
|
||||
const submitting = ref(false)
|
||||
const formData = ref({})
|
||||
|
||||
const assessmentTypes = [
|
||||
{ key: 'Braden', name: '压疮评估', icon: '🩹', items: [
|
||||
{ key: 'sensory', label: '感知能力', options: [{ label: '完全受限', value: 1, score: 1 }, { label: '严重受限', value: 2, score: 2 }, { label: '轻度受限', value: 3, score: 3 }, { label: '未受损', value: 4, score: 4 }] },
|
||||
{ key: 'moisture', label: '皮肤潮湿', options: [{ label: '持续潮湿', value: 1, score: 1 }, { label: '经常潮湿', value: 2, score: 2 }, { label: '偶尔潮湿', value: 3, score: 3 }, { label: '很少潮湿', value: 4, score: 4 }] },
|
||||
{ key: 'activity', label: '活动能力', options: [{ label: '卧床', value: 1, score: 1 }, { label: '轮椅', value: 2, score: 2 }, { label: '偶尔步行', value: 3, score: 3 }, { label: '经常步行', value: 4, score: 4 }] }
|
||||
]},
|
||||
{ key: 'Morse', name: '跌倒评估', icon: '⚠️', items: [
|
||||
{ key: 'history', label: '跌倒史', options: [{ label: '无', value: 0, score: 0 }, { label: '有', value: 25, score: 25 }] },
|
||||
{ key: 'diagnosis', label: '诊断', options: [{ label: '无', value: 0, score: 0 }, { label: '有', value: 15, score: 15 }] },
|
||||
{ key: 'ambulation', label: '行走辅助', options: [{ label: '无需', value: 0, score: 0 }, { label: '拐杖', value: 15, score: 15 }, { label: '扶墙', value: 30, score: 30 }] }
|
||||
]},
|
||||
{ key: 'NRS2002', name: '营养筛查', icon: '🍎', items: [
|
||||
{ key: 'bmi', label: 'BMI', options: [{ label: '≥20.5', value: 0, score: 0 }, { label: '18.5-20.5', value: 1, score: 1 }, { label: '<18.5', value: 2, score: 2 }] },
|
||||
{ key: 'weightLoss', label: '体重下降', options: [{ label: '无', value: 0, score: 0 }, { label: '<5%', value: 1, score: 1 }, { label: '>5%', value: 2, score: 2 }] },
|
||||
{ key: 'intake', label: '饮食摄入', options: [{ label: '正常', value: 0, score: 0 }, { label: '减少', value: 1, score: 1 }, { label: '极少', value: 2, score: 2 }] }
|
||||
]}
|
||||
]
|
||||
|
||||
const currentItems = computed(() => assessmentTypes.find(t => t.key === selectedType.value)?.items || [])
|
||||
const totalScore = computed(() => currentItems.value.reduce((sum, item) => sum + (formData.value[item.key] || 0), 0))
|
||||
const riskLevel = computed(() => {
|
||||
if (selectedType.value === 'Braden') return totalScore.value <= 12 ? 'HIGH' : totalScore.value <= 14 ? 'MEDIUM' : 'LOW'
|
||||
if (selectedType.value === 'Morse') return totalScore.value >= 45 ? 'HIGH' : totalScore.value >= 25 ? 'MEDIUM' : 'LOW'
|
||||
return totalScore.value >= 3 ? 'HIGH' : totalScore.value >= 2 ? 'MEDIUM' : 'LOW'
|
||||
})
|
||||
const riskLevelText = computed(() => ({ HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险' }[riskLevel.value]))
|
||||
|
||||
const submit = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
await nursingApi.submitAssessment({ patientId: route.params.patientId, assessmentType: selectedType.value, totalScore: totalScore.value, riskLevel: riskLevel.value, detail: JSON.stringify(formData.value) })
|
||||
ElMessage.success('评估提交成功')
|
||||
} catch (e) { ElMessage.error('提交失败') } finally { submitting.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.type-select { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
|
||||
.type-card { background: #fff; border-radius: 8px; padding: 14px; text-align: center; border: 2px solid transparent; cursor: pointer; }
|
||||
.type-card.active { border-color: #1890ff; background: #e6f7ff; }
|
||||
.type-icon { font-size: 26px; }
|
||||
.type-name { font-size: 13px; margin-top: 4px; }
|
||||
.form-content { background: #fff; border-radius: 8px; padding: 14px; }
|
||||
.form-item { margin-bottom: 14px; }
|
||||
.item-label { font-weight: 600; margin-bottom: 8px; font-size: 14px; }
|
||||
.item-options { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.option { padding: 8px 12px; background: #f0f0f0; border-radius: 6px; font-size: 13px; cursor: pointer; }
|
||||
.option.selected { background: #1890ff; color: #fff; }
|
||||
.score-result { text-align: center; padding: 14px 0; border-top: 1px solid #eee; margin-top: 10px; }
|
||||
.total-score { font-size: 22px; font-weight: 600; }
|
||||
.risk-level { font-size: 15px; margin-top: 4px; }
|
||||
.risk-HIGH { color: #f5222d; } .risk-MEDIUM { color: #fa8c16; } .risk-LOW { color: #52c41a; }
|
||||
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 10px; }
|
||||
.submit-btn:disabled { background: #91d5ff; }
|
||||
</style>
|
||||
88
healthlink-his-mobile/src/views/Home.vue
Normal file
88
healthlink-his-mobile/src/views/Home.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="welcome">
|
||||
<div class="user-info">
|
||||
<div class="avatar">{{ userInfo?.userName?.charAt(0) || '护' }}</div>
|
||||
<div><div class="name">{{ userInfo?.nickName || userInfo?.userName || '护士' }}</div><div class="dept">{{ userInfo?.orgName || '' }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="s in stats" :key="s.label">
|
||||
<div class="stat-value">{{ s.value }}</div>
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<div class="action-title">快捷操作</div>
|
||||
<div class="action-grid">
|
||||
<div class="action-item" v-for="a in actions" :key="a.label" @click="$router.push(a.path)">
|
||||
<div class="action-icon" :style="{ background: a.color }">{{ a.icon }}</div>
|
||||
<div class="action-label">{{ a.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-tasks">
|
||||
<div class="section-header"><span>待办任务</span><span class="more" @click="$router.push('/mobile/tasks')">查看全部</span></div>
|
||||
<div v-for="task in recentTasks" :key="task.id" class="task-item">
|
||||
<div class="task-dot"></div>
|
||||
<div class="task-info"><div class="task-name">{{ task.adviceName || task.taskContent || '医嘱任务' }}</div><div class="task-time">{{ task.createTime || '' }}</div></div>
|
||||
</div>
|
||||
<div v-if="recentTasks.length === 0" class="empty">暂无待办任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const userInfo = ref({})
|
||||
const stats = ref([{ label: '待执行医嘱', value: 0 }, { label: '今日体征', value: 0 }, { label: '待评估', value: 0 }, { label: '高风险', value: 0 }])
|
||||
const recentTasks = ref([])
|
||||
const actions = [
|
||||
{ icon: '📋', label: '任务列表', path: '/mobile/tasks', color: '#1890ff' },
|
||||
{ icon: '👥', label: '患者列表', path: '/mobile/patients', color: '#52c41a' },
|
||||
{ icon: '📊', label: '生命体征', path: '/mobile/vital-entry', color: '#722ed1' }
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try { const info = localStorage.getItem('userInfo'); if (info) userInfo.value = JSON.parse(info) } catch {}
|
||||
try {
|
||||
const nurseId = userInfo.value.practitionerId || userInfo.value.userId
|
||||
if (nurseId) {
|
||||
const res = await nursingApi.getTasks({ nurseId: nurseId })
|
||||
if (res.code === 200) {
|
||||
recentTasks.value = (res.data?.records || res.data?.rows || res.data || []).slice(0, 5)
|
||||
stats.value[0].value = res.data?.total || recentTasks.value.length
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home { padding: 12px; padding-bottom: 70px; }
|
||||
.welcome { background: linear-gradient(135deg, #1890ff, #096dd9); border-radius: 12px; padding: 20px; color: #fff; margin-bottom: 12px; }
|
||||
.user-info { display: flex; align-items: center; gap: 12px; }
|
||||
.avatar { width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
||||
.name { font-size: 18px; font-weight: 600; }
|
||||
.dept { font-size: 13px; opacity: 0.8; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
|
||||
.stat-card { background: #fff; border-radius: 8px; padding: 12px 8px; text-align: center; }
|
||||
.stat-value { font-size: 22px; font-weight: 600; color: #1890ff; }
|
||||
.stat-label { font-size: 11px; color: #999; margin-top: 4px; }
|
||||
.quick-actions { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; }
|
||||
.action-title { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
|
||||
.action-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||||
.action-item { text-align: center; cursor: pointer; }
|
||||
.action-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; margin: 0 auto 6px; }
|
||||
.action-label { font-size: 12px; color: #666; }
|
||||
.recent-tasks { background: #fff; border-radius: 12px; padding: 16px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 15px; font-weight: 600; }
|
||||
.more { color: #1890ff; font-size: 13px; }
|
||||
.task-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
|
||||
.task-dot { width: 8px; height: 8px; border-radius: 50%; background: #fa8c16; }
|
||||
.task-name { font-size: 14px; }
|
||||
.task-time { font-size: 12px; color: #999; }
|
||||
.empty { text-align: center; padding: 20px; color: #999; }
|
||||
</style>
|
||||
111
healthlink-his-mobile/src/views/Login.vue
Normal file
111
healthlink-his-mobile/src/views/Login.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-header">
|
||||
<div class="logo">🏥</div>
|
||||
<h1>{{ currentTenantName || 'HealthLink 移动护理' }}</h1>
|
||||
<p>护士工作站</p>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<div class="form-item">
|
||||
<label>医院/租户</label>
|
||||
<select v-model="form.tenantId" class="input" @change="onTenantChange">
|
||||
<option value="">请选择医院</option>
|
||||
<option v-for="t in tenantOptions" :key="t.value" :value="t.value">{{ t.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>用户名</label>
|
||||
<input v-model="form.username" type="text" placeholder="请输入用户名" class="input" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>密码</label>
|
||||
<input v-model="form.password" type="password" placeholder="请输入密码" class="input" @keyup.enter="handleLogin" />
|
||||
</div>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">{{ loading ? '登录中...' : '登 录' }}</button>
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { authApi } from '../api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const tenantOptions = ref([])
|
||||
const currentTenantName = ref('')
|
||||
const form = ref({ username: '', password: '', tenantId: '' })
|
||||
|
||||
const loadTenants = async () => {
|
||||
try {
|
||||
const res = await authApi.getAllTenants()
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.records || res.data || []
|
||||
tenantOptions.value = list.map(item => ({ label: item.tenantName, value: item.tenantId || item.id }))
|
||||
if (tenantOptions.value.length === 1) { form.value.tenantId = tenantOptions.value[0].value; currentTenantName.value = tenantOptions.value[0].label }
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
const onTenantChange = () => {
|
||||
const selected = tenantOptions.value.find(t => t.value === form.value.tenantId)
|
||||
currentTenantName.value = selected ? selected.label : ''
|
||||
}
|
||||
|
||||
onMounted(loadTenants)
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!form.value.username) { errorMsg.value = '请输入用户名'; return }
|
||||
if (!form.value.password) { errorMsg.value = '请输入密码'; return }
|
||||
loading.value = true; errorMsg.value = ''
|
||||
try {
|
||||
const loginRes = await authApi.login({ username: form.value.username, password: form.value.password, tenantId: form.value.tenantId, code: '', uuid: '' })
|
||||
if (loginRes.code === 200 && loginRes.token) {
|
||||
localStorage.setItem('Admin-Token', loginRes.token)
|
||||
const infoRes = await authApi.getInfo()
|
||||
if (infoRes.code === 200) {
|
||||
const user = infoRes.user || {}
|
||||
localStorage.setItem('userInfo', JSON.stringify({
|
||||
userId: user.userId,
|
||||
userName: user.userName,
|
||||
nickName: user.nickName,
|
||||
practitionerId: user.practitionerId,
|
||||
orgId: user.orgId,
|
||||
orgName: user.orgName,
|
||||
roles: user.roles,
|
||||
permissions: user.permissions
|
||||
}))
|
||||
}
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/mobile/home')
|
||||
} else {
|
||||
errorMsg.value = loginRes.msg || '登录失败'
|
||||
}
|
||||
} catch (e) {
|
||||
errorMsg.value = e.response?.data?.msg || '登录失败,请检查网络'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page { min-height: 100vh; background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; }
|
||||
.login-header { text-align: center; color: #fff; margin-bottom: 40px; }
|
||||
.logo { font-size: 60px; margin-bottom: 12px; }
|
||||
.login-header h1 { font-size: 22px; margin: 0; }
|
||||
.login-header p { font-size: 14px; opacity: 0.8; margin-top: 8px; }
|
||||
.login-form { background: #fff; border-radius: 12px; padding: 24px; width: 100%; max-width: 360px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); }
|
||||
.form-item { margin-bottom: 16px; }
|
||||
.form-item label { display: block; font-size: 14px; color: #333; margin-bottom: 6px; font-weight: 500; }
|
||||
.input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; outline: none; }
|
||||
.input:focus { border-color: #1890ff; }
|
||||
select.input { appearance: none; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 12px center; }
|
||||
.login-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 18px; font-weight: 600; cursor: pointer; }
|
||||
.login-btn:disabled { background: #91d5ff; }
|
||||
.error-msg { color: #f5222d; text-align: center; margin-top: 12px; font-size: 14px; }
|
||||
</style>
|
||||
44
healthlink-his-mobile/src/views/Mine.vue
Normal file
44
healthlink-his-mobile/src/views/Mine.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="mine">
|
||||
<div class="user-info">
|
||||
<div class="avatar">{{ userInfo?.userName?.charAt(0) || '护' }}</div>
|
||||
<div class="info"><div class="name">{{ userInfo?.userName || '护士' }}</div><div class="role">{{ userInfo?.deptName || '护理部' }} | v1.0</div></div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div class="menu-item"><span>今日工作量</span><span class="value">{{ taskCount }}</span></div>
|
||||
<div class="menu-item"><span>待处理任务</span><span class="value">{{ pendingCount }}</span></div>
|
||||
<div class="menu-item" @click="logout"><span>退出登录</span><span class="arrow">›</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const userInfo = ref({})
|
||||
const taskCount = ref(0)
|
||||
const pendingCount = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
try { const info = localStorage.getItem('userInfo'); if (info) userInfo.value = JSON.parse(info) } catch {}
|
||||
try { const res = await nursingApi.getTasks({}); if (res.code === 200) { taskCount.value = res.data?.summary?.total || 0; pendingCount.value = res.data?.summary?.pending || 0 } } catch {}
|
||||
})
|
||||
|
||||
const logout = async () => {
|
||||
try { await ElMessageBox.confirm('确认退出登录?', '提示'); localStorage.removeItem('Admin-Token'); localStorage.removeItem('userInfo'); window.location.href = '/login' } catch {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-info { background: linear-gradient(135deg, #1890ff, #096dd9); color: #fff; padding: 24px 16px; display: flex; align-items: center; gap: 16px; }
|
||||
.avatar { width: 56px; height: 56px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.name { font-size: 18px; font-weight: 600; }
|
||||
.role { font-size: 13px; opacity: 0.8; }
|
||||
.menu-list { background: #fff; margin: 12px; border-radius: 8px; overflow: hidden; }
|
||||
.menu-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; font-size: 15px; }
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.value { color: #1890ff; font-weight: 600; }
|
||||
.arrow { color: #999; font-size: 18px; }
|
||||
</style>
|
||||
45
healthlink-his-mobile/src/views/MobileLayout.vue
Normal file
45
healthlink-his-mobile/src/views/MobileLayout.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="mobile-layout">
|
||||
<div class="mobile-header" v-if="!hideHeader">
|
||||
<button v-if="canGoBack" class="back-btn" @click="$router.back()">←</button>
|
||||
<h1>{{ $route.meta.title || 'HealthLink' }}</h1>
|
||||
</div>
|
||||
<div class="mobile-content" :class="{ 'no-header': hideHeader }">
|
||||
<router-view />
|
||||
</div>
|
||||
<div class="mobile-tabs" v-if="showTabs">
|
||||
<div v-for="tab in tabs" :key="tab.path" class="tab-item" :class="{ active: $route.path === tab.path }" @click="$router.push(tab.path)">
|
||||
<span class="tab-icon">{{ tab.icon }}</span>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const canGoBack = computed(() => route.path !== '/mobile/home')
|
||||
const hideHeader = computed(() => ['/mobile/login'].includes(route.path))
|
||||
const showTabs = computed(() => route.path.startsWith('/mobile/'))
|
||||
const tabs = [
|
||||
{ path: '/mobile/home', icon: '🏠', label: '首页' },
|
||||
{ path: '/mobile/tasks', icon: '📋', label: '任务' },
|
||||
{ path: '/mobile/patients', icon: '👥', label: '患者' },
|
||||
{ path: '/mobile/mine', icon: '👤', label: '我的' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-layout { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
|
||||
.mobile-header { height: 48px; background: #1890ff; color: #fff; display: flex; align-items: center; padding: 0 16px; position: sticky; top: 0; z-index: 10; }
|
||||
.mobile-header h1 { font-size: 18px; margin: 0; flex: 1; text-align: center; }
|
||||
.back-btn { background: none; border: none; color: #fff; font-size: 20px; position: absolute; left: 16px; }
|
||||
.mobile-content { flex: 1; overflow-y: auto; }
|
||||
.mobile-content.no-header { padding-bottom: 56px; }
|
||||
.mobile-tabs { position: fixed; bottom: 0; left: 0; right: 0; height: 56px; background: #fff; display: flex; border-top: 1px solid #e8e8e8; z-index: 10; padding-bottom: env(safe-area-inset-bottom); }
|
||||
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 12px; color: #999; }
|
||||
.tab-item.active { color: #1890ff; }
|
||||
.tab-icon { font-size: 20px; margin-bottom: 2px; }
|
||||
</style>
|
||||
89
healthlink-his-mobile/src/views/PatientDetail.vue
Normal file
89
healthlink-his-mobile/src/views/PatientDetail.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="patient-detail">
|
||||
<div class="patient-header">
|
||||
<div class="avatar">{{ patient.name?.charAt(0) }}</div>
|
||||
<div class="info"><div class="name">{{ patient.name }} <span class="bed">{{ patient.bedNo }}床</span></div><div class="diag">{{ patient.diagnosis }}</div></div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<div v-for="tab in tabs" :key="tab.key" class="tab" :class="{ active: activeTab === tab.key }" @click="activeTab = tab.key">{{ tab.label }}</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div v-if="activeTab === 'orders'">
|
||||
<div v-for="order in orders" :key="order.id" class="order-item">
|
||||
<div class="order-main"><div class="order-name">{{ order.orderName || order.adviceName }}</div><div class="order-dose">{{ order.dosage }} {{ order.frequency }}</div></div>
|
||||
<button v-if="order.status === 'PENDING' || order.executeStatus === '待执行'" class="exec-btn" @click="executeOrder(order)">执行</button>
|
||||
<span v-else class="done-tag">已执行</span>
|
||||
</div>
|
||||
<div v-if="orders.length === 0" class="empty">暂无医嘱</div>
|
||||
</div>
|
||||
<div v-if="activeTab === 'vitals'">
|
||||
<div class="vital-grid"><div class="vital-item" v-for="v in latestVitals" :key="v.key"><div class="vital-value">{{ v.value || '--' }}</div><div class="vital-label">{{ v.label }}</div></div></div>
|
||||
<button class="action-btn" @click="$router.push(`/mobile/vital-entry/${$route.params.id}`)">录入体征</button>
|
||||
</div>
|
||||
<div v-if="activeTab === 'assessments'">
|
||||
<div v-for="a in assessments" :key="a.id" class="assess-item">
|
||||
<div class="assess-type">{{ a.assessmentType }}</div>
|
||||
<div class="assess-score">评分: {{ a.totalScore }} <span :class="'risk-' + a.riskLevel">{{ a.riskLevel }}</span></div>
|
||||
</div>
|
||||
<button class="action-btn" @click="$router.push(`/mobile/assessment/${$route.params.id}`)">新建评估</button>
|
||||
<div v-if="assessments.length === 0" class="empty">暂无评估记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const patient = ref({})
|
||||
const orders = ref([])
|
||||
const latestVitals = ref([])
|
||||
const assessments = ref([])
|
||||
const activeTab = ref('orders')
|
||||
const tabs = [{ key: 'orders', label: '医嘱' }, { key: 'vitals', label: '体征' }, { key: 'assessments', label: '评估' }]
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id
|
||||
try {
|
||||
const [pRes, oRes, vRes, aRes] = await Promise.all([
|
||||
nursingApi.getPatientInfo(id), nursingApi.getOrders(id),
|
||||
nursingApi.getVitalSigns(id), nursingApi.getAssessments(id)
|
||||
])
|
||||
patient.value = pRes.data || {}; orders.value = oRes.data?.records || oRes.data || []; latestVitals.value = vRes.data?.records || vRes.data || []; assessments.value = aRes.data?.records || aRes.data || []
|
||||
} catch (e) { ElMessage.error('加载失败') }
|
||||
})
|
||||
|
||||
const executeOrder = async (order) => {
|
||||
try { await nursingApi.completeTask(order.id, { result: '执行完成' }); order.status = 'COMPLETED'; ElMessage.success('医嘱已执行') } catch (e) { ElMessage.error('执行失败') }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-header { background: linear-gradient(135deg, #1890ff, #096dd9); color: #fff; padding: 16px; display: flex; align-items: center; gap: 12px; }
|
||||
.avatar { width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
||||
.name { font-size: 18px; font-weight: 600; }
|
||||
.bed { font-size: 14px; opacity: 0.8; }
|
||||
.diag { font-size: 13px; opacity: 0.8; }
|
||||
.tabs { display: flex; background: #fff; border-bottom: 1px solid #eee; position: sticky; top: 48px; z-index: 5; }
|
||||
.tab { flex: 1; text-align: center; padding: 12px; font-size: 14px; color: #666; }
|
||||
.tab.active { color: #1890ff; border-bottom: 2px solid #1890ff; font-weight: 600; }
|
||||
.tab-content { padding: 12px; }
|
||||
.order-item { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.order-name { font-weight: 600; font-size: 14px; }
|
||||
.order-dose { color: #666; font-size: 12px; margin-top: 2px; }
|
||||
.exec-btn { background: #1890ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 13px; }
|
||||
.done-tag { color: #52c41a; font-size: 12px; }
|
||||
.vital-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
|
||||
.vital-item { background: #fff; border-radius: 8px; padding: 12px; text-align: center; }
|
||||
.vital-value { font-size: 18px; font-weight: 600; color: #1890ff; }
|
||||
.vital-label { font-size: 11px; color: #999; margin-top: 4px; }
|
||||
.action-btn { width: 100%; padding: 12px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 15px; margin-top: 12px; }
|
||||
.assess-item { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; }
|
||||
.assess-type { font-weight: 600; }
|
||||
.risk-HIGH { color: #f5222d; } .risk-MEDIUM { color: #fa8c16; } .risk-LOW { color: #52c41a; }
|
||||
.empty { text-align: center; padding: 20px; color: #999; }
|
||||
</style>
|
||||
49
healthlink-his-mobile/src/views/PatientList.vue
Normal file
49
healthlink-his-mobile/src/views/PatientList.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="patient-list">
|
||||
<div class="search-bar"><input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" /></div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-for="p in displayPatients" :key="p.id" class="patient-card" @click="$router.push(`/mobile/patient-detail/${p.id}`)">
|
||||
<div class="patient-avatar" :class="'level-' + p.nursingLevel">{{ p.name?.charAt(0) }}</div>
|
||||
<div class="patient-info">
|
||||
<div class="patient-name">{{ p.name }} <span class="bed">{{ p.bedNo }}床</span></div>
|
||||
<div class="patient-diag">{{ p.diagnosis || '暂无诊断' }}</div>
|
||||
<div class="patient-tags"><span class="tag" :class="'level-' + p.nursingLevel">{{ p.nursingLevel }}级护理</span><span v-if="p.gender" class="tag">{{ p.gender }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loading && displayPatients.length === 0" class="empty">暂无患者</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const patients = ref([])
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const displayPatients = computed(() => searchText.value ? patients.value.filter(p => p.name?.includes(searchText.value) || p.bedNo?.includes(searchText.value)) : patients.value)
|
||||
|
||||
const loadPatients = async () => {
|
||||
loading.value = true
|
||||
try { const res = await nursingApi.getPatientList({}); patients.value = res.data || [] } catch (e) { ElMessage.error('加载失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(loadPatients)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-bar { padding: 8px 0; }
|
||||
.search-input { width: 100%; padding: 10px 16px; border: 1px solid #ddd; border-radius: 20px; font-size: 15px; outline: none; background: #fff; }
|
||||
.search-input:focus { border-color: #1890ff; }
|
||||
.loading { text-align: center; padding: 20px; color: #999; }
|
||||
.patient-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.patient-avatar { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #fff; }
|
||||
.level-1 { background: #f5222d; } .level-2 { background: #fa8c16; } .level-3 { background: #52c41a; }
|
||||
.patient-name { font-weight: 600; font-size: 15px; }
|
||||
.bed { color: #999; font-size: 13px; }
|
||||
.patient-diag { color: #666; font-size: 13px; margin: 2px 0; }
|
||||
.patient-tags { display: flex; gap: 6px; }
|
||||
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; }
|
||||
.empty { text-align: center; padding: 40px; color: #999; }
|
||||
</style>
|
||||
70
healthlink-his-mobile/src/views/TaskList.vue
Normal file
70
healthlink-his-mobile/src/views/TaskList.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<div class="filter-bar">
|
||||
<select v-model="filterType" class="filter-select" @change="loadTasks"><option value="">全部</option><option value="医嘱执行">医嘱执行</option><option value="生命体征">生命体征</option></select>
|
||||
<button class="refresh-btn" @click="loadTasks">刷新</button>
|
||||
</div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-for="task in filteredTasks" :key="task.id" class="task-card" @touchstart="swipeStart" @touchend="swipeEnd($event, task)">
|
||||
<div class="task-info">
|
||||
<div class="task-header"><span class="task-patient">{{ task.patientName || '患者' }}</span><span class="bed">{{ task.bedNo || '' }}</span></div>
|
||||
<div class="task-content">{{ task.adviceName || task.orderName || '医嘱任务' }}</div>
|
||||
<div class="task-meta"><span class="task-type">{{ task.adviceType || task.orderType || '医嘱' }}</span><span class="task-time">{{ task.createTime || '' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loading && filteredTasks.length === 0" class="empty">暂无任务</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const tasks = ref([])
|
||||
const loading = ref(false)
|
||||
const filterType = ref('')
|
||||
const filteredTasks = computed(() => filterType.value ? tasks.value.filter(t => (t.adviceType || '').includes(filterType.value)) : tasks.value)
|
||||
|
||||
const loadTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||||
const nurseId = userInfo.practitionerId || userInfo.userId
|
||||
if (!nurseId) { ElMessage.warning('未获取到用户信息'); return }
|
||||
const res = await nursingApi.getTasks({ nurseId: nurseId, pageNum: 1, pageSize: 50 })
|
||||
if (res.code === 200) { tasks.value = res.data?.records || res.data?.rows || [] }
|
||||
} catch (e) { ElMessage.error('加载失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
let startX = 0
|
||||
const swipeStart = (e) => { startX = e.touches[0].clientX }
|
||||
const swipeEnd = async (e, task) => {
|
||||
const diff = startX - e.changedTouches[0].clientX
|
||||
if (diff > 80) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认完成此任务?', '提示')
|
||||
await nursingApi.completeTask(task.id, { result: '完成' })
|
||||
ElMessage.success('任务已完成')
|
||||
loadTasks()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadTasks)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar { display: flex; gap: 8px; padding: 8px 0; }
|
||||
.filter-select { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: #fff; }
|
||||
.refresh-btn { padding: 8px 16px; background: #1890ff; color: #fff; border: none; border-radius: 6px; }
|
||||
.loading { text-align: center; padding: 20px; color: #999; }
|
||||
.task-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.task-header { display: flex; align-items: center; gap: 8px; }
|
||||
.task-patient { font-weight: 600; font-size: 15px; }
|
||||
.bed { color: #1890ff; font-size: 13px; }
|
||||
.task-content { color: #666; font-size: 13px; margin: 4px 0; }
|
||||
.task-meta { display: flex; gap: 12px; font-size: 12px; color: #999; }
|
||||
.task-type { background: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 4px; }
|
||||
.empty { text-align: center; padding: 40px; color: #999; }
|
||||
</style>
|
||||
77
healthlink-his-mobile/src/views/VitalSignEntry.vue
Normal file
77
healthlink-his-mobile/src/views/VitalSignEntry.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="vital-entry">
|
||||
<div class="patient-bar" v-if="patientName"><span class="label">患者:</span> {{ patientName }}</div>
|
||||
<div class="entry-grid">
|
||||
<div v-for="item in vitalItems" :key="item.key" class="entry-item">
|
||||
<div class="entry-label">{{ item.label }}</div>
|
||||
<input v-model="formData[item.key]" type="number" :placeholder="item.placeholder" class="entry-input" />
|
||||
<div class="quick-values"><span v-for="v in item.quickValues" :key="v" class="quick-val" @click="formData[item.key] = v">{{ v }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pain-section">
|
||||
<div class="entry-label">疼痛评分 (0-10)</div>
|
||||
<div class="pain-scale"><span v-for="n in 11" :key="n" class="pain-num" :class="{ active: formData.painScore === n-1 }" @click="formData.painScore = n-1">{{ n-1 }}</span></div>
|
||||
<div class="pain-label">{{ painLabel }}</div>
|
||||
</div>
|
||||
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '一键提交' }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
const patientId = route.params.patientId
|
||||
if (patientId) {
|
||||
try {
|
||||
const res = await nursingApi.getPatientInfo(patientId)
|
||||
if (res.data) patientName.value = res.data.name || ''
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const patientName = ref('')
|
||||
const formData = ref({ temperature: '', pulse: '', bloodPressureHigh: '', bloodPressureLow: '', spo2: '', respiration: '', painScore: 0 })
|
||||
const vitalItems = [
|
||||
{ key: 'temperature', label: '体温(°C)', placeholder: '36.5', quickValues: [36.0, 36.5, 37.0, 37.5, 38.0] },
|
||||
{ key: 'pulse', label: '脉搏(次/分)', placeholder: '72', quickValues: [60, 72, 80, 90, 100] },
|
||||
{ key: 'bloodPressureHigh', label: '收缩压(mmHg)', placeholder: '120', quickValues: [90, 110, 120, 130, 140] },
|
||||
{ key: 'bloodPressureLow', label: '舒张压(mmHg)', placeholder: '80', quickValues: [60, 70, 80, 90, 100] },
|
||||
{ key: 'spo2', label: '血氧(%)', placeholder: '98', quickValues: [95, 96, 97, 98, 99] },
|
||||
{ key: 'respiration', label: '呼吸(次/分)', placeholder: '18', quickValues: [14, 16, 18, 20, 22] }
|
||||
]
|
||||
const painLabel = computed(() => { const s = formData.value.painScore; return s <= 3 ? '轻度疼痛' : s <= 6 ? '中度疼痛' : '重度疼痛' })
|
||||
|
||||
const submit = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
await nursingApi.submitVitalSign({ ...formData.value, patientId: route.params.patientId })
|
||||
ElMessage.success('体征录入成功')
|
||||
} catch (e) { ElMessage.error('提交失败') } finally { submitting.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-bar { background: #e6f7ff; padding: 10px 16px; font-size: 14px; margin-bottom: 12px; border-radius: 8px; }
|
||||
.patient-bar .label { color: #666; }
|
||||
.entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.entry-item { background: #fff; border-radius: 8px; padding: 10px; }
|
||||
.entry-label { font-size: 12px; color: #666; margin-bottom: 6px; }
|
||||
.entry-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 18px; text-align: center; }
|
||||
.entry-input:focus { border-color: #1890ff; }
|
||||
.quick-values { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
||||
.quick-val { padding: 3px 8px; background: #f0f0f0; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
||||
.quick-val:active { background: #1890ff; color: #fff; }
|
||||
.pain-section { background: #fff; border-radius: 8px; padding: 12px; margin-top: 10px; }
|
||||
.pain-scale { display: flex; gap: 3px; margin-top: 8px; flex-wrap: wrap; }
|
||||
.pain-num { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: #f0f0f0; font-size: 13px; cursor: pointer; }
|
||||
.pain-num.active { background: #1890ff; color: #fff; }
|
||||
.pain-label { text-align: center; margin-top: 8px; color: #666; font-size: 13px; }
|
||||
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 16px; font-weight: 600; }
|
||||
.submit-btn:disabled { background: #91d5ff; }
|
||||
</style>
|
||||
42
healthlink-his-mobile/vite.config.js
Normal file
42
healthlink-his-mobile/vite.config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import path from 'path'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
return {
|
||||
base: '/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, './'),
|
||||
'@': path.resolve(__dirname, './src')
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
server: {
|
||||
port: 82,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/dev-api': {
|
||||
target: env.VITE_API_PROXY || 'http://localhost:18080/healthlink-his',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
cssMinify: 'esbuild'
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'legacy-js-api']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.aidiagnosis.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
|
||||
public interface IAiDiagnosisAppService {
|
||||
|
||||
R<?> suggest(Long encounterId, Long patientId, String symptomText, String source);
|
||||
|
||||
R<?> getHistory(Long patientId);
|
||||
|
||||
R<?> getHistoryByEncounter(Long encounterId);
|
||||
|
||||
R<?> acceptSuggestion(Long id);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.healthlink.his.web.aidiagnosis.appservice.impl;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
import com.healthlink.his.aidiagnosis.service.IAiDiagnosisService;
|
||||
import com.healthlink.his.web.aidiagnosis.appservice.IAiDiagnosisAppService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AiDiagnosisAppServiceImpl implements IAiDiagnosisAppService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AiDiagnosisAppServiceImpl.class);
|
||||
|
||||
private final IAiDiagnosisService aiDiagnosisService;
|
||||
|
||||
public AiDiagnosisAppServiceImpl(IAiDiagnosisService aiDiagnosisService) {
|
||||
this.aiDiagnosisService = aiDiagnosisService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> suggest(Long encounterId, Long patientId, String symptomText, String source) {
|
||||
if (encounterId == null || patientId == null) {
|
||||
return R.fail(400, "就诊ID和患者ID不能为空");
|
||||
}
|
||||
if (symptomText == null || symptomText.trim().isEmpty()) {
|
||||
return R.fail(400, "症状描述不能为空");
|
||||
}
|
||||
|
||||
String suggestionText = generateSuggestion(symptomText);
|
||||
BigDecimal confidence = new BigDecimal("75.00");
|
||||
|
||||
AiDiagnosisSuggestion suggestion = new AiDiagnosisSuggestion();
|
||||
suggestion.setEncounterId(encounterId);
|
||||
suggestion.setPatientId(patientId);
|
||||
suggestion.setSymptomText(symptomText);
|
||||
suggestion.setDiagnosisSuggestions(suggestionText);
|
||||
suggestion.setConfidenceScore(confidence);
|
||||
suggestion.setSuggestionSource(source != null ? source : "llm");
|
||||
suggestion.setAccepted(false);
|
||||
suggestion.setCreateTime(new Date());
|
||||
|
||||
aiDiagnosisService.save(suggestion);
|
||||
|
||||
log.info("AI diagnosis suggestion created: id={}, patientId={}", suggestion.getId(), patientId);
|
||||
|
||||
return R.ok(Map.of(
|
||||
"id", suggestion.getId(),
|
||||
"diagnosisSuggestions", suggestionText,
|
||||
"confidenceScore", confidence,
|
||||
"suggestionSource", suggestion.getSuggestionSource()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getHistory(Long patientId) {
|
||||
if (patientId == null) {
|
||||
return R.fail(400, "患者ID不能为空");
|
||||
}
|
||||
List<AiDiagnosisSuggestion> history = aiDiagnosisService.findByPatientId(patientId);
|
||||
return R.ok(history);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getHistoryByEncounter(Long encounterId) {
|
||||
if (encounterId == null) {
|
||||
return R.fail(400, "就诊ID不能为空");
|
||||
}
|
||||
List<AiDiagnosisSuggestion> history = aiDiagnosisService.findByEncounterId(encounterId);
|
||||
return R.ok(history);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> acceptSuggestion(Long id) {
|
||||
if (id == null) {
|
||||
return R.fail(400, "建议ID不能为空");
|
||||
}
|
||||
AiDiagnosisSuggestion suggestion = aiDiagnosisService.getById(id);
|
||||
if (suggestion == null) {
|
||||
return R.fail(404, "建议不存在");
|
||||
}
|
||||
suggestion.setAccepted(true);
|
||||
aiDiagnosisService.updateById(suggestion);
|
||||
return R.ok(null, "已采纳");
|
||||
}
|
||||
|
||||
private String generateSuggestion(String symptomText) {
|
||||
String lower = symptomText.toLowerCase();
|
||||
if (lower.contains("发热") || lower.contains("发烧")) {
|
||||
return "建议排查:1.上呼吸道感染 2.肺炎 3.泌尿系感染 4.其他感染性疾病。建议完善血常规、CRP、PCT等检查。";
|
||||
}
|
||||
if (lower.contains("咳嗽") || lower.contains("咳痰")) {
|
||||
return "建议排查:1.急性支气管炎 2.肺炎 3.慢性阻塞性肺疾病急性加重。建议完善胸部影像学检查。";
|
||||
}
|
||||
if (lower.contains("头痛")) {
|
||||
return "建议排查:1.紧张性头痛 2.偏头痛 3.高血压相关头痛 4.颅内病变。建议监测血压,必要时完善头颅CT。";
|
||||
}
|
||||
if (lower.contains("胸痛")) {
|
||||
return "建议排查:1.心绞痛 2.急性冠脉综合征 3.肺栓塞 4.气胸。建议立即完善心电图、心肌酶谱。";
|
||||
}
|
||||
if (lower.contains("腹痛")) {
|
||||
return "建议排查:1.急性胃肠炎 2.胆囊炎 3.阑尾炎 4.胰腺炎。建议完善腹部超声、血常规、肝功能检查。";
|
||||
}
|
||||
return "根据症状描述「" + symptomText + "」,建议结合患者病史、体格检查及辅助检查结果综合判断。建议完善相关实验室检查以明确诊断。";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.healthlink.his.web.aidiagnosis.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.aidiagnosis.appservice.IAiDiagnosisAppService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@Tag(name = "AI辅助诊疗")
|
||||
@RestController
|
||||
@RequestMapping("/ai-diagnosis")
|
||||
public class AiDiagnosisController {
|
||||
|
||||
@Resource
|
||||
private IAiDiagnosisAppService aiDiagnosisAppService;
|
||||
|
||||
@Operation(summary = "获取AI诊断建议")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@PostMapping("/suggest")
|
||||
public R<?> suggest(@RequestParam Long encounterId,
|
||||
@RequestParam Long patientId,
|
||||
@RequestParam String symptomText,
|
||||
@RequestParam(required = false, defaultValue = "llm") String source) {
|
||||
return aiDiagnosisAppService.suggest(encounterId, patientId, symptomText, source);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询患者AI诊断历史")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@GetMapping("/history/{patientId}")
|
||||
public R<?> getHistory(@PathVariable Long patientId) {
|
||||
return aiDiagnosisAppService.getHistory(patientId);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询就诊AI诊断历史")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@GetMapping("/history/encounter/{encounterId}")
|
||||
public R<?> getHistoryByEncounter(@PathVariable Long encounterId) {
|
||||
return aiDiagnosisAppService.getHistoryByEncounter(encounterId);
|
||||
}
|
||||
|
||||
@Operation(summary = "采纳AI诊断建议")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:edit')")
|
||||
@PostMapping("/accept/{id}")
|
||||
public R<?> acceptSuggestion(@PathVariable Long id) {
|
||||
return aiDiagnosisAppService.acceptSuggestion(id);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.healthlink.his.web.cdss.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ICdssAppService {
|
||||
|
||||
R<?> evaluateRules(Long encounterId, Long patientId, String triggerType, Long departmentId);
|
||||
@@ -11,4 +14,10 @@ public interface ICdssAppService {
|
||||
R<?> acknowledgeAlert(Long id, String remark);
|
||||
|
||||
R<?> getRules(String ruleType, String severity, String keyword);
|
||||
|
||||
R<?> getRuleStats();
|
||||
|
||||
R<?> getExecutionHistory(Long ruleId, Long encounterId, Integer page, Integer size);
|
||||
|
||||
R<?> getRulesEnhanced(String ruleType, String severity, String keyword, String category, Integer priority);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.healthlink.his.web.cdss.appservice.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.cdss.domain.CdssAlert;
|
||||
import com.healthlink.his.cdss.domain.CdssRule;
|
||||
import com.healthlink.his.cdss.domain.CdssRuleExecution;
|
||||
import com.healthlink.his.cdss.service.ICdssAlertService;
|
||||
import com.healthlink.his.cdss.service.ICdssRuleExecutionService;
|
||||
import com.healthlink.his.cdss.service.ICdssRuleService;
|
||||
import com.healthlink.his.web.cdss.appservice.ICdssAppService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -23,10 +26,13 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
||||
|
||||
private final ICdssRuleService cdssRuleService;
|
||||
private final ICdssAlertService cdssAlertService;
|
||||
private final ICdssRuleExecutionService cdssRuleExecutionService;
|
||||
|
||||
public CdssAppServiceImpl(ICdssRuleService cdssRuleService, ICdssAlertService cdssAlertService) {
|
||||
public CdssAppServiceImpl(ICdssRuleService cdssRuleService, ICdssAlertService cdssAlertService,
|
||||
ICdssRuleExecutionService cdssRuleExecutionService) {
|
||||
this.cdssRuleService = cdssRuleService;
|
||||
this.cdssAlertService = cdssAlertService;
|
||||
this.cdssRuleExecutionService = cdssRuleExecutionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,12 +44,36 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
||||
List<CdssAlert> triggeredAlerts = new ArrayList<>();
|
||||
|
||||
for (CdssRule rule : activeRules) {
|
||||
if (matchRule(rule, encounterId, patientId)) {
|
||||
CdssAlert alert = buildAlert(rule, encounterId, patientId);
|
||||
cdssAlertService.save(alert);
|
||||
triggeredAlerts.add(alert);
|
||||
log.info("CDSS rule triggered: ruleCode={}, encounterId={}", rule.getRuleCode(), encounterId);
|
||||
long startTime = System.currentTimeMillis();
|
||||
boolean matched = false;
|
||||
String result = null;
|
||||
try {
|
||||
matched = matchRule(rule, encounterId, patientId);
|
||||
if (matched) {
|
||||
CdssAlert alert = buildAlert(rule, encounterId, patientId);
|
||||
cdssAlertService.save(alert);
|
||||
triggeredAlerts.add(alert);
|
||||
result = "MATCHED";
|
||||
log.info("CDSS rule triggered: ruleCode={}, encounterId={}", rule.getRuleCode(), encounterId);
|
||||
} else {
|
||||
result = "NOT_MATCHED";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result = "ERROR: " + e.getMessage();
|
||||
log.warn("CDSS rule execution error: ruleCode={}, error={}", rule.getRuleCode(), e.getMessage());
|
||||
}
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
CdssRuleExecution execution = new CdssRuleExecution();
|
||||
execution.setRuleId(rule.getId());
|
||||
execution.setRuleCode(rule.getRuleCode());
|
||||
execution.setEncounterId(encounterId);
|
||||
execution.setPatientId(patientId);
|
||||
execution.setMatched(matched);
|
||||
execution.setExecutionTime(new Date());
|
||||
execution.setExecutionResult(result);
|
||||
execution.setDurationMs((int) duration);
|
||||
cdssRuleExecutionService.save(execution);
|
||||
}
|
||||
|
||||
return R.ok(Map.of(
|
||||
@@ -89,13 +119,42 @@ public class CdssAppServiceImpl implements ICdssAppService {
|
||||
}
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
rules = rules.stream()
|
||||
.filter(r -> r.getRuleName().contains(keyword) ||
|
||||
.filter(r -> r.getRuleName().contains(keyword) ||
|
||||
(r.getRuleCode() != null && r.getRuleCode().contains(keyword)))
|
||||
.toList();
|
||||
}
|
||||
return R.ok(rules);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getRuleStats() {
|
||||
return R.ok(cdssRuleService.getRuleStats());
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getExecutionHistory(Long ruleId, Long encounterId, Integer page, Integer size) {
|
||||
LambdaQueryWrapper<CdssRuleExecution> wrapper = new LambdaQueryWrapper<>();
|
||||
if (ruleId != null) {
|
||||
wrapper.eq(CdssRuleExecution::getRuleId, ruleId);
|
||||
}
|
||||
if (encounterId != null) {
|
||||
wrapper.eq(CdssRuleExecution::getEncounterId, encounterId);
|
||||
}
|
||||
wrapper.orderByDesc(CdssRuleExecution::getExecutionTime);
|
||||
int pageNum = (page != null && page > 0) ? page : 1;
|
||||
int pageSize = (size != null && size > 0) ? size : 20;
|
||||
wrapper.last("LIMIT " + pageSize + " OFFSET " + (pageNum - 1) * pageSize);
|
||||
List<CdssRuleExecution> history = cdssRuleExecutionService.list(wrapper);
|
||||
return R.ok(history);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getRulesEnhanced(String ruleType, String severity, String keyword,
|
||||
String category, Integer priority) {
|
||||
List<CdssRule> rules = cdssRuleService.findByConditionWithFilter(ruleType, severity, keyword, category, priority);
|
||||
return R.ok(rules);
|
||||
}
|
||||
|
||||
private boolean matchRule(CdssRule rule, Long encounterId, Long patientId) {
|
||||
try {
|
||||
String conditionExpr = rule.getConditionExpr();
|
||||
|
||||
@@ -54,4 +54,34 @@ public class CdssController {
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
return cdssAppService.getRules(ruleType, severity, keyword);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询规则列表(增强版-支持优先级/分类)")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@GetMapping("/rules/enhanced")
|
||||
public R<?> getRulesEnhanced(
|
||||
@RequestParam(value = "ruleType", required = false) String ruleType,
|
||||
@RequestParam(value = "severity", required = false) String severity,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "category", required = false) String category,
|
||||
@RequestParam(value = "priority", required = false) Integer priority) {
|
||||
return cdssAppService.getRulesEnhanced(ruleType, severity, keyword, category, priority);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取规则统计数据")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@GetMapping("/rules/stats")
|
||||
public R<?> getRuleStats() {
|
||||
return cdssAppService.getRuleStats();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取规则执行历史")
|
||||
@PreAuthorize("@ss.hasPermi('infection:cdss:list')")
|
||||
@GetMapping("/rules/history")
|
||||
public R<?> getExecutionHistory(
|
||||
@RequestParam(value = "ruleId", required = false) Long ruleId,
|
||||
@RequestParam(value = "encounterId", required = false) Long encounterId,
|
||||
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
|
||||
return cdssAppService.getExecutionHistory(ruleId, encounterId, page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.healthlink.his.web.clinical.appservice;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.healthlink.his.clinical.domain.KgClinicalPathway;
|
||||
import com.healthlink.his.clinical.domain.KgEntityRelation;
|
||||
import com.healthlink.his.clinical.domain.KgPathwayStep;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IKgRelationAppService {
|
||||
void createRelation(KgEntityRelation relation);
|
||||
IPage<KgEntityRelation> pageRelations(String sourceType, String targetType, String relationType, Integer pageNo, Integer pageSize);
|
||||
Map<String, Object> getRelationGraph(String entityType, String entityId);
|
||||
void createPathway(KgClinicalPathway pathway, List<KgPathwayStep> steps);
|
||||
IPage<KgClinicalPathway> pagePathways(String keyword, Integer pageNo, Integer pageSize);
|
||||
List<KgPathwayStep> getPathwaySteps(Long pathwayId);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.healthlink.his.web.clinical.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.healthlink.his.clinical.domain.KgClinicalPathway;
|
||||
import com.healthlink.his.clinical.domain.KgEntityRelation;
|
||||
import com.healthlink.his.clinical.domain.KgPathwayStep;
|
||||
import com.healthlink.his.clinical.service.IKgClinicalPathwayService;
|
||||
import com.healthlink.his.clinical.service.IKgEntityRelationService;
|
||||
import com.healthlink.his.clinical.service.IKgPathwayStepService;
|
||||
import com.healthlink.his.web.clinical.appservice.IKgRelationAppService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class KgRelationAppServiceImpl implements IKgRelationAppService {
|
||||
|
||||
@Autowired
|
||||
private IKgEntityRelationService relationService;
|
||||
|
||||
@Autowired
|
||||
private IKgClinicalPathwayService pathwayService;
|
||||
|
||||
@Autowired
|
||||
private IKgPathwayStepService stepService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createRelation(KgEntityRelation relation) {
|
||||
if (relation.getRelationStrength() == null) {
|
||||
relation.setRelationStrength(BigDecimal.ONE);
|
||||
}
|
||||
relation.setCreateTime(new Date());
|
||||
relationService.save(relation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgEntityRelation> pageRelations(String sourceType, String targetType, String relationType, Integer pageNo, Integer pageSize) {
|
||||
LambdaQueryWrapper<KgEntityRelation> w = new LambdaQueryWrapper<>();
|
||||
w.eq(StringUtils.hasText(sourceType), KgEntityRelation::getSourceType, sourceType)
|
||||
.eq(StringUtils.hasText(targetType), KgEntityRelation::getTargetType, targetType)
|
||||
.eq(StringUtils.hasText(relationType), KgEntityRelation::getRelationType, relationType)
|
||||
.orderByDesc(KgEntityRelation::getCreateTime);
|
||||
return relationService.page(new Page<>(pageNo, pageSize), w);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getRelationGraph(String entityType, String entityId) {
|
||||
Set<String> nodeKeys = new LinkedHashSet<>();
|
||||
List<Map<String, Object>> nodes = new ArrayList<>();
|
||||
List<Map<String, Object>> edges = new ArrayList<>();
|
||||
|
||||
// Find all relations where this entity is source or target
|
||||
LambdaQueryWrapper<KgEntityRelation> w1 = new LambdaQueryWrapper<>();
|
||||
w1.eq(KgEntityRelation::getSourceType, entityType)
|
||||
.eq(KgEntityRelation::getSourceId, entityId);
|
||||
List<KgEntityRelation> outgoing = relationService.list(w1);
|
||||
|
||||
LambdaQueryWrapper<KgEntityRelation> w2 = new LambdaQueryWrapper<>();
|
||||
w2.eq(KgEntityRelation::getTargetType, entityType)
|
||||
.eq(KgEntityRelation::getTargetId, entityId);
|
||||
List<KgEntityRelation> incoming = relationService.list(w2);
|
||||
|
||||
List<KgEntityRelation> all = new ArrayList<>();
|
||||
all.addAll(outgoing);
|
||||
all.addAll(incoming);
|
||||
|
||||
for (KgEntityRelation rel : all) {
|
||||
String srcKey = rel.getSourceType() + ":" + rel.getSourceId();
|
||||
String tgtKey = rel.getTargetType() + ":" + rel.getTargetId();
|
||||
|
||||
if (nodeKeys.add(srcKey)) {
|
||||
nodes.add(buildNode(srcKey, rel.getSourceType(), rel.getSourceId()));
|
||||
}
|
||||
if (nodeKeys.add(tgtKey)) {
|
||||
nodes.add(buildNode(tgtKey, rel.getTargetType(), rel.getTargetId()));
|
||||
}
|
||||
|
||||
Map<String, Object> edge = new LinkedHashMap<>();
|
||||
edge.put("source", srcKey);
|
||||
edge.put("target", tgtKey);
|
||||
edge.put("relationType", rel.getRelationType());
|
||||
edge.put("relationStrength", rel.getRelationStrength());
|
||||
edge.put("description", rel.getDescription());
|
||||
edges.add(edge);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("nodes", nodes);
|
||||
result.put("edges", edges);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildNode(String key, String type, String id) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", key);
|
||||
node.put("entityType", type);
|
||||
node.put("entityId", id);
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createPathway(KgClinicalPathway pathway, List<KgPathwayStep> steps) {
|
||||
pathway.setStatus("ACTIVE");
|
||||
pathway.setCreateTime(new Date());
|
||||
pathwayService.save(pathway);
|
||||
|
||||
if (steps != null) {
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
KgPathwayStep step = steps.get(i);
|
||||
step.setPathwayId(pathway.getId());
|
||||
step.setStepOrder(i + 1);
|
||||
step.setCreateTime(new Date());
|
||||
}
|
||||
stepService.saveBatch(steps);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgClinicalPathway> pagePathways(String keyword, Integer pageNo, Integer pageSize) {
|
||||
LambdaQueryWrapper<KgClinicalPathway> w = new LambdaQueryWrapper<>();
|
||||
w.eq(KgClinicalPathway::getStatus, "ACTIVE")
|
||||
.and(StringUtils.hasText(keyword), q -> q
|
||||
.like(KgClinicalPathway::getPathwayName, keyword)
|
||||
.or().like(KgClinicalPathway::getDiseaseName, keyword)
|
||||
.or().like(KgClinicalPathway::getPathwayCode, keyword))
|
||||
.orderByDesc(KgClinicalPathway::getCreateTime);
|
||||
return pathwayService.page(new Page<>(pageNo, pageSize), w);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KgPathwayStep> getPathwaySteps(Long pathwayId) {
|
||||
LambdaQueryWrapper<KgPathwayStep> w = new LambdaQueryWrapper<>();
|
||||
w.eq(KgPathwayStep::getPathwayId, pathwayId)
|
||||
.orderByAsc(KgPathwayStep::getStepOrder);
|
||||
return stepService.list(w);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.healthlink.his.web.clinical.controller;
|
||||
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.clinical.domain.KgClinicalPathway;
|
||||
import com.healthlink.his.clinical.domain.KgEntityRelation;
|
||||
import com.healthlink.his.clinical.domain.KgPathwayStep;
|
||||
import com.healthlink.his.web.clinical.appservice.IKgRelationAppService;
|
||||
import com.healthlink.his.web.clinical.dto.KgPathwayDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "知识图谱管理")
|
||||
@RestController
|
||||
@RequestMapping("/knowledgegraph")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class KgRelationController {
|
||||
|
||||
private final IKgRelationAppService kgRelationAppService;
|
||||
|
||||
@Operation(summary = "创建关系")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/relation")
|
||||
public AjaxResult createRelation(@RequestBody KgEntityRelation relation) {
|
||||
kgRelationAppService.createRelation(relation);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "关系分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/relation/page")
|
||||
public R<?> pageRelations(
|
||||
@RequestParam(value = "sourceType", required = false) String sourceType,
|
||||
@RequestParam(value = "targetType", required = false) String targetType,
|
||||
@RequestParam(value = "relationType", required = false) String relationType,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
return R.ok(kgRelationAppService.pageRelations(sourceType, targetType, relationType, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询实体关系图")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/relation/graph/{entityType}/{entityId}")
|
||||
public R<?> getRelationGraph(@PathVariable String entityType, @PathVariable String entityId) {
|
||||
return R.ok(kgRelationAppService.getRelationGraph(entityType, entityId));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建临床路径")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/pathway")
|
||||
public AjaxResult createPathway(@RequestBody KgPathwayDto dto) {
|
||||
kgRelationAppService.createPathway(dto.getPathway(), dto.getSteps());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "路径分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/pathway/page")
|
||||
public R<?> pagePathways(
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
return R.ok(kgRelationAppService.pagePathways(keyword, pageNo, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询路径步骤")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/pathway/{id}/steps")
|
||||
public R<?> getPathwaySteps(@PathVariable Long id) {
|
||||
return R.ok(kgRelationAppService.getPathwaySteps(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.web.clinical.dto;
|
||||
|
||||
import com.healthlink.his.clinical.domain.KgClinicalPathway;
|
||||
import com.healthlink.his.clinical.domain.KgPathwayStep;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class KgPathwayDto {
|
||||
private KgClinicalPathway pathway;
|
||||
private List<KgPathwayStep> steps;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.healthlink.his.web.datacollection.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
|
||||
public interface IDataCollectionAppService {
|
||||
R<?> collectClinicalData(String startDate, String endDate, Long departmentId);
|
||||
R<?> collectOperationalData(String startDate, String endDate);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.healthlink.his.web.datacollection.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
|
||||
public interface IDataDashboardAppService {
|
||||
R<?> getRealtimeData();
|
||||
R<?> getHistoricalData(String period);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.healthlink.his.web.datacollection.appservice.impl;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.quality.service.IBusinessAnalyticsService;
|
||||
import com.healthlink.his.quality.domain.BusinessAnalytics;
|
||||
import com.healthlink.his.web.datacollection.appservice.IDataCollectionAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class DataCollectionAppServiceImpl implements IDataCollectionAppService {
|
||||
|
||||
private final IBusinessAnalyticsService analyticsService;
|
||||
|
||||
@Override
|
||||
public R<?> collectClinicalData(String startDate, String endDate, Long departmentId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
int collected = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (departmentId != null && !departmentId.equals(ba.getDepartmentId())) {
|
||||
continue;
|
||||
}
|
||||
if (startDate != null && ba.getStatDate() != null && ba.getStatDate().compareTo(startDate) < 0) {
|
||||
continue;
|
||||
}
|
||||
if (endDate != null && ba.getStatDate() != null && ba.getStatDate().compareTo(endDate) > 0) {
|
||||
continue;
|
||||
}
|
||||
collected++;
|
||||
}
|
||||
result.put("status", "success");
|
||||
result.put("recordCount", collected);
|
||||
result.put("message", "临床数据采集完成,共采集 " + collected + " 条记录");
|
||||
log.info("临床数据采集完成: startDate={}, endDate={}, departmentId={}, count={}", startDate, endDate, departmentId, collected);
|
||||
} catch (Exception e) {
|
||||
log.error("临床数据采集失败", e);
|
||||
result.put("status", "error");
|
||||
result.put("message", "采集失败: " + e.getMessage());
|
||||
return R.fail("采集失败: " + e.getMessage());
|
||||
}
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> collectOperationalData(String startDate, String endDate) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
int collected = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (startDate != null && ba.getStatDate() != null && ba.getStatDate().compareTo(startDate) < 0) {
|
||||
continue;
|
||||
}
|
||||
if (endDate != null && ba.getStatDate() != null && ba.getStatDate().compareTo(endDate) > 0) {
|
||||
continue;
|
||||
}
|
||||
collected++;
|
||||
}
|
||||
result.put("status", "success");
|
||||
result.put("recordCount", collected);
|
||||
result.put("message", "运营数据采集完成,共采集 " + collected + " 条记录");
|
||||
log.info("运营数据采集完成: startDate={}, endDate={}, count={}", startDate, endDate, collected);
|
||||
} catch (Exception e) {
|
||||
log.error("运营数据采集失败", e);
|
||||
result.put("status", "error");
|
||||
result.put("message", "采集失败: " + e.getMessage());
|
||||
return R.fail("采集失败: " + e.getMessage());
|
||||
}
|
||||
return R.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.healthlink.his.web.datacollection.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.quality.domain.BusinessAnalytics;
|
||||
import com.healthlink.his.quality.service.IBusinessAnalyticsService;
|
||||
import com.healthlink.his.crossmodule.domain.DrgPerformance;
|
||||
import com.healthlink.his.crossmodule.service.IDrgPerformanceService;
|
||||
import com.healthlink.his.web.datacollection.appservice.IDataDashboardAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class DataDashboardAppServiceImpl implements IDataDashboardAppService {
|
||||
|
||||
private final IBusinessAnalyticsService analyticsService;
|
||||
private final IDrgPerformanceService drgPerformanceService;
|
||||
|
||||
@Override
|
||||
public R<?> getRealtimeData() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
int totalPatients = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (ba.getRevenue() != null) totalRevenue = totalRevenue.add(ba.getRevenue());
|
||||
if (ba.getCost() != null) totalCost = totalCost.add(ba.getCost());
|
||||
if (ba.getPatientCount() != null) totalPatients += ba.getPatientCount();
|
||||
}
|
||||
|
||||
data.put("totalRevenue", totalRevenue);
|
||||
data.put("totalCost", totalCost);
|
||||
data.put("totalProfit", totalRevenue.subtract(totalCost));
|
||||
data.put("totalPatients", totalPatients);
|
||||
data.put("totalRecords", allData.size());
|
||||
data.put("timestamp", new Date().toString());
|
||||
|
||||
LambdaQueryWrapper<DrgPerformance> perfW = new LambdaQueryWrapper<>();
|
||||
perfW.orderByDesc(DrgPerformance::getStatMonth).last("LIMIT 1");
|
||||
List<DrgPerformance> latestPerf = drgPerformanceService.list(perfW);
|
||||
if (!latestPerf.isEmpty()) {
|
||||
DrgPerformance p = latestPerf.get(0);
|
||||
data.put("latestCmiValue", p.getCmiValue());
|
||||
data.put("latestCostControlRate", p.getCostControlRate());
|
||||
data.put("latestDrgCases", p.getTotalCases());
|
||||
}
|
||||
|
||||
Map<String, BigDecimal> deptRevenue = allData.stream()
|
||||
.filter(ba -> ba.getDepartmentName() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
BusinessAnalytics::getDepartmentName,
|
||||
Collectors.reducing(BigDecimal.ZERO, ba -> ba.getRevenue() != null ? ba.getRevenue() : BigDecimal.ZERO, BigDecimal::add)));
|
||||
List<Map<String, Object>> deptChart = new ArrayList<>();
|
||||
deptRevenue.forEach((k, v) -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("department", k);
|
||||
item.put("revenue", v);
|
||||
deptChart.add(item);
|
||||
});
|
||||
data.put("departmentChart", deptChart);
|
||||
|
||||
log.info("实时数据查询完成: records={}", allData.size());
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getHistoricalData(String period) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("period", period);
|
||||
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
String cutoffDate = null;
|
||||
if ("week".equals(period)) {
|
||||
cutoffDate = java.time.LocalDate.now().minusWeeks(1).toString();
|
||||
} else if ("month".equals(period)) {
|
||||
cutoffDate = java.time.LocalDate.now().minusMonths(1).toString();
|
||||
} else if ("quarter".equals(period)) {
|
||||
cutoffDate = java.time.LocalDate.now().minusMonths(3).toString();
|
||||
} else if ("year".equals(period)) {
|
||||
cutoffDate = java.time.LocalDate.now().minusYears(1).toString();
|
||||
}
|
||||
|
||||
final String finalCutoff = cutoffDate;
|
||||
if (finalCutoff != null) {
|
||||
allData = allData.stream()
|
||||
.filter(ba -> ba.getStatDate() != null && ba.getStatDate().compareTo(finalCutoff) >= 0)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
int totalPatients = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (ba.getRevenue() != null) totalRevenue = totalRevenue.add(ba.getRevenue());
|
||||
if (ba.getCost() != null) totalCost = totalCost.add(ba.getCost());
|
||||
if (ba.getPatientCount() != null) totalPatients += ba.getPatientCount();
|
||||
}
|
||||
|
||||
data.put("totalRevenue", totalRevenue);
|
||||
data.put("totalCost", totalCost);
|
||||
data.put("totalProfit", totalRevenue.subtract(totalCost));
|
||||
data.put("totalPatients", totalPatients);
|
||||
data.put("totalRecords", allData.size());
|
||||
data.put("cutoffDate", cutoffDate);
|
||||
data.put("timestamp", new Date().toString());
|
||||
|
||||
Map<String, BigDecimal> monthlyRevenue = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> monthlyCost = new LinkedHashMap<>();
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
String month = ba.getStatDate() != null && ba.getStatDate().length() >= 7
|
||||
? ba.getStatDate().substring(0, 7) : "未知";
|
||||
monthlyRevenue.merge(month, ba.getRevenue() != null ? ba.getRevenue() : BigDecimal.ZERO, BigDecimal::add);
|
||||
monthlyCost.merge(month, ba.getCost() != null ? ba.getCost() : BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
List<Map<String, Object>> trendChart = new ArrayList<>();
|
||||
monthlyRevenue.forEach((k, v) -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("month", k);
|
||||
item.put("revenue", v);
|
||||
item.put("cost", monthlyCost.getOrDefault(k, BigDecimal.ZERO));
|
||||
trendChart.add(item);
|
||||
});
|
||||
data.put("trendChart", trendChart);
|
||||
|
||||
log.info("历史数据查询完成: period={}, records={}", period, allData.size());
|
||||
return R.ok(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.healthlink.his.web.datacollection.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.datacollection.appservice.IDataCollectionAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/data/collect")
|
||||
@Slf4j
|
||||
public class DataCollectionController {
|
||||
|
||||
private final IDataCollectionAppService dataCollectionAppService;
|
||||
|
||||
@PostMapping("/clinical")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:edit')")
|
||||
public R<?> collectClinicalData(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) Long departmentId) {
|
||||
return dataCollectionAppService.collectClinicalData(startDate, endDate, departmentId);
|
||||
}
|
||||
|
||||
@PostMapping("/operational")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:edit')")
|
||||
public R<?> collectOperationalData(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return dataCollectionAppService.collectOperationalData(startDate, endDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.healthlink.his.web.datacollection.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.datacollection.appservice.IDataDashboardAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/data/dashboard")
|
||||
@Slf4j
|
||||
public class DataDashboardController {
|
||||
|
||||
private final IDataDashboardAppService dataDashboardAppService;
|
||||
|
||||
@GetMapping("/realtime")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:list')")
|
||||
public R<?> getRealtimeData() {
|
||||
return dataDashboardAppService.getRealtimeData();
|
||||
}
|
||||
|
||||
@GetMapping("/historical")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:list')")
|
||||
public R<?> getHistoricalData(
|
||||
@RequestParam(required = false, defaultValue = "month") String period) {
|
||||
return dataDashboardAppService.getHistoricalData(period);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.healthlink.his.web.datacollection.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DataCollectionDto {
|
||||
private String collectionType;
|
||||
private String startDate;
|
||||
private String endDate;
|
||||
private Long departmentId;
|
||||
private Integer recordCount;
|
||||
private String status;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice;
|
||||
|
||||
import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface IKgDataImportAppService {
|
||||
|
||||
ImportResultDto importDiseaseFromCsv(MultipartFile file);
|
||||
|
||||
ImportResultDto importDrugFromCsv(MultipartFile file);
|
||||
|
||||
ImportResultDto importRelationsFromCsv(MultipartFile file);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
|
||||
public interface IKgEntityAppService {
|
||||
|
||||
Boolean addDisease(KgDiseaseDto dto);
|
||||
|
||||
Boolean updateDisease(KgDiseaseDto dto);
|
||||
|
||||
Boolean deleteDisease(Long id);
|
||||
|
||||
KgDiseaseDto getDiseaseById(Long id);
|
||||
|
||||
IPage<KgDiseaseDto> pageDisease(String keyword, String category, Integer pageNo, Integer pageSize);
|
||||
|
||||
Boolean addSymptom(KgSymptomDto dto);
|
||||
|
||||
Boolean updateSymptom(KgSymptomDto dto);
|
||||
|
||||
Boolean deleteSymptom(Long id);
|
||||
|
||||
KgSymptomDto getSymptomById(Long id);
|
||||
|
||||
IPage<KgSymptomDto> pageSymptom(String keyword, String symptomType, Integer pageNo, Integer pageSize);
|
||||
|
||||
Boolean addDrug(KgDrugDto dto);
|
||||
|
||||
Boolean updateDrug(KgDrugDto dto);
|
||||
|
||||
Boolean deleteDrug(Long id);
|
||||
|
||||
KgDrugDto getDrugById(Long id);
|
||||
|
||||
IPage<KgDrugDto> pageDrug(String keyword, String category, Integer pageNo, Integer pageSize);
|
||||
|
||||
Boolean addExamination(KgExaminationDto dto);
|
||||
|
||||
Boolean updateExamination(KgExaminationDto dto);
|
||||
|
||||
Boolean deleteExamination(Long id);
|
||||
|
||||
KgExaminationDto getExaminationById(Long id);
|
||||
|
||||
IPage<KgExaminationDto> pageExamination(String keyword, String examType, Integer pageNo, Integer pageSize);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice;
|
||||
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IKgReasoningAppService {
|
||||
|
||||
List<DiagnosisResultDto> suggestDiagnosis(List<String> symptoms, Integer topN);
|
||||
|
||||
List<ExaminationResultDto> suggestExaminations(String diseaseCode, Integer topN);
|
||||
|
||||
List<DrugInteractionResultDto> checkDrugInteractions(List<String> drugCodes);
|
||||
|
||||
Map<String, Object> suggestPathway(String diseaseCode);
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.clinical.domain.KgEntityRelation;
|
||||
import com.healthlink.his.clinical.service.IKgEntityRelationService;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgDisease;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgDrug;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgDiseaseService;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgDrugService;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgDataImportAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class KgDataImportAppServiceImpl implements IKgDataImportAppService {
|
||||
|
||||
@Autowired
|
||||
private IKgDiseaseService diseaseService;
|
||||
@Autowired
|
||||
private IKgDrugService drugService;
|
||||
@Autowired
|
||||
private IKgEntityRelationService relationService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ImportResultDto importDiseaseFromCsv(MultipartFile file) {
|
||||
ImportResultDto result = new ImportResultDto();
|
||||
List<KgDisease> batch = new ArrayList<>();
|
||||
int totalRows = 0;
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String header = reader.readLine();
|
||||
if (header == null) {
|
||||
result.setMessage("CSV文件为空");
|
||||
result.setTotalRows(0);
|
||||
result.setSuccessCount(0);
|
||||
result.setFailCount(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
totalRows++;
|
||||
try {
|
||||
String[] parts = parseCsvLine(line);
|
||||
if (parts.length < 2) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
KgDisease disease = new KgDisease();
|
||||
disease.setDiseaseCode(getField(parts, 0));
|
||||
disease.setDiseaseName(getField(parts, 1));
|
||||
disease.setCategory(getField(parts, 2));
|
||||
disease.setDepartment(getField(parts, 3));
|
||||
disease.setSeverityLevel(getField(parts, 4));
|
||||
disease.setDescription(getField(parts, 5));
|
||||
disease.setKeywords(getField(parts, 6));
|
||||
|
||||
if (hasText(disease.getDiseaseCode()) && hasText(disease.getDiseaseName())) {
|
||||
batch.add(disease);
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析疾病CSV行失败: {}", line, e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.isEmpty()) {
|
||||
diseaseService.saveBatch(batch);
|
||||
successCount = batch.size();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("导入疾病CSV失败", e);
|
||||
failCount = totalRows - successCount;
|
||||
}
|
||||
|
||||
result.setTotalRows(totalRows);
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ImportResultDto importDrugFromCsv(MultipartFile file) {
|
||||
ImportResultDto result = new ImportResultDto();
|
||||
List<KgDrug> batch = new ArrayList<>();
|
||||
int totalRows = 0;
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String header = reader.readLine();
|
||||
if (header == null) {
|
||||
result.setMessage("CSV文件为空");
|
||||
result.setTotalRows(0);
|
||||
result.setSuccessCount(0);
|
||||
result.setFailCount(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
totalRows++;
|
||||
try {
|
||||
String[] parts = parseCsvLine(line);
|
||||
if (parts.length < 2) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
KgDrug drug = new KgDrug();
|
||||
drug.setDrugCode(getField(parts, 0));
|
||||
drug.setDrugName(getField(parts, 1));
|
||||
drug.setGenericName(getField(parts, 2));
|
||||
drug.setCategory(getField(parts, 3));
|
||||
drug.setDosageForm(getField(parts, 4));
|
||||
drug.setContraindications(getField(parts, 5));
|
||||
drug.setSideEffects(getField(parts, 6));
|
||||
|
||||
if (hasText(drug.getDrugCode()) && hasText(drug.getDrugName())) {
|
||||
batch.add(drug);
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析药物CSV行失败: {}", line, e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.isEmpty()) {
|
||||
drugService.saveBatch(batch);
|
||||
successCount = batch.size();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("导入药物CSV失败", e);
|
||||
failCount = totalRows - successCount;
|
||||
}
|
||||
|
||||
result.setTotalRows(totalRows);
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ImportResultDto importRelationsFromCsv(MultipartFile file) {
|
||||
ImportResultDto result = new ImportResultDto();
|
||||
List<KgEntityRelation> batch = new ArrayList<>();
|
||||
int totalRows = 0;
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String header = reader.readLine();
|
||||
if (header == null) {
|
||||
result.setMessage("CSV文件为空");
|
||||
result.setTotalRows(0);
|
||||
result.setSuccessCount(0);
|
||||
result.setFailCount(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
totalRows++;
|
||||
try {
|
||||
String[] parts = parseCsvLine(line);
|
||||
if (parts.length < 5) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
KgEntityRelation relation = new KgEntityRelation();
|
||||
relation.setSourceType(getField(parts, 0));
|
||||
relation.setSourceId(getField(parts, 1));
|
||||
relation.setTargetType(getField(parts, 2));
|
||||
relation.setTargetId(getField(parts, 3));
|
||||
relation.setRelationType(getField(parts, 4));
|
||||
|
||||
String strengthStr = getField(parts, 5);
|
||||
if (hasText(strengthStr)) {
|
||||
try {
|
||||
relation.setRelationStrength(new BigDecimal(strengthStr));
|
||||
} catch (NumberFormatException e) {
|
||||
relation.setRelationStrength(BigDecimal.ONE);
|
||||
}
|
||||
} else {
|
||||
relation.setRelationStrength(BigDecimal.ONE);
|
||||
}
|
||||
relation.setDescription(getField(parts, 6));
|
||||
relation.setEvidenceSource(getField(parts, 7));
|
||||
|
||||
if (hasText(relation.getSourceType()) && hasText(relation.getSourceId())
|
||||
&& hasText(relation.getTargetType()) && hasText(relation.getTargetId())) {
|
||||
batch.add(relation);
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析关系CSV行失败: {}", line, e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.isEmpty()) {
|
||||
relationService.saveBatch(batch);
|
||||
successCount = batch.size();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("导入关系CSV失败", e);
|
||||
failCount = totalRows - successCount;
|
||||
}
|
||||
|
||||
result.setTotalRows(totalRows);
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setMessage(String.format("导入完成: 共%d行, 成功%d条, 失败%d条", totalRows, successCount, failCount));
|
||||
return result;
|
||||
}
|
||||
|
||||
private String[] parseCsvLine(String line) {
|
||||
List<String> fields = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inQuotes = false;
|
||||
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (c == ',' && !inQuotes) {
|
||||
fields.add(current.toString().trim());
|
||||
current = new StringBuilder();
|
||||
} else {
|
||||
current.append(c);
|
||||
}
|
||||
}
|
||||
fields.add(current.toString().trim());
|
||||
return fields.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private String getField(String[] parts, int index) {
|
||||
if (index < parts.length) {
|
||||
String val = parts[index];
|
||||
return hasText(val) ? val : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasText(String s) {
|
||||
return s != null && !s.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.healthlink.his.knowledgegraph.domain.*;
|
||||
import com.healthlink.his.knowledgegraph.service.*;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgEntityAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Service
|
||||
public class KgEntityAppServiceImpl implements IKgEntityAppService {
|
||||
|
||||
private final IKgDiseaseService kgDiseaseService;
|
||||
private final IKgSymptomService kgSymptomService;
|
||||
private final IKgDrugService kgDrugService;
|
||||
private final IKgExaminationService kgExaminationService;
|
||||
|
||||
public KgEntityAppServiceImpl(IKgDiseaseService kgDiseaseService,
|
||||
IKgSymptomService kgSymptomService,
|
||||
IKgDrugService kgDrugService,
|
||||
IKgExaminationService kgExaminationService) {
|
||||
this.kgDiseaseService = kgDiseaseService;
|
||||
this.kgSymptomService = kgSymptomService;
|
||||
this.kgDrugService = kgDrugService;
|
||||
this.kgExaminationService = kgExaminationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean addDisease(KgDiseaseDto dto) {
|
||||
KgDisease entity = new KgDisease();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgDiseaseService.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateDisease(KgDiseaseDto dto) {
|
||||
if (dto.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
KgDisease entity = new KgDisease();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgDiseaseService.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteDisease(Long id) {
|
||||
return kgDiseaseService.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KgDiseaseDto getDiseaseById(Long id) {
|
||||
KgDisease entity = kgDiseaseService.getById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
KgDiseaseDto dto = new KgDiseaseDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgDiseaseDto> pageDisease(String keyword, String category, Integer pageNo, Integer pageSize) {
|
||||
Page<KgDisease> page = new Page<>(pageNo, pageSize);
|
||||
LambdaQueryWrapper<KgDisease> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w.like(KgDisease::getDiseaseName, keyword)
|
||||
.or().like(KgDisease::getDiseaseCode, keyword));
|
||||
}
|
||||
if (StringUtils.hasText(category)) {
|
||||
wrapper.eq(KgDisease::getCategory, category);
|
||||
}
|
||||
wrapper.orderByDesc(KgDisease::getCreateTime);
|
||||
Page<KgDisease> result = kgDiseaseService.page(page, wrapper);
|
||||
return result.convert(entity -> {
|
||||
KgDiseaseDto dto = new KgDiseaseDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean addSymptom(KgSymptomDto dto) {
|
||||
KgSymptom entity = new KgSymptom();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgSymptomService.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateSymptom(KgSymptomDto dto) {
|
||||
if (dto.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
KgSymptom entity = new KgSymptom();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgSymptomService.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteSymptom(Long id) {
|
||||
return kgSymptomService.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KgSymptomDto getSymptomById(Long id) {
|
||||
KgSymptom entity = kgSymptomService.getById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
KgSymptomDto dto = new KgSymptomDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgSymptomDto> pageSymptom(String keyword, String symptomType, Integer pageNo, Integer pageSize) {
|
||||
Page<KgSymptom> page = new Page<>(pageNo, pageSize);
|
||||
LambdaQueryWrapper<KgSymptom> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w.like(KgSymptom::getSymptomName, keyword)
|
||||
.or().like(KgSymptom::getSymptomCode, keyword));
|
||||
}
|
||||
if (StringUtils.hasText(symptomType)) {
|
||||
wrapper.eq(KgSymptom::getSymptomType, symptomType);
|
||||
}
|
||||
wrapper.orderByDesc(KgSymptom::getCreateTime);
|
||||
Page<KgSymptom> result = kgSymptomService.page(page, wrapper);
|
||||
return result.convert(entity -> {
|
||||
KgSymptomDto dto = new KgSymptomDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean addDrug(KgDrugDto dto) {
|
||||
KgDrug entity = new KgDrug();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgDrugService.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateDrug(KgDrugDto dto) {
|
||||
if (dto.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
KgDrug entity = new KgDrug();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgDrugService.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteDrug(Long id) {
|
||||
return kgDrugService.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KgDrugDto getDrugById(Long id) {
|
||||
KgDrug entity = kgDrugService.getById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
KgDrugDto dto = new KgDrugDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgDrugDto> pageDrug(String keyword, String category, Integer pageNo, Integer pageSize) {
|
||||
Page<KgDrug> page = new Page<>(pageNo, pageSize);
|
||||
LambdaQueryWrapper<KgDrug> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w.like(KgDrug::getDrugName, keyword)
|
||||
.or().like(KgDrug::getDrugCode, keyword));
|
||||
}
|
||||
if (StringUtils.hasText(category)) {
|
||||
wrapper.eq(KgDrug::getCategory, category);
|
||||
}
|
||||
wrapper.orderByDesc(KgDrug::getCreateTime);
|
||||
Page<KgDrug> result = kgDrugService.page(page, wrapper);
|
||||
return result.convert(entity -> {
|
||||
KgDrugDto dto = new KgDrugDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean addExamination(KgExaminationDto dto) {
|
||||
KgExamination entity = new KgExamination();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgExaminationService.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateExamination(KgExaminationDto dto) {
|
||||
if (dto.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
KgExamination entity = new KgExamination();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
return kgExaminationService.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteExamination(Long id) {
|
||||
return kgExaminationService.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KgExaminationDto getExaminationById(Long id) {
|
||||
KgExamination entity = kgExaminationService.getById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
KgExaminationDto dto = new KgExaminationDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<KgExaminationDto> pageExamination(String keyword, String examType, Integer pageNo, Integer pageSize) {
|
||||
Page<KgExamination> page = new Page<>(pageNo, pageSize);
|
||||
LambdaQueryWrapper<KgExamination> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w.like(KgExamination::getExamName, keyword)
|
||||
.or().like(KgExamination::getExamCode, keyword));
|
||||
}
|
||||
if (StringUtils.hasText(examType)) {
|
||||
wrapper.eq(KgExamination::getExamType, examType);
|
||||
}
|
||||
wrapper.orderByDesc(KgExamination::getCreateTime);
|
||||
Page<KgExamination> result = kgExaminationService.page(page, wrapper);
|
||||
return result.convert(entity -> {
|
||||
KgExaminationDto dto = new KgExaminationDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.healthlink.his.web.knowledgegraph.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.clinical.domain.KgClinicalPathway;
|
||||
import com.healthlink.his.clinical.domain.KgEntityRelation;
|
||||
import com.healthlink.his.clinical.domain.KgPathwayStep;
|
||||
import com.healthlink.his.clinical.service.IKgClinicalPathwayService;
|
||||
import com.healthlink.his.clinical.service.IKgEntityRelationService;
|
||||
import com.healthlink.his.clinical.service.IKgPathwayStepService;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgDisease;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgDrug;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgExamination;
|
||||
import com.healthlink.his.knowledgegraph.domain.KgSymptom;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgDiseaseService;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgDrugService;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgExaminationService;
|
||||
import com.healthlink.his.knowledgegraph.service.IKgSymptomService;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgReasoningAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class KgReasoningAppServiceImpl implements IKgReasoningAppService {
|
||||
|
||||
@Autowired
|
||||
private IKgEntityRelationService relationService;
|
||||
@Autowired
|
||||
private IKgSymptomService symptomService;
|
||||
@Autowired
|
||||
private IKgDiseaseService diseaseService;
|
||||
@Autowired
|
||||
private IKgExaminationService examinationService;
|
||||
@Autowired
|
||||
private IKgDrugService drugService;
|
||||
@Autowired
|
||||
private IKgClinicalPathwayService pathwayService;
|
||||
@Autowired
|
||||
private IKgPathwayStepService pathwayStepService;
|
||||
|
||||
@Override
|
||||
public List<DiagnosisResultDto> suggestDiagnosis(List<String> symptoms, Integer topN) {
|
||||
if (symptoms == null || symptoms.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (topN == null || topN <= 0) {
|
||||
topN = 5;
|
||||
}
|
||||
|
||||
// 1. Find symptom IDs by name/code
|
||||
LambdaQueryWrapper<KgSymptom> sw = new LambdaQueryWrapper<>();
|
||||
sw.and(w -> {
|
||||
for (String symptom : symptoms) {
|
||||
w.or().like(KgSymptom::getSymptomName, symptom)
|
||||
.or().like(KgSymptom::getSymptomCode, symptom);
|
||||
}
|
||||
});
|
||||
List<KgSymptom> matchedSymptoms = symptomService.list(sw);
|
||||
if (matchedSymptoms.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> symptomIds = matchedSymptoms.stream()
|
||||
.map(s -> String.valueOf(s.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 2. Query relations: symptom -> disease
|
||||
LambdaQueryWrapper<KgEntityRelation> rw = new LambdaQueryWrapper<>();
|
||||
rw.eq(KgEntityRelation::getSourceType, "symptom")
|
||||
.eq(KgEntityRelation::getTargetType, "disease")
|
||||
.in(KgEntityRelation::getSourceId, symptomIds)
|
||||
.orderByDesc(KgEntityRelation::getRelationStrength);
|
||||
List<KgEntityRelation> relations = relationService.list(rw);
|
||||
|
||||
// 3. Group by disease, sum scores
|
||||
Map<String, BigDecimal> diseaseScoreMap = new LinkedHashMap<>();
|
||||
Map<String, List<String>> diseaseSymptomMap = new LinkedHashMap<>();
|
||||
for (KgEntityRelation rel : relations) {
|
||||
String diseaseId = rel.getTargetId();
|
||||
BigDecimal strength = rel.getRelationStrength() != null ? rel.getRelationStrength() : BigDecimal.ONE;
|
||||
diseaseScoreMap.merge(diseaseId, strength, BigDecimal::add);
|
||||
diseaseSymptomMap.computeIfAbsent(diseaseId, k -> new ArrayList<>())
|
||||
.add(rel.getSourceId());
|
||||
}
|
||||
|
||||
// 4. Sort by score descending, take top N
|
||||
List<Map.Entry<String, BigDecimal>> sorted = diseaseScoreMap.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
|
||||
.limit(topN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 5. Build results with disease info
|
||||
List<DiagnosisResultDto> results = new ArrayList<>();
|
||||
for (Map.Entry<String, BigDecimal> entry : sorted) {
|
||||
KgDisease disease = diseaseService.getById(Long.parseLong(entry.getKey()));
|
||||
if (disease == null) continue;
|
||||
|
||||
DiagnosisResultDto dto = new DiagnosisResultDto();
|
||||
dto.setDiseaseCode(disease.getDiseaseCode());
|
||||
dto.setDiseaseName(disease.getDiseaseName());
|
||||
dto.setCategory(disease.getCategory());
|
||||
dto.setDepartment(disease.getDepartment());
|
||||
dto.setScore(entry.getValue());
|
||||
|
||||
List<String> matched = diseaseSymptomMap.getOrDefault(entry.getKey(), Collections.emptyList());
|
||||
dto.setMatchedSymptoms(String.join(",", matched));
|
||||
results.add(dto);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ExaminationResultDto> suggestExaminations(String diseaseCode, Integer topN) {
|
||||
if (!StringUtils.hasText(diseaseCode)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (topN == null || topN <= 0) {
|
||||
topN = 10;
|
||||
}
|
||||
|
||||
// 1. Find disease by code
|
||||
LambdaQueryWrapper<KgDisease> dw = new LambdaQueryWrapper<>();
|
||||
dw.eq(KgDisease::getDiseaseCode, diseaseCode);
|
||||
KgDisease disease = diseaseService.getOne(dw);
|
||||
if (disease == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. Query relations: disease -> examination
|
||||
LambdaQueryWrapper<KgEntityRelation> rw = new LambdaQueryWrapper<>();
|
||||
rw.eq(KgEntityRelation::getSourceType, "disease")
|
||||
.eq(KgEntityRelation::getTargetType, "examination")
|
||||
.eq(KgEntityRelation::getSourceId, String.valueOf(disease.getId()))
|
||||
.orderByDesc(KgEntityRelation::getRelationStrength);
|
||||
List<KgEntityRelation> relations = relationService.list(rw);
|
||||
|
||||
// 3. Build results
|
||||
List<ExaminationResultDto> results = new ArrayList<>();
|
||||
for (KgEntityRelation rel : relations) {
|
||||
if (results.size() >= topN) break;
|
||||
KgExamination exam = examinationService.getById(Long.parseLong(rel.getTargetId()));
|
||||
if (exam == null) continue;
|
||||
|
||||
ExaminationResultDto dto = new ExaminationResultDto();
|
||||
dto.setExamCode(exam.getExamCode());
|
||||
dto.setExamName(exam.getExamName());
|
||||
dto.setExamType(exam.getExamType());
|
||||
dto.setClinicalSignificance(exam.getClinicalSignificance());
|
||||
dto.setScore(rel.getRelationStrength());
|
||||
results.add(dto);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DrugInteractionResultDto> checkDrugInteractions(List<String> drugCodes) {
|
||||
if (drugCodes == null || drugCodes.size() < 2) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 1. Find drug IDs by code
|
||||
LambdaQueryWrapper<KgDrug> dw = new LambdaQueryWrapper<>();
|
||||
dw.in(KgDrug::getDrugCode, drugCodes);
|
||||
List<KgDrug> drugs = drugService.list(dw);
|
||||
if (drugs.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<String, KgDrug> drugMap = drugs.stream()
|
||||
.collect(Collectors.toMap(KgDrug::getDrugCode, d -> d, (a, b) -> a));
|
||||
|
||||
List<String> drugIds = drugs.stream()
|
||||
.map(d -> String.valueOf(d.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 2. Query drug-drug interaction relations
|
||||
LambdaQueryWrapper<KgEntityRelation> rw = new LambdaQueryWrapper<>();
|
||||
rw.eq(KgEntityRelation::getSourceType, "drug")
|
||||
.eq(KgEntityRelation::getTargetType, "drug")
|
||||
.in(KgEntityRelation::getSourceId, drugIds)
|
||||
.in(KgEntityRelation::getTargetId, drugIds);
|
||||
List<KgEntityRelation> relations = relationService.list(rw);
|
||||
|
||||
// 3. Build results
|
||||
List<DrugInteractionResultDto> results = new ArrayList<>();
|
||||
Set<String> added = new HashSet<>();
|
||||
|
||||
for (KgEntityRelation rel : relations) {
|
||||
KgDrug drugA = drugService.getById(Long.parseLong(rel.getSourceId()));
|
||||
KgDrug drugB = drugService.getById(Long.parseLong(rel.getTargetId()));
|
||||
if (drugA == null || drugB == null) continue;
|
||||
|
||||
String key = Collections.min(Arrays.asList(drugA.getDrugCode(), drugB.getDrugCode()))
|
||||
+ "-" + Collections.max(Arrays.asList(drugA.getDrugCode(), drugB.getDrugCode()));
|
||||
if (!added.add(key)) continue;
|
||||
|
||||
DrugInteractionResultDto dto = new DrugInteractionResultDto();
|
||||
dto.setDrugCodeA(drugA.getDrugCode());
|
||||
dto.setDrugNameA(drugA.getDrugName());
|
||||
dto.setDrugCodeB(drugB.getDrugCode());
|
||||
dto.setDrugNameB(drugB.getDrugName());
|
||||
dto.setInteractionType(rel.getRelationType());
|
||||
dto.setDescription(rel.getDescription());
|
||||
dto.setSeverity(rel.getRelationStrength() != null
|
||||
? (rel.getRelationStrength().compareTo(new BigDecimal("0.7")) >= 0 ? "严重" : "一般")
|
||||
: "一般");
|
||||
results.add(dto);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> suggestPathway(String diseaseCode) {
|
||||
if (!StringUtils.hasText(diseaseCode)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 1. Find pathway by disease code
|
||||
LambdaQueryWrapper<KgClinicalPathway> pw = new LambdaQueryWrapper<>();
|
||||
pw.eq(KgClinicalPathway::getDiseaseCode, diseaseCode)
|
||||
.eq(KgClinicalPathway::getStatus, "ACTIVE");
|
||||
KgClinicalPathway pathway = pathwayService.getOne(pw);
|
||||
if (pathway == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 2. Get pathway steps
|
||||
LambdaQueryWrapper<KgPathwayStep> sw = new LambdaQueryWrapper<>();
|
||||
sw.eq(KgPathwayStep::getPathwayId, pathway.getId())
|
||||
.orderByAsc(KgPathwayStep::getStepOrder);
|
||||
List<KgPathwayStep> steps = pathwayStepService.list(sw);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("pathway", pathway);
|
||||
result.put("steps", steps);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.healthlink.his.web.knowledgegraph.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgDataImportAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.ImportResultDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "知识图谱-数据导入")
|
||||
@RestController
|
||||
@RequestMapping("/knowledgegraph/import")
|
||||
@AllArgsConstructor
|
||||
public class KgDataImportController {
|
||||
|
||||
private final IKgDataImportAppService kgDataImportAppService;
|
||||
|
||||
@Operation(summary = "导入疾病数据")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/disease")
|
||||
public R<ImportResultDto> importDisease(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
ImportResultDto result = kgDataImportAppService.importDiseaseFromCsv(file);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("导入疾病数据失败", e);
|
||||
return R.fail("导入疾病数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "导入药物数据")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/drug")
|
||||
public R<ImportResultDto> importDrug(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
ImportResultDto result = kgDataImportAppService.importDrugFromCsv(file);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("导入药物数据失败", e);
|
||||
return R.fail("导入药物数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "导入关系数据")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/relation")
|
||||
public R<ImportResultDto> importRelations(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
ImportResultDto result = kgDataImportAppService.importRelationsFromCsv(file);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("导入关系数据失败", e);
|
||||
return R.fail("导入关系数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "下载导入模板")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/template/{type}")
|
||||
public void downloadTemplate(@PathVariable String type, jakarta.servlet.http.HttpServletResponse response) throws Exception {
|
||||
String filename;
|
||||
String content;
|
||||
|
||||
switch (type) {
|
||||
case "disease":
|
||||
filename = "疾病导入模板.csv";
|
||||
content = "疾病编码,疾病名称,分类,科室,严重等级,描述,关键词\n"
|
||||
+ "J06.900,急性上呼吸道感染,感染性疾病,呼吸内科,轻度,急性上呼吸道感染,发热;咳嗽;咽痛\n";
|
||||
break;
|
||||
case "drug":
|
||||
filename = "药物导入模板.csv";
|
||||
content = "药物编码,药物名称,通用名,分类,剂型,禁忌症,不良反应\n"
|
||||
+ "D00001,阿莫西林胶囊,阿莫西林,抗生素,胶囊剂,青霉素过敏者禁用,皮疹;腹泻\n";
|
||||
break;
|
||||
case "relation":
|
||||
filename = "关系导入模板.csv";
|
||||
content = "来源类型,来源ID,目标类型,目标ID,关系类型,关系强度,描述,证据来源\n"
|
||||
+ "symptom,1001,disease,2001,has_symptom,0.85,发热是急性上呼吸道感染的常见症状,临床指南\n";
|
||||
break;
|
||||
default:
|
||||
response.setStatus(400);
|
||||
response.getWriter().write("不支持的模板类型");
|
||||
return;
|
||||
}
|
||||
|
||||
response.setContentType("text/csv;charset=UTF-8");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
|
||||
response.getOutputStream().write("\uFEFF".getBytes(StandardCharsets.UTF_8));
|
||||
response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.healthlink.his.web.knowledgegraph.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgEntityAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "知识图谱-实体管理")
|
||||
@RestController
|
||||
@RequestMapping("/knowledgegraph")
|
||||
public class KgEntityController {
|
||||
|
||||
private final IKgEntityAppService kgEntityAppService;
|
||||
|
||||
public KgEntityController(IKgEntityAppService kgEntityAppService) {
|
||||
this.kgEntityAppService = kgEntityAppService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建疾病")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/disease")
|
||||
public R<String> addDisease(@RequestBody KgDiseaseDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.addDisease(dto);
|
||||
return result ? R.ok("创建成功") : R.fail("创建失败");
|
||||
} catch (Exception e) {
|
||||
log.error("创建疾病失败", e);
|
||||
return R.fail("创建疾病失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "疾病分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/disease/page")
|
||||
public R<IPage<KgDiseaseDto>> pageDisease(
|
||||
@Parameter(description = "关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "分类") @RequestParam(required = false) String category,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
try {
|
||||
IPage<KgDiseaseDto> page = kgEntityAppService.pageDisease(keyword, category, pageNo, pageSize);
|
||||
return R.ok(page);
|
||||
} catch (Exception e) {
|
||||
log.error("查询疾病列表失败", e);
|
||||
return R.fail("查询疾病列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新疾病")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PutMapping("/disease")
|
||||
public R<String> updateDisease(@RequestBody KgDiseaseDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.updateDisease(dto);
|
||||
return result ? R.ok("更新成功") : R.fail("更新失败");
|
||||
} catch (Exception e) {
|
||||
log.error("更新疾病失败", e);
|
||||
return R.fail("更新疾病失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除疾病")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@DeleteMapping("/disease/{id}")
|
||||
public R<String> deleteDisease(@PathVariable Long id) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.deleteDisease(id);
|
||||
return result ? R.ok("删除成功") : R.fail("删除失败");
|
||||
} catch (Exception e) {
|
||||
log.error("删除疾病失败", e);
|
||||
return R.fail("删除疾病失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "疾病详情")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/disease/{id}")
|
||||
public R<KgDiseaseDto> getDiseaseById(@PathVariable Long id) {
|
||||
try {
|
||||
KgDiseaseDto dto = kgEntityAppService.getDiseaseById(id);
|
||||
return dto != null ? R.ok(dto) : R.fail("未找到疾病信息");
|
||||
} catch (Exception e) {
|
||||
log.error("获取疾病详情失败", e);
|
||||
return R.fail("获取疾病详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "创建症状")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/symptom")
|
||||
public R<String> addSymptom(@RequestBody KgSymptomDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.addSymptom(dto);
|
||||
return result ? R.ok("创建成功") : R.fail("创建失败");
|
||||
} catch (Exception e) {
|
||||
log.error("创建症状失败", e);
|
||||
return R.fail("创建症状失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "症状分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/symptom/page")
|
||||
public R<IPage<KgSymptomDto>> pageSymptom(
|
||||
@Parameter(description = "关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "症状类型") @RequestParam(required = false) String symptomType,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
try {
|
||||
IPage<KgSymptomDto> page = kgEntityAppService.pageSymptom(keyword, symptomType, pageNo, pageSize);
|
||||
return R.ok(page);
|
||||
} catch (Exception e) {
|
||||
log.error("查询症状列表失败", e);
|
||||
return R.fail("查询症状列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新症状")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PutMapping("/symptom")
|
||||
public R<String> updateSymptom(@RequestBody KgSymptomDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.updateSymptom(dto);
|
||||
return result ? R.ok("更新成功") : R.fail("更新失败");
|
||||
} catch (Exception e) {
|
||||
log.error("更新症状失败", e);
|
||||
return R.fail("更新症状失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除症状")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@DeleteMapping("/symptom/{id}")
|
||||
public R<String> deleteSymptom(@PathVariable Long id) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.deleteSymptom(id);
|
||||
return result ? R.ok("删除成功") : R.fail("删除失败");
|
||||
} catch (Exception e) {
|
||||
log.error("删除症状失败", e);
|
||||
return R.fail("删除症状失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "症状详情")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/symptom/{id}")
|
||||
public R<KgSymptomDto> getSymptomById(@PathVariable Long id) {
|
||||
try {
|
||||
KgSymptomDto dto = kgEntityAppService.getSymptomById(id);
|
||||
return dto != null ? R.ok(dto) : R.fail("未找到症状信息");
|
||||
} catch (Exception e) {
|
||||
log.error("获取症状详情失败", e);
|
||||
return R.fail("获取症状详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "创建药物")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/drug")
|
||||
public R<String> addDrug(@RequestBody KgDrugDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.addDrug(dto);
|
||||
return result ? R.ok("创建成功") : R.fail("创建失败");
|
||||
} catch (Exception e) {
|
||||
log.error("创建药物失败", e);
|
||||
return R.fail("创建药物失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "药物分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/drug/page")
|
||||
public R<IPage<KgDrugDto>> pageDrug(
|
||||
@Parameter(description = "关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "分类") @RequestParam(required = false) String category,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
try {
|
||||
IPage<KgDrugDto> page = kgEntityAppService.pageDrug(keyword, category, pageNo, pageSize);
|
||||
return R.ok(page);
|
||||
} catch (Exception e) {
|
||||
log.error("查询药物列表失败", e);
|
||||
return R.fail("查询药物列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新药物")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PutMapping("/drug")
|
||||
public R<String> updateDrug(@RequestBody KgDrugDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.updateDrug(dto);
|
||||
return result ? R.ok("更新成功") : R.fail("更新失败");
|
||||
} catch (Exception e) {
|
||||
log.error("更新药物失败", e);
|
||||
return R.fail("更新药物失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除药物")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@DeleteMapping("/drug/{id}")
|
||||
public R<String> deleteDrug(@PathVariable Long id) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.deleteDrug(id);
|
||||
return result ? R.ok("删除成功") : R.fail("删除失败");
|
||||
} catch (Exception e) {
|
||||
log.error("删除药物失败", e);
|
||||
return R.fail("删除药物失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "药物详情")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/drug/{id}")
|
||||
public R<KgDrugDto> getDrugById(@PathVariable Long id) {
|
||||
try {
|
||||
KgDrugDto dto = kgEntityAppService.getDrugById(id);
|
||||
return dto != null ? R.ok(dto) : R.fail("未找到药物信息");
|
||||
} catch (Exception e) {
|
||||
log.error("获取药物详情失败", e);
|
||||
return R.fail("获取药物详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "创建检查")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PostMapping("/examination")
|
||||
public R<String> addExamination(@RequestBody KgExaminationDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.addExamination(dto);
|
||||
return result ? R.ok("创建成功") : R.fail("创建失败");
|
||||
} catch (Exception e) {
|
||||
log.error("创建检查失败", e);
|
||||
return R.fail("创建检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "检查分页查询")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/examination/page")
|
||||
public R<IPage<KgExaminationDto>> pageExamination(
|
||||
@Parameter(description = "关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "检查类型") @RequestParam(required = false) String examType,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
try {
|
||||
IPage<KgExaminationDto> page = kgEntityAppService.pageExamination(keyword, examType, pageNo, pageSize);
|
||||
return R.ok(page);
|
||||
} catch (Exception e) {
|
||||
log.error("查询检查列表失败", e);
|
||||
return R.fail("查询检查列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新检查")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@PutMapping("/examination")
|
||||
public R<String> updateExamination(@RequestBody KgExaminationDto dto) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.updateExamination(dto);
|
||||
return result ? R.ok("更新成功") : R.fail("更新失败");
|
||||
} catch (Exception e) {
|
||||
log.error("更新检查失败", e);
|
||||
return R.fail("更新检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除检查")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:edit')")
|
||||
@DeleteMapping("/examination/{id}")
|
||||
public R<String> deleteExamination(@PathVariable Long id) {
|
||||
try {
|
||||
Boolean result = kgEntityAppService.deleteExamination(id);
|
||||
return result ? R.ok("删除成功") : R.fail("删除失败");
|
||||
} catch (Exception e) {
|
||||
log.error("删除检查失败", e);
|
||||
return R.fail("删除检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "检查详情")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/examination/{id}")
|
||||
public R<KgExaminationDto> getExaminationById(@PathVariable Long id) {
|
||||
try {
|
||||
KgExaminationDto dto = kgEntityAppService.getExaminationById(id);
|
||||
return dto != null ? R.ok(dto) : R.fail("未找到检查信息");
|
||||
} catch (Exception e) {
|
||||
log.error("获取检查详情失败", e);
|
||||
return R.fail("获取检查详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.healthlink.his.web.knowledgegraph.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.knowledgegraph.appservice.IKgReasoningAppService;
|
||||
import com.healthlink.his.web.knowledgegraph.dto.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "知识图谱-推理引擎")
|
||||
@RestController
|
||||
@RequestMapping("/knowledgegraph/reasoning")
|
||||
@AllArgsConstructor
|
||||
public class KgReasoningController {
|
||||
|
||||
private final IKgReasoningAppService kgReasoningAppService;
|
||||
|
||||
@Operation(summary = "诊断推荐 - 基于症状推荐诊断")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@PostMapping("/diagnosis")
|
||||
public R<List<DiagnosisResultDto>> suggestDiagnosis(@RequestBody DiagnosisSuggestDto dto) {
|
||||
try {
|
||||
List<DiagnosisResultDto> results = kgReasoningAppService.suggestDiagnosis(dto.getSymptoms(), dto.getTopN());
|
||||
return R.ok(results);
|
||||
} catch (Exception e) {
|
||||
log.error("诊断推荐失败", e);
|
||||
return R.fail("诊断推荐失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "检查推荐 - 基于诊断推荐检查")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@PostMapping("/examination")
|
||||
public R<List<ExaminationResultDto>> suggestExaminations(@RequestBody ExaminationSuggestDto dto) {
|
||||
try {
|
||||
List<ExaminationResultDto> results = kgReasoningAppService.suggestExaminations(dto.getDiseaseCode(), dto.getTopN());
|
||||
return R.ok(results);
|
||||
} catch (Exception e) {
|
||||
log.error("检查推荐失败", e);
|
||||
return R.fail("检查推荐失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "药物相互作用检查")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@PostMapping("/drug-interaction")
|
||||
public R<List<DrugInteractionResultDto>> checkDrugInteractions(@RequestBody DrugInteractionDto dto) {
|
||||
try {
|
||||
List<DrugInteractionResultDto> results = kgReasoningAppService.checkDrugInteractions(dto.getDrugCodes());
|
||||
return R.ok(results);
|
||||
} catch (Exception e) {
|
||||
log.error("药物相互作用检查失败", e);
|
||||
return R.fail("药物相互作用检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "临床路径推荐")
|
||||
@PreAuthorize("@ss.hasPermi('system:knowledgegraph:list')")
|
||||
@GetMapping("/pathway/{diseaseCode}")
|
||||
public R<Map<String, Object>> suggestPathway(@PathVariable String diseaseCode) {
|
||||
try {
|
||||
Map<String, Object> result = kgReasoningAppService.suggestPathway(diseaseCode);
|
||||
return result.isEmpty() ? R.fail("未找到临床路径") : R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("临床路径推荐失败", e);
|
||||
return R.fail("临床路径推荐失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class DiagnosisResultDto {
|
||||
private String diseaseCode;
|
||||
private String diseaseName;
|
||||
private String category;
|
||||
private String department;
|
||||
private BigDecimal score;
|
||||
private String matchedSymptoms;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DiagnosisSuggestDto {
|
||||
private List<String> symptoms;
|
||||
private Integer topN = 5;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DrugInteractionDto {
|
||||
private List<String> drugCodes;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DrugInteractionResultDto {
|
||||
private String drugCodeA;
|
||||
private String drugNameA;
|
||||
private String drugCodeB;
|
||||
private String drugNameB;
|
||||
private String interactionType;
|
||||
private String description;
|
||||
private String severity;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class ExaminationResultDto {
|
||||
private String examCode;
|
||||
private String examName;
|
||||
private String examType;
|
||||
private String clinicalSignificance;
|
||||
private BigDecimal score;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ExaminationSuggestDto {
|
||||
private String diseaseCode;
|
||||
private Integer topN = 10;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ImportResultDto {
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
private int totalRows;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class KgDiseaseDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
private String diseaseCode;
|
||||
|
||||
private String diseaseName;
|
||||
|
||||
private String category;
|
||||
|
||||
private String department;
|
||||
|
||||
private String severityLevel;
|
||||
|
||||
private String description;
|
||||
|
||||
private String keywords;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class KgDrugDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
private String drugCode;
|
||||
|
||||
private String drugName;
|
||||
|
||||
private String genericName;
|
||||
|
||||
private String category;
|
||||
|
||||
private String dosageForm;
|
||||
|
||||
private String contraindications;
|
||||
|
||||
private String sideEffects;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class KgExaminationDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
private String examCode;
|
||||
|
||||
private String examName;
|
||||
|
||||
private String examType;
|
||||
|
||||
private String department;
|
||||
|
||||
private String referenceRange;
|
||||
|
||||
private String clinicalSignificance;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.healthlink.his.web.knowledgegraph.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class KgSymptomDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
private String symptomCode;
|
||||
|
||||
private String symptomName;
|
||||
|
||||
private String bodyPart;
|
||||
|
||||
private String symptomType;
|
||||
|
||||
private String severityIndicator;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.healthlink.his.web.miniprogram.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.miniprogram.dto.AssessmentSubmitDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.TaskCompleteDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.VitalSignSubmitDto;
|
||||
|
||||
/**
|
||||
* 移动护理小程序 AppService
|
||||
*/
|
||||
public interface IMpNursingAppService {
|
||||
|
||||
/**
|
||||
* 获取护士任务列表
|
||||
* @param nurseId 护士ID
|
||||
* @param status 任务状态(可选)
|
||||
*/
|
||||
R<?> getTaskList(Long nurseId, String status);
|
||||
|
||||
/**
|
||||
* 完成任务
|
||||
* @param taskId 任务ID
|
||||
* @param dto 完成结果
|
||||
*/
|
||||
R<?> completeTask(Long taskId, TaskCompleteDto dto);
|
||||
|
||||
/**
|
||||
* 获取患者信息(精简版)
|
||||
* @param patientId 患者ID
|
||||
*/
|
||||
R<?> getPatientInfo(Long patientId);
|
||||
|
||||
/**
|
||||
* 获取生命体征趋势
|
||||
* @param patientId 患者ID
|
||||
* @param days 查询天数(默认7天)
|
||||
*/
|
||||
R<?> getVitalSigns(Long patientId, Integer days);
|
||||
|
||||
/**
|
||||
* 录入生命体征
|
||||
* @param dto 体征数据
|
||||
*/
|
||||
R<?> submitVitalSign(VitalSignSubmitDto dto);
|
||||
|
||||
/**
|
||||
* 获取评估记录列表
|
||||
* @param patientId 患者ID
|
||||
*/
|
||||
R<?> getAssessmentList(Long patientId);
|
||||
|
||||
/**
|
||||
* 提交护理评估
|
||||
* @param dto 评估数据
|
||||
*/
|
||||
R<?> submitAssessment(AssessmentSubmitDto dto);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.healthlink.his.web.miniprogram.appservice.impl;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.miniprogram.domain.MpAssessmentRecord;
|
||||
import com.healthlink.his.miniprogram.domain.MpNursingTask;
|
||||
import com.healthlink.his.miniprogram.domain.MpVitalSignRecord;
|
||||
import com.healthlink.his.miniprogram.mapper.MpAssessmentRecordMapper;
|
||||
import com.healthlink.his.miniprogram.mapper.MpNursingTaskMapper;
|
||||
import com.healthlink.his.miniprogram.mapper.MpVitalSignRecordMapper;
|
||||
import com.healthlink.his.miniprogram.service.IMpAssessmentRecordService;
|
||||
import com.healthlink.his.miniprogram.service.IMpNursingTaskService;
|
||||
import com.healthlink.his.miniprogram.service.IMpVitalSignRecordService;
|
||||
import com.healthlink.his.web.miniprogram.appservice.IMpNursingAppService;
|
||||
import com.healthlink.his.web.miniprogram.dto.AssessmentSubmitDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.TaskCompleteDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.VitalSignSubmitDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 移动护理小程序 AppService实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MpNursingAppServiceImpl implements IMpNursingAppService {
|
||||
|
||||
@Resource
|
||||
private IMpNursingTaskService nursingTaskService;
|
||||
|
||||
@Resource
|
||||
private MpNursingTaskMapper nursingTaskMapper;
|
||||
|
||||
@Resource
|
||||
private IMpVitalSignRecordService vitalSignRecordService;
|
||||
|
||||
@Resource
|
||||
private MpVitalSignRecordMapper vitalSignRecordMapper;
|
||||
|
||||
@Resource
|
||||
private IMpAssessmentRecordService assessmentRecordService;
|
||||
|
||||
@Resource
|
||||
private MpAssessmentRecordMapper assessmentRecordMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public R<?> getTaskList(Long nurseId, String status) {
|
||||
List<MpNursingTask> tasks = nursingTaskMapper.selectTaskListByNurse(nurseId, status);
|
||||
long pendingCount = tasks.stream().filter(t -> "PENDING".equals(t.getTaskStatus())).count();
|
||||
long inProgressCount = tasks.stream().filter(t -> "IN_PROGRESS".equals(t.getTaskStatus())).count();
|
||||
long completedCount = tasks.stream().filter(t -> "COMPLETED".equals(t.getTaskStatus())).count();
|
||||
|
||||
return R.ok(Map.of(
|
||||
"tasks", tasks,
|
||||
"summary", Map.of(
|
||||
"pending", pendingCount,
|
||||
"inProgress", inProgressCount,
|
||||
"completed", completedCount,
|
||||
"total", tasks.size()
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> completeTask(Long taskId, TaskCompleteDto dto) {
|
||||
MpNursingTask task = nursingTaskService.getById(taskId);
|
||||
if (task == null) {
|
||||
return R.fail("任务不存在");
|
||||
}
|
||||
if ("COMPLETED".equals(task.getTaskStatus())) {
|
||||
return R.fail("任务已完成");
|
||||
}
|
||||
|
||||
task.setTaskStatus("COMPLETED");
|
||||
task.setCompleteTime(LocalDateTime.now());
|
||||
nursingTaskService.updateById(task);
|
||||
|
||||
log.info("任务完成: taskId={}, nurseId={}, result={}", taskId, task.getNurseId(), dto.getResult());
|
||||
return R.ok("任务已完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public R<?> getPatientInfo(Long patientId) {
|
||||
return R.ok(Map.of(
|
||||
"patientId", patientId,
|
||||
"message", "患者信息查询待接入基础数据模块"
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public R<?> getVitalSigns(Long patientId, Integer days) {
|
||||
if (days == null || days <= 0) {
|
||||
days = 7;
|
||||
}
|
||||
List<MpVitalSignRecord> records = vitalSignRecordMapper.selectByPatientId(patientId, days);
|
||||
return R.ok(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> submitVitalSign(VitalSignSubmitDto dto) {
|
||||
if (dto.getPatientId() == null || dto.getNurseId() == null) {
|
||||
return R.fail("患者ID和护士ID不能为空");
|
||||
}
|
||||
if (dto.getRecordTime() == null) {
|
||||
dto.setRecordTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
MpVitalSignRecord record = new MpVitalSignRecord();
|
||||
record.setPatientId(dto.getPatientId());
|
||||
record.setEncounterId(dto.getEncounterId());
|
||||
record.setNurseId(dto.getNurseId());
|
||||
record.setRecordTime(dto.getRecordTime());
|
||||
record.setTemperature(dto.getTemperature());
|
||||
record.setPulse(dto.getPulse());
|
||||
record.setRespiration(dto.getRespiration());
|
||||
record.setSystolicBp(dto.getSystolicBp());
|
||||
record.setDiastolicBp(dto.getDiastolicBp());
|
||||
record.setBloodOxygen(dto.getBloodOxygen());
|
||||
record.setHeight(dto.getHeight());
|
||||
record.setWeight(dto.getWeight());
|
||||
|
||||
vitalSignRecordService.save(record);
|
||||
log.info("生命体征录入: patientId={}, nurseId={}, recordId={}",
|
||||
dto.getPatientId(), dto.getNurseId(), record.getId());
|
||||
return R.ok(Map.of("recordId", record.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public R<?> getAssessmentList(Long patientId) {
|
||||
List<MpAssessmentRecord> records = assessmentRecordMapper.selectByPatientId(patientId);
|
||||
return R.ok(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> submitAssessment(AssessmentSubmitDto dto) {
|
||||
if (dto.getPatientId() == null || dto.getNurseId() == null || dto.getAssessmentType() == null) {
|
||||
return R.fail("患者ID、护士ID和评估类型不能为空");
|
||||
}
|
||||
if (dto.getRecordTime() == null) {
|
||||
dto.setRecordTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
MpAssessmentRecord record = new MpAssessmentRecord();
|
||||
record.setPatientId(dto.getPatientId());
|
||||
record.setEncounterId(dto.getEncounterId());
|
||||
record.setNurseId(dto.getNurseId());
|
||||
record.setAssessmentType(dto.getAssessmentType());
|
||||
record.setAssessmentContent(dto.getAssessmentContent());
|
||||
record.setAssessmentResult(dto.getAssessmentResult());
|
||||
record.setScore(dto.getScore());
|
||||
record.setRiskLevel(dto.getRiskLevel());
|
||||
record.setRecordTime(dto.getRecordTime());
|
||||
|
||||
assessmentRecordService.save(record);
|
||||
log.info("护理评估提交: patientId={}, type={}, recordId={}",
|
||||
dto.getPatientId(), dto.getAssessmentType(), record.getId());
|
||||
return R.ok(Map.of("recordId", record.getId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.healthlink.his.web.miniprogram.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.miniprogram.appservice.IMpNursingAppService;
|
||||
import com.healthlink.his.web.miniprogram.dto.AssessmentSubmitDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.TaskCompleteDto;
|
||||
import com.healthlink.his.web.miniprogram.dto.VitalSignSubmitDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 移动护理小程序 Controller
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "移动护理小程序")
|
||||
@RestController
|
||||
@RequestMapping("/mp/nursing")
|
||||
public class MpNursingController {
|
||||
|
||||
private final IMpNursingAppService mpNursingAppService;
|
||||
|
||||
public MpNursingController(IMpNursingAppService mpNursingAppService) {
|
||||
this.mpNursingAppService = mpNursingAppService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取护士任务列表")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:list')")
|
||||
@GetMapping("/tasks")
|
||||
public R<?> getTaskList(@RequestParam Long nurseId,
|
||||
@RequestParam(required = false) String status) {
|
||||
return mpNursingAppService.getTaskList(nurseId, status);
|
||||
}
|
||||
|
||||
@Operation(summary = "完成任务")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:edit')")
|
||||
@PostMapping("/tasks/{id}/complete")
|
||||
public R<?> completeTask(@PathVariable Long id,
|
||||
@RequestBody TaskCompleteDto dto) {
|
||||
return mpNursingAppService.completeTask(id, dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取患者信息")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:list')")
|
||||
@GetMapping("/patient/{id}")
|
||||
public R<?> getPatientInfo(@PathVariable Long id) {
|
||||
return mpNursingAppService.getPatientInfo(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取生命体征趋势")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:list')")
|
||||
@GetMapping("/vital-signs/{patientId}")
|
||||
public R<?> getVitalSigns(@PathVariable Long patientId,
|
||||
@RequestParam(required = false, defaultValue = "7") Integer days) {
|
||||
return mpNursingAppService.getVitalSigns(patientId, days);
|
||||
}
|
||||
|
||||
@Operation(summary = "录入生命体征")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:edit')")
|
||||
@PostMapping("/vital-sign")
|
||||
public R<?> submitVitalSign(@RequestBody VitalSignSubmitDto dto) {
|
||||
return mpNursingAppService.submitVitalSign(dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取评估记录列表")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:list')")
|
||||
@GetMapping("/assessments/{patientId}")
|
||||
public R<?> getAssessmentList(@PathVariable Long patientId) {
|
||||
return mpNursingAppService.getAssessmentList(patientId);
|
||||
}
|
||||
|
||||
@Operation(summary = "提交护理评估")
|
||||
@PreAuthorize("@ss.hasPermi('nursing:nursing:edit')")
|
||||
@PostMapping("/assessment")
|
||||
public R<?> submitAssessment(@RequestBody AssessmentSubmitDto dto) {
|
||||
return mpNursingAppService.submitAssessment(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.healthlink.his.web.miniprogram.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 护理评估提交请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class AssessmentSubmitDto {
|
||||
|
||||
private Long patientId;
|
||||
|
||||
private Long encounterId;
|
||||
|
||||
private Long nurseId;
|
||||
|
||||
private String assessmentType;
|
||||
|
||||
private String assessmentContent;
|
||||
|
||||
private String assessmentResult;
|
||||
|
||||
private BigDecimal score;
|
||||
|
||||
private String riskLevel;
|
||||
|
||||
private LocalDateTime recordTime;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.healthlink.his.web.miniprogram.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 患者信息精简版DTO
|
||||
*/
|
||||
@Data
|
||||
public class PatientInfoDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String patientName;
|
||||
|
||||
private String gender;
|
||||
|
||||
private LocalDate birthDate;
|
||||
|
||||
private String medicalNo;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String departmentName;
|
||||
|
||||
private String bedNo;
|
||||
|
||||
private String diagnosis;
|
||||
|
||||
private String nurseLevel;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.miniprogram.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 任务完成请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class TaskCompleteDto {
|
||||
|
||||
private String result;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.healthlink.his.web.miniprogram.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 生命体征录入请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class VitalSignSubmitDto {
|
||||
|
||||
private Long patientId;
|
||||
|
||||
private Long encounterId;
|
||||
|
||||
private Long nurseId;
|
||||
|
||||
private LocalDateTime recordTime;
|
||||
|
||||
private BigDecimal temperature;
|
||||
|
||||
private Integer pulse;
|
||||
|
||||
private Integer respiration;
|
||||
|
||||
private Integer systolicBp;
|
||||
|
||||
private Integer diastolicBp;
|
||||
|
||||
private BigDecimal bloodOxygen;
|
||||
|
||||
private BigDecimal height;
|
||||
|
||||
private BigDecimal weight;
|
||||
}
|
||||
@@ -11,4 +11,9 @@ public interface INursingMobileAppService {
|
||||
Map<String, Object> executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId);
|
||||
NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto vitalSign);
|
||||
NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days);
|
||||
NursingMobileAssessmentDto submitAssessment(NursingMobileAssessmentDto dto);
|
||||
List<NursingMobileAssessmentDto> getAssessmentList(Long patientId);
|
||||
NursingMobileInfusionDto startInfusion(NursingMobileInfusionDto dto);
|
||||
NursingMobileInfusionDto addPatrol(NursingMobileInfusionDto dto);
|
||||
List<NursingMobileInfusionDto> getInfusionStatus(Long patientId);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.healthlink.his.web.nursing.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.nursing.domain.NursingAssessment;
|
||||
import com.healthlink.his.nursing.domain.NursingInfusionPatrol;
|
||||
import com.healthlink.his.nursing.domain.NursingVitalSignsChart;
|
||||
import com.healthlink.his.nursing.service.INursingAssessmentService;
|
||||
import com.healthlink.his.nursing.service.INursingInfusionPatrolService;
|
||||
import com.healthlink.his.nursing.service.INursingVitalSignsChartService;
|
||||
import com.healthlink.his.web.nursing.appservice.INursingMobileAppService;
|
||||
import com.healthlink.his.web.nursing.dto.*;
|
||||
import com.healthlink.his.web.nursing.mapper.NursingMobileAppMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -24,6 +29,14 @@ public class NursingMobileAppServiceImpl implements INursingMobileAppService {
|
||||
@Resource
|
||||
private INursingVitalSignsChartService vitalSignsChartService;
|
||||
|
||||
@Resource
|
||||
private INursingAssessmentService assessmentService;
|
||||
|
||||
@Resource
|
||||
private INursingInfusionPatrolService infusionPatrolService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public List<NursingMobilePatientDto> getMobilePatientList(String wardName, String searchKey) {
|
||||
return mobileMapper.selectMobilePatientList(wardName, searchKey);
|
||||
@@ -156,4 +169,158 @@ public class NursingMobileAppServiceImpl implements INursingMobileAppService {
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public NursingMobileAssessmentDto submitAssessment(NursingMobileAssessmentDto dto) {
|
||||
NursingAssessment assessment = new NursingAssessment();
|
||||
assessment.setEncounterId(dto.getEncounterId());
|
||||
assessment.setPatientId(dto.getPatientId());
|
||||
assessment.setPatientName(dto.getPatientName());
|
||||
assessment.setAssessorId(dto.getAssessorId());
|
||||
assessment.setAssessorName(dto.getAssessorName());
|
||||
assessment.setAssessmentType(dto.getAssessmentType());
|
||||
assessment.setAssessmentTool(dto.getAssessmentTool());
|
||||
assessment.setTotalScore(dto.getTotalScore());
|
||||
assessment.setRiskLevel(calculateRiskLevel(dto.getAssessmentTool(), dto.getTotalScore()));
|
||||
assessment.setDetail(dto.getDetail());
|
||||
assessment.setAssessmentTime(dto.getAssessmentTime() != null ? dto.getAssessmentTime() : new Date());
|
||||
assessment.setDeleteFlag("0");
|
||||
try {
|
||||
assessment.setItemScores(dto.getItemScores() != null ? objectMapper.writeValueAsString(dto.getItemScores()) : null);
|
||||
} catch (Exception e) {
|
||||
assessment.setItemScores(null);
|
||||
}
|
||||
assessmentService.save(assessment);
|
||||
dto.setId(assessment.getId());
|
||||
dto.setRiskLevel(assessment.getRiskLevel());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NursingMobileAssessmentDto> getAssessmentList(Long patientId) {
|
||||
LambdaQueryWrapper<NursingAssessment> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(NursingAssessment::getPatientId, patientId)
|
||||
.orderByDesc(NursingAssessment::getAssessmentTime);
|
||||
List<NursingAssessment> records = assessmentService.list(wrapper);
|
||||
List<NursingMobileAssessmentDto> result = new ArrayList<>();
|
||||
for (NursingAssessment r : records) {
|
||||
NursingMobileAssessmentDto dto = new NursingMobileAssessmentDto();
|
||||
dto.setId(r.getId());
|
||||
dto.setEncounterId(r.getEncounterId());
|
||||
dto.setPatientId(r.getPatientId());
|
||||
dto.setPatientName(r.getPatientName());
|
||||
dto.setAssessorName(r.getAssessorName());
|
||||
dto.setAssessmentType(r.getAssessmentType());
|
||||
dto.setAssessmentTool(r.getAssessmentTool());
|
||||
dto.setTotalScore(r.getTotalScore());
|
||||
dto.setRiskLevel(r.getRiskLevel());
|
||||
dto.setDetail(r.getDetail());
|
||||
dto.setAssessmentTime(r.getAssessmentTime());
|
||||
try {
|
||||
if (r.getItemScores() != null) {
|
||||
dto.setItemScores(objectMapper.readValue(r.getItemScores(), Map.class));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
dto.setItemScores(null);
|
||||
}
|
||||
result.add(dto);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public NursingMobileInfusionDto startInfusion(NursingMobileInfusionDto dto) {
|
||||
NursingInfusionPatrol patrol = new NursingInfusionPatrol();
|
||||
patrol.setEncounterId(dto.getEncounterId());
|
||||
patrol.setPatientId(dto.getPatientId());
|
||||
patrol.setPatientName(dto.getPatientName());
|
||||
patrol.setOrderId(dto.getOrderId());
|
||||
patrol.setDrugName(dto.getDrugName());
|
||||
patrol.setInfusionRate(dto.getInfusionRate());
|
||||
patrol.setTotalVolume(dto.getTotalVolume());
|
||||
patrol.setStartTime(dto.getStartTime() != null ? dto.getStartTime() : new Date());
|
||||
patrol.setPatencyStatus("NORMAL");
|
||||
patrol.setPatrolNurseId(dto.getPatrolNurseId());
|
||||
patrol.setPatrolNurseName(dto.getPatrolNurseName());
|
||||
patrol.setCreateTime(new Date());
|
||||
infusionPatrolService.save(patrol);
|
||||
dto.setId(patrol.getId());
|
||||
dto.setPatencyStatus("NORMAL");
|
||||
dto.setStatus("RUNNING");
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public NursingMobileInfusionDto addPatrol(NursingMobileInfusionDto dto) {
|
||||
NursingInfusionPatrol patrol = new NursingInfusionPatrol();
|
||||
patrol.setEncounterId(dto.getEncounterId());
|
||||
patrol.setPatientId(dto.getPatientId());
|
||||
patrol.setPatientName(dto.getPatientName());
|
||||
patrol.setOrderId(dto.getOrderId());
|
||||
patrol.setDrugName(dto.getDrugName());
|
||||
patrol.setPatrolTime(new Date());
|
||||
patrol.setDripRate(dto.getDripRate());
|
||||
patrol.setPatencyStatus(dto.getPatencyStatus());
|
||||
patrol.setAdverseReaction(dto.getAdverseReaction());
|
||||
patrol.setPatrolNurseId(dto.getPatrolNurseId());
|
||||
patrol.setPatrolNurseName(dto.getPatrolNurseName());
|
||||
patrol.setCreateTime(new Date());
|
||||
infusionPatrolService.save(patrol);
|
||||
dto.setId(patrol.getId());
|
||||
dto.setPatrolTime(patrol.getPatrolTime());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NursingMobileInfusionDto> getInfusionStatus(Long patientId) {
|
||||
LambdaQueryWrapper<NursingInfusionPatrol> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(NursingInfusionPatrol::getPatientId, patientId)
|
||||
.orderByDesc(NursingInfusionPatrol::getStartTime);
|
||||
List<NursingInfusionPatrol> records = infusionPatrolService.list(wrapper);
|
||||
Map<Long, NursingMobileInfusionDto> latestMap = new LinkedHashMap<>();
|
||||
for (NursingInfusionPatrol r : records) {
|
||||
Long orderId = r.getOrderId();
|
||||
if (orderId == null) orderId = r.getId();
|
||||
if (!latestMap.containsKey(orderId)) {
|
||||
NursingMobileInfusionDto dto = new NursingMobileInfusionDto();
|
||||
dto.setId(r.getId());
|
||||
dto.setEncounterId(r.getEncounterId());
|
||||
dto.setPatientId(r.getPatientId());
|
||||
dto.setPatientName(r.getPatientName());
|
||||
dto.setOrderId(r.getOrderId());
|
||||
dto.setDrugName(r.getDrugName());
|
||||
dto.setInfusionRate(r.getInfusionRate());
|
||||
dto.setTotalVolume(r.getTotalVolume());
|
||||
dto.setStartTime(r.getStartTime());
|
||||
dto.setPatrolTime(r.getPatrolTime());
|
||||
dto.setDripRate(r.getDripRate());
|
||||
dto.setPatencyStatus(r.getPatencyStatus());
|
||||
dto.setAdverseReaction(r.getAdverseReaction());
|
||||
dto.setPatrolNurseName(r.getPatrolNurseName());
|
||||
dto.setStatus("RUNNING");
|
||||
latestMap.put(orderId, dto);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(latestMap.values());
|
||||
}
|
||||
|
||||
private String calculateRiskLevel(String tool, Integer score) {
|
||||
if (score == null) return "NORMAL";
|
||||
if ("BRADEN".equals(tool)) {
|
||||
if (score <= 12) return "HIGH";
|
||||
if (score <= 14) return "MEDIUM";
|
||||
return "LOW";
|
||||
} else if ("MORSE".equals(tool)) {
|
||||
if (score >= 45) return "HIGH";
|
||||
if (score >= 25) return "MEDIUM";
|
||||
return "LOW";
|
||||
} else if ("NRS2002".equals(tool)) {
|
||||
if (score >= 3) return "HIGH";
|
||||
return "LOW";
|
||||
}
|
||||
return "NORMAL";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +69,44 @@ public class NursingMobileController {
|
||||
NursingMobileVitalSignTrendDto trend = mobileAppService.getVitalSignTrend(patientId, days);
|
||||
return R.ok(trend);
|
||||
}
|
||||
|
||||
@Operation(summary = "提交护理评估")
|
||||
@PostMapping("/assessment/submit")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
|
||||
public R<?> submitAssessment(@RequestBody NursingMobileAssessmentDto assessment) {
|
||||
NursingMobileAssessmentDto saved = mobileAppService.submitAssessment(assessment);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询评估记录")
|
||||
@GetMapping("/assessment/list/{patientId}")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:list')")
|
||||
public R<?> getAssessmentList(@PathVariable Long patientId) {
|
||||
List<NursingMobileAssessmentDto> list = mobileAppService.getAssessmentList(patientId);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "开始输液")
|
||||
@PostMapping("/infusion/start")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
|
||||
public R<?> startInfusion(@RequestBody NursingMobileInfusionDto infusion) {
|
||||
NursingMobileInfusionDto saved = mobileAppService.startInfusion(infusion);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@Operation(summary = "输液巡视记录")
|
||||
@PostMapping("/infusion/patrol")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
|
||||
public R<?> addPatrol(@RequestBody NursingMobileInfusionDto patrol) {
|
||||
NursingMobileInfusionDto saved = mobileAppService.addPatrol(patrol);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@Operation(summary = "输液状态查询")
|
||||
@GetMapping("/infusion/status/{patientId}")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:list')")
|
||||
public R<?> getInfusionStatus(@PathVariable Long patientId) {
|
||||
List<NursingMobileInfusionDto> list = mobileAppService.getInfusionStatus(patientId);
|
||||
return R.ok(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class NursingMobileAssessmentDto {
|
||||
private Long id;
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private Long assessorId;
|
||||
private String assessorName;
|
||||
private String assessmentType;
|
||||
private String assessmentTool;
|
||||
private Integer totalScore;
|
||||
private String riskLevel;
|
||||
private Map<String, Integer> itemScores;
|
||||
private String detail;
|
||||
private Date assessmentTime;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class NursingMobileInfusionDto {
|
||||
private Long id;
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private Long orderId;
|
||||
private String drugName;
|
||||
private String infusionRate;
|
||||
private Integer totalVolume;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date patrolTime;
|
||||
private Integer dripRate;
|
||||
private String patencyStatus;
|
||||
private String adverseReaction;
|
||||
private Long patrolNurseId;
|
||||
private String patrolNurseName;
|
||||
private String status;
|
||||
private Integer remainingVolume;
|
||||
}
|
||||
@@ -196,7 +196,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
|
||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||
.collect(Collectors.toList());
|
||||
// 耗材 🔧 Bug #147 修复
|
||||
@@ -1081,7 +1080,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
// 诊疗(包含 医疗活动=3、手术=6、文字医嘱=8、护理=26 等,都属于 service_request)
|
||||
List<AdviceBatchOpParam> activityList = paramList.stream()
|
||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||
.collect(Collectors.toList());
|
||||
@@ -1156,14 +1154,17 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
.orElse(new Date());
|
||||
// 获取当前操作用户昵称作为停嘱医生
|
||||
String stopUserName = SecurityUtils.getNickName();
|
||||
// 药品
|
||||
// 药品(包含出院带药adviceType=7,与handleDeleteOperations保持一致)
|
||||
List<AdviceBatchOpParam> medicineList = paramList.stream()
|
||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 7))
|
||||
.collect(Collectors.toList());
|
||||
List<Long> medicineRequestIds
|
||||
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||
// 诊疗(包含护理adviceType=26、文字医嘱adviceType=8)
|
||||
// 诊疗(包含护理adviceType=26、手术adviceType=6、文字医嘱adviceType=8,与saveRegAdvice保持一致)
|
||||
List<AdviceBatchOpParam> activityList = paramList.stream()
|
||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||
.collect(Collectors.toList());
|
||||
@@ -1183,6 +1184,40 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
.set(ServiceRequest::getStatusEnum, RequestStatus.PENDING_STOP.getValue())
|
||||
.set(ServiceRequest::getUpdateBy, stopUserName));
|
||||
}
|
||||
// 耗材(adviceType=2)
|
||||
List<AdviceBatchOpParam> deviceList = paramList.stream()
|
||||
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||
List<Long> deviceRequestIds
|
||||
= deviceList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||
if (!deviceRequestIds.isEmpty()) {
|
||||
iDeviceRequestService.update(new LambdaUpdateWrapper<DeviceRequest>()
|
||||
.in(DeviceRequest::getId, deviceRequestIds)
|
||||
.set(DeviceRequest::getUseEndTime, stopTime)
|
||||
.set(DeviceRequest::getStatusEnum, RequestStatus.PENDING_STOP.getValue())
|
||||
.set(DeviceRequest::getUpdateBy, stopUserName));
|
||||
}
|
||||
|
||||
// 🔧 Bug #782 兜底处理:未被以上类型过滤器捕获的未知医嘱类型
|
||||
// 将所有未匹配类型的医嘱统一按诊疗请求(ServiceRequest)处理
|
||||
Set<Long> handledIds = new HashSet<>();
|
||||
handledIds.addAll(medicineRequestIds);
|
||||
handledIds.addAll(activityRequestIds);
|
||||
handledIds.addAll(deviceRequestIds);
|
||||
List<Long> fallbackRequestIds = paramList.stream()
|
||||
.map(AdviceBatchOpParam::getRequestId)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(id -> !handledIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
if (!fallbackRequestIds.isEmpty()) {
|
||||
log.info("Bug #782 兜底停嘱:处理未匹配类型的医嘱,requestIds: {}, 共{}条",
|
||||
fallbackRequestIds, fallbackRequestIds.size());
|
||||
iServiceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
|
||||
.in(ServiceRequest::getId, fallbackRequestIds)
|
||||
.set(ServiceRequest::getOccurrenceEndTime, stopTime)
|
||||
.set(ServiceRequest::getStatusEnum, RequestStatus.PENDING_STOP.getValue())
|
||||
.set(ServiceRequest::getUpdateBy, stopUserName));
|
||||
}
|
||||
|
||||
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"医嘱停止"}));
|
||||
|
||||
}
|
||||
@@ -1201,14 +1236,17 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
*/
|
||||
@Override
|
||||
public R<?> cancelStopRegAdvice(List<AdviceBatchOpParam> paramList) {
|
||||
// 药品
|
||||
// 药品(包含出院带药adviceType=7,与handleDeleteOperations保持一致)
|
||||
List<AdviceBatchOpParam> medicineList = paramList.stream()
|
||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 7))
|
||||
.collect(Collectors.toList());
|
||||
List<Long> medicineRequestIds
|
||||
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||
// 诊疗(包含护理adviceType=26、文字医嘱adviceType=8)
|
||||
// 诊疗(包含护理adviceType=26、手术adviceType=6、文字医嘱adviceType=8,与saveRegAdvice保持一致)
|
||||
List<AdviceBatchOpParam> activityList = paramList.stream()
|
||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
||||
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|
||||
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.healthlink.his.web.reportmanage.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IBiReportAppService {
|
||||
R<?> generateBiReport(String type, Map<String, Object> filters);
|
||||
R<?> getReportDashboard();
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.healthlink.his.web.reportmanage.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.quality.domain.BusinessAnalytics;
|
||||
import com.healthlink.his.quality.service.IBusinessAnalyticsService;
|
||||
import com.healthlink.his.crossmodule.domain.DrgPerformance;
|
||||
import com.healthlink.his.crossmodule.service.IDrgPerformanceService;
|
||||
import com.healthlink.his.web.reportmanage.appservice.IBiReportAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class BiReportAppServiceImpl implements IBiReportAppService {
|
||||
|
||||
private final IBusinessAnalyticsService analyticsService;
|
||||
private final IDrgPerformanceService drgPerformanceService;
|
||||
|
||||
@Override
|
||||
public R<?> generateBiReport(String type, Map<String, Object> filters) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("reportType", type);
|
||||
result.put("generatedAt", new Date().toString());
|
||||
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
|
||||
if (filters != null && filters.containsKey("departmentId")) {
|
||||
Long deptId = Long.valueOf(filters.get("departmentId").toString());
|
||||
allData = allData.stream()
|
||||
.filter(ba -> deptId.equals(ba.getDepartmentId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (filters != null && filters.containsKey("startDate")) {
|
||||
String start = filters.get("startDate").toString();
|
||||
allData = allData.stream()
|
||||
.filter(ba -> ba.getStatDate() != null && ba.getStatDate().compareTo(start) >= 0)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (filters != null && filters.containsKey("endDate")) {
|
||||
String end = filters.get("endDate").toString();
|
||||
allData = allData.stream()
|
||||
.filter(ba -> ba.getStatDate() != null && ba.getStatDate().compareTo(end) <= 0)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
int totalPatients = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (ba.getRevenue() != null) totalRevenue = totalRevenue.add(ba.getRevenue());
|
||||
if (ba.getCost() != null) totalCost = totalCost.add(ba.getCost());
|
||||
if (ba.getPatientCount() != null) totalPatients += ba.getPatientCount();
|
||||
}
|
||||
summary.put("totalRevenue", totalRevenue);
|
||||
summary.put("totalCost", totalCost);
|
||||
summary.put("totalProfit", totalRevenue.subtract(totalCost));
|
||||
summary.put("totalPatients", totalPatients);
|
||||
summary.put("totalRecords", allData.size());
|
||||
BigDecimal profitRate = totalRevenue.compareTo(BigDecimal.ZERO) > 0
|
||||
? totalRevenue.subtract(totalCost).multiply(new BigDecimal("100")).divide(totalRevenue, 2, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
summary.put("profitRate", profitRate);
|
||||
result.put("summary", summary);
|
||||
|
||||
Map<String, Object> charts = new HashMap<>();
|
||||
|
||||
if ("revenue".equals(type) || "overview".equals(type)) {
|
||||
Map<String, BigDecimal> monthlyRevenue = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> monthlyCost = new LinkedHashMap<>();
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
String month = ba.getStatDate() != null && ba.getStatDate().length() >= 7
|
||||
? ba.getStatDate().substring(0, 7) : "未知";
|
||||
monthlyRevenue.merge(month, ba.getRevenue() != null ? ba.getRevenue() : BigDecimal.ZERO, BigDecimal::add);
|
||||
monthlyCost.merge(month, ba.getCost() != null ? ba.getCost() : BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
List<Map<String, Object>> revenueChart = new ArrayList<>();
|
||||
monthlyRevenue.forEach((k, v) -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("month", k);
|
||||
item.put("revenue", v);
|
||||
item.put("cost", monthlyCost.getOrDefault(k, BigDecimal.ZERO));
|
||||
revenueChart.add(item);
|
||||
});
|
||||
charts.put("revenueChart", revenueChart);
|
||||
}
|
||||
|
||||
if ("department".equals(type) || "overview".equals(type)) {
|
||||
Map<String, BigDecimal> deptRevenue = allData.stream()
|
||||
.filter(ba -> ba.getDepartmentName() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
BusinessAnalytics::getDepartmentName,
|
||||
Collectors.reducing(BigDecimal.ZERO, ba -> ba.getRevenue() != null ? ba.getRevenue() : BigDecimal.ZERO, BigDecimal::add)));
|
||||
List<Map<String, Object>> deptChart = new ArrayList<>();
|
||||
deptRevenue.forEach((k, v) -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("department", k);
|
||||
item.put("revenue", v);
|
||||
deptChart.add(item);
|
||||
});
|
||||
charts.put("departmentChart", deptChart);
|
||||
}
|
||||
|
||||
if ("drg".equals(type) || "overview".equals(type)) {
|
||||
LambdaQueryWrapper<DrgPerformance> perfW = new LambdaQueryWrapper<>();
|
||||
perfW.orderByAsc(DrgPerformance::getStatMonth);
|
||||
List<DrgPerformance> perfList = drgPerformanceService.list(perfW);
|
||||
List<Map<String, Object>> cmiChart = new ArrayList<>();
|
||||
for (DrgPerformance p : perfList) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("month", p.getStatMonth());
|
||||
item.put("cmiValue", p.getCmiValue());
|
||||
item.put("costControlRate", p.getCostControlRate());
|
||||
item.put("totalCases", p.getTotalCases());
|
||||
cmiChart.add(item);
|
||||
}
|
||||
charts.put("drgChart", cmiChart);
|
||||
}
|
||||
|
||||
result.put("charts", charts);
|
||||
result.put("records", allData.stream().limit(100).map(ba -> {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("statDate", ba.getStatDate());
|
||||
row.put("departmentName", ba.getDepartmentName());
|
||||
row.put("revenue", ba.getRevenue());
|
||||
row.put("cost", ba.getCost());
|
||||
row.put("patientCount", ba.getPatientCount());
|
||||
return row;
|
||||
}).collect(Collectors.toList()));
|
||||
|
||||
log.info("BI报表生成完成: type={}, records={}", type, allData.size());
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> getReportDashboard() {
|
||||
Map<String, Object> dashboard = new HashMap<>();
|
||||
|
||||
List<BusinessAnalytics> allData = analyticsService.list();
|
||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
int totalPatients = 0;
|
||||
for (BusinessAnalytics ba : allData) {
|
||||
if (ba.getRevenue() != null) totalRevenue = totalRevenue.add(ba.getRevenue());
|
||||
if (ba.getCost() != null) totalCost = totalCost.add(ba.getCost());
|
||||
if (ba.getPatientCount() != null) totalPatients += ba.getPatientCount();
|
||||
}
|
||||
|
||||
dashboard.put("totalRevenue", totalRevenue);
|
||||
dashboard.put("totalCost", totalCost);
|
||||
dashboard.put("totalProfit", totalRevenue.subtract(totalCost));
|
||||
dashboard.put("totalPatients", totalPatients);
|
||||
dashboard.put("totalRecords", allData.size());
|
||||
dashboard.put("reportTypes", Arrays.asList(
|
||||
Map.of("value", "overview", "label", "综合概览"),
|
||||
Map.of("value", "revenue", "label", "收入分析"),
|
||||
Map.of("value", "department", "label", "科室分析"),
|
||||
Map.of("value", "drg", "label", "DRG分析")
|
||||
));
|
||||
|
||||
LambdaQueryWrapper<DrgPerformance> perfW = new LambdaQueryWrapper<>();
|
||||
perfW.orderByDesc(DrgPerformance::getStatMonth).last("LIMIT 1");
|
||||
List<DrgPerformance> latestPerf = drgPerformanceService.list(perfW);
|
||||
if (!latestPerf.isEmpty()) {
|
||||
DrgPerformance p = latestPerf.get(0);
|
||||
dashboard.put("latestCmiValue", p.getCmiValue());
|
||||
dashboard.put("latestCostControlRate", p.getCostControlRate());
|
||||
dashboard.put("latestDrgCases", p.getTotalCases());
|
||||
}
|
||||
|
||||
return R.ok(dashboard);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthlink.his.web.reportmanage.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.reportmanage.appservice.IBiReportAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController("reportBiReportController")
|
||||
@RequestMapping("/report/bi")
|
||||
@Slf4j
|
||||
public class BiReportController {
|
||||
|
||||
private final IBiReportAppService biReportAppService;
|
||||
|
||||
@PostMapping("/generate")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:edit')")
|
||||
public R<?> generateBiReport(
|
||||
@RequestParam(required = false, defaultValue = "overview") String type,
|
||||
@RequestBody(required = false) Map<String, Object> filters) {
|
||||
return biReportAppService.generateBiReport(type, filters);
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@PreAuthorize("hasAuthority('reportmanage:report:list')")
|
||||
public R<?> getReportDashboard() {
|
||||
return biReportAppService.getReportDashboard();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.healthlink.his.web.reportmanage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class BiReportDto {
|
||||
private String reportType;
|
||||
private String title;
|
||||
private List<Map<String, Object>> records;
|
||||
private Map<String, Object> summary;
|
||||
private List<Map<String, Object>> charts;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.healthlink.his.web.telehealth.appservice;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
|
||||
|
||||
public interface ITelehealthAppService {
|
||||
|
||||
Long createConsultation(TelehealthConsultationDto dto);
|
||||
|
||||
Page<TelehealthConsultationDto> pageConsultation(TelehealthConsultationDto dto);
|
||||
|
||||
Boolean replyConsultation(TelehealthConsultationDto dto);
|
||||
|
||||
Boolean prescribeConsultation(TelehealthConsultationDto dto);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.healthlink.his.web.telehealth.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.healthlink.his.web.telehealth.appservice.ITelehealthAppService;
|
||||
import com.healthlink.his.web.telehealth.domain.TelehealthConsultation;
|
||||
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
|
||||
import com.healthlink.his.web.telehealth.mapper.TelehealthConsultationMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TelehealthAppServiceImpl implements ITelehealthAppService {
|
||||
|
||||
@Resource
|
||||
private TelehealthConsultationMapper telehealthConsultationMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createConsultation(TelehealthConsultationDto dto) {
|
||||
TelehealthConsultation entity = new TelehealthConsultation();
|
||||
entity.setPatientId(dto.getPatientId());
|
||||
entity.setDoctorId(dto.getDoctorId());
|
||||
entity.setConsultationType(dto.getConsultationType());
|
||||
entity.setStatus("PENDING");
|
||||
entity.setChiefComplaint(dto.getChiefComplaint());
|
||||
entity.setConsultationTime(new Date());
|
||||
entity.setTenantId(SecurityUtils.getLoginUser().getTenantId());
|
||||
telehealthConsultationMapper.insert(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<TelehealthConsultationDto> pageConsultation(TelehealthConsultationDto dto) {
|
||||
Page<TelehealthConsultation> page = new Page<>(dto.getPageNum(), dto.getPageSize());
|
||||
LambdaQueryWrapper<TelehealthConsultation> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (StringUtils.hasText(dto.getStatus())) {
|
||||
wrapper.eq(TelehealthConsultation::getStatus, dto.getStatus());
|
||||
}
|
||||
if (dto.getDoctorId() != null) {
|
||||
wrapper.eq(TelehealthConsultation::getDoctorId, dto.getDoctorId());
|
||||
}
|
||||
if (dto.getPatientId() != null) {
|
||||
wrapper.eq(TelehealthConsultation::getPatientId, dto.getPatientId());
|
||||
}
|
||||
wrapper.orderByDesc(TelehealthConsultation::getCreateTime);
|
||||
|
||||
Page<TelehealthConsultation> result = telehealthConsultationMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<TelehealthConsultationDto> dtoPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
|
||||
List<TelehealthConsultationDto> dtoList = result.getRecords().stream()
|
||||
.map(this::toDto)
|
||||
.collect(Collectors.toList());
|
||||
dtoPage.setRecords(dtoList);
|
||||
return dtoPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean replyConsultation(TelehealthConsultationDto dto) {
|
||||
TelehealthConsultation entity = telehealthConsultationMapper.selectById(dto.getId());
|
||||
if (entity == null) {
|
||||
throw new RuntimeException("问诊记录不存在");
|
||||
}
|
||||
entity.setDiagnosis(dto.getDiagnosis());
|
||||
entity.setStatus("IN_PROGRESS");
|
||||
telehealthConsultationMapper.updateById(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean prescribeConsultation(TelehealthConsultationDto dto) {
|
||||
TelehealthConsultation entity = telehealthConsultationMapper.selectById(dto.getId());
|
||||
if (entity == null) {
|
||||
throw new RuntimeException("问诊记录不存在");
|
||||
}
|
||||
entity.setPrescription(dto.getPrescription());
|
||||
entity.setDiagnosis(dto.getDiagnosis());
|
||||
entity.setStatus("COMPLETED");
|
||||
entity.setEndTime(new Date());
|
||||
telehealthConsultationMapper.updateById(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
private TelehealthConsultationDto toDto(TelehealthConsultation entity) {
|
||||
TelehealthConsultationDto dto = new TelehealthConsultationDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.healthlink.his.web.telehealth.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.healthlink.his.web.telehealth.appservice.ITelehealthAppService;
|
||||
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "互联网医院-在线问诊")
|
||||
@RestController
|
||||
@RequestMapping("/telehealth/consultation")
|
||||
public class TelehealthController {
|
||||
|
||||
@Resource
|
||||
private ITelehealthAppService telehealthAppService;
|
||||
|
||||
@Operation(summary = "创建问诊")
|
||||
@PostMapping("/create")
|
||||
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
|
||||
public R<Long> create(@RequestBody TelehealthConsultationDto dto) {
|
||||
try {
|
||||
Long id = telehealthAppService.createConsultation(dto);
|
||||
return R.ok(id);
|
||||
} catch (Exception e) {
|
||||
log.error("创建问诊失败", e);
|
||||
return R.fail("创建问诊失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "问诊列表")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:list')")
|
||||
public R<Page<TelehealthConsultationDto>> page(TelehealthConsultationDto dto) {
|
||||
try {
|
||||
Page<TelehealthConsultationDto> result = telehealthAppService.pageConsultation(dto);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("查询问诊列表失败", e);
|
||||
return R.fail("查询问诊列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "医生回复")
|
||||
@PostMapping("/reply")
|
||||
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
|
||||
public R<String> reply(@RequestBody TelehealthConsultationDto dto) {
|
||||
try {
|
||||
telehealthAppService.replyConsultation(dto);
|
||||
return R.ok("回复成功");
|
||||
} catch (Exception e) {
|
||||
log.error("医生回复失败", e);
|
||||
return R.fail("回复失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "复诊开方")
|
||||
@PostMapping("/prescribe")
|
||||
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
|
||||
public R<String> prescribe(@RequestBody TelehealthConsultationDto dto) {
|
||||
try {
|
||||
telehealthAppService.prescribeConsultation(dto);
|
||||
return R.ok("开方成功");
|
||||
} catch (Exception e) {
|
||||
log.error("复诊开方失败", e);
|
||||
return R.fail("开方失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.healthlink.his.web.telehealth.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.core.common.core.domain.HisBaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("telehealth_consultation")
|
||||
public class TelehealthConsultation extends HisBaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
@TableField("patient_id")
|
||||
private Long patientId;
|
||||
|
||||
@TableField("doctor_id")
|
||||
private Long doctorId;
|
||||
|
||||
@TableField("consultation_type")
|
||||
private String consultationType;
|
||||
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
@TableField("chief_complaint")
|
||||
private String chiefComplaint;
|
||||
|
||||
@TableField("diagnosis")
|
||||
private String diagnosis;
|
||||
|
||||
@TableField("prescription")
|
||||
private String prescription;
|
||||
|
||||
@TableField("consultation_time")
|
||||
private Date consultationTime;
|
||||
|
||||
@TableField("end_time")
|
||||
private Date endTime;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.healthlink.his.web.telehealth.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class TelehealthConsultationDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long patientId;
|
||||
|
||||
private String patientName;
|
||||
|
||||
private Long doctorId;
|
||||
|
||||
private String doctorName;
|
||||
|
||||
private String consultationType;
|
||||
|
||||
private String status;
|
||||
|
||||
private String chiefComplaint;
|
||||
|
||||
private String diagnosis;
|
||||
|
||||
private String prescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date consultationTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private Integer pageNum = 1;
|
||||
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.healthlink.his.web.telehealth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.web.telehealth.domain.TelehealthConsultation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TelehealthConsultationMapper extends BaseMapper<TelehealthConsultation> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- V85: 互联网医院 - 在线问诊+复诊开方
|
||||
CREATE TABLE IF NOT EXISTS telehealth_consultation (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
patient_id BIGINT NOT NULL,
|
||||
doctor_id BIGINT NOT NULL,
|
||||
consultation_type VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'PENDING',
|
||||
chief_complaint TEXT,
|
||||
diagnosis TEXT,
|
||||
prescription TEXT,
|
||||
consultation_time TIMESTAMP,
|
||||
end_time TIMESTAMP,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
delete_flag CHAR(1) DEFAULT '0',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(64),
|
||||
update_time TIMESTAMP,
|
||||
update_by VARCHAR(64)
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
-- V86: CDSS规则引擎升级 - 添加优先级/分类字段 + 规则执行历史
|
||||
|
||||
ALTER TABLE cdss_rule ADD COLUMN IF NOT EXISTS priority INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE cdss_rule ADD COLUMN IF NOT EXISTS category VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN cdss_rule.priority IS '规则优先级(0普通 1紧急 2最高)';
|
||||
COMMENT ON COLUMN cdss_rule.category IS '规则分类';
|
||||
|
||||
CREATE TABLE cdss_rule_execution (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
rule_id BIGINT NOT NULL,
|
||||
rule_code VARCHAR(64) NOT NULL,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
patient_id BIGINT NOT NULL,
|
||||
matched BOOLEAN DEFAULT FALSE,
|
||||
execution_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
execution_result TEXT,
|
||||
duration_ms INT,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
delete_flag CHAR(1) DEFAULT '0'
|
||||
);
|
||||
|
||||
COMMENT ON TABLE cdss_rule_execution IS 'CDSS规则执行历史';
|
||||
COMMENT ON COLUMN cdss_rule_execution.id IS '执行记录ID';
|
||||
COMMENT ON COLUMN cdss_rule_execution.rule_id IS '规则ID';
|
||||
COMMENT ON COLUMN cdss_rule_execution.rule_code IS '规则编码';
|
||||
COMMENT ON COLUMN cdss_rule_execution.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN cdss_rule_execution.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN cdss_rule_execution.matched IS '是否命中';
|
||||
COMMENT ON COLUMN cdss_rule_execution.execution_time IS '执行时间';
|
||||
COMMENT ON COLUMN cdss_rule_execution.execution_result IS '执行结果';
|
||||
COMMENT ON COLUMN cdss_rule_execution.duration_ms IS '执行耗时(毫秒)';
|
||||
|
||||
CREATE INDEX idx_cdss_exec_rule ON cdss_rule_execution(rule_id);
|
||||
CREATE INDEX idx_cdss_exec_encounter ON cdss_rule_execution(encounter_id);
|
||||
CREATE INDEX idx_cdss_exec_patient ON cdss_rule_execution(patient_id);
|
||||
CREATE INDEX idx_cdss_exec_time ON cdss_rule_execution(execution_time);
|
||||
@@ -0,0 +1,32 @@
|
||||
-- V87: AI辅助诊疗 - AI诊断建议表
|
||||
|
||||
CREATE TABLE ai_diagnosis_suggestion (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
patient_id BIGINT NOT NULL,
|
||||
symptom_text TEXT,
|
||||
diagnosis_suggestions TEXT,
|
||||
confidence_score DECIMAL(5,2),
|
||||
suggestion_source VARCHAR(32),
|
||||
accepted BOOLEAN DEFAULT FALSE,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
delete_flag CHAR(1) DEFAULT '0',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(64),
|
||||
update_time TIMESTAMP,
|
||||
update_by VARCHAR(64)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_diagnosis_suggestion IS 'AI辅助诊疗建议';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.id IS '建议ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.symptom_text IS '症状描述';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.diagnosis_suggestions IS '诊断建议';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.confidence_score IS '置信度(0-100)';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.suggestion_source IS '建议来源(llm/rule/manual)';
|
||||
COMMENT ON COLUMN ai_diagnosis_suggestion.accepted IS '是否采纳';
|
||||
|
||||
CREATE INDEX idx_ai_diag_encounter ON ai_diagnosis_suggestion(encounter_id);
|
||||
CREATE INDEX idx_ai_diag_patient ON ai_diagnosis_suggestion(patient_id);
|
||||
CREATE INDEX idx_ai_diag_source ON ai_diagnosis_suggestion(suggestion_source);
|
||||
@@ -0,0 +1,62 @@
|
||||
-- V89: 知识图谱 - 添加缺失字段 (description, keywords, severityIndicator, sideEffects, clinicalSignificance, evidenceSource, department, required)
|
||||
|
||||
-- kg_disease: 添加 description, keywords
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_disease ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_disease ADD COLUMN IF NOT EXISTS keywords VARCHAR(512);
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_disease.description IS '疾病描述';
|
||||
COMMENT ON COLUMN kg_disease.keywords IS '关键词';
|
||||
|
||||
-- kg_symptom: 添加 severity_indicator
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_symptom ADD COLUMN IF NOT EXISTS severity_indicator VARCHAR(32);
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_symptom.severity_indicator IS '严重程度指标';
|
||||
|
||||
-- kg_drug: 添加 side_effects
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_drug ADD COLUMN IF NOT EXISTS side_effects TEXT;
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_drug.side_effects IS '不良反应';
|
||||
|
||||
-- kg_examination: 添加 clinical_significance
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_examination ADD COLUMN IF NOT EXISTS clinical_significance TEXT;
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_examination.clinical_significance IS '临床意义';
|
||||
|
||||
-- kg_entity_relation: 添加 evidence_source
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_entity_relation ADD COLUMN IF NOT EXISTS evidence_source VARCHAR(512);
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_entity_relation.evidence_source IS '证据来源';
|
||||
|
||||
-- kg_clinical_pathway: 添加 department
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_clinical_pathway ADD COLUMN IF NOT EXISTS department VARCHAR(128);
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_clinical_pathway.department IS '所属科室';
|
||||
|
||||
-- kg_pathway_step: 添加 required
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE kg_pathway_step ADD COLUMN IF NOT EXISTS required CHAR(1) DEFAULT '1';
|
||||
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN kg_pathway_step.required IS '是否必选(1-是 0-否)';
|
||||
@@ -0,0 +1,93 @@
|
||||
-- 移动护理小程序 - 护理任务表
|
||||
CREATE TABLE IF NOT EXISTS mp_nursing_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
patient_id BIGINT NOT NULL,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
nurse_id BIGINT NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL,
|
||||
task_content TEXT,
|
||||
task_status VARCHAR(20) DEFAULT 'PENDING',
|
||||
due_time TIMESTAMP,
|
||||
complete_time TIMESTAMP,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
delete_flag CHAR(1) DEFAULT '0',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(64)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE mp_nursing_task IS '移动护理-护理任务';
|
||||
COMMENT ON COLUMN mp_nursing_task.id IS '主键ID';
|
||||
COMMENT ON COLUMN mp_nursing_task.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN mp_nursing_task.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN mp_nursing_task.nurse_id IS '护士ID';
|
||||
COMMENT ON COLUMN mp_nursing_task.task_type IS '任务类型';
|
||||
COMMENT ON COLUMN mp_nursing_task.task_content IS '任务内容';
|
||||
COMMENT ON COLUMN mp_nursing_task.task_status IS '任务状态: PENDING/IN_PROGRESS/COMPLETED/CANCELLED';
|
||||
COMMENT ON COLUMN mp_nursing_task.due_time IS '截止时间';
|
||||
COMMENT ON COLUMN mp_nursing_task.complete_time IS '完成时间';
|
||||
|
||||
-- 移动护理小程序 - 生命体征记录表
|
||||
CREATE TABLE IF NOT EXISTS mp_vital_sign_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
patient_id BIGINT NOT NULL,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
nurse_id BIGINT NOT NULL,
|
||||
record_time TIMESTAMP NOT NULL,
|
||||
temperature DECIMAL(4,1),
|
||||
pulse INTEGER,
|
||||
respiration INTEGER,
|
||||
systolic_bp INTEGER,
|
||||
diastolic_bp INTEGER,
|
||||
blood_oxygen DECIMAL(5,2),
|
||||
height DECIMAL(5,1),
|
||||
weight DECIMAL(5,1),
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
delete_flag CHAR(1) DEFAULT '0',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(64)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE mp_vital_sign_record IS '移动护理-生命体征记录';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.id IS '主键ID';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.nurse_id IS '记录护士ID';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.record_time IS '记录时间';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.temperature IS '体温(℃)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.pulse IS '脉搏(次/分)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.respiration IS '呼吸(次/分)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.systolic_bp IS '收缩压(mmHg)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.diastolic_bp IS '舒张压(mmHg)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.blood_oxygen IS '血氧饱和度(%)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.height IS '身高(cm)';
|
||||
COMMENT ON COLUMN mp_vital_sign_record.weight IS '体重(kg)';
|
||||
|
||||
-- 移动护理小程序 - 护理评估记录表
|
||||
CREATE TABLE IF NOT EXISTS mp_assessment_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
patient_id BIGINT NOT NULL,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
nurse_id BIGINT NOT NULL,
|
||||
assessment_type VARCHAR(32) NOT NULL,
|
||||
assessment_content TEXT,
|
||||
assessment_result TEXT,
|
||||
score DECIMAL(5,1),
|
||||
risk_level VARCHAR(20),
|
||||
record_time TIMESTAMP NOT NULL,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
delete_flag CHAR(1) DEFAULT '0',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(64)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE mp_assessment_record IS '移动护理-护理评估记录';
|
||||
COMMENT ON COLUMN mp_assessment_record.id IS '主键ID';
|
||||
COMMENT ON COLUMN mp_assessment_record.patient_id IS '患者ID';
|
||||
COMMENT ON COLUMN mp_assessment_record.encounter_id IS '就诊ID';
|
||||
COMMENT ON COLUMN mp_assessment_record.nurse_id IS '评估护士ID';
|
||||
COMMENT ON COLUMN mp_assessment_record.assessment_type IS '评估类型';
|
||||
COMMENT ON COLUMN mp_assessment_record.assessment_content IS '评估内容';
|
||||
COMMENT ON COLUMN mp_assessment_record.assessment_result IS '评估结果';
|
||||
COMMENT ON COLUMN mp_assessment_record.score IS '评分';
|
||||
COMMENT ON COLUMN mp_assessment_record.risk_level IS '风险等级';
|
||||
COMMENT ON COLUMN mp_assessment_record.record_time IS '评估时间';
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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.healthlink.his.aidiagnosis.mapper.AiDiagnosisSuggestionMapper">
|
||||
|
||||
<resultMap type="com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion" id="AiDiagnosisSuggestionResult">
|
||||
<id column="id" property="id"/>
|
||||
<result column="encounter_id" property="encounterId"/>
|
||||
<result column="patient_id" property="patientId"/>
|
||||
<result column="symptom_text" property="symptomText"/>
|
||||
<result column="diagnosis_suggestions" property="diagnosisSuggestions"/>
|
||||
<result column="confidence_score" property="confidenceScore"/>
|
||||
<result column="suggestion_source" property="suggestionSource"/>
|
||||
<result column="accepted" property="accepted"/>
|
||||
<result column="tenant_id" property="tenantId"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_by" property="updateBy"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="delete_flag" property="deleteFlag"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, encounter_id, patient_id, symptom_text, diagnosis_suggestions,
|
||||
confidence_score, suggestion_source, accepted,
|
||||
tenant_id, create_by, create_time, update_by, update_time, delete_flag
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.healthlink.his.cdss.mapper.CdssRuleExecutionMapper">
|
||||
|
||||
<resultMap type="com.healthlink.his.cdss.domain.CdssRuleExecution" id="CdssRuleExecutionResult">
|
||||
<id column="id" property="id"/>
|
||||
<result column="rule_id" property="ruleId"/>
|
||||
<result column="rule_code" property="ruleCode"/>
|
||||
<result column="encounter_id" property="encounterId"/>
|
||||
<result column="patient_id" property="patientId"/>
|
||||
<result column="matched" property="matched"/>
|
||||
<result column="execution_time" property="executionTime"/>
|
||||
<result column="execution_result" property="executionResult"/>
|
||||
<result column="duration_ms" property="durationMs"/>
|
||||
<result column="tenant_id" property="tenantId"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="delete_flag" property="deleteFlag"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, rule_id, rule_code, encounter_id, patient_id,
|
||||
matched, execution_time, execution_result, duration_ms,
|
||||
tenant_id, create_by, create_time, delete_flag
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
@@ -15,6 +15,8 @@
|
||||
<result column="department_id" property="departmentId"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="sort_order" property="sortOrder"/>
|
||||
<result column="priority" property="priority"/>
|
||||
<result column="category" property="category"/>
|
||||
<result column="tenant_id" property="tenantId"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
@@ -26,7 +28,8 @@
|
||||
<sql id="Base_Column_List">
|
||||
id, rule_code, rule_name, rule_type, severity, trigger_type,
|
||||
condition_expr, action_expr, description, department_id,
|
||||
status, sort_order, tenant_id, create_by, create_time,
|
||||
status, sort_order, priority, category,
|
||||
tenant_id, create_by, create_time,
|
||||
update_by, update_time, delete_flag
|
||||
</sql>
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
T1.diagnosis_time AS diagnosisTime,
|
||||
T1.doctor AS diagnosisDoctor,
|
||||
T1.long_term_flag AS longTermFlag,
|
||||
T1.classification,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM infectious_card T4
|
||||
WHERE T4.diag_id = T2.id AND T4.delete_flag = '0' AND T4.status >= 1
|
||||
@@ -174,5 +175,4 @@
|
||||
) AS counts
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
T1.classification,
|
||||
</mapper>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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.healthlink.his.miniprogram.mapper.MpAssessmentRecordMapper">
|
||||
|
||||
<select id="selectByPatientId" resultType="com.healthlink.his.miniprogram.domain.MpAssessmentRecord">
|
||||
SELECT id, patient_id, encounter_id, nurse_id, assessment_type,
|
||||
assessment_content, assessment_result, score, risk_level,
|
||||
record_time, create_time, create_by
|
||||
FROM mp_assessment_record
|
||||
WHERE patient_id = #{patientId}
|
||||
AND delete_flag = '0'
|
||||
ORDER BY record_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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.healthlink.his.miniprogram.mapper.MpNursingTaskMapper">
|
||||
|
||||
<select id="selectTaskListByNurse" resultType="com.healthlink.his.miniprogram.domain.MpNursingTask">
|
||||
SELECT id, patient_id, encounter_id, nurse_id, task_type, task_content,
|
||||
task_status, due_time, complete_time, create_time, create_by
|
||||
FROM mp_nursing_task
|
||||
WHERE nurse_id = #{nurseId}
|
||||
AND delete_flag = '0'
|
||||
<if test="taskStatus != null and taskStatus != ''">
|
||||
AND task_status = #{taskStatus}
|
||||
</if>
|
||||
ORDER BY
|
||||
CASE task_status
|
||||
WHEN 'IN_PROGRESS' THEN 1
|
||||
WHEN 'PENDING' THEN 2
|
||||
WHEN 'COMPLETED' THEN 3
|
||||
WHEN 'CANCELLED' THEN 4
|
||||
END,
|
||||
due_time ASC NULLS LAST
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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.healthlink.his.miniprogram.mapper.MpVitalSignRecordMapper">
|
||||
|
||||
<select id="selectByPatientId" resultType="com.healthlink.his.miniprogram.domain.MpVitalSignRecord">
|
||||
SELECT id, patient_id, encounter_id, nurse_id, record_time,
|
||||
temperature, pulse, respiration, systolic_bp, diastolic_bp,
|
||||
blood_oxygen, height, weight, create_time, create_by
|
||||
FROM mp_vital_sign_record
|
||||
WHERE patient_id = #{patientId}
|
||||
AND delete_flag = '0'
|
||||
<if test="days != null">
|
||||
AND record_time >= CURRENT_TIMESTAMP - INTERVAL CONCAT(#{days}, ' days')
|
||||
</if>
|
||||
ORDER BY record_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.healthlink.his.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum KgEntityType implements HisEnumInterface {
|
||||
|
||||
DISEASE(1, "disease", "疾病"),
|
||||
SYMPTOM(2, "symptom", "症状"),
|
||||
DRUG(3, "drug", "药物"),
|
||||
EXAM(4, "exam", "检查");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
private final String info;
|
||||
|
||||
public static KgEntityType getByCode(String code) {
|
||||
if (code == null) return null;
|
||||
for (KgEntityType e : values()) {
|
||||
if (e.getCode().equals(code)) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.healthlink.his.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum KgRelationType implements HisEnumInterface {
|
||||
|
||||
CAUSES(1, "CAUSES", "导致"),
|
||||
TREATS(2, "TREATS", "治疗"),
|
||||
CONTRAINDICATES(3, "CONTRAINDICATES", "禁忌"),
|
||||
INTERACTS_WITH(4, "INTERACTS_WITH", "相互作用"),
|
||||
REQUIRES_EXAM(5, "REQUIRES_EXAM", "需要检查"),
|
||||
HAS_SYMPTOM(6, "HAS_SYMPTOM", "具有症状"),
|
||||
SIDE_EFFECT(7, "SIDE_EFFECT", "副作用"),
|
||||
ALTERNATIVE(8, "ALTERNATIVE", "替代");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
private final String info;
|
||||
|
||||
public static KgRelationType getByCode(String code) {
|
||||
if (code == null) return null;
|
||||
for (KgRelationType e : values()) {
|
||||
if (e.getCode().equals(code)) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,7 @@ public class EncounterLocationServiceImpl extends ServiceImpl<EncounterLocationM
|
||||
// 创建 LambdaQueryWrapper
|
||||
LambdaQueryWrapper<EncounterLocation> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(EncounterLocation::getEncounterId, encounterLocation.getEncounterId())
|
||||
.eq(EncounterLocation::getFormEnum, encounterLocation.getFormEnum())
|
||||
// 状态为使用中
|
||||
.eq(EncounterLocation::getFormEnum, encounterLocation.getFormEnum());
|
||||
|
||||
// 查询是否存在记录
|
||||
EncounterLocation existingRecord = baseMapper.selectOne(queryWrapper);
|
||||
@@ -168,9 +167,7 @@ public class EncounterLocationServiceImpl extends ServiceImpl<EncounterLocationM
|
||||
}
|
||||
return baseMapper.selectList(queryWrapper);
|
||||
}
|
||||
}
|
||||
.eq(EncounterLocation::getStatusEnum, EncounterActivityStatus.ACTIVE.getValue())
|
||||
.last("LIMIT 1");
|
||||
|
||||
/**
|
||||
* 根据encounterId和formEnum清理重复的ACTIVE记录,保留最早的一条
|
||||
*
|
||||
@@ -193,3 +190,4 @@ public class EncounterLocationServiceImpl extends ServiceImpl<EncounterLocationM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.healthlink.his.aidiagnosis.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.core.common.core.domain.HisBaseEntity;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("ai_diagnosis_suggestion")
|
||||
public class AiDiagnosisSuggestion extends HisBaseEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long encounterId;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long patientId;
|
||||
|
||||
private String symptomText;
|
||||
|
||||
private String diagnosisSuggestions;
|
||||
|
||||
private BigDecimal confidenceScore;
|
||||
|
||||
private String suggestionSource;
|
||||
|
||||
private Boolean accepted;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.healthlink.his.aidiagnosis.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AiDiagnosisSuggestionMapper extends BaseMapper<AiDiagnosisSuggestion> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.aidiagnosis.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.healthlink.his.aidiagnosis.domain.AiDiagnosisSuggestion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAiDiagnosisService extends IService<AiDiagnosisSuggestion> {
|
||||
|
||||
List<AiDiagnosisSuggestion> findByPatientId(Long patientId);
|
||||
|
||||
List<AiDiagnosisSuggestion> findByEncounterId(Long encounterId);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user