# 切片 2 — 多租户与产品版本 xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一组元数据表服务多个客户;一个客户内部又可能有多个子公司;客户之间还有多个产品*版本*(基础版、EBC-MDM、EBC-SD、EBC-RD 等)。本切片追踪这些作用域如何被强制执行。 多租户是基础能力:做错了,一个客户就会读到另一个客户的订单。因此本切片也是 Wiki 中最与安全相关的章节之一。 ## 三条作用域轴 | 轴 | 携带位置 | 粒度 | 用途 | |---|---|---|---| | **`sBrandsId`**(加工商ID) | 几乎每条业务行 | 每行 | “这行属于哪个加工商 / 公司?” | | **`sSubsidiaryId`**(子公司ID) | 几乎每条业务行 | 每行 | “公司内哪个子公司?” | | **`sVersionFlowId`**(版本流程ID) | 仅 `gdsmodule` | 每模块 | “这个模块属于哪个产品版本?” | 前两者是**每行**作用域。第三者是模块列表加载时的**每模块**过滤。机制不同,层级不同。 ## 每行作用域(`sBrandsId` + `sSubsidiaryId`) ### 覆盖多广 `xlyweberp_saas_ai` 的 1,212 张表 / 视图(901 张基础表 + 311 个视图)中,**1,008 个同时带有 `sBrandsId` 和 `sSubsidiaryId`**。另有 1 个只带 `sBrandsId`,0 个只带 `sSubsidiaryId`。这几乎覆盖所有业务数据表和框架元数据表。 ### 如何注入 每个已认证 REST 端点在请求到达后都会调用: ```java RequestAddParamUtil.me().addParams(params, userInfo); ``` `RequestAddParamUtil` 位于 `xlyPersist/src/main/java/com/xly/utils/RequestAddParamUtil.java`(56 行,短且值得完整阅读)。它从 `UserInfo` 中取出认证用户身份,并向请求 `params` map 写入 16 个 key:`sBrandsId`、`sSubsidiaryId`、`sBrId` / `sSuId`、`sLoginId`、`sUserId`、`userId`、`sUserType`、`sUserName`、`sMakePerson`、`sLanguage`、`sIpAddress`、`sComputeName`、`sTeamId`、`sMachineId`、`CURRENT_USER_LOGIN_TYPE`。 xlyApi 在 `xlyApi/src/main/java/com/xly/api/util/RequestAddParamUtil.java` 中有几乎相同的 57 行副本;可视为跨两个服务复制的同一个工具。 任何下游 MyBatis 查询和存储过程调用,只要引用 `#{sBrandsId}` / `#{sSubsidiaryId}`,就会自动受作用域约束。前端不能影响这些值;它们来自服务端 session,经 `@CurrentUser` 参数解析器传入。 ### 查询中如何体现 切片 1 的 `getModelBysId` 调用最终在每张元数据表上都带有: ```sql WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId} ``` 同样谓词出现在代码库中几乎每个业务数据查询里。这就是运行时的多租户边界。 ### 失效模式 忘记按 `sBrandsId` 过滤的查询会返回所有租户的行,这是灾难性数据泄漏。切片 1 已经在保存端点上下文中提出该安全关注(前端直接提供 `sTable`);同样问题会在任何漏掉租户谓词的存储过程或 MyBatis mapper 中重现。Wiki 的[维护人员权限参考](../reference/maintainer/runtime.md)应持续强调这一点。 ## 每版本过滤(`sVersionFlowId`) ### “版本”是什么意思 xly 以多个版本销售:**基础版**、**EBC-MDM**、**EBC-SD**、**EBC-RD** 等。每个版本暴露不同模块集合。基础版客户看不到高级模块;高级客户看到基础版内容以及额外模块。 ### 版本在哪里定义 `sisversionflow` 表(此 dev DB 中 1 行): | 列 | 值 | 含义 | |---|---|---| | `sId` | `17551378250008601172639655149000` | 版本主键 | | `sCode` | `8S_001` | `gdsmodule.sVersionFlowCode` 引用的短码 | | `sFlowName` | `基础版` | 显示名 | | `bEbcErpPremium`, `bEbcMes`, `bEbcMesStandard`, `bSass` | flags | 该版本属于哪些产品变体 | 真实 SaaS 中这里会有更多行,每个不同版本一行。`gdsmodule` 中看到的 `EBC-MDM-002`、`EBC-SD-002`、`EBC-RD-007` flow code,应对应多版本生产 DB 中的行。 ### 模块如何按版本过滤 `sVersionFlowId` 只在三张表上: - `gdsmodule`(实时模块目录)。 - `gdsmodule_0923bak`(备份快照)。 - `gdsmodule_copy1`(另一个快照)。 因此每版本过滤**只发生在模块发现时**,不是每个业务查询上。用户登录时,框架解析租户所属版本,然后把可见模块列表过滤到匹配 `gdsmodule.sVersionFlowId` 的模块。之后,每个加载的模块照常用 `sBrandsId` / `sSubsidiaryId` 读取数据。 `xlyweberp_saas_ai` 中按 `sVersionFlowId, sVersionFlowCode` 分组的图景: | Flow code | 模块数 | 覆盖内容 | |---|---|---| | 空 | 1,002 | 未标记,多为框架内部模块和不受版本 gate 的项目 | | `8S_001`(基础版) | 322 | Essentials baseline | | `EBC-SD-002` | 15 | 销售 / 交付 | | `EBC-RD-007` | 6 | 研发 | | `EBC-MDM-002` | 5 | 主数据管理 | | `EBC_001` | 4 | 基础 EBC bundle | | `EBC-SD-003` | 2 | SD 变体 | | `EBC-SD-001` | 1 | SD 变体 | | `EBC-COM-001` | 1 | 公共组件 | 322 个基础版标记模块是通用授权核心;1,002 个未标记行大多是框架内部模块,不受版本 gate 影响。其余具名 flow 是版本特定 add-on。 ## 为什么 dev 看起来小 本 Wiki 使用的 `xlyweberp_saas_ai` 只有一个品牌(`sBrandsId = '1111111111'`)、一个子公司(同值)和一个填充版本(`8S_001`)。多租户机制已接好,但几乎没有压力测试。生产环境应预期几十个品牌 × 每品牌几十个子公司 × 多个版本,全部通过同样的每行过滤模式隔离。 ## 本切片引入的概念 - *多租户作用域*:`sBrandsId` / `sSubsidiaryId` 是每行租户边界;框架的通用注入器是 `RequestAddParamUtil`。 - *产品版本*:`sVersionFlowId` 通过 `sisversionflow` 实现每模块可见性过滤;区分 scoping(每行)和 gating(每模块)。 ## 本切片使用的参考 - [运行时](../reference/maintainer/runtime.md):`RequestAddParamUtil` 属于运行时章节,是通用租户上下文注入器。 ## 待验证项 1. **按版本过滤模块发现。** 机制合理,但尚未定位精确代码路径,候选是 `GdsmoduleController` 或 `GdsmoduleServiceImpl`。 2. **Activiti 工作流。** `sVersionFlowId` 不是工作流 id(尽管名字里有 flow)。实际工作流表均为空;未来切片 7 在有活动流程 DB 时记录。 3. **session 级租户解析。** JWT / session 如何把登录用户映射到 `sBrandsId` / `sSubsidiaryId`,位于 `RequestAddParamUtil` 下面一层,值得在维护章节追踪。