切片 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 端点在请求到达后都会调用:
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 调用最终在每张元数据表上都带有:
WHERE sBrandsId = #{sBrandsId} AND sSubsidiaryId = #{sSubsidiaryId}
同样谓词出现在代码库中几乎每个业务数据查询里。这就是运行时的多租户边界。
失效模式
忘记按 sBrandsId 过滤的查询会返回所有租户的行,这是灾难性数据泄漏。切片 1 已经在保存端点上下文中提出该安全关注(前端直接提供 sTable);同样问题会在任何漏掉租户谓词的存储过程或 MyBatis mapper 中重现。Wiki 的维护人员权限参考应持续强调这一点。
每版本过滤(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(每模块)。
本切片使用的参考
-
运行时:
RequestAddParamUtil属于运行时章节,是通用租户上下文注入器。
待验证项
-
按版本过滤模块发现。 机制合理,但尚未定位精确代码路径,候选是
GdsmoduleController或GdsmoduleServiceImpl。 -
Activiti 工作流。
sVersionFlowId不是工作流 id(尽管名字里有 flow)。实际工作流表均为空;未来切片 7 在有活动流程 DB 时记录。 -
session 级租户解析。 JWT / session 如何把登录用户映射到
sBrandsId/sSubsidiaryId,位于RequestAddParamUtil下面一层,值得在维护章节追踪。