# 多租户与产品版本 xly 是多租户 SaaS。同一套代码库、同一套数据库 schema、同一套元数据服务多个客户。框架通过三条相互独立的轴来强制租户边界,它们作用在不同层级。 真实模块中的贯穿示例见[切片 2](../slices/02-multi-tenancy.md)。本页是可从任何地方链接过来的规范摘要。 ## 三条轴 | 轴 | 携带位置 | 粒度 | 事实来源 | |---|---|---|---| | **`sBrandsId`**(加工商ID) | 几乎每条业务行 | 每行 | 用户 session(`UserInfo.getsBrandsId()`) | | **`sSubsidiaryId`**(子公司ID) | 几乎每条业务行 | 每行 | 用户 session | | **`sVersionFlowId`**(版本流程ID) | 仅 `gdsmodule` | 每模块 | 用户所属产品版本(对应 `sisversionflow`) | 实时 DB 中每行作用域非常普遍:`sBrandsId` 出现在 1,009 张表 / 视图上,`sSubsidiaryId` 出现在 1,008 张表 / 视图上。几乎所有业务数据表和框架元数据表都带有它们。 每模块 gating(`sVersionFlowId`)只出现在 **3** 张表上,并且只有 `gdsmodule` 是实时表,另外两张是备份。因此版本 gating 是模块发现阶段的一次性过滤,不是每行检查。 ## 如何强制执行 每个已认证 REST 端点在请求到达后都会运行同一行: ```java RequestAddParamUtil.me().addParams(params, userInfo); ``` 见 `xlyPersist/.../RequestAddParamUtil.java`。这个 56 行工具类从 `UserInfo` 中取出用户身份,并向请求 `params` map 写入 16 个 key(`sBrandsId`、`sSubsidiaryId`、`sBrId`、`sSuId`、`sLoginId`、`sIpAddress`、`sComputeName`、`sUserId`、`userId`、`sLanguage`、`sUserType`、`sUserName`、`sMakePerson`、`sTeamId`、`sMachineId`,以及 `CURRENT_USER_LOGIN_TYPE`)。xlyApi 在 `xlyApi/.../api/util/RequestAddParamUtil.java` 中有几乎相同的 57 行副本。任何下游 MyBatis 查询只要引用 `#{sBrandsId}` 和 `#{sSubsidiaryId}`,就会自动受租户作用域约束。前端不能影响这些值;它们来自服务端 session,并通过 `@CurrentUser` 参数解析器进入。 ## 失效模式 忘记按 `sBrandsId` 过滤的查询会返回所有租户的行,这是灾难性数据泄漏。需要关注三处: 1. **运行时拼动态 SQL 的存储过程**,开发者必须记得注入 `sBrandsId`。代码库中基本一致,但数据库不会强制。 2. **数据库视图。** 几乎所有 `viw_*` 视图都会从主基础表带出租户列,但手写视图如果漏掉这些列,就可能造成按行泄漏。维护审计脚本应标记此类视图。 3. **保存端点**(`addUpdateDelBusinessData`),它允许前端在 payload 中直接提供 `sTable`。如果运行时不校验该表是否属于表单授权范围,这就是权限提升面。见[切片 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save)。 ## 为什么这个实时 DB 看起来很小 `xlyweberp_saas_ai` 只有**一个**品牌(`sBrandsId = '1111111111'`)、**一个**子公司,以及**一个**有数据的版本(`8S_001 / 基础版`)。多租户机制已经接好,但这里没有充分使用。生产租户会有几十个品牌、每个品牌下几十个子公司,以及多个版本;设计通过行数扩展,而不是通过代码分支扩展。