|
|
@@ -0,0 +1,297 @@
|
|
|
+---
|
|
|
+doc_id: DDS-202606-004
|
|
|
+feature_id: FEAT-202606-005-saas-tenant-isolation
|
|
|
+type: detailed-design
|
|
|
+title: HIS 多医院动态路由详细设计
|
|
|
+status: reviewing
|
|
|
+owner: 医梦研发团队
|
|
|
+created_at: 2026-06-27
|
|
|
+updated_at: 2026-06-27
|
|
|
+reviewers: []
|
|
|
+related_docs:
|
|
|
+ - INT-202606-005
|
|
|
+ - DEV-202606-005
|
|
|
+ - DDS-202606-001
|
|
|
+related_modules:
|
|
|
+ - emoon-openplatform
|
|
|
+ - emoon-ai-agent
|
|
|
+ - emoon-ai-mcp-api
|
|
|
+ - emoon-ai-mcp
|
|
|
+tags:
|
|
|
+ - SaaS
|
|
|
+ - HIS
|
|
|
+ - 多医院路由
|
|
|
+ - Provider SPI
|
|
|
+---
|
|
|
+
|
|
|
+# HIS 多医院动态路由详细设计
|
|
|
+
|
|
|
+## 实现范围
|
|
|
+
|
|
|
+本设计覆盖 SaaS P0 中的 HIS 多医院动态路由代码:
|
|
|
+
|
|
|
+- 根据可信的 `tenantId + projectId + hospitalId` 精确选择 HIS Provider。
|
|
|
+- 支持配置文件声明医院路由,不新增数据库表。
|
|
|
+- 为不同 HIS 厂商保留 Provider SPI,不猜测或统一厂商协议。
|
|
|
+- MCP 标准工具调用和卡片动作编排使用同一套路由规则。
|
|
|
+- 保留现有单 `HisClient` 和 Mock 路径,避免阻塞已有本地联调。
|
|
|
+
|
|
|
+本设计不覆盖:
|
|
|
+
|
|
|
+- 真实医院 HIS/LIS 接口协议实现。
|
|
|
+- 专线、证书、DNS、防火墙和网络连通。
|
|
|
+- 凭证明文存储或密钥管理系统建设。
|
|
|
+- 管理后台动态维护路由。
|
|
|
+- 数据库 SQL 执行和真实双医院验收。
|
|
|
+
|
|
|
+## 总体方案
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart LR
|
|
|
+ Tool["ToolCallRequest"]
|
|
|
+ Card["Card Action"]
|
|
|
+ Conversation["Tenant-scoped Conversation"]
|
|
|
+ Context["HisRouteContext"]
|
|
|
+ Router["HisClientRouter"]
|
|
|
+ Config["HisRoutingProperties"]
|
|
|
+ SPI["HisClientProvider SPI"]
|
|
|
+ Mock["Legacy Mock HisClient"]
|
|
|
+ A["Hospital A Adapter"]
|
|
|
+ B["Hospital B Adapter"]
|
|
|
+
|
|
|
+ Tool --> Context
|
|
|
+ Card --> Conversation --> Context
|
|
|
+ Context --> Router
|
|
|
+ Config --> Router
|
|
|
+ Router --> SPI
|
|
|
+ Router --> Mock
|
|
|
+ SPI --> A
|
|
|
+ SPI --> B
|
|
|
+```
|
|
|
+
|
|
|
+路由只负责选择客户端。字段转换、签名、证书、错误码归一和厂商协议由具体
|
|
|
+`HisClientProvider` 创建或持有的 `HisClient` 完成。
|
|
|
+
|
|
|
+## 数据模型
|
|
|
+
|
|
|
+本次不新增数据库表和字段。
|
|
|
+
|
|
|
+### 路由上下文
|
|
|
+
|
|
|
+内部值对象 `HisRouteContext` 包含:
|
|
|
+
|
|
|
+| 字段 | 必填条件 | 来源 |
|
|
|
+|---|---|---|
|
|
|
+| `tenantId` | 严格模式必填 | 鉴权项目解析 |
|
|
|
+| `projectId` | 严格模式必填 | 鉴权项目解析 |
|
|
|
+| `hospitalId` | 严格模式必填 | MCP 请求或租户作用域内会话 |
|
|
|
+
|
|
|
+路由键固定为三个字段的完整组合,不支持通配符、仅医院匹配或跨租户回退。
|
|
|
+
|
|
|
+### 配置模型
|
|
|
+
|
|
|
+`HisRoutingProperties` 使用前缀 `emoon.his.routing`:
|
|
|
+
|
|
|
+```yaml
|
|
|
+emoon:
|
|
|
+ his:
|
|
|
+ routing:
|
|
|
+ strict: false
|
|
|
+ fallback-enabled: true
|
|
|
+ routes:
|
|
|
+ - route-id: hospital-a
|
|
|
+ tenant-id: tenant-a
|
|
|
+ project-id: "7"
|
|
|
+ hospital-id: hospital-a
|
|
|
+ provider: vendor-a
|
|
|
+ endpoint: ${HIS_HOSPITAL_A_ENDPOINT:}
|
|
|
+ credential-ref: ${HIS_HOSPITAL_A_CREDENTIAL_REF:}
|
|
|
+ connect-timeout-ms: 3000
|
|
|
+ read-timeout-ms: 10000
|
|
|
+```
|
|
|
+
|
|
|
+约束:
|
|
|
+
|
|
|
+- `route-id` 和三字段路由键均必须唯一。
|
|
|
+- `provider` 必须能匹配一个 Provider SPI 实现。
|
|
|
+- `endpoint` 允许为空,由 Provider 决定是否必需。
|
|
|
+- `credential-ref` 只保存凭证引用,不保存用户名、密码、Token 或私钥明文。
|
|
|
+- 配置校验失败时禁止启动严格路由。
|
|
|
+
|
|
|
+## 服务和对象职责
|
|
|
+
|
|
|
+| 类或组件 | 职责 | 备注 |
|
|
|
+|---|---|---|
|
|
|
+| `HisRouteContext` | 承载租户、项目、医院上下文 | 不包含凭证 |
|
|
|
+| `HisRouteDefinition` | 表示单条路由配置 | 不输出敏感字段 |
|
|
|
+| `HisRoutingProperties` | 绑定并校验配置文件 | 默认兼容模式 |
|
|
|
+| `HisClientProvider` | 根据路由定义提供厂商客户端 | Provider 不参与业务编排 |
|
|
|
+| `HisClientRouter` | 精确匹配路由、选择 Provider、执行兼容回退 | 不处理 HIS 字段 |
|
|
|
+| `HisToolInvokeService` | 从 `ToolCallRequest` 传递路由上下文 | 标准 MCP 入口 |
|
|
|
+| `McpToolService` | 提供带路由上下文的工具重载 | 旧方法保持不变 |
|
|
|
+| `AgentActionOrchestrator` | 从作用域会话解析医院并传给 MCP | 不信任动作请求租户 |
|
|
|
+| `ConversationTerminalService` | 按项目和租户读取会话路由事实 | 不跨租户查询 |
|
|
|
+
|
|
|
+## 内部接口
|
|
|
+
|
|
|
+建议内部接口保持最小:
|
|
|
+
|
|
|
+```java
|
|
|
+public interface HisClientRouter {
|
|
|
+ HisClient route(HisRouteContext context);
|
|
|
+}
|
|
|
+
|
|
|
+public interface HisClientProvider {
|
|
|
+ String providerType();
|
|
|
+ HisClient getClient(HisRouteDefinition definition);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Provider 可自行缓存客户端。Router 不持有或打印真实凭证。
|
|
|
+
|
|
|
+## 调用链
|
|
|
+
|
|
|
+### MCP 标准工具入口
|
|
|
+
|
|
|
+```text
|
|
|
+ToolController / HisMcpController
|
|
|
+→ ToolCallRequest(projectId, tenantId, hospitalId)
|
|
|
+→ HisToolInvokeService
|
|
|
+→ HisClientRouter.route(context)
|
|
|
+→ HisClientProvider
|
|
|
+→ Hospital HisClient
|
|
|
+```
|
|
|
+
|
|
|
+### 卡片动作入口
|
|
|
+
|
|
|
+```text
|
|
|
+CardActionController
|
|
|
+→ AgentActionOrchestrator(conversationId, projectId, tenantId)
|
|
|
+→ ConversationTerminalService.findByScope(...)
|
|
|
+→ hospitalId
|
|
|
+→ McpToolService(context)
|
|
|
+→ HisClientRouter.route(context)
|
|
|
+→ Hospital HisClient
|
|
|
+```
|
|
|
+
|
|
|
+前端卡片动作请求不新增 `tenantId` 或 `hospitalId` 参数。医院事实来自当前租户下的会话,
|
|
|
+避免客户端伪造医院路由。
|
|
|
+
|
|
|
+## 路由和兼容规则
|
|
|
+
|
|
|
+### 兼容模式
|
|
|
+
|
|
|
+默认配置:
|
|
|
+
|
|
|
+```yaml
|
|
|
+strict: false
|
|
|
+fallback-enabled: true
|
|
|
+```
|
|
|
+
|
|
|
+行为:
|
|
|
+
|
|
|
+- 完整上下文命中配置时使用对应 Provider。
|
|
|
+- 未配置、上下文缺失或 Provider 不存在时使用现有默认 `HisClient`。
|
|
|
+- 记录 routeId 或上下文缺失类型,不记录 endpoint、credentialRef 和请求敏感字段。
|
|
|
+
|
|
|
+### 严格模式
|
|
|
+
|
|
|
+生产环境应配置:
|
|
|
+
|
|
|
+```yaml
|
|
|
+strict: true
|
|
|
+fallback-enabled: false
|
|
|
+```
|
|
|
+
|
|
|
+以下情况直接失败,不调用任何 HIS:
|
|
|
+
|
|
|
+- 任一路由上下文字段为空。
|
|
|
+- 没有精确匹配路由。
|
|
|
+- Provider 不存在或重复。
|
|
|
+- 路由配置重复或被禁用。
|
|
|
+
|
|
|
+严格模式禁止回退到其他医院路由,也禁止仅凭 `hospitalId` 匹配。
|
|
|
+
|
|
|
+## 异常处理
|
|
|
+
|
|
|
+| 场景 | 行为 |
|
|
|
+|---|---|
|
|
|
+| 上下文不完整 | 严格模式失败;兼容模式回退 |
|
|
|
+| 路由不存在 | 严格模式失败;兼容模式回退 |
|
|
|
+| Provider 不存在 | 严格模式失败;兼容模式回退并告警 |
|
|
|
+| 重复路由 | 配置校验失败,阻止严格模式启动 |
|
|
|
+| Provider 创建客户端失败 | 返回工具调用失败,不切换其他医院 |
|
|
|
+| HIS 超时或熔断 | 由 Provider/客户端转换为平台异常,不跨路由重试 |
|
|
|
+
|
|
|
+错误信息不得包含 endpoint、凭证引用内容、Token、证书路径或患者敏感数据。
|
|
|
+
|
|
|
+## 安全和合规
|
|
|
+
|
|
|
+1. 路由租户和项目来自后端鉴权上下文。
|
|
|
+2. 卡片动作的医院来自租户作用域内会话,不接受前端覆盖。
|
|
|
+3. 配置文件只保存 `credentialRef`,真实凭证使用环境变量或密钥系统。
|
|
|
+4. 日志只记录 routeId、tenantId、projectId、hospitalId 和 traceId。
|
|
|
+5. 禁止日志记录 Header、签名原文、患者身份证、手机号或完整请求体。
|
|
|
+6. 不允许路由失败后尝试其他医院,避免医疗数据跨院发送。
|
|
|
+
|
|
|
+仓库当前 `application.yml` 存在历史明文配置,本次只保证新增 HIS 路由不继续使用该模式;
|
|
|
+历史配置清理应单独执行密钥轮换,不能仅删除文件内容而不更换已暴露凭证。
|
|
|
+
|
|
|
+## 配置和部署
|
|
|
+
|
|
|
+推荐发布顺序:
|
|
|
+
|
|
|
+1. 保持 `strict=false` 部署代码,验证原 Mock 路径无回归。
|
|
|
+2. 配置医院 A、医院 B 的路由和 Provider。
|
|
|
+3. 分别验证两家医院查询与写工具。
|
|
|
+4. 确认无未配置调用后切换 `strict=true`、`fallback-enabled=false`。
|
|
|
+5. 观察错误率、超时和路由未命中日志,不记录敏感信息。
|
|
|
+
|
|
|
+本次不修改现有 HTTP URL、请求体和响应结构。
|
|
|
+
|
|
|
+## 测试策略
|
|
|
+
|
|
|
+### 单元测试
|
|
|
+
|
|
|
+- 完整三字段命中指定 Provider。
|
|
|
+- 两个租户使用相同业务参数时选择不同客户端。
|
|
|
+- 严格模式拒绝空上下文和未配置路由。
|
|
|
+- 兼容模式未命中时回退现有客户端。
|
|
|
+- 重复路由和重复 Provider 被拒绝。
|
|
|
+- Provider 异常时不尝试其他医院。
|
|
|
+
|
|
|
+### 应用服务测试
|
|
|
+
|
|
|
+- `HisToolInvokeService` 使用 `ToolCallRequest` 的三字段上下文。
|
|
|
+- `AgentActionOrchestrator` 从作用域会话获取 `hospitalId`。
|
|
|
+- 跨租户会话查询不到时不调用 HIS。
|
|
|
+- 旧的无上下文 `McpToolService` 方法仍调用默认客户端。
|
|
|
+
|
|
|
+### 回归验证
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp \
|
|
|
+ -DskipTests=false -Dprofiles.active= test
|
|
|
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent \
|
|
|
+ -DskipTests=false -Dprofiles.active= test
|
|
|
+mvn -pl emoon-openplatform -am -DskipTests compile
|
|
|
+mvn -pl emoon-admin -DskipTests=false -Dprofiles.active= \
|
|
|
+ -Dtest=AiPlatformArchitectureTest test
|
|
|
+```
|
|
|
+
|
|
|
+### 真实环境验收
|
|
|
+
|
|
|
+- 医院 A 请求只到达医院 A endpoint。
|
|
|
+- 医院 B 请求只到达医院 B endpoint。
|
|
|
+- 交换医院 ID、项目 ID 或租户 ID 后均不得调用 HIS。
|
|
|
+- 两家医院使用相同幂等键时互不影响。
|
|
|
+- 严格模式下未配置医院返回明确失败,不使用 Mock。
|
|
|
+
|
|
|
+## 验收标准
|
|
|
+
|
|
|
+- 现有接口路径、请求体和旧内部方法保持兼容。
|
|
|
+- 代码可按三字段精确路由到不同 Provider。
|
|
|
+- 严格模式不存在跨医院回退。
|
|
|
+- 新增配置不含 HIS 明文凭证。
|
|
|
+- 自动化测试证明两个租户路由到不同客户端。
|
|
|
+- 真实医院协议和网络未接入时,不宣称真实双医院联调完成。
|