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:
本设计覆盖 SaaS P0 中的 HIS 多医院动态路由代码:
tenantId + projectId + hospitalId 精确选择 HIS Provider。HisClient 和 Mock 路径,避免阻塞已有本地联调。本设计不覆盖:
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:
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 |
按项目和租户读取会话路由事实 | 不跨租户查询 |
建议内部接口保持最小:
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。
ToolController / HisMcpController
→ ToolCallRequest(projectId, tenantId, hospitalId)
→ HisToolInvokeService
→ HisClientRouter.route(context)
→ HisClientProvider
→ Hospital HisClient
CardActionController
→ AgentActionOrchestrator(conversationId, projectId, tenantId)
→ ConversationTerminalService.findByScope(...)
→ hospitalId
→ McpToolService(context)
→ HisClientRouter.route(context)
→ Hospital HisClient
前端卡片动作请求不新增 tenantId 或 hospitalId 参数。医院事实来自当前租户下的会话,
避免客户端伪造医院路由。
默认配置:
strict: false
fallback-enabled: true
行为:
HisClient。生产环境应配置:
strict: true
fallback-enabled: false
以下情况直接失败,不调用任何 HIS:
严格模式禁止回退到其他医院路由,也禁止仅凭 hospitalId 匹配。
| 场景 | 行为 |
|---|---|
| 上下文不完整 | 严格模式失败;兼容模式回退 |
| 路由不存在 | 严格模式失败;兼容模式回退 |
| Provider 不存在 | 严格模式失败;兼容模式回退并告警 |
| 重复路由 | 配置校验失败,阻止严格模式启动 |
| Provider 创建客户端失败 | 返回工具调用失败,不切换其他医院 |
| HIS 超时或熔断 | 由 Provider/客户端转换为平台异常,不跨路由重试 |
错误信息不得包含 endpoint、凭证引用内容、Token、证书路径或患者敏感数据。
credentialRef,真实凭证使用环境变量或密钥系统。仓库当前 application.yml 存在历史明文配置,本次只保证新增 HIS 路由不继续使用该模式;
历史配置清理应单独执行密钥轮换,不能仅删除文件内容而不更换已暴露凭证。
推荐发布顺序:
strict=false 部署代码,验证原 Mock 路径无回归。strict=true、fallback-enabled=false。本次不修改现有 HTTP URL、请求体和响应结构。
截至 2026-06-27,以下代码已完成:
McpToolService 上下文重载。未完成内容:
HisClientProvider 实现。credential-ref、超时和网络策略配置。本实现没有新增数据库表、字段或迁移 SQL。
HisToolInvokeService 使用 ToolCallRequest 的三字段上下文。AgentActionOrchestrator 从作用域会话获取 hospitalId。McpToolService 方法仍调用默认客户端。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