saas 多租户使用文档
saas 多租户使用文档
💎 VIP 付费内容
本文档涉及 SaaS 多租户 相关功能,属于 VIP 付费内容。
- 后端:需在服务端开通对应授权后方可启用相关接口。
- 前端:需开通 钻石会员 才能完整使用本模块的全部功能。
未开通会员时,部分功能将受限或不可用,详情可参考会员说明。
本项目 采用 DB-per-tenant(独立数据库)多租户架构。功能开关由「套餐菜单配置」驱动(用户可见菜单/权限 = 角色菜单 ∩ 套餐菜单)。
一、架构概述
每个租户拥有独立的数据库实例,租户之间业务数据完全隔离;租户信息、套餐、菜单等共享数据集中存放在主库。
┌──────────────────────────────────────────────────────────┐
│ 主库 (MainDb) │
│ sys_tenant · sys_tenant_plan · sys_menu · sys_dict_* │
│ sys_tenant_plan_menu · sys_tenant_plan_binding │
└──────────────────────────────────────────────────────────┘
│ │
┌────▼────┐ ┌──────▼──────┐
│ 租户库 A │ │ 租户库 B │
│ Tenant=X │ │ Tenant=Y │
│ · 用户 │ │ · 用户 │
│ · 角色 │ │ · 角色 │
│ · 部门 │ │ · 部门 │
│ · 日志 │ │ · 日志 │
│ · 商城 │ │ · 商城 │
└─────────┘ └─────────────┘
核心能力
| 能力 | 说明 |
|---|---|
| 多租户开关 | 通过 TenantSettings:UseTenant 一键启停 |
| 独立数据库 | 每个租户对应独立的数据库实例 |
| 租户生命周期 | 开通、初始化、停服、续费、注销全流程管理 |
| 套餐体系 | 预置免费版/专业版套餐,支持自定义套餐(CRUD) |
| 套餐菜单授权 | 为每个套餐勾选可用菜单,租户能力由套餐菜单决定 |
| 用户配额 | 按套餐限制租户用户数上限,支持单租户覆盖 |
| 到期管理 | 自动到期校验、到期提醒 |
| 平台菜单隔离 | 平台专属菜单(租户管理、菜单管理等)仅主租户可见 |
二、快速开始
2.1 启用多租户模式
在 ZR.Admin.WebApi/appsettings.json 中配置:
{
"MainDb": "0", // 主库 ConfigId(存放租户、套餐、菜单等共享数据)
"MallDb": "0", // 商城库 ConfigId(非 SaaS 模式下商城实体使用)
"TenantSettings": {
"UseTenant": true, // 是否启用多租户 0/false=禁用, 1/true/on=启用
"PlatformMenuPermPrefixes": [
"system:menu:",
"system:tenant:",
"system:tenantplanmenu:",
"monitor:job:",
"tool",
"system:lang:",
"monitor:server",
"monitor:cache",
"system:datascreen:list"
]
},
"dbConfigs": [
{
"Conn": "Server=localhost;Database=ZrAdmin_Main;...",
"DbType": 1, // 0=MySql, 1=SqlServer, 3=Oracle, 4=PgSql
"ConfigId": "0", // 主库
"IsAutoCloseConnection": true
},
{
"Conn": "Server=localhost;Database=ZrAdmin_TenantA;...",
"DbType": 1,
"ConfigId": "tenant_a", // 租户A的数据库(需与 sys_tenant.TenantId 一致)
"IsAutoCloseConnection": true
}
]
}
关键点:
TenantSettings:UseTenant控制多租户总开关(已从根节点UseTenant迁移到TenantSettings节点内)MainDb指定主库 ConfigId,租户/套餐/菜单等共享数据存储在主库PlatformMenuPermPrefixes定义平台专属权限前缀,非主租户禁止访问(前缀匹配)- 每新增一个租户,需在
dbConfigs中增加对应数据库连接dbConfigs[].ConfigId必须与租户表sys_tenant.TenantId严格一致
2.2 新增租户数据库
- 创建新数据库(如
ZrAdmin_TenantC) - 在
dbConfigs中添加连接串:
{
"Conn": "Server=localhost;Database=ZrAdmin_TenantC;...",
"DbType": 1,
"ConfigId": "tenant_c",
"IsAutoCloseConnection": true
}
- 重启应用生效
- 调用
POST /system/tenant/provision一键开通(建档 + 初始化 + 种子数据 + 默认套餐)
三、核心概念
3.1 数据库路由机制
BaseRepository<T>
├── UseTenant=1 (启用多租户)
│ ├── 实体实现 IMainDbEntity → 路由到主库 (MainDb)
│ └── 其他实体 → 路由到当前租户库 (TenantId)
│
└── UseTenant=0 (未启用)
├── 实体有 [Tenant("X")] 特性 → 路由到 ConfigId=X
└── 无特性 → 路由到 ConfigId="0"
3.2 租户上下文解析
系统按以下优先级确定当前租户 ID:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | TenantContext.CurrentTenantId | AsyncLocal,当前异步流显式设置 |
| 2 | HttpContext.Items["TenantId"] | 中间件注入 |
| 3 | HTTP Header tenantId | 请求头传递 |
| 4 | JWT Claims | 登录后 Token 中携带 |
| 5 | MainDb 配置值 | 默认兜底值(通常为 "0") |
3.3 主库实体 (IMainDbEntity)
以下实体实现了 IMainDbEntity 接口并标注 [Tenant("0")],数据始终存储在主库:
| 实体类 | 对应表 | 说明 |
|---|---|---|
SysTenant | sys_tenant | 租户信息表 |
SysTenantPlan | sys_tenant_plan | 套餐定义 |
SysTenantPlanBinding | sys_tenant_plan_binding | 租户-套餐绑定 |
SysTenantPlanMenu | sys_tenant_plan_menu | 套餐-菜单授权 |
SysMenu | sys_menu | 系统菜单(菜单集中在主库统一维护) |
SysDictData | sys_dict_data | 字典数据(主库基准) |
SysDictType | sys_dict_type | 字典类型 |
开发提示: 新增需要在主库存储的实体,实现
IMainDbEntity接口并添加[Tenant("0")]特性即可。
3.4 租户库实体
未实现 IMainDbEntity 的业务实体(如 SysUser、SysRole、SysDept、SysRoleMenu、SysLogininfor、SysOperLog 等)会自动路由到当前租户的数据库。
3.5 权限模型:角色菜单 ∩ 套餐菜单
这是当前架构的核心变化。租户用户最终可见的菜单/权限由两层交集决定:
用户可见菜单 = 角色分配的菜单 (SysRoleMenu, 租户库)
∩ 套餐授权的菜单 (SysTenantPlanMenu, 主库)
- 主租户(
MainDb对应租户):不走套餐过滤,拥有全部主库菜单 - 子租户:即便角色分配了某菜单,若套餐未授权该菜单,用户仍不可见/不可用
- 相关服务方法:
SelectMenuTreeByUserIdForTenant、SelectMenuPermsByUserIdForTenant
四、套餐体系
4.1 预置套餐
系统首次运行时(EnsureDefaultPlans)自动创建两个默认套餐写入数据库;数据库已有套餐后不再写入。之后可通过后台套餐管理页面自行新增/编辑/删除套餐,无需修改配置文件。
| 套餐 | PlanCode | 用户上限 | 是否默认 |
|---|---|---|---|
| 免费版 | free | 5 | 是 |
| 专业版 | pro | 100 | 否 |
套餐能力(可访问哪些菜单/功能)通过「套餐菜单配置」为每个套餐勾选菜单。
4.2 套餐菜单授权(能力控制核心)
在套餐管理页面点击某套餐的「分配菜单」,弹出菜单树勾选框,为该套餐授权可用菜单。保存后:
- 该套餐下的所有租户将只能访问被勾选菜单对应的功能
- 中间件
TenantFeatureMiddleware会以套餐菜单权限作为兜底拦截 - 目录节点(含子节点的 M 型菜单)勾选状态由子节点驱动,不单独持久化勾选
4.3 自定义套餐
通过前端「套餐管理」页面新增/编辑/删除套餐,或调用套餐 CRUD API(见 5.6)。
// 等价的实体结构
var plan = new SysTenantPlan
{
PlanCode = "enterprise", // 唯一,不可重复
PlanName = "企业版",
MaxUsers = -1, // -1 表示不限制
Status = 0, // 0=可用, 1=禁用
IsDefault = 0,
Sort = 30
};
新增套餐后,记得为其「分配菜单」,否则该套餐下的租户无任何可用功能。
五、租户与套餐管理 API
接口基础路径:
/system/tenant与/system/tenantPlanMenu
5.1 租户开通(一键式)
POST /system/tenant/provision 权限:system:tenant:add
自动完成:建档 → 数据库初始化 → 基础表创建 → 种子数据复制 → 默认套餐分配
{
"tenantId": "tenant_c",
"tenantName": "客户C",
"companyName": "客户C有限公司",
"contactName": "张三",
"contactPhone": "13800000000",
"expireTime": "2027-01-01",
"remark": "备注",
"initializeNow": true,
"seedFromMain": true,
"enableAfterInit": true
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| tenantId | string | 是 | 租户标识,需与 dbConfigs.ConfigId 一致 |
| tenantName | string | 否 | 租户名称 |
| companyName / contactName / contactPhone | string | 否 | 企业与联系人信息 |
| expireTime | datetime | 否 | 到期时间 |
| initializeNow | bool | 否 | 是否立即初始化数据库,默认 true |
| seedFromMain | bool | 否 | 是否从主库复制种子数据,默认 true |
| enableAfterInit | bool | 否 | 初始化后是否启用,默认 true |
5.2 租户初始化
POST /system/tenant/initialize 权限:system:tenant:update
单独执行数据库初始化(创建表、复制种子数据)。
{
"tenantId": "tenant_c",
"createDatabaseIfNotExists": true,
"seedFromMain": true,
"ensureMainTenantRecord": false
}
5.3 租户停服
POST /system/tenant/suspend?tenantId=tenant_c&remark=xxx 权限:system:tenant:update
将租户状态设为停用,用户无法登录。默认租户不允许停服。
5.4 租户续费
POST /system/tenant/renew 权限:system:tenant:update
{ "tenantId": "tenant_c", "extendDays": 365, "remark": "续费一年" }
或直接指定新到期时间:
{ "tenantId": "tenant_c", "newExpireTime": "2027-07-12", "remark": "续费到指定日期" }
5.5 租户注销
POST /system/tenant/decommission 权限:system:tenant:remove
{ "tenantId": "tenant_c", "deleteRecord": false, "remark": "客户主动注销" }
deleteRecord=false:停服但保留记录deleteRecord=true:停服并逻辑删除
5.6 套餐 CRUD
| 操作 | 方法 | 路径 | 权限 |
|---|---|---|---|
| 套餐列表 | GET | /system/tenant/plan/list | system:tenant:list |
| 套餐详情 | GET | /system/tenant/plan/{id} | system:tenant:list |
| 新增套餐 | POST | /system/tenant/plan | system:tenant:update |
| 编辑套餐 | PUT | /system/tenant/plan | system:tenant:update |
| 删除套餐 | DELETE | /system/tenant/plan/{id} | system:tenant:update |
新增/编辑请求体(SysTenantPlan):
{
"id": 0,
"planCode": "enterprise",
"planName": "企业版",
"maxUsers": -1,
"status": 0,
"isDefault": 0,
"sort": 30
}
删除保护: 套餐下仍有生效中的租户绑定时拒绝删除;删除时会同步清理该套餐的菜单授权关联。
5.7 套餐菜单授权 API
接口基础路径:
/system/tenantPlanMenu
| 操作 | 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|---|
| 套餐菜单树 | GET | /tree?planCode=xxx | system:tenant:list | 返回带勾选状态的菜单树 |
| 当前租户菜单树 | GET | /myTree | system:tenant:list | 子租户查看本租户套餐菜单 |
| 保存套餐菜单 | POST | /save | system:tenant:update | 全量替换 |
| 复制套餐菜单 | POST | /copy | system:tenant:update | 从源套餐复制到目标套餐 |
| 套餐权限列表 | GET | /perms?planCode=xxx | system:tenant:list | 返回套餐对应的权限标识 |
保存请求体:
{ "planCode": "pro", "menuIds": [1, 2, 3, 100, 101] }
复制请求体:
{ "sourcePlanCode": "free", "targetPlanCode": "pro" }
5.8 分配套餐
POST /system/tenant/plan/assign 权限:system:tenant:update
{
"tenantId": "tenant_c",
"planCode": "pro",
"startTime": "2026-07-01",
"endTime": "2027-07-01",
"maxUsersOverride": 200,
"remark": "客户升级专业版"
}
覆盖参数:
maxUsersOverride可覆盖套餐默认用户上限(留空则用套餐默认值)。 分配新套餐会自动将该租户旧的绑定置为失效。功能能力由套餐菜单配置决定,因此不再有featureFlagsOverride参数。
5.9 查询与用量
| 操作 | 方法 | 路径 | 权限 |
|---|---|---|---|
| 租户当前套餐 | GET | /system/tenant/plan/current?tenantId=xxx | system:tenant:list |
| 套餐用量面板 | GET | /system/tenant/plan/usage?tenantId=xxx | system:tenant:list |
| 到期提醒 | GET | /system/tenant/expire/reminders?withinDays=30 | system:tenant:list |
| 当前租户自助信息 | GET | /system/tenant/my | tenant:my |
5.10 租户 CRUD
| 操作 | 方法 | 路径 | 权限 |
|---|---|---|---|
| 列表 | GET | /system/tenant/list | system:tenant:list |
| 详情 | GET | /system/tenant/{id} | system:tenant:query |
| 新增 | POST | /system/tenant | system:tenant:add |
| 修改 | PUT | /system/tenant | system:tenant:update |
| 修改状态 | PUT | /system/tenant/changeStatus?id=&status= | system:tenant:update |
| 删除 | DELETE | /system/tenant/{ids} | system:tenant:remove |
注意: 默认租户(MainDb 对应租户)不允许停用或删除。
六、业务集成点
6.1 登录流程
1. 前端带上 tenantId 调用登录接口
2. 后端 CheckTenant(tenantId)
├── 租户不存在 → 报错
├── 租户已停用 → 报错
└── 租户已过期 → 报错
3. TenantContext.Change(tenantId) 切换上下文
4. 在租户库中查询用户、验证密码
5. JWT Token 中写入 TenantId
6. Token 返回前端,后续请求自动识别租户
前端登录请求示例:
{ "username": "admin", "password": "admin123", "tenantId": "tenant_c", "uuid": "...", "code": "..." }
6.2 菜单路由过滤
GET /system/login/getRouters 获取路由时:
- 主租户用户 → 返回完整主库菜单
- 子租户用户 → 返回「角色菜单 ∩ 套餐菜单」的交集,并剔除平台专属菜单(
PlatformMenuPermPrefixes)
6.3 功能权限拦截(兜底)
权限核心过滤由 ActionPermissionFilter 完成。TenantFeatureMiddleware 中间件作为兜底,防止未经过滤器的新增接口直接暴露套餐外能力:
- 检查是否启用多租户
- 跳过匿名接口和 Admin 用户
- 读取接口
ActionPermissionFilter.Permission - 取当前租户套餐菜单权限集合
GetPermsByTenantId(tenantId) - 权限不在集合内 → 返回 403:
当前租户套餐未包含该权限
6.4 平台菜单隔离
TenantFeaturePolicy 仅负责平台专属菜单过滤:
- 权限前缀命中
TenantSettings:PlatformMenuPermPrefixes的菜单被视为平台菜单 - 非主租户请求时,这些平台菜单会被
FilterPlatformMenusForNonMainTenant从菜单树中剔除 - 主租户不受影响
6.5 用户配额控制
新增用户(含批量导入)时自动调用 EnsureTenantUserQuotaForAdd:
- 超过套餐用户上限 → 返回错误:
当前租户套餐用户上限为N,现有M,新增后将超限 - 套餐
MaxUsers=-1→ 不限制
6.6 字典数据扩展
主库 sys_dict_data / sys_dict_type 提供基准字典数据(IMainDbEntity),普通租户可在自己的租户库中通过 SysTenantDictData 扩展表进行覆写(相同 DictType + DictValue 覆盖主库数据),读取时自动合并。
七、开发指南
7.1 注册新的主库实体
[SugarTable("my_shared_table", "共享数据表")]
[Tenant("0")]
public class MySharedEntity : SysBase, IMainDbEntity
{
// 字段定义
}
7.2 使用 TenantContext 手动切换
// 在 using 作用域内切换租户,退出自动还原
using var scope = TenantContext.Change("tenant_c");
var users = _userService.Queryable().ToList(); // 路由到 tenant_c
7.3 常用工具方法
string tenantId = App.GetCurrentTenantId(); // 获取当前租户ID
bool enabled = App.IsTenantEnabled(); // 多租户是否启用
string mainDb = App.MainDbConfigId; // 主库 ConfigId(统一入口,勿直接读配置)
string mallDb = App.MallDbConfigId; // 商城库 ConfigId
7.4 在 Service 中显式切换数据源
// 操作租户库
var tenantDb = ResolveTenantDb(); // 当前请求租户
var tenantDb2 = ResolveTenantDb("T2"); // 指定租户
// 操作主库
var mainDb = ResolveMainDb();
// 查询示例
var dictList = mainDb.Queryable<SysDictData>().ToList(); // 主库
var userList = tenantDb.Queryable<SysUser>().Where(...).ToList(); // 租户库
7.5 扩展套餐能力(新增功能)
- 在系统菜单中新增该功能对应的菜单/按钮(主库
sys_menu) - 在「套餐管理 → 分配菜单」中为目标套餐勾选该菜单
- 保存后,该套餐下租户即可访问
7.6 平台专属功能配置
若某功能应仅主租户可用,在 appsettings.json 的 TenantSettings:PlatformMenuPermPrefixes 中加入其权限前缀即可,无需改代码、无需重新编译。
7.7 多租户事务
// 单库事务
_baseRepo.UseTran(() =>
{
_repo1.Insert(data1);
_repo2.Update(data2);
});
// 跨库事务(多租户)
var client = DbScoped.SugarScope;
client.BeginTran();
try
{
client.GetConnectionScope("0").Insertable(...).ExecuteCommand(); // 主库
client.GetConnectionScope(tenantId).Insertable(...).ExecuteCommand(); // 租户库
client.CommitTran();
}
catch
{
client.RollbackTran();
}
7.8 模块化初始化扩展
实现 ITenantModuleInitializer 接口可在租户初始化流程中注入自定义的建表 / 种子逻辑:
public interface ITenantModuleInitializer
{
void InitializeNonSaaS();
}
各业务模块(如 ZR.Mall)可实现该接口,在非 SaaS 模式下执行本模块特有的初始化操作。InitTable.RunInitDb() 遍历所有 ITenantModuleInitializer 实现并自动调用。
7.9 存量库迁移
MigrateTenantColumns() 为 sys_user_msg、sys_file、sys_file_group 表自动补加 TenantId 列(varchar(64) NULL),幂等操作,列已存在或表不存在时跳过。
该迁移在 SqlsugarSetup.AddDb() 中默认执行(所有环境),确保从旧版升级时已有表包含 TenantId 列。新装库由 CodeFirst 自动建列,此处对缺列的存量库补列;表/列不存在时仅告警不中断。
八、配置参考
appsettings.json 关键配置项
| 配置项 | 说明 | 可选值 |
|---|---|---|
TenantSettings:UseTenant | 多租户开关 | 0/false 禁用;1/true/on/yes/enabled 启用 |
TenantSettings:PlatformMenuPermPrefixes | 平台专属权限前缀(非主租户禁止访问) | 字符串数组,前缀匹配 |
MainDb | 主库 ConfigId | 字符串,对应 dbConfigs 中的 ConfigId |
MallDb | 商城库 ConfigId | 非 SaaS 模式下商城实体使用 |
dbConfigs[].ConfigId | 数据库标识 | 字符串,租户库需与 TenantId 一致 |
dbConfigs[].DbType | 数据库类型 | 0=MySql, 1=SqlServer, 2=Sqlite, 3=Oracle, 4=PgSql |
九、数据库表结构
sys_tenant(租户信息表,主库)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | bigint | 主键 |
| TenantId | varchar(64) | 租户标识,对应 dbConfigs.ConfigId |
| TenantName | varchar(100) | 租户名称 |
| CompanyName | varchar(200) | 企业名称 |
| ContactName | varchar(50) | 联系人 |
| ContactPhone | varchar(50) | 联系电话 |
| Status | int | 0=正常, 1=停用 |
| ExpireTime | datetime | 过期时间,null=永不过期 |
| DelFlag | int | 0=存在, 2=删除 |
sys_tenant_plan(套餐定义表,主库)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | bigint | 主键 |
| PlanCode | varchar(64) | 套餐编码(唯一,如 free, pro) |
| PlanName | varchar(100) | 套餐名称 |
| MaxUsers | int | 最大用户数,-1=不限制 |
| Status | int | 0=可用, 1=禁用 |
| IsDefault | int | 0=否, 1=默认套餐 |
| Sort | int | 排序 |
| DelFlag | int | 0=存在, 1=删除 |
FeatureFlags字段已移除,套餐能力改由sys_tenant_plan_menu授权。
sys_tenant_plan_binding(套餐绑定表,主库)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | bigint | 主键 |
| TenantId | varchar(64) | 租户标识 |
| PlanCode | varchar(64) | 套餐编码 |
| Status | int | 0=生效, 1=已失效 |
| StartTime | datetime | 生效时间 |
| EndTime | datetime | 失效时间,null=永不过期 |
| MaxUsersOverride | int | 用户数覆盖(null 则用套餐默认) |
| DelFlag | int | 0=存在, 1=删除 |
sys_tenant_plan_menu(套餐菜单授权表,主库)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | bigint | 主键 |
| PlanCode | varchar(64) | 套餐编码 |
| MenuId | bigint | 菜单ID,关联主库 sys_menu.MenuId |
十、常见场景
场景1:新开一个租户
curl -X POST /system/tenant/provision \
-H "Content-Type: application/json" \
-d '{"tenantId":"new_company","tenantName":"新公司","expireTime":"2027-07-12"}'
自动完成建表、种子数据、默认套餐分配,一步到位。
场景2:租户到期续费
curl -X POST /system/tenant/renew \
-H "Content-Type: application/json" \
-d '{"tenantId":"tenant_c","extendDays":365,"remark":"续费一年"}'
系统自动恢复租户为启用状态。
场景3:租户升级套餐
curl -X POST /system/tenant/plan/assign \
-H "Content-Type: application/json" \
-d '{"tenantId":"tenant_c","planCode":"pro","remark":"客户升级专业版"}'
系统自动将旧套餐置为失效,新套餐立即生效;可访问功能由 pro 套餐已授权菜单决定。
场景4:新增自定义套餐并授权菜单
# 1) 新增套餐
curl -X POST /system/tenant/plan \
-H "Content-Type: application/json" \
-d '{"planCode":"enterprise","planName":"企业版","maxUsers":-1,"status":0,"sort":30}'
# 2) 为套餐授权菜单
curl -X POST /system/tenantPlanMenu/save \
-H "Content-Type: application/json" \
-d '{"planCode":"enterprise","menuIds":[1,2,3,100,101,102]}'
也可直接从已有套餐复制菜单:
curl -X POST /system/tenantPlanMenu/copy \
-H "Content-Type: application/json" \
-d '{"sourcePlanCode":"pro","targetPlanCode":"enterprise"}'
场景5:为单个租户调整用户上限
curl -X POST /system/tenant/plan/assign \
-H "Content-Type: application/json" \
-d '{"tenantId":"tenant_c","planCode":"free","maxUsersOverride":10,"remark":"临时提升免费版用户上限"}'
场景6:批量查询即将到期的租户
curl GET "/system/tenant/expire/reminders?withinDays=30"
运维人员可根据结果主动联系客户续费。
十一、注意事项
- ConfigId 一致性:
dbConfigs[].ConfigId必须与sys_tenant.TenantId严格一致 - 默认租户保护: MainDb 对应的默认租户不允许停用或删除,且默认拥有全部菜单
- 权限双层交集: 子租户用户最终权限 = 角色菜单 ∩ 套餐菜单,两者缺一不可
- 套餐必授权: 新建套餐后务必「分配菜单」,否则该套餐租户无任何可用功能
- 退出切换: 使用
TenantContext.Change()时务必配合using语句 - 异步安全:
TenantContext使用AsyncLocal,在异步流中自动传播 - 菜单在主库:
SysMenu为主库实体,所有租户共享同一套菜单定义;租户差异仅体现在套餐授权 - 平台菜单隔离: 平台专属权限前缀在
PlatformMenuPermPrefixes中维护,非主租户不可见 - WRITE 不过滤器: INSERT/UPDATE 操作不会经过 QueryFilter,写入时需手动设置
TenantId - MallDb 配置: SaaS 模式下商城实体按租户自动路由;非 SaaS 模式下按
MallDb配置的 ConfigId 路由
