doc_id: DEV-202606-005 feature_id: FEAT-202606-005-saas-tenant-isolation type: dev-progress title: SaaS 化代码改造与下游交接说明 status: reviewing owner: 医梦研发团队 created_at: 2026-06-27 updated_at: 2026-06-27 reviewers: [] related_docs:
使用以下命令查看本阶段完整代码差异:
git diff --stat 074c4db4..HEAD
git diff 074c4db4..HEAD
git log --oneline 074c4db4..HEAD
数据库适配完成后启动后端,再按本文第 9 节进行前端和接口联调。
| 提交 | 内容 |
|---|---|
99a608b4 |
完成开放平台租户隔离第一阶段改造 |
f9c656c6 |
补齐卡片动作租户隔离 |
95083df3 |
收敛文件元数据项目隔离查询 |
9ef94dc2 |
隔离跨项目设备重复注册 |
db2555ac |
补齐设备命令租户隔离 |
c40b4779 |
补齐租户上下文与 HIS 路由上下文 |
1875c356 |
补充 SaaS 化改造与 SQL 交接说明 |
a0c8a925 |
补齐任务与卡片动作租户隔离 |
19a0447b |
增加 HIS 多医院路由核心 |
066a3d4c |
接入 MCP 工具医院路由 |
013c28ab |
补充 HIS 路由上下文重载 |
756ac3ee |
贯通 Agent 与 Card 医院路由上下文 |
文件数和行数会随第二阶段续作变化,下游应以
git diff --stat 074c4db4..HEAD 的实时结果为准。
当前代码已经具备“从鉴权项目解析租户,并在主要 OpenPlatform 调用链中传递
projectId + tenantId”的基础能力。会话、智能体配置、卡片、卡片动作、设备命令、
文件元数据和任务查询的关键入口已经增加作用域约束。
但当前仍是 SaaS 第一阶段,不应直接判断为生产级完整 SaaS:
tenantId + projectId + hospitalId 精确选择 Provider;真实医院 Provider、专线地址和
凭据仍需下游实现和配置。涉及模块:emoon-openplatform、emoon-system。
000000,而是从 sys_project.tenant_id 解析。注意:
tenant_id 为空时部分兼容代码仍回退为 000000。生产数据必须避免空租户。TenantConstants.DEFAULT_TENANT_ID,不再散落租户字面量;
这只是迁移期兼容策略,不代表生产环境允许项目缺少租户。涉及模块:emoon-ai-agent、emoon-openplatform。
projectId 和 tenantId。兼容策略:
@Deprecated;租户感知方法是
IAgentChatApplicationService 的强制实现契约,避免新实现静默忽略租户。涉及模块:emoon-ai-card、emoon-openplatform。
tenant_id 限定租户。第二阶段续作:
AiCardActionLogDO 已增加 tenantId,新动作日志直接写入租户。tenant_id + idempotency_key 查询,不再通过卡片实例联表推断。tenant_id,回填后把全局唯一索引改为租户复合唯一索引。涉及模块:emoon-ai-file。
file_id 改为按 file_id + project_id 查询。project_id,本阶段没有新增文件表字段。已知限制:
涉及模块:emoon-ai-device、emoon-openplatform。
projectId、tenantId 上下文。兼容策略:
涉及模块:emoon-ai-agent。
AiTaskInstanceDO 新增 tenantId。conversationId + projectId + tenantId + status 作用域。第二阶段续作:
advanceStep、patchContext、completeTask、failTask 已增加项目和租户作用域重载。task_id + project_id + tenant_id 查找,越权任务不会更新。涉及模块:emoon-ai-mcp-api、emoon-ai-mcp。
ToolCallRequest 新增 hospitalId。X-Emoon-Project-Id、X-Emoon-Tenant-Id、
X-Emoon-Hospital-Id。hospitalId 同时补充到工具输入。HisClientRouter 只按 tenantId + projectId + hospitalId 三字段精确匹配,不支持
单字段、通配符或跨医院回退。HisClientProvider 作为厂商 SPI,根据路由定义创建对应 HisClient。McpToolService、Agent 对话降级和 Card 动作链已传递医院上下文。conversationId + projectId + tenantId 作用域会话读取医院,不接受
Card payload 覆盖。HisClient.isMock() 区分真实和 Mock 客户端,响应不再固定标记为 Mock。默认配置保持兼容,避免阻塞现有环境:
emoon:
his:
routing:
strict: false
fallback-enabled: true
routes: []
生产联调完成前不得开启严格模式。下游配置真实路由时只写
credential-ref,不得在仓库中提交 Token、密码或私钥。
| 类型 | 影响 |
|---|---|
| URL 和 HTTP 方法 | 本阶段未主动修改 |
| 请求体 | 主要保持不变 |
| 鉴权语义 | 同一凭证只能访问其项目和租户下数据 |
| 卡片接口 | 查询和动作会校验鉴权租户 |
| 设备命令 | 轮询和回执会校验项目及租户 |
| 文件查询 | 必须属于当前项目 |
| HIS legacy 接口 | 新增可选上下文 Header,旧调用可继续不传 |
| 错误表现 | 跨租户数据通常表现为不存在、拒绝或空结果 |
前端不得新增 tenantId 输入框,也不得从页面参数决定租户。租户必须由后端根据登录
账号、Token 或项目密钥解析。
2026-06-26 在 c40b4779 提交前执行过以下验证:
| 模块/用例 | 结果 |
|---|---|
OpenPlatformAuthInterceptorTest |
9 个通过 |
AgentChatApplicationServiceImplTest |
3 个通过 |
TaskStateServiceTest |
8 个通过 |
HisMcpControllerTest |
1 个通过 |
emoon-ai-agent 编译 |
通过 |
emoon-openplatform 编译 |
通过 |
git diff --check |
通过 |
TerminalMvpAcceptanceTest 有 3 个既有业务链失败:
robotNavigationCreatesRouteCardkioskRegistrationCreatesTaskAndCardtraceIdPropagatesThroughFullChain失败现象是降级引擎路径没有创建预期卡片,与本次租户字段传递没有直接证据关联。 下游不能忽略该失败,应在工程启动后结合实际引擎配置重新判断。
2026-06-27 第二阶段续作新增验证:
| 模块/用例 | 结果 |
|---|---|
emoon-ai-card 全量测试 |
12 个通过 |
emoon-ai-agent 全量测试 |
74 个通过 |
CardActionControllerWebMvcTest |
2 个通过 |
依赖模块 -DskipTests install |
41 个 reactor 模块构建成功 |
emoon-openplatform -am -DskipTests compile |
49 个 reactor 模块编译成功 |
AiPlatformArchitectureTest |
1 个通过,新增违规 0 |
2026-06-27 HIS 多医院路由新增验证:
| 模块/用例 | 结果 |
|---|---|
HisClientRouterTest + HisMockSqliteConfigTest |
18 个通过 |
HisToolInvokeServiceTest |
15 个通过 |
McpToolServiceTest |
6 个通过 |
emoon-ai-mcp 全量测试 |
64 个通过 |
emoon-ai-agent 全量测试 |
78 个通过 |
AgentChatApplicationServiceImplTest |
4 个通过 |
IAgentChatApplicationServiceTest |
1 个通过 |
交叉 Review 修复(2026-06-27):
HisToolInvokeServiceTest 源码实际包含 15 个 @Test,Maven 也执行 15 个,文档无需改为 14。McpToolService 的 11 个无 HisRouteContext 旧重载已标记为 @Deprecated,签名和行为保持兼容。ConversationTerminalService 无租户旧重载已标记为 @Deprecated。IAgentChatApplicationService 改为强制实现带 tenantId 的方法,旧三参数方法仅作为默认租户兼容入口。最终 reactor 编译、架构测试和 OpenPlatform 已知失败复核见本分支后续验证记录;下游 仍需以目标环境重新执行。
联合执行 emoon-openplatform -am 指定测试时,既有 emoon-mcp-api 模块因没有测试引擎
却配置 Surefire groups 而提前失败。本轮通过先安装当前依赖、再单独运行目标测试规避,
该构建配置问题不属于本次 SaaS 代码改造。
最终验证(2026-06-27):
| 命令 | 结果 |
|---|---|
emoon-ai-mcp test |
64 个通过 |
emoon-ai-agent test |
78 个通过 |
emoon-openplatform -am -DskipTests compile |
49 个 reactor 模块通过 |
AiPlatformArchitectureTest |
1 个通过,新增违规 0 |
emoon-openplatform test |
58 个执行,3 个失败、3 个错误 |
emoon-openplatform 全量测试失败集合与改造前已记录集合一致:
TerminalMvpAcceptanceTest 降级引擎未创建卡片。IModelServiceImplTest 因缺少 weaviate.className 配置,Spring 上下文加载失败。OpenAdminClientTest、RagServiceImplTest 复用同一失败上下文而报错。本轮新增的路由、MCP、Agent、Card 和对话降级测试均通过,没有新增失败;但上述 6 项关闭前不能宣称 OpenPlatform 全量回归通过。
| 优先级 | 项目 | 当前风险 | 建议 |
|---|---|---|---|
| P0 | 真实 HIS Provider 与专线联调 | 路由框架已完成,但尚无真实厂商协议和网络配置 | 实现 Provider、配置两家医院并启用严格模式 |
| P0 | 数据库字段和索引适配 | 新代码可能因缺列启动或运行失败 | 按数据库 Runbook 执行 |
| P0 | 双租户实库越权测试 | 单元测试不能证明全链路隔离 | 准备两个租户和同名业务数据 |
| P1 | IntentStackService 持久化 |
当前内存态不适合多实例 | 改为 Redis 或数据库并使用租户复合键 |
| P1 | CardExecutorImpl 上下文持久化 |
重启或多实例后状态不一致 | 改为 Redis 或数据库 |
| P1 | Bearer Token 模型 | 静态 Token 配置不便运营和轮换 | 建立凭证表或短期 Token/JWT |
| P1 | Workflow/RAG 管理端验收 | 本分支未逐接口验证 | 按租户执行 CRUD 和越权测试 |
已完成但等待数据库 SQL:
| 项目 | 代码状态 | 下游动作 |
|---|---|---|
| 任务写操作作用域 | 已完成 | 增加任务作用域索引并完成双租户验证 |
| 卡片动作幂等键租户化 | 已完成 | 增加并回填 tenant_id,切换复合唯一索引 |
不要直接执行 script/sql/ai-terminal-mvp.sql 作为本次 SaaS 迁移。该文件是终端 MVP
建表脚本,包含当前环境可能未启用的表和演示 Seed 数据。
下游应执行:
sys_project.tenant_id 非空且正确。projectId、tenantId、hospitalId 映射关系。mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent -am -DskipTests compile
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -am -DskipTests compile
mvn -pl emoon-openplatform -am -DskipTests compile
mvn -pl emoon-admin -DskipTests=false -Dprofiles.active= \
-Dtest=AiPlatformArchitectureTest test
涉及模块的测试应按目标环境可用性分别执行,不能用 -DskipTests 作为上线验收。
tenantId。准备租户 A、租户 B,各自绑定独立项目,并创建可辨识的同类数据。
| 编号 | 场景 | 预期 |
|---|---|---|
| SAAS-001 | A 查询自己的会话 | 成功 |
| SAAS-002 | A 查询 B 的会话 ID | 不存在或拒绝 |
| SAAS-003 | A 查询 B 的卡片实例 | 不存在或拒绝 |
| SAAS-004 | A 对 B 的卡片提交动作 | 不执行、不写动作日志 |
| SAAS-005 | A 查询 B 的文件 ID | 不存在或拒绝 |
| SAAS-006 | A 轮询 B 设备的命令 | 返回空或拒绝 |
| SAAS-007 | A 回执 B 的命令 | 不更新 |
| SAAS-008 | A 使用 B 的 Agent/Engine 配置 | 不存在或拒绝 |
| SAAS-009 | A 与 B 使用相同业务幂等键 | 不应互相影响 |
| SAAS-010 | A、B 分别调用各自医院 HIS | 路由到不同 endpoint 和凭证 |
SAAS-009 已完成代码与候选 SQL,仍需目标数据库验证;SAAS-010 已完成路由框架和
自动化客户端隔离验证,仍需真实 Provider、专线和两家医院环境联调。
只有以下条件全部满足后,才能将本专题标记为 implemented: