02-multi-tenancy.md 6.45 KB

切片 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 个同时带有 sBrandsIdsSubsidiaryId。另有 1 个只带 sBrandsId,0 个只带 sSubsidiaryId。这几乎覆盖所有业务数据表和框架元数据表。

如何注入

每个已认证 REST 端点在请求到达后都会调用:

RequestAddParamUtil.me().addParams(params, userInfo);

RequestAddParamUtil 位于 xlyPersist/src/main/java/com/xly/utils/RequestAddParamUtil.java(56 行,短且值得完整阅读)。它从 UserInfo 中取出认证用户身份,并向请求 params map 写入 16 个 key:sBrandsIdsSubsidiaryIdsBrId / sSuIdsLoginIdsUserIduserIdsUserTypesUserNamesMakePersonsLanguagesIpAddresssComputeNamesTeamIdsMachineIdCURRENT_USER_LOGIN_TYPE

xlyApi 在 xlyApi/src/main/java/com/xly/api/util/RequestAddParamUtil.java 中有几乎相同的 57 行副本;可视为跨两个服务复制的同一个工具。

任何下游 MyBatis 查询和存储过程调用,只要引用 #{sBrandsId} / #{sSubsidiaryId},就会自动受作用域约束。前端不能影响这些值;它们来自服务端 session,经 @CurrentUser 参数解析器传入。

查询中如何体现

切片 1 的 getModelBysId 调用最终在每张元数据表上都带有:

WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId}

同样谓词出现在代码库中几乎每个业务数据查询里。这就是运行时的多租户边界。

失效模式

忘记按 sBrandsId 过滤的查询会返回所有租户的行,这是灾难性数据泄漏。切片 1 已经在保存端点上下文中提出该安全关注(前端直接提供 sTable);同样问题会在任何漏掉租户谓词的存储过程或 MyBatis mapper 中重现。Wiki 的维护人员权限参考应持续强调这一点。

每版本过滤(sVersionFlowId

“版本”是什么意思

xly 以多个版本销售:基础版EBC-MDMEBC-SDEBC-RD 等。每个版本暴露不同模块集合。基础版客户看不到高级模块;高级客户看到基础版内容以及额外模块。

版本在哪里定义

sisversionflow 表(此 dev DB 中 1 行):

含义
sId 17551378250008601172639655149000 版本主键
sCode 8S_001 gdsmodule.sVersionFlowCode 引用的短码
sFlowName 基础版 显示名
bEbcErpPremium, bEbcMes, bEbcMesStandard, bSass flags 该版本属于哪些产品变体

真实 SaaS 中这里会有更多行,每个不同版本一行。gdsmodule 中看到的 EBC-MDM-002EBC-SD-002EBC-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(每模块)。

本切片使用的参考

  • 运行时RequestAddParamUtil 属于运行时章节,是通用租户上下文注入器。

待验证项

  1. 按版本过滤模块发现。 机制合理,但尚未定位精确代码路径,候选是 GdsmoduleControllerGdsmoduleServiceImpl
  2. Activiti 工作流。 sVersionFlowId 不是工作流 id(尽管名字里有 flow)。实际工作流表均为空;未来切片 7 在有活动流程 DB 时记录。
  3. session 级租户解析。 JWT / session 如何把登录用户映射到 sBrandsId / sSubsidiaryId,位于 RequestAddParamUtil 下面一层,值得在维护章节追踪。