news 2026/9/10 14:32:51

Payload Multi-Tenant 插件指南:在管理后台内实现按租户的数据隔离与管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Payload Multi-Tenant 插件指南:在管理后台内实现按租户的数据隔离与管理

Payload Multi-Tenant 插件指南:在管理后台内实现按租户的数据隔离与管理

【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload

多租户(Multi-Tenancy)是 SaaS 类应用最常见的需求之一。本指南以当前仓库中@payloadcms/plugin-multi-tenant(源码位于 packages/plugin-multi-tenant)为讲解对象,说明如何在不写大量重复代码的前提下,通过插件向 Payload 的集合注入租户字段、租户选择器、列表过滤与访问控制,实现"一套 Payload 实例、多租户数据天然隔离"。读完本文你将掌握:插件全部配置项与默认值、如何把集合当 Global 使用、如何自定义访问控制以支持跨租户共享文档、如何手动接管 users 集合上的租户数组字段,以及插件底层(集合改造、base access 包装、Cookie 上下文)是如何工作的。

一、插件要解决什么问题

该插件解决的核心问题是在 Payload 中快速搭建多租户能力。其底层实现(入口文件 src/index.ts)是一个标准的definePlugin,在 Payload 构建配置时做"集合改造",主要完成:

  • 向所有开启多租户的集合注入一个指向tenants集合的tenant关联字段(默认字段名为tenant);
  • 在 Admin UI 中注入一个租户选择器(Tenant Selector),让管理员可以"切换当前上下文租户";
  • 依据当前选中的租户自动过滤列表视图结果,并对关系字段的候选值(filterOptions)做同样约束;
  • 在 users 集合上注入一个tenants数组字段,记录用户可访问的租户,并把约束写入访问控制;
  • 支持把某些集合"当作 Global 使用"(每租户一份文档,隐藏列表视图);
  • 新文档自动带上当前选中租户作为默认值;
  • 在删除租户时,默认清理关联文档并移除用户对该租户的引用。

这些能力都可以从 源码结构 中一一得到印证,例如filters/filterDocumentsByTenants.tsutilities/addCollectionAccess.tscomponents/TenantSelector/hooks/afterTenantDelete.ts等模块各司其职。

二、安装与运行前提

安装插件(package.json 中定义包名为@payloadcms/plugin-multi-tenant):

pnpm add @payloadcms/plugin-multi-tenant

安装完成后还需满足两个硬性前提:

  1. 你必须自己创建tenants集合,并自行决定它包含哪些字段(例如nameslugdomain)。插件不会替你创建它。若在你的配置里找不到对应 slug 的集合,插件会直接抛出Tenants collection not found with slug: ...错误(见 src/index.ts)。
  2. 配置中必须存在一个开启 auth 的集合(通常是users)。插件会用它作为管理员用户集合,并注入租户数组字段;如果找不到,会抛出An auth enabled collection was not found(见 src/index.ts)。该插件把payload@payloadcms/ui声明为 peer 依赖,需要与你的 Payload 版本配套使用。

三、插件配置项全解

multiTenantPlugin的配置结构定义在 src/types.ts,其中每个字段的默认值集中在 src/defaults.ts。下面按类别逐一说明。

3.1 顶层开关类

配置项类型默认值作用
enabledbooleantrue设为false直接禁用插件(src/index.ts)。
debugbooleanfalse开启调试模式,让tenant字段在 Admin UI 中可见(正常模式下它会被隐藏,改由独立的 Assign Tenant 操作维护)。
tenantsSlugstring'tenants'指定 tenants 集合的 slug,用于插件定位它。
cleanupAfterTenantDeletebooleantrue删除租户后是否清理其关联文档、并把该租户从所有用户的tenants数组中移除。

3.2 collections:按集合粒度开启多租户

collections是必传的核心选项,键为集合 slug:

collections: { pages: {}, navigation: { isGlobal: true }, media: { useTenantAccess: false }, }

每个集合可用的子选项如下:

子选项默认值作用
isGlobalfalse设为true后集合按 Global 方式工作,隐藏列表视图,并且每个租户只允许一条文档(插件会为其 tenant 字段加unique约束并禁止复制,见 src/index.ts)。
useBaseFiltertrue设为false表示你不希望插件自动叠加"按当前选中租户过滤列表"的 baseFilter,改为手动实现。
useBaseListFiltertrue(已废弃)旧版选项,功能被useBaseFilter取代;源码中当两者同时出现时useBaseFilter优先(src/types.ts)。
useTenantAccesstrue设为false表示该集合访问控制完全由你手动接管,不再叠加插件生成的租户约束。适用于需要"部分文档跨租户共享"的场景(见第六节)。
customTenantFieldfalse设为true后插件不为该集合注入tenant字段,你需要用插件导出的tenantField自行放置。
tenantFieldOverrides-该集合独有的 tenant 字段覆盖配置,优先于顶层tenantField(见 src/index.ts)。
accessResultOverride-覆写该集合访问控制结果。回调接收accessResult(原始访问结果)、accessKeycreate/read/update/delete/readVersions/unlock)以及其余 Access 参数,返回新的AccessResult(src/types.ts)。

3.3 tenantField:注入业务集合的租户字段

控制添加到所有开启多租户集合上的那个关联字段(默认名tenant)。可用配置:

子选项默认值作用
name'tenant'字段名称。
access{}字段级访问控制,类型为RelationshipField['access']

从字段工厂 src/fields/tenantField/index.ts 可以看到该字段的真实形态:一个指向tenantsSlugrelationship字段(hasMany默认false),放置于 Admin 侧边栏(position: 'sidebar'),关闭了创建/编辑与列/筛选/分组显示(allowCreate/allowEdit为 false,column/filter/groupBy禁用),并默认做了两件事:

  • 默认值自动填充:优先读取当前请求携带的payload-tenantCookie 对应的租户(校验其有效性后作为默认值);若集合开启了 autosave,则回退到用户tenants数组中的第一个租户(src/fields/tenantField/index.ts)。
  • filterOptions 约束:该字段在 UI 中只能从"当前用户已分配的租户"里选择(src/fields/tenantField/index.ts)。

3.4 tenantsArrayField:注入 users 集合的租户数组字段

控制添加到用户集合上的tenants数组字段(记录"这个用户能访问哪些租户")。配置为联合类型:

tenantsArrayField?: { arrayFieldAccess?: ArrayField['access'] // 数组字段本身的访问控制 arrayFieldName?: string // 数组字段名,默认 'tenants' arrayTenantFieldName?: string // 数组内租户关系字段名,默认 'tenant' includeDefaultField?: true // 默认行为:自动注入到 users 集合 rowFields?: Field[] // 在每行追加的自定义字段 tenantFieldAccess?: RelationshipField['access'] // 行内租户关系字段的访问控制 } // 或 includeDefaultField: false 时,其余选项不可用(见第五节的联合类型定义)

字段工厂 src/fields/tenantsArrayField/index.ts 会生成一个名为tenantsarray,每行包含一个指向 tenants 集合的必填 relationship(required: trueindex: true),并且数组与行内字段都设置了saveToJWT: true,保证租户 ID 会进入 JWT,便于服务端在各处校验。注意联合类型约束:当includeDefaultFieldfalse时,arrayFieldAccessrowFieldstenantFieldAccess均不允许再配置(类型上为never),因为你将手动接管整个字段。

3.5 全局类选项

配置项类型默认值作用
userHasAccessToAllTenants(user) => boolean恒返回false判定某用户是否为可访问所有租户的超级管理员。若返回true,插件会跳过大部分租户约束。
usersAccessResultOverrideCollectionAccessResultOverride-覆写 users 集合访问控制结果,签名同集合级accessResultOverride
useUsersTenantFilterbooleantrue是否在 users 集合上叠加"按当前选中租户过滤列表"的 baseFilter。
useTenantsCollectionAccessbooleantrue是否给 tenants 集合本身加上"只能访问被分配租户"的访问约束。
useTenantsListFilterbooleantrue是否给 tenants 集合叠加"按当前选中租户过滤列表"的 baseFilter。
tenantSelectorLabelstringRecord<语言代码, string>'Tenant'自定义租户选择器文案,支持 i18n 多语言对象;在 types.ts 中被标记为已废弃,推荐改用i18n
i18n{ translations: { [locale]: {...} } }-覆盖插件界面文案,可用 key 包括assign-tenant-button-label(默认'Assign Tenant')、assign-tenant-modal-title(默认'Assign "{{title}}"')、field-assignedTenant-label(默认'Assigned Tenant')、nav-tenantSelector-label(默认'Filter by Tenant')。

需要指出的是,collections的键不能包含 users 集合——插件会打印警告并跳过对该集合的 tenant 字段注入,以避免双重访问控制(src/index.ts)。

四、基础用法:配置 payload.config.ts

在 配置文档 docs/plugins/multi-tenant.mdx 中给出了完整示例。核心步骤是:先定义好tenants集合(字段完全归你所有),再在plugins数组中调用multiTenantPlugin并指定哪些集合开启多租户:

import { buildConfig } from 'payload' import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' import type { Config } from './payload-types' const config = buildConfig({ collections: [ { slug: 'tenants', admin: { useAsTitle: 'name', // 租户选择器依赖该字段展示名称 }, fields: [ // tenants 集合的字段由你定义,以下只是建议示例 { name: 'name', type: 'text', required: true }, { name: 'slug', type: 'text', required: true }, { name: 'domain', type: 'text', required: true }, ], }, ], plugins: [ multiTenantPlugin<Config>({ collections: { pages: {}, navigation: { isGlobal: true, }, }, }), ], }) export default config

插件允许传入 Payload 生成的类型(<Config>),这样userHasAccessToAllTenants等回调可以获得精确的user类型。插件对集合的改造包括:在pages中注入tenant字段;把navigation处理成"每租户一份"的 Global 式集合;向users注入租户数组字段;在 Admin 的providersactionsbeforeNav中分别挂入TenantSelectionProvider、Global 重定向组件与租户选择器(见 src/index.ts)。

五、插件默认的数据模型与上下文机制

为理解插件的各种开关,先掌握它默认在数据层与运行时做了什么:

users 上的关联结构。插件默认向 users 注入:

tenants: Array<{ tenant: Relationship -> tenants 集合(required, saveToJWT) // + 你通过 rowFields 追加的字段 }>

业务集合上的 tenant 字段。每个开启多租户的集合拥有一个tenantrelationship(指向 tenants),全局/Global 式集合该字段是unique

运行时"当前租户"上下文。租户选择器选中的值会写入名为payload-tenant的 Cookie(Cookie 名可在 src/hooks/afterTenantDelete.ts 的清理逻辑中看到,那里会用generateCookie以相同名称清除过期 Cookie)。列表过滤(src/filters/filterDocumentsByTenants.ts)的执行顺序是:优先使用"当前文档的租户/显式传入的docTenantID",其次读取payload-tenantCookie,再次回退到用户被分配的租户集合,最后返回null交由访问控制兜底。

关系字段候选值也被过滤。插件会递归扫描开启多租户集合上的 relationship 字段(含 blocks 内引用)并注入filterOptions,避免出现"可以选择其他租户的文档"这种越权场景,相关逻辑见 src/utilities/addFilterOptionsToFields.ts。

六、把集合配置成"Global"(每租户一份内容)

多租户场景下,很多"全站性"内容(导航、站点设置、页头页脚)也需要按租户区分,但 Global 本身无法按租户分数据。官方给出的方案是:Global 需要用集合实现,再在插件里把它标记为isGlobal。这样插件会隐藏它的列表视图、为 tenant 字段加unique约束(同一租户只能存在一份),并自动处理文档切换/重定向逻辑:

multiTenantPlugin({ collections: { navigation: { isGlobal: true, }, }, })

对应实现中:isGlobal的集合会进入globalCollectionSlugs分组,被设置disableDuplicate = true,并在存在这类集合时为 Admin 注入GlobalViewRedirectaction 组件(见 src/index.ts)。

七、访问控制原理与自定义

7.1 默认行为:AND 合并租户约束

插件默认把你自己写的访问控制结果,与"文档 tenant 必须 ∈ 当前用户可访问租户"的约束做AND合并,施加在baseAccess层(对 read/update/delete 等均生效,见 src/utilities/addCollectionAccess.ts)。也就是说,用户自己声明的 access 条件与租户约束必须同时满足;若用户未分配任何租户,则只能看到自己的用户记录或什么都看不到(src/utilities/addCollectionAccess.ts)。约束查询体由 getTenantAccess 生成,形如:

{ [fieldName]: { in: userAssignedTenantIDs } }

7.2 自定义:手动接入租户约束以支持共享文档

AND 语义并不适合"某些文档在所有租户间共享"。此时官方推荐:为该集合关闭useTenantAccess,在自定义read访问控制中,把"共享条件"与租户约束用OR组合起来。README(packages/plugin-multi-tenant/README.md)给出了完整可运行示例,关键思路是复用插件导出的getTenantAccess来构造租户约束,而不是自己手写:

// File: payload.config.ts import { buildConfig } from 'payload' import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities' import { Config as ConfigTypes } from './payload-types' export default buildConfig({ plugins: [ multiTenantPlugin<ConfigTypes>({ collections: { media: { useTenantAccess: false, // 关闭默认租户访问约束,改为手动控制 }, }, }), ], collections: [ { slug: 'media', fields: [ { name: 'isShared', type: 'checkbox', defaultValue: false, // 建议对这个"是否共享"字段设置访问控制,防止普通用户随意修改 }, ], access: { read: ({ req, doc }) => { if (!req.user) return false const whereConstraint = { or: [ { isShared: { equals: true } }, // 共享文档对所有登录用户可见 ], } const tenantAccessResult = getTenantAccess({ user: req.user }) if (tenantAccessResult) { whereConstraint.or.push(tenantAccessResult) } return whereConstraint }, }, }, ], })

这里getTenantAccess返回{ tenant: { in: [...] } }形式的 Where 约束,把它 push 进or数组,即得到"共享文档归属于我所在租户的文档"这一符合业务直觉的可见性规则。若使用自定义 access 后仍希望租户约束生效,可用集合级accessResultOverride/usersAccessResultOverride在插件结果之上再做一层覆写。

八、手动放置 users 集合上的租户数组字段

默认情况下插件会把tenants数组字段追加到 users 集合字段的末尾。若你想把它放进 Tab、侧边栏或行/折叠区,或修改其部分属性,可设置tenantsArrayField.includeDefaultField: false,然后手动把插件导出的数组字段合并进自己的字段定义:

import type { CollectionConfig } from 'payload' import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields' const customTenantsArrayField = tenantsArrayField({ arrayFieldAccess: {}, // 数组字段的访问控制 tenantFieldAccess: {}, // 行内租户关系字段的访问控制 rowFields: [], // 每行追加的自定义字段 }) export const UsersCollection: CollectionConfig = { slug: 'users', fields: [ { ...customTenantsArrayField, label: 'Associated Tenants', }, ], }

README 特别强调了这个字段的摆放限制:该字段不能嵌套在具名字段(group、命名 Tab、array)内部,但可以放进 row、未命名 Tab 或 collapsible 中(packages/plugin-multi-tenant/README.md)。导出的tenantsArrayField工厂位于 src/fields/tenantsArrayField/index.ts,同目录下还导出了用于业务集合的tenantField(模块导出清单见 src/exports/fields.ts)。同样地,若你希望某个业务集合自行摆放 tenant 字段,可设置customTenantField: true后用导出的tenantField放置。

九、租户删除时的清理行为与安全提示

插件会在 tenants 集合上注册afterDelete钩子(src/hooks/afterTenantDelete.ts),当租户被删除时默认执行两件事:

  1. 删除关联文档:对每个开启多租户的集合执行批量deletewhere: { tenant: { in: [deletedId] } });
  2. 清理用户引用:查找tenants.tenant包含该租户的用户,从其数组中移除对应行;若被删租户恰好是当前 Cookie 中选中的租户,还会下发一条过期的payload-tenantSet-Cookie,把上下文清除。

正因删除副作用较大,配置文档 特别提醒:必须为 tenants 集合配置足够严格的访问控制,防止未授权用户删除租户。若你希望完全自己掌控删除流程,可将顶层cleanupAfterTenantDelete设为false关闭该行为(src/index.ts)。

十、前端如何按租户消费数据

插件只负责管理与隔离数据,前端查询仍需你自己完成。由于tenant就是普通关系字段,可以直接在查询条件里按 tenants 集合上的业务字段过滤(例如按slugdomain匹配当前请求的租户):

const pagesBySlug = await payload.find({ collection: 'pages', depth: 1, draft: false, limit: 1000, overrideAccess: false, where: { 'tenant.slug': { equals: 'gold' }, // 具体约束取决于你在 tenants 集合上定义的字段 }, })

若采用/[tenantDomain]/[slug]的路由结构 + Next.js rewrites,可按域名改写请求路径,把域名解析为租户标识后用于上面的查询:

async rewrites() { return [ { source: '/((?!admin|api)):path*', destination: '/:tenantDomain/:path*', has: [{ type: 'host', value: '(?<tenantDomain>.*)' }], }, ] }

编写自定义 Admin 组件时使用 useTenantSelection

插件从@payloadcms/plugin-multi-tenant/client导出了若干客户端模块(见 src/exports/client.ts):TenantFieldAssignTenantFieldTriggerWatchTenantCollectionuseTenantSelection。在 官方配置文档 中记录了useTenantSelection的用法与返回上下文:

import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client' const tenantContext = useTenantSelection() // { // options, // 可选的租户选项数组 // selectedTenantID, // 当前选中的租户 ID // setPreventRefreshOnChange, // 切换租户时是否阻止页面刷新(浏览 Global 时建议 true) // setTenant: ({ id, refresh }) => void, // 切换租户,可选刷新 // }

同目录的providers/TenantSelectionProvider/提供实现,它由插件在 Admin 的providers中自动注入;TenantSelectorAssignTenantFieldModal等界面组件也依赖同一套上下文。

十一、插件内部工作流与常见问题排查

把 src/index.ts 从头读一遍,可看到插件执行的关键流水线,这对排查问题很有帮助:

  1. 处理enabled === false的提前退出,合并所有默认值(defaults.ts:tenantsslug、tenant字段名、tenants数组字段名、选择器默认文案);
  2. 定位 auth 集合(admin.user优先,其次遍历找带auth的集合),注入tenants数组字段;
  3. 为 users 集合叠加按选中租户过滤的 baseFilter(useUsersTenantFilter !== false时);
  4. 遍历所有集合:找到 tenants 集合后叠加useTenantsCollectionAccess访问约束、useTenantsListFilter列表过滤、租户删除清理钩子、用于监听租户变更的_watchTenantUI 字段与getTenantOptions端点;对其他声明过的集合注入tenant字段、为关系字段注入 filterOptions、叠加 baseFilter 与租户访问约束;
  5. 校验"声明的集合都存在于配置中"——若有集合在pluginConfig.collections里声明却未找到,会打印黄色警告missing collections ... try placing the multi-tenant plugin after other plugins(src/index.ts)。这意味着插件顺序很重要:如果其他插件也会新增/重命名集合,请把本插件放在它们之后;
  6. 把收集到的所有租户访问约束统一写入config.baseAccess(addCollectionAccess.ts);
  7. 挂载 Admin 组件与 45 种语言的翻译文案(见 src/translations 目录)。

模块导出速查(定义在 package.json 的exports中):

导入路径内容
@payloadcms/plugin-multi-tenantmultiTenantPlugin主入口
@payloadcms/plugin-multi-tenant/fieldstenantFieldtenantsArrayField字段工厂
@payloadcms/plugin-multi-tenant/utilitiesdefaultsgetTenantAccessgetTenantListFilter(即filterDocumentsByTenants)、getTenantFromCookiegetUserTenantIDs
@payloadcms/plugin-multi-tenant/clientTenantFieldAssignTenantFieldTriggerWatchTenantCollectionuseTenantSelection
@payloadcms/plugin-multi-tenant/rscTenantSelectionProviderTenantSelectorGlobalViewRedirect等服务端组件
@payloadcms/plugin-multi-tenant/typesMultiTenantPluginConfig等类型
@payloadcms/plugin-multi-tenant/translations/languages/*单语言翻译文件

综上,@payloadcms/plugin-multi-tenant的核心价值是把"多租户"这一横切关注点(字段、上下文、过滤、访问控制、清理)收敛为一份声明式配置:collections声明哪些集合受管、isGlobal声明哪些集合按每租户单文档工作,其余注入与约束由插件自动完成。当你需要突破默认行为时,includeDefaultField: falsecustomTenantField: trueuseTenantAccess: false配合./fields./utilities导出的构件,则提供了从"插件默认"平滑过渡到"完全自定义"的完整梯度。建议进一步阅读 插件主 README、配置文档 docs/plugins/multi-tenant.mdx 以及 测试目录 test/plugin-multi-tenant 中的用例,以获得更多可运行的集成参考。

【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 14:32:46

List集合核心原理与Java性能优化实践

1. List集合基础解析List作为编程中最基础也最常用的数据结构之一&#xff0c;几乎存在于所有主流编程语言中。我第一次接触List是在大学数据结构课上&#xff0c;当时教授用"火车车厢"来比喻List的特性——元素像车厢一样按顺序连接&#xff0c;可以随时增加或减少车…

作者头像 李华
网站建设 2026/9/10 14:32:30

跨境电商UPS折扣物流的技术原理与成本优化实践

1. 项目背景与核心价值跨境物流成本一直是困扰跨境电商卖家的痛点问题。以美国境内快递为例&#xff0c;UPS作为主流服务商&#xff0c;其标准费率对于中小卖家而言往往难以承受。而市场上出现的"美区境内UPS折扣快递"服务&#xff0c;本质上是通过合法合规的批量议价…

作者头像 李华
网站建设 2026/9/10 14:32:11

C# WinForms条形码生成工具开发实战

1. 项目概述&#xff1a;C# WinForms条形码生成工具开发实录去年接手一个零售库存管理系统时&#xff0c;客户特别强调需要内置条形码生成功能。市面上虽然有不少现成工具&#xff0c;但要么功能过剩要么无法集成。于是我用C# WinForms开发了这个轻量级条形码生成器&#xff0c…

作者头像 李华
网站建设 2026/9/10 14:31:55

原生PHP图书管理系统:零框架部署与实战运维指南

简介&#xff1a;这是一套面向PHP初学者与高校课程设计者的图书管理系统完整开发资源&#xff0c;聚焦图书馆日常管理场景&#xff0c;涵盖用户登录、图书借阅、读者信息维护等核心功能模块。资源包共111个文件&#xff0c;包含50个PHP业务逻辑文件、11个MySQL数据表结构&#…

作者头像 李华
网站建设 2026/9/10 14:24:23

四步上手 Tracy Profiler:从集成到定位帧内卡顿

四步上手 Tracy Profiler&#xff1a;从集成到定位帧内卡顿 【免费下载链接】tracy Frame profiler 项目地址: https://gitcode.com/GitHub_Trending/tr/tracy 你的游戏平时每帧 16 毫秒&#xff0c;偶尔却飙到 40 毫秒&#xff0c;采样式分析器只给平均值&#xff0c;看…

作者头像 李华