Просмотр исходного кода

docs: 补充HIS多医院路由详细设计

WangKang 6 дней назад
Родитель
Сommit
f097992eaf

+ 297 - 0
docs/initiatives/FEAT-202606-005-saas-tenant-isolation/HIS多医院路由详细设计.md

@@ -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 明文凭证。
+- 自动化测试证明两个租户路由到不同客户端。
+- 真实医院协议和网络未接入时,不宣称真实双医院联调完成。

+ 2 - 1
docs/initiatives/FEAT-202606-005-saas-tenant-isolation/专题索引.md

@@ -38,6 +38,7 @@ tags:
 | 类型 | 文档 | 状态 | 用途 |
 |---|---|---|---|
 | 需求录入 | [需求录入.md](需求录入.md) | approved | 记录目标、边界和交付方式 |
+| 详细设计 | [HIS多医院路由详细设计.md](HIS多医院路由详细设计.md) | reviewing | P0 多医院 HIS 路由、Provider SPI 和安全约束 |
 | 开发进度 | [SaaS化代码改造与下游交接说明.md](SaaS化代码改造与下游交接说明.md) | reviewing | 代码改动、验证结果、遗留项和联调步骤 |
 | Runbook | [SaaS化数据库候选变更与执行清单.md](SaaS化数据库候选变更与执行清单.md) | reviewing | 数据库盘点、候选 SQL、执行顺序和校验 |
 
@@ -80,7 +81,7 @@ tags:
 |---|---|---|---|---|
 | 需求文档 | 是 | 改造已在文档生命周期建立前完成 | 验收口径不够完整 | `需求录入.md` 与交接说明 |
 | 概要设计 | 是 | 本阶段未改变既有模块边界 | 缺少正式架构评审记录 | 既有平台概要设计与交接说明 |
-| 详细设计 | 是 | 本阶段以最小兼容改造为主 | 数据迁移规则需目标环境确认 | 数据库 Runbook |
+| 详细设计 | 否 | P0 HIS 多医院路由涉及配置、鉴权上下文和安全边界 | 已通过专题详细设计补齐 | `HIS多医院路由详细设计.md` |
 | 接口说明 | 是 | 未新增对外 URL,主要增加内部作用域参数和可选上下文头 | 下游可能忽略鉴权语义变化 | 交接说明中的接口影响章节 |
 | 测试计划 | 是 | 代码改造期间按模块补充回归测试 | 未覆盖真实数据库和多医院联调 | 交接说明中的验收清单 |
 | 测试结果 | 是 | 当前只有开发环境自动化验证记录 | 不能据此判断可上线 | 交接说明中的已执行验证 |