--- 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: - INT-202606-005 - RUN-202606-001 - STD-202605-001 - API-202606-002 related_modules: - emoon-openplatform - emoon-system-api - emoon-system - emoon-ai-agent-api - emoon-ai-agent - emoon-ai-card-api - emoon-ai-card - emoon-ai-device - emoon-ai-file - emoon-ai-mcp-api - emoon-ai-mcp tags: - SaaS - 代码交接 - 多租户 - 前后端联调 --- # SaaS 化代码改造与下游交接说明 ## 1. 给下游人员和 AI Coding 工具的读取顺序 1. 阅读本文件,确认代码已经完成的范围和遗留项。 2. 阅读 [SaaS化数据库候选变更与执行清单.md](SaaS化数据库候选变更与执行清单.md), 先盘点目标数据库,再选择 SQL。 3. 使用以下命令查看本阶段完整代码差异: ```bash git diff --stat 074c4db4..HEAD git diff 074c4db4..HEAD git log --oneline 074c4db4..HEAD ``` 4. 数据库适配完成后启动后端,再按本文第 9 节进行前端和接口联调。 ## 2. 分支和提交 | 提交 | 内容 | |---|---| | `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` 的实时结果为准。 ## 3. 本阶段总体结论 当前代码已经具备“从鉴权项目解析租户,并在主要 OpenPlatform 调用链中传递 `projectId + tenantId`”的基础能力。会话、智能体配置、卡片、卡片动作、设备命令、 文件元数据和任务查询的关键入口已经增加作用域约束。 但当前仍是 SaaS 第一阶段,不应直接判断为生产级完整 SaaS: - 数据库字段和索引尚未在目标环境确认或执行。 - HIS 路由框架和主要调用链已经按 `tenantId + projectId + hospitalId` 精确选择 Provider;真实医院 Provider、专线地址和 凭据仍需下游实现和配置。 - 部分运行时状态仍保存在单 JVM 内存中,不支持可靠多实例部署。 - Workflow、RAG 和管理后台权限未在本分支做完整的双租户实库验收。 - 兼容旧逻辑时保留了无作用域重载,内部调用若继续使用旧方法,仍可能绕过严格隔离。 ## 4. 已完成的代码改造 ### 4.1 鉴权和租户上下文 涉及模块:`emoon-openplatform`、`emoon-system`。 - HMAC 鉴权不再固定写入租户 `000000`,而是从 `sys_project.tenant_id` 解析。 - Bearer Token 仍由配置映射项目,但会继续查询项目并解析真实租户。 - 项目不存在时拒绝请求,避免只凭项目编号构造上下文。 - 系统 Facade 在租户上下文中查询 Agent App 和 Engine Config,并同时约束项目。 - 现有接口路径没有因本次改造发生变化。 注意: - Bearer Token 目前仍是静态配置到项目的映射,不是完整的租户 Token/JWT 体系。 - `tenant_id` 为空时部分兼容代码仍回退为 `000000`。生产数据必须避免空租户。 - 代码中的兼容默认值统一引用 `TenantConstants.DEFAULT_TENANT_ID`,不再散落租户字面量; 这只是迁移期兼容策略,不代表生产环境允许项目缺少租户。 ### 4.2 会话和 Agent 调用 涉及模块:`emoon-ai-agent`、`emoon-openplatform`。 - 会话创建时写入租户。 - 会话详情、列表、消息计数和 Token 累加按项目和租户过滤。 - 终端会话加载同时校验 `projectId` 和 `tenantId`。 - Agent App 和 Engine Config 按项目和租户查询。 - OpenPlatform 将租户传入同步和流式 Agent 调用链。 兼容策略: - 保留旧方法重载,未传租户时继续按旧逻辑工作,减少对现有调用方的阻塞。 - 无租户的会话和 Agent 流式入口已经标记为 `@Deprecated`;租户感知方法是 `IAgentChatApplicationService` 的强制实现契约,避免新实现静默忽略租户。 - 新的 OpenPlatform 主链路已经使用带租户参数的方法。 ### 4.3 卡片实例和卡片动作 涉及模块:`emoon-ai-card`、`emoon-openplatform`。 - 卡片实例详情、会话卡片列表、可执行动作和状态更新支持租户过滤。 - 卡片动作提交、完成和失败前先按租户校验卡片实例。 - 幂等查询通过动作日志关联卡片实例,以卡片实例的 `tenant_id` 限定租户。 - Controller 从鉴权上下文传入租户,不接受前端自行指定租户。 第二阶段续作: - `AiCardActionLogDO` 已增加 `tenantId`,新动作日志直接写入租户。 - 带租户调用按 `tenant_id + idempotency_key` 查询,不再通过卡片实例联表推断。 - 旧的无租户方法仍保留,并从已经加载的卡片实例继承租户,避免阻塞旧调用方。 - 数据库仍需按 Runbook 增加 `tenant_id`,回填后把全局唯一索引改为租户复合唯一索引。 ### 4.4 文件元数据 涉及模块:`emoon-ai-file`。 - 文件元数据查询由只按 `file_id` 改为按 `file_id + project_id` 查询。 - 现有文件表已经使用 `project_id`,本阶段没有新增文件表字段。 已知限制: - 本阶段只收敛了已使用的元数据查询入口。 - 对象存储路径、签名 URL、删除和下载链路仍需在真实环境验证是否始终校验项目。 ### 4.5 设备注册和设备命令 涉及模块:`emoon-ai-device`、`emoon-openplatform`。 - 设备注册发现同名设备属于其他项目时拒绝复用,避免跨项目接管设备。 - 设备命令新增 `projectId`、`tenantId` 上下文。 - 命令创建、轮询和回执按设备、项目、租户共同过滤。 - Device Controller 从鉴权上下文传入项目和租户。 兼容策略: - 保留旧的无作用域方法,用于未迁移的内部调用。 - 新的 OpenPlatform 路径已经使用带作用域方法。 ### 4.6 任务实例 涉及模块:`emoon-ai-agent`。 - `AiTaskInstanceDO` 新增 `tenantId`。 - 创建任务时写入项目和租户。 - 查询会话活跃任务时支持 `conversationId + projectId + tenantId + status` 作用域。 第二阶段续作: - `advanceStep`、`patchContext`、`completeTask`、`failTask` 已增加项目和租户作用域重载。 - 带作用域写操作先按 `task_id + project_id + tenant_id` 查找,越权任务不会更新。 - Agent 路由、卡片动作编排和 OpenPlatform Controller 已传递可信项目、租户上下文。 - 旧的无作用域重载仍保留,未迁移内部调用可以继续工作。 ### 4.7 HIS 多医院路由 涉及模块:`emoon-ai-mcp-api`、`emoon-ai-mcp`。 - `ToolCallRequest` 新增 `hospitalId`。 - HIS legacy 接口支持可选请求头: `X-Emoon-Project-Id`、`X-Emoon-Tenant-Id`、 `X-Emoon-Hospital-Id`。 - 上下文会写入工具调用请求,`hospitalId` 同时补充到工具输入。 - `HisClientRouter` 只按 `tenantId + projectId + hospitalId` 三字段精确匹配,不支持 单字段、通配符或跨医院回退。 - `HisClientProvider` 作为厂商 SPI,根据路由定义创建对应 `HisClient`。 - MCP 标准工具入口、`McpToolService`、Agent 对话降级和 Card 动作链已传递医院上下文。 - Card 动作从 `conversationId + projectId + tenantId` 作用域会话读取医院,不接受 Card payload 覆盖。 - 严格模式下,缺少上下文、路由或 Provider 会失败;兼容模式保留旧单客户端回退。 - `HisClient.isMock()` 区分真实和 Mock 客户端,响应不再固定标记为 Mock。 默认配置保持兼容,避免阻塞现有环境: ```yaml emoon: his: routing: strict: false fallback-enabled: true routes: [] ``` 生产联调完成前不得开启严格模式。下游配置真实路由时只写 `credential-ref`,不得在仓库中提交 Token、密码或私钥。 ## 5. 对现有接口的影响 | 类型 | 影响 | |---|---| | URL 和 HTTP 方法 | 本阶段未主动修改 | | 请求体 | 主要保持不变 | | 鉴权语义 | 同一凭证只能访问其项目和租户下数据 | | 卡片接口 | 查询和动作会校验鉴权租户 | | 设备命令 | 轮询和回执会校验项目及租户 | | 文件查询 | 必须属于当前项目 | | HIS legacy 接口 | 新增可选上下文 Header,旧调用可继续不传 | | 错误表现 | 跨租户数据通常表现为不存在、拒绝或空结果 | 前端不得新增 `tenantId` 输入框,也不得从页面参数决定租户。租户必须由后端根据登录 账号、Token 或项目密钥解析。 ## 6. 已执行的开发验证 2026-06-26 在 `c40b4779` 提交前执行过以下验证: | 模块/用例 | 结果 | |---|---| | `OpenPlatformAuthInterceptorTest` | 9 个通过 | | `AgentChatApplicationServiceImplTest` | 3 个通过 | | `TaskStateServiceTest` | 8 个通过 | | `HisMcpControllerTest` | 1 个通过 | | `emoon-ai-agent` 编译 | 通过 | | `emoon-openplatform` 编译 | 通过 | | `git diff --check` | 通过 | `TerminalMvpAcceptanceTest` 有 3 个既有业务链失败: - `robotNavigationCreatesRouteCard` - `kioskRegistrationCreatesTaskAndCard` - `traceIdPropagatesThroughFullChain` 失败现象是降级引擎路径没有创建预期卡片,与本次租户字段传递没有直接证据关联。 下游不能忽略该失败,应在工程启动后结合实际引擎配置重新判断。 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` 的方法,旧三参数方法仅作为默认租户兼容入口。 - OpenPlatform 正式 Controller 继续调用带租户方法;HTTP URL、方法、请求体和响应结构均未改变。 - 鉴权和 Controller 的空租户兼容回退仍保留,等待下游完成数据库回填后再评估关闭,避免当前阶段阻塞旧数据。 最终 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` 全量测试失败集合与改造前已记录集合一致: - 3 个失败仍是前文记录的 `TerminalMvpAcceptanceTest` 降级引擎未创建卡片。 - `IModelServiceImplTest` 因缺少 `weaviate.className` 配置,Spring 上下文加载失败。 - `OpenAdminClientTest`、`RagServiceImplTest` 复用同一失败上下文而报错。 本轮新增的路由、MCP、Agent、Card 和对话降级测试均通过,没有新增失败;但上述 6 项关闭前不能宣称 OpenPlatform 全量回归通过。 ## 7. 尚未完成的代码工作 | 优先级 | 项目 | 当前风险 | 建议 | |---|---|---|---| | 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`,切换复合唯一索引 | ## 8. 下游数据库适配 不要直接执行 `script/sql/ai-terminal-mvp.sql` 作为本次 SaaS 迁移。该文件是终端 MVP 建表脚本,包含当前环境可能未启用的表和演示 Seed 数据。 下游应执行: 1. 备份目标数据库。 2. 运行数据库 Runbook 的只读盘点 SQL。 3. 确认实际启用的模块和表。 4. 只对启用表执行对应候选 SQL。 5. 回填租户数据并确认不存在未映射记录。 6. 启动后端并做双租户验证。 ## 9. 后端启动和前端联调顺序 ### 9.1 后端启动前 - 确认所有启用项目的 `sys_project.tenant_id` 非空且正确。 - 确认 HMAC 公钥/私钥或 Bearer Token 映射到正确项目。 - 确认新代码实际访问的表已经具备所需字段。 - 确认每家医院的 `projectId`、`tenantId`、`hospitalId` 映射关系。 - 确认真实 HIS 接入仍处于单客户端还是已完成医院路由。 ### 9.2 推荐验证命令 ```bash 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` 作为上线验收。 ### 9.3 前端适配原则 - 前端继续使用现有接口路径。 - 登录或项目凭证决定租户,前端不传可信 `tenantId`。 - 对 404、空列表、无权限结果按“当前租户不可见”处理,禁止自动切换其他项目重试。 - 卡片、会话、设备和文件缓存键至少包含当前登录项目。 - 切换医院账号时清空本地会话、卡片、文件和设备缓存。 - 前端日志不得输出 HMAC 私钥、Bearer Token、患者敏感信息。 ### 9.4 必须完成的双租户联调用例 准备租户 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、专线和两家医院环境联调。 ## 10. 完成交接的判断标准 只有以下条件全部满足后,才能将本专题标记为 `implemented`: - 目标环境 SQL 已按启用表执行并留存执行记录。 - 所有启用项目都能唯一映射到租户和医院。 - 后端编译、架构测试和相关模块测试通过。 - 后端工程成功启动,无缺列或 Mapper SQL 异常。 - 前端完成账号切换和缓存隔离适配。 - 双租户用例完成,跨租户读写全部被阻止。 - 真实 HIS 多医院路由完成并通过至少两家医院验证。 - 已知内存态组件的部署限制被接受,或已完成持久化改造。