SaaS化代码改造与下游交接说明.md 17 KB


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, 先盘点目标数据库,再选择 SQL。
  3. 使用以下命令查看本阶段完整代码差异:

    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-openplatformemoon-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-agentemoon-openplatform

  • 会话创建时写入租户。
  • 会话详情、列表、消息计数和 Token 累加按项目和租户过滤。
  • 终端会话加载同时校验 projectIdtenantId
  • Agent App 和 Engine Config 按项目和租户查询。
  • OpenPlatform 将租户传入同步和流式 Agent 调用链。

兼容策略:

  • 保留旧方法重载,未传租户时继续按旧逻辑工作,减少对现有调用方的阻塞。
  • 无租户的会话和 Agent 流式入口已经标记为 @Deprecated;租户感知方法是 IAgentChatApplicationService 的强制实现契约,避免新实现静默忽略租户。
  • 新的 OpenPlatform 主链路已经使用带租户参数的方法。

4.3 卡片实例和卡片动作

涉及模块:emoon-ai-cardemoon-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-deviceemoon-openplatform

  • 设备注册发现同名设备属于其他项目时拒绝复用,避免跨项目接管设备。
  • 设备命令新增 projectIdtenantId 上下文。
  • 命令创建、轮询和回执按设备、项目、租户共同过滤。
  • Device Controller 从鉴权上下文传入项目和租户。

兼容策略:

  • 保留旧的无作用域方法,用于未迁移的内部调用。
  • 新的 OpenPlatform 路径已经使用带作用域方法。

4.6 任务实例

涉及模块:emoon-ai-agent

  • AiTaskInstanceDO 新增 tenantId
  • 创建任务时写入项目和租户。
  • 查询会话活跃任务时支持 conversationId + projectId + tenantId + status 作用域。

第二阶段续作:

  • advanceSteppatchContextcompleteTaskfailTask 已增加项目和租户作用域重载。
  • 带作用域写操作先按 task_id + project_id + tenant_id 查找,越权任务不会更新。
  • Agent 路由、卡片动作编排和 OpenPlatform Controller 已传递可信项目、租户上下文。
  • 旧的无作用域重载仍保留,未迁移内部调用可以继续工作。

4.7 HIS 多医院路由

涉及模块:emoon-ai-mcp-apiemoon-ai-mcp

  • ToolCallRequest 新增 hospitalId
  • HIS legacy 接口支持可选请求头: X-Emoon-Project-IdX-Emoon-Tenant-IdX-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。

默认配置保持兼容,避免阻塞现有环境:

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 上下文加载失败。
  • OpenAdminClientTestRagServiceImplTest 复用同一失败上下文而报错。

本轮新增的路由、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 映射到正确项目。
  • 确认新代码实际访问的表已经具备所需字段。
  • 确认每家医院的 projectIdtenantIdhospitalId 映射关系。
  • 确认真实 HIS 接入仍处于单客户端还是已完成医院路由。

9.2 推荐验证命令

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 多医院路由完成并通过至少两家医院验证。
  • 已知内存态组件的部署限制被接受,或已完成持久化改造。