--- 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 执行和真实双医院验收。 ## 本次改造原则 1. 所有医院统一按租户和项目隔离。HIS 路由不得削弱 Workflow、RAG、Agent、Card、 会话、任务、设备和文件已有的租户边界。 2. 默认不修改现有 HTTP URL、方法、请求体和响应结构。内部方法优先增加兼容重载; 如实现中确需修改外部接口,必须先同步更新本专题接口影响说明。 3. 所有涉及的 DDL 和 DML 只写入 [SaaS化数据库候选变更与执行清单.md](SaaS化数据库候选变更与执行清单.md), 本阶段不连接目标数据库、不执行迁移 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: 3s read-timeout: 10s ``` 约束: - `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 不持有或打印真实凭证。 `HisClient` 增加默认方法 `isMock()`,默认返回 `false`;现有 `MockHisClient`、`SqliteMockHisClient` 返回 `true`。工具响应中的 `mock` 标记必须取自 实际路由客户端,真实 Provider 不得继续返回 `mock=true`。 ## 调用链 ### 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、请求体和响应结构。 ## 实现状态 截至 2026-06-27,以下代码已完成: - 路由配置对象、三元精确索引、严格模式和兼容回退。 - Mock 客户端互斥注册与真实/Mock 类型标识。 - MCP 标准工具入口和 `McpToolService` 上下文重载。 - Agent、Card 与对话降级链医院上下文传递。 - 跨租户会话不存在时禁止 Card 链调用 HIS。 未完成内容: - 真实 HIS 厂商 `HisClientProvider` 实现。 - 目标环境 endpoint、`credential-ref`、超时和网络策略配置。 - 两家医院专线环境的真实联调与严格模式切换。 本实现没有新增数据库表、字段或迁移 SQL。 ## 测试策略 ### 单元测试 - 完整三字段命中指定 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 明文凭证。 - 自动化测试证明两个租户路由到不同客户端。 - 真实医院协议和网络未接入时,不宣称真实双医院联调完成。