docs(iron-rules): 新增铁律15+16 + 业务逻辑设计文档 + 后端增强
铁律15: 模块设计必须分析业务逻辑,不能只做CRUD - 必须查阅标准规范、梳理业务流程、设计状态流转、定义业务规则 - 附设计文档模板和医疗HIS参考标准清单 铁律16: 模块优化必须分析现有业务流并说明促进作用 - 必须回答5个问题:位置/关联/促进/兼容/冲突 - 附业务逻辑分析文档模板 业务逻辑设计文档: - MD/specs/SURGERY_MANAGEMENT_DESIGN.md (139行) - 状态机: 待申请→待审批→已审批→待手术→手术中→已完成 - 7条业务规则: 分级权限/术前讨论/术前评估/手术室冲突/禁食/随访/安全核查 - MD/specs/ORDER_MANAGEMENT_DESIGN.md - 状态机: 新开→签发→执行中→已完成/已停止/已签退 - 6条业务规则: 停止时限/用药审核/查对/紧急标识/修改限制/皮试联动 - MD/specs/BED_MANAGEMENT_DESIGN.md - 状态机: 空闲↔占用↔清洁中↔维修中 - 5条业务规则: 分配校验/科室匹配/自动清洁/使用率统计/预约 后端业务逻辑增强: - SurgeryAppService: +手术室冲突校验 +手术统计 - BedController: +床位使用率统计 +分配校验 +出院自动清洁 - EsbMessageController: +消息路由校验 +消息轨迹 +死信队列处理
This commit is contained in:
@@ -496,4 +496,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
@@ -496,4 +496,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -496,4 +496,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
@@ -496,4 +496,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
@@ -496,4 +496,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
@@ -497,4 +497,4 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
||||
|
||||
---
|
||||
|
||||
> 📅 最后同步: 2026-06-06 11:19 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
> 📅 最后同步: 2026-06-06 14:02 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||
|
||||
72
MD/specs/BED_MANAGEMENT_DESIGN.md
Normal file
72
MD/specs/BED_MANAGEMENT_DESIGN.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 床位管理模块设计文档
|
||||
|
||||
> **文档类型**: 业务设计
|
||||
> **版本**: v1.0
|
||||
> **编制日期**: 2026-06-06
|
||||
> **依据标准**: 《三级医院评审标准(2022版)》床位使用率指标
|
||||
|
||||
---
|
||||
|
||||
## 一、业务背景
|
||||
|
||||
床位管理直接影响医院运营效率。三甲医院评审要求床位使用率≥85%,床位周转次数达标。需要实时掌握床位状态,支持智能分配。
|
||||
|
||||
---
|
||||
|
||||
## 二、状态流转
|
||||
|
||||
### 2.1 床位状态机
|
||||
|
||||
```
|
||||
空闲(0) → 占用(1) → 清洁中(2) → 空闲(0)
|
||||
↓
|
||||
维修中(3) → 空闲(0)
|
||||
```
|
||||
|
||||
| 状态 | 值 | 触发条件 | 允许操作 |
|
||||
|------|-----|---------|---------|
|
||||
| 空闲 | 0 | 清洁完成/新床 | 分配患者 |
|
||||
| 占用 | 1 | 患者入院分配 | 患者转科/出院 |
|
||||
| 清洁中 | 2 | 患者出院后 | 清洁完成→空闲 |
|
||||
| 维修中 | 3 | 设备故障 | 维修完成→空闲 |
|
||||
|
||||
---
|
||||
|
||||
## 三、业务规则
|
||||
|
||||
| 规则编号 | 规则名称 | 规则描述 | 触发时机 |
|
||||
|---------|---------|---------|---------|
|
||||
| BR-001 | 床位分配校验 | 只有"空闲"状态的床位才能分配 | 入院登记时 |
|
||||
| BR-002 | 科室匹配 | 床位所属科室必须与患者入院科室一致 | 入院登记时 |
|
||||
| BR-003 | 出院自动清洁 | 患者出院后床位自动变为"清洁中" | 出院结算时 |
|
||||
| BR-004 | 使用率统计 | 实时计算科室/全院床位使用率 | 定时任务 |
|
||||
| BR-005 | 床位预约 | 支持预约指定床位(限时保留) | 预约住院时 |
|
||||
|
||||
---
|
||||
|
||||
## 四、数据模型
|
||||
|
||||
### 床位使用率计算公式
|
||||
```
|
||||
科室床位使用率 = (占用床位数 / 总床位数) × 100%
|
||||
全院床位使用率 = (全院占用床位数 / 全院总床位数) × 100%
|
||||
床位周转次数 = 出院人次 / 平均开放床位数
|
||||
```
|
||||
|
||||
### 床位占用时长统计
|
||||
```
|
||||
平均住院天数 = Σ(出院日期 - 入院日期) / 出院人次
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、测试用例
|
||||
|
||||
| 用例编号 | 场景 | 预期结果 |
|
||||
|---------|------|---------|
|
||||
| TC-B001 | 正常分配 | 空闲床位→占用,状态正确 |
|
||||
| TC-B002 | 分配已占用床位 | 返回"该床位已被占用" |
|
||||
| TC-B003 | 出院自动清洁 | 出院后床位变为"清洁中" |
|
||||
| TC-B004 | 使用率计算 | 数据准确反映实际使用情况 |
|
||||
| TC-B005 | 维修中分配 | 返回"该床位维修中" |
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
| #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 |
|
||||
| #9 | 开发前必须审核原有代码 | P0 | 全量开发 |
|
||||
| #10 | 设计文档必须包含UI设计和调用流程 | P0 | 设计文档/前端开发 |
|
||||
| #11 | 模块设计必须分析业务逻辑,不能只做CRUD | P0 | 全量模块设计 |
|
||||
| #12 | 模块优化必须分析现有业务流并说明促进作用 | P0 | 全量模块优化 |
|
||||
|
||||
---
|
||||
|
||||
@@ -309,6 +311,129 @@ npm run lint
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 铁律 #12: 模块优化必须分析现有业务流并说明促进作用
|
||||
|
||||
**任何模块新增/优化前,必须先分析现有业务流程全貌。**
|
||||
|
||||
#### 必须回答的5个问题
|
||||
|
||||
| # | 问题 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | 该模块在整体业务流中处于什么位置? | 上游/下游/并行 |
|
||||
| 2 | 该模块与哪些现有模块有数据流转关系? | 列出所有关联模块 |
|
||||
| 3 | 优化对上下游模块有什么促进作用? | 减少重复、提升一致性、加快流程 |
|
||||
| 4 | 变更是否影响现有业务流程? | 兼容性评估 |
|
||||
| 5 | 业务规则是否与现有模块冲突? | 规则一致性检查 |
|
||||
|
||||
#### 业务逻辑分析文档模板
|
||||
|
||||
```
|
||||
# 模块名 — 业务逻辑分析
|
||||
|
||||
## 1. 整体业务流程定位
|
||||
[该模块在HIS系统中的位置,上下游关系图]
|
||||
|
||||
## 2. 关联模块分析
|
||||
| 关联模块 | 数据流向 | 交互方式 | 影响程度 |
|
||||
|---------|---------|---------|---------|
|
||||
|
||||
## 3. 优化促进作用
|
||||
| 维度 | 优化前 | 优化后 | 提升效果 |
|
||||
|------|--------|--------|---------|
|
||||
|
||||
## 4. 兼容性评估
|
||||
- 对现有模块的影响
|
||||
- 数据迁移需求
|
||||
- 接口变更影响
|
||||
|
||||
## 5. 规则一致性检查
|
||||
- 新增规则是否与现有规则冲突
|
||||
- 状态流转是否与现有状态机兼容
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 铁律 #11: 模块设计必须分析业务逻辑,不能只做CRUD
|
||||
|
||||
**任何新模块/功能开发前,必须先进行业务逻辑分析和梳理。**
|
||||
|
||||
#### 禁止行为
|
||||
- ❌ 拿到需求就直接写CRUD,不思考业务流程
|
||||
- ❌ 不查阅标准规范就开发医疗业务模块
|
||||
- ❌ 没有设计文档就直接编码
|
||||
- ❌ 把"能增删改查"当成"功能完成"
|
||||
|
||||
#### 必须完成的设计步骤
|
||||
|
||||
| # | 步骤 | 产出物 | 说明 |
|
||||
|---|------|--------|------|
|
||||
| 1 | 查阅标准规范 | 参考文档清单 | 国家卫健委标准、医保局规范、HL7/FHIR、三甲评审标准 |
|
||||
| 2 | 梳理业务流程 | 流程图/文字描述 | 正常流程 + 异常流程 + 边界场景 |
|
||||
| 3 | 设计状态流转 | 状态机图 | 每个实体的生命周期、状态转换条件 |
|
||||
| 4 | 定义业务规则 | 规则清单 | 如:药品相互作用规则、医保审核规则、危急值判定规则 |
|
||||
| 5 | 设计交互时序 | 时序图 | 用户操作 → 前端事件 → API → 后端处理 → 持久化 → 响应 |
|
||||
| 6 | 编写设计文档 | MD文件 | 保存到 `MD/specs/` 或 `MD/architecture/` |
|
||||
|
||||
#### 医疗HIS业务逻辑参考标准
|
||||
|
||||
| 标准/规范 | 适用模块 | 获取途径 |
|
||||
|----------|---------|---------|
|
||||
| 三级医院评审标准(2022版) | 全量 | 卫健委官网 |
|
||||
| 电子病历应用水平分级评价 | 电子病历/质控 | 卫健委官网 |
|
||||
| 互联互通标准化成熟度测评 | ESB/集成平台 | 卫健委官网 |
|
||||
| 医保基金使用监督管理条例 | 医保审核/结算 | 医保局官网 |
|
||||
| HL7 FHIR R4 | 数据交换/ESB | hl7.org |
|
||||
| 处方管理办法 | 合理用药/处方 | 卫健委官网 |
|
||||
| 抗菌药物临床应用管理办法 | 抗菌药物管理 | 卫健委官网 |
|
||||
| 医院感染管理办法 | 院感管理 | 卫健委官网 |
|
||||
| 病案管理与质量控制标准 | 病案管理 | 卫健委官网 |
|
||||
|
||||
#### 设计文档模板
|
||||
|
||||
```
|
||||
# 模块名 设计文档
|
||||
|
||||
## 1. 业务背景
|
||||
- 依据什么标准/规范
|
||||
- 解决什么业务问题
|
||||
|
||||
## 2. 业务流程
|
||||
### 2.1 正常流程
|
||||
[流程描述/流程图]
|
||||
|
||||
### 2.2 异常流程
|
||||
[异常场景及处理方式]
|
||||
|
||||
### 2.3 边界场景
|
||||
[特殊情况处理]
|
||||
|
||||
## 3. 状态流转
|
||||
| 状态 | 值 | 触发条件 | 下一状态 |
|
||||
|------|-----|---------|---------|
|
||||
|
||||
## 4. 业务规则
|
||||
| 规则编号 | 规则名称 | 规则描述 | 触发时机 |
|
||||
|---------|---------|---------|---------|
|
||||
|
||||
## 5. 数据模型
|
||||
[实体关系图/表结构设计]
|
||||
|
||||
## 6. 接口设计
|
||||
[API列表+参数+返回值]
|
||||
|
||||
## 7. 前端页面设计
|
||||
[UI布局+交互+调用流程]
|
||||
|
||||
## 8. 测试用例
|
||||
[关键业务场景测试]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 铁律 #10: 设计文档必须包含UI设计和调用流程
|
||||
|
||||
**所有新模块/页面的设计文档必须包含以下要素,缺一不可:**
|
||||
|
||||
91
MD/specs/ORDER_MANAGEMENT_DESIGN.md
Normal file
91
MD/specs/ORDER_MANAGEMENT_DESIGN.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 医嘱管理模块设计文档
|
||||
|
||||
> **文档类型**: 业务设计
|
||||
> **版本**: v1.0
|
||||
> **编制日期**: 2026-06-06
|
||||
> **依据标准**: 《三级医院评审标准(2022版)》医嘱管理制度
|
||||
|
||||
---
|
||||
|
||||
## 一、业务背景
|
||||
|
||||
医嘱管理是住院诊疗的核心环节。依据《病历书写基本规范》和《处方管理办法》,医嘱必须经过开具→审核→执行→完成的完整闭环。
|
||||
|
||||
---
|
||||
|
||||
## 二、状态流转
|
||||
|
||||
### 2.1 医嘱状态机
|
||||
|
||||
```
|
||||
新开(0) → 已签发(1) → 执行中(2) → 已完成(3)
|
||||
↓
|
||||
已停止(4) → 已取消停嘱(恢复)(2)
|
||||
↓
|
||||
已签退(5)
|
||||
```
|
||||
|
||||
| 状态 | 值 | 触发条件 | 允许操作 |
|
||||
|------|-----|---------|---------|
|
||||
| 新开 | 0 | 医生新开医嘱 | 签发/删除 |
|
||||
| 已签发 | 1 | 医生签发 | 护士执行/签退 |
|
||||
| 执行中 | 2 | 护士开始执行 | 停止/完成 |
|
||||
| 已完成 | 3 | 执行完毕 | 查看 |
|
||||
| 已停止 | 4 | 医生停止医嘱 | 恢复(取消停嘱) |
|
||||
| 已签退 | 5 | 护士签退 | 查看 |
|
||||
|
||||
---
|
||||
|
||||
## 三、业务规则
|
||||
|
||||
| 规则编号 | 规则名称 | 规则描述 | 触发时机 |
|
||||
|---------|---------|---------|---------|
|
||||
| OR-001 | 长期医嘱停止时限 | 长期医嘱停止必须在执行时间之前2小时 | 停止医嘱时 |
|
||||
| OR-002 | 用药医嘱审核 | 用药医嘱必须经过合理用药系统审核 | 签发用药医嘱时 |
|
||||
| OR-003 | 医嘱查对 | 执行医嘱前必须双人查对 | 护士执行时 |
|
||||
| OR-004 | 紧急医嘱标识 | 紧急医嘱需要特殊标识和优先执行 | 开具医嘱时 |
|
||||
| OR-005 | 医嘱修改限制 | 已签发的医嘱不能修改,只能停止后新开 | 修改医嘱时 |
|
||||
| OR-006 | 皮试医嘱联动 | 需要皮试的药物必须关联皮试医嘱 | 开具需皮试药物时 |
|
||||
|
||||
---
|
||||
|
||||
## 四、前后端交互时序
|
||||
|
||||
### 4.1 签发医嘱
|
||||
```
|
||||
用户操作: 医生点击"签发医嘱"
|
||||
→ 前端: 收集选中医嘱列表
|
||||
→ API: POST /reg-doctorstation/advice-manage/sign-reg-advice
|
||||
→ 后端: AdviceManageController.signRegAdvice()
|
||||
→ 校验医嘱状态必须为"新开"(OR-005)
|
||||
→ 用药医嘱调用合理用药系统审核(OR-002)
|
||||
→ 设置签发时间+签发人
|
||||
→ 更新状态=已签发(1)
|
||||
→ 返回: {code:200, msg:"签发成功"}
|
||||
→ 前端: 刷新医嘱列表
|
||||
```
|
||||
|
||||
### 4.2 停止医嘱
|
||||
```
|
||||
用户操作: 医生点击"停止医嘱"
|
||||
→ 前端: 弹出确认框+填写停嘱原因
|
||||
→ API: POST /reg-doctorstation/advice-manage/stop-reg-advice
|
||||
→ 后端: 校验医嘱状态必须为"执行中"
|
||||
→ 长期医嘱校验停止时限(OR-001)
|
||||
→ 设置停嘱时间+停嘱原因
|
||||
→ 更新状态=已停止(4)
|
||||
→ 返回: {code:200, msg:"停嘱成功"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、测试用例
|
||||
|
||||
| 用例编号 | 场景 | 预期结果 |
|
||||
|---------|------|---------|
|
||||
| TC-O001 | 正常签发流程 | 新开→签发→执行→完成 |
|
||||
| TC-O002 | 签发后修改 | 返回"已签发医嘱不能修改" |
|
||||
| TC-O003 | 停止后恢复 | 已停止→恢复→执行中 |
|
||||
| TC-O004 | 用药审核拦截 | 有相互作用的药物签发时被拦截 |
|
||||
| TC-O005 | 紧急医嘱优先 | 紧急医嘱在列表中高亮显示 |
|
||||
|
||||
139
MD/specs/SURGERY_MANAGEMENT_DESIGN.md
Normal file
139
MD/specs/SURGERY_MANAGEMENT_DESIGN.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 手术管理模块设计文档
|
||||
|
||||
> **文档类型**: 业务设计
|
||||
> **版本**: v1.0
|
||||
> **编制日期**: 2026-06-06
|
||||
> **依据标准**: 《三级医院评审标准(2022版)》手术质量安全核心制度
|
||||
|
||||
---
|
||||
|
||||
## 一、业务背景
|
||||
|
||||
手术管理是三甲医院评审的核心检查项。依据《医疗质量安全核心制度》中的"手术分级管理制度"和"术前讨论制度",手术必须经过完整的术前评估→审批→执行→术后跟踪流程。
|
||||
|
||||
### 参考标准
|
||||
- 三级医院评审标准(2022版) — 手术质量安全核心指标
|
||||
- 电子病历应用水平分级评价 — 4级要求手术信息全院共享
|
||||
- 《手术分级管理办法》— 手术分级授权管理
|
||||
- 《病案管理与质量控制标准》— 手术记录规范
|
||||
|
||||
---
|
||||
|
||||
## 二、状态流转
|
||||
|
||||
### 2.1 手术状态机
|
||||
|
||||
```
|
||||
待申请(0) → 待审批(1) → 已审批(2) → 待手术(3) → 手术中(4) → 已完成(5)
|
||||
↓ ↓
|
||||
已驳回(6) 已取消(7)
|
||||
```
|
||||
|
||||
| 状态 | 值 | 触发条件 | 允许操作 |
|
||||
|------|-----|---------|---------|
|
||||
| 待申请 | 0 | 医生提交手术申请 | 编辑/删除/提交审批 |
|
||||
| 待审批 | 1 | 提交审批 | 审批/驳回 |
|
||||
| 已审批 | 2 | 科主任审批通过 | 安排手术室/取消 |
|
||||
| 待手术 | 3 | 安排手术室和时间 | 开始手术/取消 |
|
||||
| 手术中 | 4 | 主刀医生确认开始 | 记录术中事件/完成 |
|
||||
| 已完成 | 5 | 主刀医生确认完成 | 查看/打印记录 |
|
||||
| 已驳回 | 6 | 科主任驳回 | 编辑后重新提交 |
|
||||
| 已取消 | 7 | 任意阶段取消 | 查看 |
|
||||
|
||||
### 2.2 手术分级
|
||||
|
||||
| 级别 | 名称 | 审批权限 | 示例 |
|
||||
|------|------|---------|------|
|
||||
| 一级 | 一级手术 | 住院医师可独立完成 | 阑尾切除术 |
|
||||
| 二级 | 二级手术 | 主治医师以上 | 胃大部切除术 |
|
||||
| 三级 | 三级手术 | 副主任医师以上 | 心脏搭桥术 |
|
||||
| 四级 | 四级手术 | 科主任审批+医务部备案 | 器官移植术 |
|
||||
|
||||
---
|
||||
|
||||
## 三、业务规则
|
||||
|
||||
| 规则编号 | 规则名称 | 规则描述 | 触发时机 |
|
||||
|---------|---------|---------|---------|
|
||||
| SR-001 | 手术分级权限校验 | 医生只能申请其权限范围内的手术级别 | 提交申请时 |
|
||||
| SR-002 | 术前讨论记录 | 三级/四级手术必须有术前讨论记录 | 提交审批时 |
|
||||
| SR-003 | 术前评估 | 必须完成麻醉评估和手术风险评估 | 安排手术时 |
|
||||
| SR-004 | 手术室冲突检查 | 同一手术室同一时间不能安排两台手术 | 安排手术室时 |
|
||||
| SR-005 | 术前禁食提醒 | 手术前8小时禁止进食,4小时禁止饮水 | 手术前1天 |
|
||||
| SR-006 | 术后随访 | 手术后24h/48h/72h必须有随访记录 | 完成手术后 |
|
||||
| SR-007 | 手术安全核查 | 术前/术中/术后三次安全核查(WS/T 313) | 手术各阶段 |
|
||||
|
||||
---
|
||||
|
||||
## 四、前后端交互时序
|
||||
|
||||
### 4.1 提交手术申请
|
||||
```
|
||||
用户操作: 医生点击"提交手术申请"
|
||||
→ 前端: 校验表单(必填项+业务规则前端预检)
|
||||
→ API: POST /clinical-manage/surgery/surgery
|
||||
→ 后端: SurgeryController.addSurgery()
|
||||
→ SurgeryAppService.addSurgery()
|
||||
→ 校验手术分级权限(SR-001)
|
||||
→ 校验三级/四级手术术前讨论(SR-002)
|
||||
→ 设置状态=待申请(0)
|
||||
→ 保存到数据库
|
||||
→ 返回: {code:200, msg:"申请已提交"}
|
||||
→ 前端: ElMessage.success → 刷新列表
|
||||
```
|
||||
|
||||
### 4.2 安排手术室
|
||||
```
|
||||
用户操作: 护士长点击"安排手术"
|
||||
→ 前端: 弹出安排弹窗(选择手术室/时间/麻醉医生)
|
||||
→ API: PUT /clinical-manage/surgery/surgery (携带手术室+时间信息)
|
||||
→ 后端: 校验手术室冲突(SR-004)
|
||||
→ 校验术前评估完成(SR-003)
|
||||
→ 更新状态=待手术(3)
|
||||
→ 返回: {code:200, msg:"安排成功"}
|
||||
```
|
||||
|
||||
### 4.3 开始/完成手术
|
||||
```
|
||||
用户操作: 主刀医生点击"开始手术"
|
||||
→ API: PUT /clinical-manage/surgery/surgery-status?id=&statusEnum=4
|
||||
→ 后端: 更新状态=手术中(4), 记录开始时间
|
||||
|
||||
用户操作: 主刀医生点击"完成手术"
|
||||
→ API: PUT /clinical-manage/surgery/surgery-status?id=&statusEnum=5
|
||||
→ 后端: 更新状态=已完成(5), 记录结束时间
|
||||
→ 触发术后随访提醒(SR-006)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型扩展
|
||||
|
||||
现有 `SurgeryDto` 需增加以下字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| surgeryLevel | String | 手术级别(1/2/3/4) |
|
||||
| surgeryRoom | String | 手术室 |
|
||||
| anesthesiaType | String | 麻醉方式(全麻/局麻/脊麻/硬膜外) |
|
||||
| preopDiagnosis | String | 术前诊断 |
|
||||
| postopDiagnosis | String | 术后诊断 |
|
||||
| startTime | DateTime | 实际开始时间 |
|
||||
| endTime | DateTime | 实际结束时间 |
|
||||
| complications | String | 并发症记录 |
|
||||
| bloodLoss | Integer | 术中出血量(ml) |
|
||||
| specimenSent | Boolean | 是否送检标本 |
|
||||
|
||||
---
|
||||
|
||||
## 六、测试用例
|
||||
|
||||
| 用例编号 | 场景 | 操作步骤 | 预期结果 |
|
||||
|---------|------|---------|---------|
|
||||
| TC-S001 | 正常申请流程 | 医生提交→科主任审批→护士安排→手术完成 | 状态正确流转 |
|
||||
| TC-S002 | 越级申请拒绝 | 住院医师申请四级手术 | 返回权限不足错误 |
|
||||
| TC-S003 | 手术室冲突 | 同一时间安排两台手术到同一手术室 | 返回冲突提示 |
|
||||
| TC-S004 | 驳回后重新提交 | 科主任驳回→医生修改→重新提交 | 状态从驳回回到待审批 |
|
||||
| TC-S005 | 取消手术 | 已审批的手术点击取消 | 状态变为已取消 |
|
||||
| TC-S006 | 缺少术前讨论 | 三级手术无术前讨论记录直接提交 | 拦截并提示 |
|
||||
|
||||
@@ -52,4 +52,56 @@ public class BedController {
|
||||
Bed bed = new Bed(); bed.setId(id); bed.setStatus(status);
|
||||
return bedService.updateById(bed) ? R.ok("状态更新成功") : R.fail("状态更新失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取床位使用统计(铁律15: BR-004)
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public R<?> getStatistics(@RequestParam(value = "deptId", required = false) Long deptId) {
|
||||
LambdaQueryWrapper<Bed> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (deptId != null) baseWrapper.eq(Bed::getDeptId, deptId);
|
||||
|
||||
long total = bedService.count(baseWrapper);
|
||||
long occupied = bedService.count(baseWrapper.clone().eq(Bed::getStatus, 1));
|
||||
long available = bedService.count(baseWrapper.clone().eq(Bed::getStatus, 0));
|
||||
long cleaning = bedService.count(baseWrapper.clone().eq(Bed::getStatus, 2));
|
||||
long maintenance = bedService.count(baseWrapper.clone().eq(Bed::getStatus, 3));
|
||||
double usageRate = total > 0 ? (double) occupied / total * 100 : 0;
|
||||
|
||||
return R.ok(java.util.Map.of(
|
||||
"total", total, "occupied", occupied, "available", available,
|
||||
"cleaning", cleaning, "maintenance", maintenance,
|
||||
"usageRate", Math.round(usageRate * 100.0) / 100.0
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配床位(铁律15: BR-001 + BR-002)
|
||||
*/
|
||||
@PutMapping("/assign")
|
||||
public R<?> assignBed(@RequestParam Long bedId, @RequestParam Long patientId, @RequestParam Long deptId) {
|
||||
Bed bed = bedService.getById(bedId);
|
||||
if (bed == null) return R.fail("床位不存在");
|
||||
if (bed.getStatus() != 0) return R.fail("该床位当前不可分配(状态: " +
|
||||
java.util.Map.of(0, "空闲", 1, "占用", 2, "清洁中", 3, "维修中").getOrDefault(bed.getStatus(), "未知") + ")");
|
||||
if (bed.getDeptId() != null && !bed.getDeptId().equals(deptId)) {
|
||||
return R.fail("床位所属科室与患者入院科室不匹配");
|
||||
}
|
||||
bed.setStatus(1);
|
||||
bedService.updateById(bed);
|
||||
return R.ok("分配成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 出院释放床位(铁律15: BR-003)
|
||||
*/
|
||||
@PutMapping("/discharge")
|
||||
public R<?> dischargeBed(@RequestParam Long bedId) {
|
||||
Bed bed = bedService.getById(bedId);
|
||||
if (bed == null) return R.fail("床位不存在");
|
||||
bed.setStatus(2); // 清洁中
|
||||
bedService.updateById(bed);
|
||||
return R.ok("已标记为清洁中");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.healthlink.his.administration.domain.Encounter;
|
||||
import com.healthlink.his.web.clinicalmanage.dto.SurgeryDto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 手术管理应用Service接口
|
||||
@@ -77,4 +78,24 @@ public interface ISurgeryAppService {
|
||||
* @return 就诊列表
|
||||
*/
|
||||
R<List<Encounter>> getEncounterListByPatientId(Long patientId);
|
||||
|
||||
/**
|
||||
* 校验手术室冲突(铁律15: SR-004)
|
||||
* 检查同一手术室同一时间是否有其他手术安排
|
||||
*
|
||||
* @param surgeryRoom 手术室
|
||||
* @param plannedTime 计划时间
|
||||
* @param excludeId 排除的手术ID(编辑时用)
|
||||
* @return 校验结果
|
||||
*/
|
||||
R<?> checkOperatingRoomConflict(String surgeryRoom, String plannedTime, Long excludeId);
|
||||
|
||||
/**
|
||||
* 获取手术统计信息
|
||||
*
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 统计数据
|
||||
*/
|
||||
Map<String, Object> getSurgeryStatistics(String startDate, String endDate);
|
||||
}
|
||||
|
||||
@@ -835,4 +835,44 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
||||
log.info("清除就诊手术列表缓存 - encounterId: {}", surgery.getEncounterId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验手术室冲突(铁律15: SR-004)
|
||||
*/
|
||||
@Override
|
||||
public R<?> checkOperatingRoomConflict(String surgeryRoom, String plannedTime, Long excludeId) {
|
||||
if (surgeryRoom == null || plannedTime == null) {
|
||||
return R.ok(false);
|
||||
}
|
||||
LambdaQueryWrapper<Surgery> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Surgery::getOperatingRoomName, surgeryRoom)
|
||||
.eq(Surgery::getPlannedTime, plannedTime)
|
||||
.in(Surgery::getStatusEnum, 2, 3) // 已审批或待手术
|
||||
.ne(excludeId != null, Surgery::getId, excludeId);
|
||||
long conflictCount = surgeryService.count(wrapper);
|
||||
if (conflictCount > 0) {
|
||||
return R.fail("该手术室在该时间段已有手术安排,请选择其他时间或手术室");
|
||||
}
|
||||
return R.ok(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手术统计信息(铁律15: 统计分析)
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getSurgeryStatistics(String startDate, String endDate) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
LambdaQueryWrapper<Surgery> wrapper = new LambdaQueryWrapper<>();
|
||||
if (startDate != null && endDate != null) {
|
||||
wrapper.between(Surgery::getCreateTime, startDate, endDate);
|
||||
}
|
||||
stats.put("total", surgeryService.count(wrapper));
|
||||
stats.put("pending", surgeryService.count(wrapper.clone().eq(Surgery::getStatusEnum, 0)));
|
||||
stats.put("approved", surgeryService.count(wrapper.clone().eq(Surgery::getStatusEnum, 2)));
|
||||
stats.put("inProgress", surgeryService.count(wrapper.clone().eq(Surgery::getStatusEnum, 4)));
|
||||
stats.put("completed", surgeryService.count(wrapper.clone().eq(Surgery::getStatusEnum, 5)));
|
||||
stats.put("cancelled", surgeryService.count(wrapper.clone().eq(Surgery::getStatusEnum, 7)));
|
||||
return stats;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -115,4 +115,26 @@ public class SurgeryController {
|
||||
public R<List<Encounter>> getEncounterListByPatientId(@RequestParam Long patientId) {
|
||||
return surgeryAppService.getEncounterListByPatientId(patientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验手术室冲突(铁律15: SR-004)
|
||||
*/
|
||||
@GetMapping(value = "/check-room-conflict")
|
||||
public R<?> checkOperatingRoomConflict(
|
||||
@RequestParam String surgeryRoom,
|
||||
@RequestParam String plannedTime,
|
||||
@RequestParam(value = "excludeId", required = false) Long excludeId) {
|
||||
return surgeryAppService.checkOperatingRoomConflict(surgeryRoom, plannedTime, excludeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手术统计信息
|
||||
*/
|
||||
@GetMapping(value = "/statistics")
|
||||
public R<?> getSurgeryStatistics(
|
||||
@RequestParam(value = "startDate", required = false) String startDate,
|
||||
@RequestParam(value = "endDate", required = false) String endDate) {
|
||||
return R.ok(surgeryAppService.getSurgeryStatistics(startDate, endDate));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.healthlink.his.web.esbmanage.controller;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.esb.domain.EsbMessage;
|
||||
import com.healthlink.his.esb.service.IEsbMessageService;
|
||||
import com.healthlink.his.esb.domain.EsbServiceRegistry;
|
||||
import com.healthlink.his.esb.service.IEsbServiceRegistryService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.UUID;
|
||||
@RestController
|
||||
@RequestMapping("/esb/message")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class EsbMessageController {
|
||||
private final IEsbMessageService esbMessageService;
|
||||
private final IEsbServiceRegistryService registryService;
|
||||
@GetMapping("/page")
|
||||
public R<?> getPage(@RequestParam(value = "sourceSystem", required = false) String sourceSystem,
|
||||
@RequestParam(value = "targetSystem", required = false) String targetSystem,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
LambdaQueryWrapper<EsbMessage> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(StringUtils.hasText(sourceSystem), EsbMessage::getSourceSystem, sourceSystem)
|
||||
.eq(StringUtils.hasText(targetSystem), EsbMessage::getTargetSystem, targetSystem)
|
||||
.eq(StringUtils.hasText(status), EsbMessage::getStatus, status)
|
||||
.orderByDesc(EsbMessage::getCreateTime);
|
||||
return R.ok(esbMessageService.page(new Page<>(pageNo, pageSize), wrapper));
|
||||
}
|
||||
@PostMapping("/send")
|
||||
public R<?> sendMessage(@RequestBody EsbMessage message) {
|
||||
message.setMessageId(UUID.randomUUID().toString().replace("-", ""));
|
||||
message.setStatus("待发送");
|
||||
message.setRetryCount(0);
|
||||
boolean result = esbMessageService.save(message);
|
||||
return result ? R.ok("消息已提交") : R.fail("提交失败");
|
||||
}
|
||||
@PutMapping("/retry/{id}")
|
||||
public R<?> retryMessage(@PathVariable Long id) {
|
||||
EsbMessage msg = esbMessageService.getById(id);
|
||||
if (msg == null) return R.fail("消息不存在");
|
||||
msg.setStatus("重试中");
|
||||
msg.setRetryCount(msg.getRetryCount() + 1);
|
||||
esbMessageService.updateById(msg);
|
||||
return R.ok("重试已提交");
|
||||
}
|
||||
@GetMapping("/stats")
|
||||
public R<?> getStats() {
|
||||
long pending = esbMessageService.count(new LambdaQueryWrapper<EsbMessage>().eq(EsbMessage::getStatus, "待发送"));
|
||||
long sent = esbMessageService.count(new LambdaQueryWrapper<EsbMessage>().eq(EsbMessage::getStatus, "已发送"));
|
||||
long failed = esbMessageService.count(new LambdaQueryWrapper<EsbMessage>().eq(EsbMessage::getStatus, "发送失败"));
|
||||
long total = esbMessageService.count();
|
||||
return R.ok(java.util.Map.of("pending", pending, "sent", sent, "failed", failed, "total", total));
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息路由校验(铁律15: ESB业务规则)
|
||||
* 校验目标系统是否已注册且状态为启用
|
||||
*/
|
||||
@PostMapping("/route-check")
|
||||
public R<?> checkRoute(@RequestParam String targetSystem) {
|
||||
LambdaQueryWrapper<EsbServiceRegistry> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EsbServiceRegistry::getServiceName, targetSystem)
|
||||
.eq(EsbServiceRegistry::getServiceStatus, "启用");
|
||||
boolean exists = registryService.count(wrapper) > 0;
|
||||
if (!exists) {
|
||||
return R.fail("目标系统 '" + targetSystem + "' 未注册或已停用,无法路由消息");
|
||||
}
|
||||
return R.ok("路由校验通过");
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息轨迹查询
|
||||
*/
|
||||
@GetMapping("/trace/{messageId}")
|
||||
public R<?> getTrace(@PathVariable String messageId) {
|
||||
LambdaQueryWrapper<EsbMessage> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EsbMessage::getMessageId, messageId).orderByAsc(EsbMessage::getCreateTime);
|
||||
return R.ok(esbMessageService.list(wrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 死信队列处理(铁律15: 失败消息处理)
|
||||
* 重置失败消息状态为待发送
|
||||
*/
|
||||
@PutMapping("/reset-dead-letter")
|
||||
public R<?> resetDeadLetter(@RequestParam Long id) {
|
||||
EsbMessage msg = esbMessageService.getById(id);
|
||||
if (msg == null) return R.fail("消息不存在");
|
||||
if (!"发送失败".equals(msg.getStatus())) return R.fail("只能重置失败消息");
|
||||
msg.setStatus("待发送");
|
||||
msg.setRetryCount(0);
|
||||
msg.setErrorMessage(null);
|
||||
esbMessageService.updateById(msg);
|
||||
return R.ok("消息已重置为待发送");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.healthlink.his.web.esbmanage.controller;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.esb.domain.EsbServiceRegistry;
|
||||
import com.healthlink.his.esb.service.IEsbServiceRegistryService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/esb/registry")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class EsbServiceRegistryController {
|
||||
private final IEsbServiceRegistryService registryService;
|
||||
@GetMapping("/page")
|
||||
public R<?> getPage(@RequestParam(value = "serviceName", required = false) String serviceName,
|
||||
@RequestParam(value = "serviceStatus", required = false) String serviceStatus,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
LambdaQueryWrapper<EsbServiceRegistry> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.like(StringUtils.hasText(serviceName), EsbServiceRegistry::getServiceName, serviceName)
|
||||
.eq(StringUtils.hasText(serviceStatus), EsbServiceRegistry::getServiceStatus, serviceStatus)
|
||||
.orderByAsc(EsbServiceRegistry::getServiceName);
|
||||
return R.ok(registryService.page(new Page<>(pageNo, pageSize), wrapper));
|
||||
}
|
||||
@GetMapping("/list")
|
||||
public R<List<EsbServiceRegistry>> getList() {
|
||||
return R.ok(registryService.list(new LambdaQueryWrapper<EsbServiceRegistry>().orderByAsc(EsbServiceRegistry::getServiceName)));
|
||||
}
|
||||
@PostMapping("/add")
|
||||
public R<?> add(@RequestBody EsbServiceRegistry registry) {
|
||||
registry.setServiceStatus("启用");
|
||||
return registryService.save(registry) ? R.ok("注册成功") : R.fail("注册失败");
|
||||
}
|
||||
@PutMapping("/update")
|
||||
public R<?> update(@RequestBody EsbServiceRegistry registry) {
|
||||
return registryService.updateById(registry) ? R.ok("修改成功") : R.fail("修改失败");
|
||||
}
|
||||
@DeleteMapping("/delete")
|
||||
public R<?> delete(@RequestParam Long id) {
|
||||
return registryService.removeById(id) ? R.ok("删除成功") : R.fail("删除失败");
|
||||
}
|
||||
@PutMapping("/status")
|
||||
public R<?> updateStatus(@RequestParam Long id, @RequestParam String status) {
|
||||
EsbServiceRegistry reg = new EsbServiceRegistry(); reg.setId(id); reg.setServiceStatus(status);
|
||||
return registryService.updateById(reg) ? R.ok("状态更新成功") : R.fail("状态更新失败");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE TABLE IF NOT EXISTS sys_esb_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
message_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
message_type VARCHAR(50),
|
||||
source_system VARCHAR(50),
|
||||
target_system VARCHAR(50),
|
||||
message_content TEXT,
|
||||
message_format VARCHAR(20),
|
||||
status VARCHAR(20) DEFAULT '待发送',
|
||||
retry_count INT DEFAULT 0,
|
||||
error_message TEXT,
|
||||
send_time TIMESTAMP,
|
||||
ack_time TIMESTAMP,
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP,
|
||||
tenant_id INT DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_esb_msg_status ON sys_esb_message(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_esb_msg_source ON sys_esb_message(source_system);
|
||||
CREATE INDEX IF NOT EXISTS idx_esb_msg_target ON sys_esb_message(target_system);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_esb_service_registry (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
service_name VARCHAR(100) NOT NULL,
|
||||
service_version VARCHAR(20),
|
||||
service_endpoint VARCHAR(500),
|
||||
service_description TEXT,
|
||||
service_status VARCHAR(20) DEFAULT '启用',
|
||||
protocol VARCHAR(20) DEFAULT 'HTTP',
|
||||
timeout_ms INT DEFAULT 5000,
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP,
|
||||
tenant_id INT DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_esb_reg_name ON sys_esb_service_registry(service_name);
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.healthlink.his.esb.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 lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.util.Date;
|
||||
@Data
|
||||
@TableName("sys_esb_message")
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class EsbMessage extends HisBaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String messageId;
|
||||
private String messageType;
|
||||
private String sourceSystem;
|
||||
private String targetSystem;
|
||||
private String messageContent;
|
||||
private String messageFormat;
|
||||
private String status;
|
||||
private Integer retryCount;
|
||||
private String errorMessage;
|
||||
private Date sendTime;
|
||||
private Date ackTime;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.healthlink.his.esb.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 lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@Data
|
||||
@TableName("sys_esb_service_registry")
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class EsbServiceRegistry extends HisBaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String serviceName;
|
||||
private String serviceVersion;
|
||||
private String serviceEndpoint;
|
||||
private String serviceDescription;
|
||||
private String serviceStatus;
|
||||
private String protocol;
|
||||
private Integer timeoutMs;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.healthlink.his.esb.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.esb.domain.EsbMessage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface EsbMessageMapper extends BaseMapper<EsbMessage> {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.healthlink.his.esb.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.esb.domain.EsbServiceRegistry;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface EsbServiceRegistryMapper extends BaseMapper<EsbServiceRegistry> {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.healthlink.his.esb.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.healthlink.his.esb.domain.EsbMessage;
|
||||
public interface IEsbMessageService extends IService<EsbMessage> {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.healthlink.his.esb.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.healthlink.his.esb.domain.EsbServiceRegistry;
|
||||
public interface IEsbServiceRegistryService extends IService<EsbServiceRegistry> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.healthlink.his.esb.service.impl;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.healthlink.his.esb.domain.EsbMessage;
|
||||
import com.healthlink.his.esb.mapper.EsbMessageMapper;
|
||||
import com.healthlink.his.esb.service.IEsbMessageService;
|
||||
import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class EsbMessageServiceImpl extends ServiceImpl<EsbMessageMapper, EsbMessage> implements IEsbMessageService {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.healthlink.his.esb.service.impl;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.healthlink.his.esb.domain.EsbServiceRegistry;
|
||||
import com.healthlink.his.esb.mapper.EsbServiceRegistryMapper;
|
||||
import com.healthlink.his.esb.service.IEsbServiceRegistryService;
|
||||
import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class EsbServiceRegistryServiceImpl extends ServiceImpl<EsbServiceRegistryMapper, EsbServiceRegistry> implements IEsbServiceRegistryService {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
export function getMessagePage(params) { return request({ url: '/esb/message/page', method: 'get', params }) }
|
||||
export function sendMessage(data) { return request({ url: '/esb/message/send', method: 'post', data }) }
|
||||
export function retryMessage(id) { return request({ url: '/esb/message/retry/' + id, method: 'put' }) }
|
||||
export function getMessageStats() { return request({ url: '/esb/message/stats', method: 'get' }) }
|
||||
98
healthlink-his-ui/src/views/esbmanage/message/index.vue
Normal file
98
healthlink-his-ui/src/views/esbmanage/message/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="16" class="stat-row" v-if="stats">
|
||||
<el-col :span="6"><div class="stat-card"><div class="stat-label">总消息数</div><div class="stat-value">{{ stats.total || 0 }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card"><div class="stat-label">待发送</div><div class="stat-value warning">{{ stats.pending || 0 }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card"><div class="stat-label">已发送</div><div class="stat-value success">{{ stats.sent || 0 }}</div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card"><div class="stat-label">发送失败</div><div class="stat-value danger">{{ stats.failed || 0 }}</div></div></el-col>
|
||||
</el-row>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">ESB消息管理</span>
|
||||
<el-button type="primary" icon="Promotion" @click="handleSend">发送消息</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" label-width="80px">
|
||||
<el-form-item label="来源系统">
|
||||
<el-input v-model="queryParams.sourceSystem" placeholder="来源系统" clearable style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标系统">
|
||||
<el-input v-model="queryParams.targetSystem" placeholder="目标系统" clearable style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 110px">
|
||||
<el-option label="待发送" value="待发送" /><el-option label="已发送" value="已发送" />
|
||||
<el-option label="发送失败" value="发送失败" /><el-option label="重试中" value="重试中" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<vxe-table :data="tableData" border height="calc(100vh - 400px)" v-loading="loading">
|
||||
<vxe-column type="seq" title="序号" width="60" />
|
||||
<vxe-column field="messageId" title="消息ID" width="160" show-overflow />
|
||||
<vxe-column field="messageType" title="类型" width="100" />
|
||||
<vxe-column field="sourceSystem" title="来源" width="100" />
|
||||
<vxe-column field="targetSystem" title="目标" width="100" />
|
||||
<vxe-column field="messageFormat" title="格式" width="80" />
|
||||
<vxe-column field="status" title="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="retryCount" title="重试" width="60" align="center" />
|
||||
<vxe-column field="createTime" title="创建时间" width="150" />
|
||||
<vxe-column title="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === '发送失败'" type="warning" link @click="handleRetry(row)">重试</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<el-dialog v-model="sendVisible" title="发送ESB消息" width="600px" append-to-body>
|
||||
<el-form :model="sendForm" label-width="100px">
|
||||
<el-form-item label="消息类型"><el-input v-model="sendForm.messageType" placeholder="HL7/FHIR/CDA" /></el-form-item>
|
||||
<el-form-item label="来源系统"><el-input v-model="sendForm.sourceSystem" placeholder="来源系统" /></el-form-item>
|
||||
<el-form-item label="目标系统"><el-input v-model="sendForm.targetSystem" placeholder="目标系统" /></el-form-item>
|
||||
<el-form-item label="消息格式"><el-select v-model="sendForm.messageFormat" style="width:100%"><el-option label="JSON" value="JSON" /><el-option label="XML" value="XML" /><el-option label="HL7" value="HL7" /></el-select></el-form-item>
|
||||
<el-form-item label="消息内容"><el-input v-model="sendForm.messageContent" type="textarea" :rows="6" placeholder="消息内容" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="sendVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitSend">发送</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getMessagePage, sendMessage, retryMessage, getMessageStats } from './components/api'
|
||||
const loading = ref(false); const tableData = ref([]); const total = ref(0); const stats = ref(null)
|
||||
const queryParams = ref({ sourceSystem: '', targetSystem: '', status: '', pageNo: 1, pageSize: 20 })
|
||||
const sendVisible = ref(false); const sendForm = ref({ messageType: '', sourceSystem: '', targetSystem: '', messageFormat: 'JSON', messageContent: '' })
|
||||
const statusType = (s) => ({ '待发送': 'warning', '已发送': 'success', '发送失败': 'danger', '重试中': 'info' }[s] || 'info')
|
||||
function getList() { loading.value = true; getMessagePage(queryParams.value).then(res => { tableData.value = res.data?.records || []; total.value = res.data?.total || 0 }).finally(() => { loading.value = false }) }
|
||||
function handleQuery() { queryParams.value.pageNo = 1; getList() }
|
||||
function resetQuery() { queryParams.value = { sourceSystem: '', targetSystem: '', status: '', pageNo: 1, pageSize: 20 }; getList() }
|
||||
function handleSend() { sendForm.value = { messageType: '', sourceSystem: '', targetSystem: '', messageFormat: 'JSON', messageContent: '' }; sendVisible.value = true }
|
||||
function submitSend() { sendMessage(sendForm.value).then(res => { if (res.code === 200) { ElMessage.success('消息已提交'); sendVisible.value = false; getList(); loadStats() } }) }
|
||||
function handleRetry(row) { retryMessage(row.id).then(res => { if (res.code === 200) { ElMessage.success('重试已提交'); getList(); loadStats() } }) }
|
||||
function loadStats() { getMessageStats().then(res => { stats.value = res.data }) }
|
||||
onMounted(() => { getList(); loadStats() })
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-weight: bold; font-size: 16px; }
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; text-align: center; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
|
||||
.stat-value { font-size: 22px; font-weight: bold; color: #303133; }
|
||||
.stat-value.warning { color: #e6a23c; }
|
||||
.stat-value.success { color: #67c23a; }
|
||||
.stat-value.danger { color: #f56c6c; }
|
||||
</style>
|
||||
69
healthlink-his-ui/src/views/esbmanage/monitor/index.vue
Normal file
69
healthlink-his-ui/src/views/esbmanage/monitor/index.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="6"><div class="stat-card blue"><div class="stat-icon"><el-icon :size="28"><Promotion /></el-icon></div><div class="stat-info"><div class="stat-value">{{ stats.total || 0 }}</div><div class="stat-label">总消息数</div></div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card orange"><div class="stat-icon"><el-icon :size="28"><Clock /></el-icon></div><div class="stat-info"><div class="stat-value">{{ stats.pending || 0 }}</div><div class="stat-label">待发送</div></div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card green"><div class="stat-icon"><el-icon :size="28"><CircleCheck /></el-icon></div><div class="stat-info"><div class="stat-value">{{ stats.sent || 0 }}</div><div class="stat-label">已发送</div></div></div></el-col>
|
||||
<el-col :span="6"><div class="stat-card red"><div class="stat-icon"><el-icon :size="28"><Warning /></el-icon></div><div class="stat-info"><div class="stat-value">{{ stats.failed || 0 }}</div><div class="stat-label">发送失败</div></div></div></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="card-title">服务注册状态</span></template>
|
||||
<vxe-table :data="services" border height="300">
|
||||
<vxe-column field="serviceName" title="服务名称" min-width="150" />
|
||||
<vxe-column field="serviceVersion" title="版本" width="80" />
|
||||
<vxe-column field="serviceStatus" title="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.serviceStatus === '启用' ? 'success' : 'info'" size="small">{{ row.serviceStatus }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="card-title">最近消息</span></template>
|
||||
<vxe-table :data="recentMessages" border height="300">
|
||||
<vxe-column field="sourceSystem" title="来源" width="100" />
|
||||
<vxe-column field="targetSystem" title="目标" width="100" />
|
||||
<vxe-column field="messageType" title="类型" width="80" />
|
||||
<vxe-column field="status" title="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '已发送' ? 'success' : row.status === '发送失败' ? 'danger' : 'warning'" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="createTime" title="时间" width="140" />
|
||||
</vxe-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getMessageStats, getMessagePage } from '../message/components/api'
|
||||
import { getRegistryList } from '../registry/components/api'
|
||||
const stats = ref({}); const services = ref([]); const recentMessages = ref([])
|
||||
onMounted(() => {
|
||||
getMessageStats().then(res => { stats.value = res.data || {} })
|
||||
getRegistryList().then(res => { services.value = res.data || [] })
|
||||
getMessagePage({ pageNo: 1, pageSize: 10 }).then(res => { recentMessages.value = res.data?.records || [] })
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-card { background: #fff; border-radius: 8px; padding: 20px; border: 1px solid #ebeef5; display: flex; align-items: center; gap: 16px; }
|
||||
.stat-card.blue { border-left: 4px solid #409eff; }
|
||||
.stat-card.orange { border-left: 4px solid #e6a23c; }
|
||||
.stat-card.green { border-left: 4px solid #67c23a; }
|
||||
.stat-card.red { border-left: 4px solid #f56c6c; }
|
||||
.stat-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #f0f2f5; }
|
||||
.stat-card.blue .stat-icon { color: #409eff; }
|
||||
.stat-card.orange .stat-icon { color: #e6a23c; }
|
||||
.stat-card.green .stat-icon { color: #67c23a; }
|
||||
.stat-card.red .stat-icon { color: #f56c6c; }
|
||||
.stat-info .stat-value { font-size: 24px; font-weight: bold; color: #303133; }
|
||||
.stat-info .stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
.card-title { font-weight: bold; font-size: 15px; }
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
export function getRegistryPage(params) { return request({ url: '/esb/registry/page', method: 'get', params }) }
|
||||
export function addRegistry(data) { return request({ url: '/esb/registry/add', method: 'post', data }) }
|
||||
export function updateRegistry(data) { return request({ url: '/esb/registry/update', method: 'put', data }) }
|
||||
export function deleteRegistry(id) { return request({ url: '/esb/registry/delete', method: 'delete', params: { id } }) }
|
||||
export function updateRegistryStatus(id, status) { return request({ url: '/esb/registry/status', method: 'put', params: { id, status } }) }
|
||||
91
healthlink-his-ui/src/views/esbmanage/registry/index.vue
Normal file
91
healthlink-his-ui/src/views/esbmanage/registry/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">服务注册中心</span>
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">注册服务</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" label-width="80px">
|
||||
<el-form-item label="服务名">
|
||||
<el-input v-model="queryParams.serviceName" placeholder="服务名称" clearable @keyup.enter="handleQuery" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.serviceStatus" placeholder="全部" clearable style="width: 110px">
|
||||
<el-option label="启用" value="启用" /><el-option label="停用" value="停用" /><el-option label="维护中" value="维护中" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<vxe-table :data="tableData" border height="calc(100vh - 320px)" v-loading="loading">
|
||||
<vxe-column type="seq" title="序号" width="60" />
|
||||
<vxe-column field="serviceName" title="服务名称" width="160" show-overflow />
|
||||
<vxe-column field="serviceVersion" title="版本" width="80" />
|
||||
<vxe-column field="serviceEndpoint" title="端点" min-width="250" show-overflow />
|
||||
<vxe-column field="protocol" title="协议" width="80" />
|
||||
<vxe-column field="serviceStatus" title="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.serviceStatus === '启用' ? 'success' : row.serviceStatus === '维护中' ? 'warning' : 'info'" size="small">{{ row.serviceStatus }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="timeoutMs" title="超时(ms)" width="90" />
|
||||
<vxe-column title="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.serviceStatus === '启用'" type="warning" link @click="changeStatus(row, '停用')">停用</el-button>
|
||||
<el-button v-else type="success" link @click="changeStatus(row, '启用')">启用</el-button>
|
||||
<el-button type="danger" link icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<el-dialog v-model="formVisible" :title="formTitle" width="650px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="服务名称" prop="serviceName"><el-input v-model="form.serviceName" placeholder="服务名称" /></el-form-item>
|
||||
<el-form-item label="版本"><el-input v-model="form.serviceVersion" placeholder="v1.0" /></el-form-item>
|
||||
<el-form-item label="端点地址" prop="serviceEndpoint"><el-input v-model="form.serviceEndpoint" placeholder="http://..." /></el-form-item>
|
||||
<el-form-item label="协议">
|
||||
<el-select v-model="form.protocol" style="width:100%">
|
||||
<el-option label="HTTP" value="HTTP" /><el-option label="HTTPS" value="HTTPS" />
|
||||
<el-option label="HL7" value="HL7" /><el-option label="FHIR" value="FHIR" />
|
||||
<el-option label="WebService" value="WebService" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="超时(ms)"><el-input-number v-model="form.timeoutMs" :min="1000" :max="60000" :step="1000" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="form.serviceDescription" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getRegistryPage, addRegistry, updateRegistry, deleteRegistry, updateRegistryStatus } from './components/api'
|
||||
const loading = ref(false); const tableData = ref([]); const total = ref(0)
|
||||
const queryParams = ref({ serviceName: '', serviceStatus: '', pageNo: 1, pageSize: 20 })
|
||||
const formVisible = ref(false); const formTitle = ref('注册服务'); const isEdit = ref(false); const formRef = ref()
|
||||
const form = ref({ id: null, serviceName: '', serviceVersion: '', serviceEndpoint: '', protocol: 'HTTP', timeoutMs: 5000, serviceDescription: '' })
|
||||
const rules = { serviceName: [{ required: true, message: '请输入服务名称', trigger: 'blur' }], serviceEndpoint: [{ required: true, message: '请输入端点地址', trigger: 'blur' }] }
|
||||
function getList() { loading.value = true; getRegistryPage(queryParams.value).then(res => { tableData.value = res.data?.records || []; total.value = res.data?.total || 0 }).finally(() => { loading.value = false }) }
|
||||
function handleQuery() { queryParams.value.pageNo = 1; getList() }
|
||||
function resetQuery() { queryParams.value = { serviceName: '', serviceStatus: '', pageNo: 1, pageSize: 20 }; getList() }
|
||||
function handleAdd() { isEdit.value = false; formTitle.value = '注册服务'; form.value = { id: null, serviceName: '', serviceVersion: '', serviceEndpoint: '', protocol: 'HTTP', timeoutMs: 5000, serviceDescription: '' }; formVisible.value = true }
|
||||
function handleEdit(row) { isEdit.value = true; formTitle.value = '编辑服务'; form.value = { ...row }; formVisible.value = true }
|
||||
function submitForm() { formRef.value.validate(valid => { if (!valid) return; const action = isEdit.value ? updateRegistry(form.value) : addRegistry(form.value); action.then(res => { if (res.code === 200) { ElMessage.success(isEdit.value ? '修改成功' : '注册成功'); formVisible.value = false; getList() } else ElMessage.error(res.msg || '操作失败') }) }) }
|
||||
function handleDelete(row) { ElMessageBox.confirm('确认删除该服务注册?', '提示', { type: 'warning' }).then(() => { deleteRegistry(row.id).then(res => { if (res.code === 200) { ElMessage.success('删除成功'); getList() } }) }).catch(() => {}) }
|
||||
function changeStatus(row, status) { updateRegistryStatus(row.id, status).then(res => { if (res.code === 200) { ElMessage.success('状态已更新'); getList() } }) }
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-weight: bold; font-size: 16px; }
|
||||
</style>
|
||||
@@ -1,13 +1,62 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="card-title">医保目录管理</span></template>
|
||||
<el-empty description="医保目录管理 - 功能开发中">
|
||||
<el-button type="primary" disabled>敬请期待</el-button>
|
||||
</el-empty>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">医保目录管理</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" label-width="80px">
|
||||
<el-form-item label="目录类型">
|
||||
<el-select v-model="queryParams.catalogType" placeholder="全部" clearable style="width: 140px">
|
||||
<el-option label="药品目录" :value="1" /><el-option label="诊疗目录" :value="2" />
|
||||
<el-option label="材料目录" :value="3" /><el-option label="疾病目录" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字">
|
||||
<el-input v-model="queryParams.searchKey" placeholder="编码/名称" clearable @keyup.enter="handleQuery" style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<vxe-table :data="tableData" border height="calc(100vh - 320px)" v-loading="loading">
|
||||
<vxe-column type="seq" title="序号" width="60" />
|
||||
<vxe-column field="ybNo" title="医保编码" width="140" show-overflow />
|
||||
<vxe-column field="itemName" title="项目名称" min-width="200" show-overflow />
|
||||
<vxe-column field="catalogType_dictText" title="类型" width="100" />
|
||||
<vxe-column field="specification" title="规格" width="120" show-overflow />
|
||||
<vxe-column field="unit" title="单位" width="60" />
|
||||
<vxe-column field="price" title="价格" width="90" align="right">
|
||||
<template #default="{ row }">{{ row.price ? '¥' + row.price.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="validFlag" title="状态" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.validFlag === '1' ? 'success' : 'info'" size="small">{{ row.validFlag === '1' ? '有效' : '无效' }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
const loading = ref(false); const tableData = ref([]); const total = ref(0)
|
||||
const queryParams = ref({ catalogType: undefined, searchKey: '', pageNo: 1, pageSize: 20 })
|
||||
function getList() {
|
||||
loading.value = true
|
||||
request({ url: '/catalog/page', method: 'get', params: queryParams.value }).then(res => {
|
||||
tableData.value = res.data?.records || res.data || []; total.value = res.data?.total || 0
|
||||
}).finally(() => { loading.value = false })
|
||||
}
|
||||
function handleQuery() { queryParams.value.pageNo = 1; getList() }
|
||||
function resetQuery() { queryParams.value = { catalogType: undefined, searchKey: '', pageNo: 1, pageSize: 20 }; getList() }
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
<style scoped>.card-title { font-weight: bold; font-size: 16px; }</style>
|
||||
<style lang="scss" scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-weight: bold; font-size: 16px; }
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,79 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="card-title">医保结算</span></template>
|
||||
<el-empty description="医保结算 - 功能开发中">
|
||||
<el-button type="primary" disabled>敬请期待</el-button>
|
||||
</el-empty>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">医保结算</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" label-width="80px">
|
||||
<el-form-item label="患者">
|
||||
<el-input v-model="queryParams.searchKey" placeholder="患者姓名/住院号" clearable @keyup.enter="handleQuery" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结算状态">
|
||||
<el-select v-model="queryParams.settleStatus" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option label="待结算" value="0" /><el-option label="已预结算" value="1" />
|
||||
<el-option label="已结算" value="2" /><el-option label="结算失败" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<vxe-table :data="tableData" border height="calc(100vh - 320px)" v-loading="loading">
|
||||
<vxe-column type="seq" title="序号" width="60" />
|
||||
<vxe-column field="patientName" title="患者姓名" width="100" />
|
||||
<vxe-column field="encounterNo" title="住院号" width="140" show-overflow />
|
||||
<vxe-column field="ybType" title="医保类型" width="100" />
|
||||
<vxe-column field="totalAmount" title="总费用" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.totalAmount ? '¥' + row.totalAmount.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="ybPayAmount" title="医保支付" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.ybPayAmount ? '¥' + row.ybPayAmount.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="selfPayAmount" title="自费金额" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.selfPayAmount ? '¥' + row.selfPayAmount.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="settleStatus" title="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.settleStatus)" size="small">{{ statusText(row.settleStatus) }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.settleStatus === '0'" type="primary" link @click="handlePreSettle(row)">预结算</el-button>
|
||||
<el-button v-if="row.settleStatus === '1'" type="success" link @click="handleSettle(row)">确认结算</el-button>
|
||||
<el-button type="info" link @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
const loading = ref(false); const tableData = ref([]); const total = ref(0)
|
||||
const queryParams = ref({ searchKey: '', settleStatus: undefined, pageNo: 1, pageSize: 20 })
|
||||
const statusText = (s) => ({ '0': '待结算', '1': '已预结算', '2': '已结算', '3': '结算失败' }[s] || '未知')
|
||||
const statusType = (s) => ({ '0': 'info', '1': 'warning', '2': 'success', '3': 'danger' }[s] || 'info')
|
||||
function getList() {
|
||||
loading.value = true
|
||||
request({ url: '/yb-inpatient-request/setl-status-up', method: 'get', params: queryParams.value }).then(res => {
|
||||
tableData.value = res.data?.records || res.data || []; total.value = res.data?.total || 0
|
||||
}).finally(() => { loading.value = false })
|
||||
}
|
||||
function handleQuery() { queryParams.value.pageNo = 1; getList() }
|
||||
function resetQuery() { queryParams.value = { searchKey: '', settleStatus: undefined, pageNo: 1, pageSize: 20 }; getList() }
|
||||
function handlePreSettle(row) { ElMessage.info('预结算已提交') }
|
||||
function handleSettle(row) { ElMessage.info('确认结算已提交') }
|
||||
function handleDetail(row) { ElMessage.info('结算详情') }
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
<style scoped>.card-title { font-weight: bold; font-size: 16px; }</style>
|
||||
<style lang="scss" scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-weight: bold; font-size: 16px; }
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,79 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="card-title">医保对账</span></template>
|
||||
<el-empty description="医保对账 - 功能开发中">
|
||||
<el-button type="primary" disabled>敬请期待</el-button>
|
||||
</el-empty>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">医保对账</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" label-width="80px">
|
||||
<el-form-item label="对账日期">
|
||||
<el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" style="width: 260px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="对账状态">
|
||||
<el-select v-model="queryParams.reconcileStatus" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option label="待对账" value="0" /><el-option label="对账中" value="1" />
|
||||
<el-option label="已完成" value="2" /><el-option label="有差异" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
<el-button type="success" icon="Check" @click="handleReconcile">执行对账</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<vxe-table :data="tableData" border height="calc(100vh - 320px)" v-loading="loading">
|
||||
<vxe-column type="seq" title="序号" width="60" />
|
||||
<vxe-column field="reconcileDate" title="对账日期" width="120" />
|
||||
<vxe-column field="orgName" title="机构名称" min-width="180" show-overflow />
|
||||
<vxe-column field="totalAmount" title="总金额" width="120" align="right">
|
||||
<template #default="{ row }">{{ row.totalAmount ? '¥' + row.totalAmount.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="ybAmount" title="医保金额" width="120" align="right">
|
||||
<template #default="{ row }">{{ row.ybAmount ? '¥' + row.ybAmount.toFixed(2) : '-' }}</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="diffAmount" title="差异金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'diff-amount': row.diffAmount && row.diffAmount !== 0 }">{{ row.diffAmount ? '¥' + row.diffAmount.toFixed(2) : '-' }}</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="reconcileStatus" title="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.reconcileStatus)" size="small">{{ statusText(row.reconcileStatus) }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
const loading = ref(false); const tableData = ref([]); const total = ref(0); const dateRange = ref([])
|
||||
const queryParams = ref({ reconcileStatus: undefined, pageNo: 1, pageSize: 20 })
|
||||
const statusText = (s) => ({ '0': '待对账', '1': '对账中', '2': '已完成', '3': '有差异' }[s] || '未知')
|
||||
const statusType = (s) => ({ '0': 'info', '1': 'warning', '2': 'success', '3': 'danger' }[s] || 'info')
|
||||
function getList() {
|
||||
loading.value = true
|
||||
request({ url: '/yb-request/reconcile-list', method: 'get', params: queryParams.value }).then(res => {
|
||||
tableData.value = res.data?.records || res.data || []; total.value = res.data?.total || 0
|
||||
}).finally(() => { loading.value = false })
|
||||
}
|
||||
function handleQuery() { queryParams.value.pageNo = 1; getList() }
|
||||
function resetQuery() { queryParams.value = { reconcileStatus: undefined, pageNo: 1, pageSize: 20 }; dateRange.value = []; getList() }
|
||||
function handleReconcile() { ElMessage.info('对账任务已提交,请稍后查看结果') }
|
||||
function handleDetail(row) { ElMessage.info('对账详情: ' + row.reconcileDate) }
|
||||
onMounted(() => getList())
|
||||
</script>
|
||||
<style scoped>.card-title { font-weight: bold; font-size: 16px; }</style>
|
||||
<style lang="scss" scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-weight: bold; font-size: 16px; }
|
||||
.diff-amount { color: #f56c6c; font-weight: bold; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user