多租户插件

https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant

该插件通过在你的 Admin Panel 中设置,为应用实现多租户功能。具体做法是为所有指定集合添加 tenant 字段。你的前端应用随后可以按租户查询数据。你必须添加 Tenants 集合,以便控制每个租户可用的字段。

该插件完全开源,源代码可在此处查看。 如需帮助,请查阅我们的社区帮助。如发现 bug, 请提交新 issue 并提供尽可能多的细节。

核心功能

  • 为每个指定集合添加 tenant 字段
  • 在管理面板添加租户选择器,允许你在租户间切换
  • 按所选租户筛选列表视图结果
  • 按所选租户筛选关联字段
  • 支持创建类似"全局"的集合,每个租户一个文档
  • 自动为新文档分配租户

警告

默认情况下,该插件会在租户删除时清理相关文档。你应确保在租户集合上设置严格的访问控制,防止未授权用户删除租户。

你可以通过在插件选项中设置 cleanupAfterTenantDeletefalse 来禁用此行为。

安装

使用 pnpmnpmYarn 等 JavaScript 包管理器安装插件:

  pnpm add @payloadcms/plugin-multi-tenant

选项

该插件接受一个包含以下属性的对象:

type MultiTenantPluginConfig<ConfigTypes = unknown> = {
  /**
   * 当租户被删除后,插件会尝试清理相关文档
   * - 移除包含该租户ID的文档
   * - 从用户中移除该租户
   *
   * @default true
   */
  cleanupAfterTenantDelete?: boolean
  /**
   * 自动配置
   */
  collections: {
    [key in CollectionSlug]?: {
      /**
       * 设置为 `true` 如果你希望该集合表现为全局集合
       *
       * @default false
       */
      isGlobal?: boolean
      /**
       * 设置为 `false` 如果你想手动应用 baseListFilter
       *
       * @default true
       */
      useBaseListFilter?: boolean
      /**
       * 设置为 `false` 如果你想手动处理集合访问而不应用多租户约束
       *
       * @default true
       */
      useTenantAccess?: boolean
    }
  }
  /**
   * 启用调试模式
   * - 在管理界面中使租户字段在适用集合中可见
   *
   * @default false
   */
  debug?: boolean
  /**
   * 启用多租户插件
   *
   * @default true
   */
  enabled?: boolean
  /**
   * 添加到所有启用租户的集合中的字段配置
   */
  tenantField?: {
    access?: RelationshipField['access']
    /**
     * 添加到所有启用租户的集合中的字段名称
     *
     * @default 'tenant'
     */
    name?: string
  }
  /**
   * 添加到用户集合中的字段配置
   *
   * 如果 `includeDefaultField` 为 `false`,你必须手动在用户集合中包含该字段
   * 这在你想自定义字段或将字段放置在特定位置时很有用
   */
  tenantsArrayField?:
    | {
        /**
         * 数组字段的访问配置
         */
        arrayFieldAccess?: ArrayField['access']
        /**
         * 数组字段的名称
         *
         * @default 'tenants'
         */
        arrayFieldName?: string
        /**
         * 租户字段的名称
         *
         * @default 'tenant'
         */
        arrayTenantFieldName?: string
        /**
         * 当 `includeDefaultField` 为 `true` 时,字段会自动添加到用户集合中
         */
        includeDefaultField?: true
        /**
         * 包含在租户数组字段上的额外字段
         */
        rowFields?: Field[]
        /**
         * 租户字段的访问配置
         */
        tenantFieldAccess?: RelationshipField['access']
      }
    | {
        arrayFieldAccess?: never
        arrayFieldName?: string
        arrayTenantFieldName?: string
        /**
         * 当 `includeDefaultField` 为 `false` 时,你必须手动在用户集合中包含该字段
         */
        includeDefaultField?: false
        rowFields?: never
        tenantFieldAccess?: never
      }
  /**
   * 自定义租户选择器标签
   *
   * 可以是字符串,也可以是键为i18n代码、值为字符串标签的对象
   */
  tenantSelectorLabel?:
    | Partial<{
        [key in AcceptedLanguages]?: string
      }>
    | string
  /**
   * 租户集合的slug
   *
   * @default 'tenants'
   */
  tenantsSlug?: string
  /**
   * 判断用户是否有权访问_所有_租户的函数
   *
   * 适用于超级管理员类型的用户
   */
  userHasAccessToAllTenants?: (
    user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
  ) => boolean
  /**
   * 选择不向租户集合添加访问约束
   */
  useTenantsCollectionAccess?: boolean
  /**
   * 选择不包含 baseListFilter 来按所选租户过滤租户
   */
  useTenantsListFilter?: boolean
  /**
   * 选择不包含 baseListFilter 来按所选租户过滤用户
   */
  useUsersTenantFilter?: boolean
}

基本用法

在你的 Payload Configplugins 数组中,使用 options 调用插件:

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: [
        // 记住,这些字段由你定义
        // 以下仅为建议/示例
        {
        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

前端使用

该插件为你搭建了按租户分离数据所需的一切。你可以在前端应用中使用 tenant 字段来过滤启用集合中的数据。

在前端,你可以通过以下方式按租户查询和约束数据:

const pagesBySlug = await payload.find({
  collection: 'pages',
  depth: 1,
  draft: false,
  limit: 1000,
  overrideAccess: false,
  where: {
    // 你的约束条件取决于
    // 你在租户集合中添加的字段
    // 这里我们假设租户集合中存在一个 slug 字段
    // 如上例所示
    'tenant.slug': {
      equals: 'gold',
    },
  },
})

NextJS 重写规则

使用 NextJS 的重写功能和 /[tenantDomain]/[slug] 这样的路由结构,我们可以针对请求的域名进行特定的路由重写:

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

React Hooks

以下是该插件导出的 hooks,你可以将它们导入到自定义组件中使用。

useTenantSelection

你可以这样导入该 hook:

import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'

...

const tenantContext = useTenantSelection()

该 hook 返回以下上下文:

type ContextType = {
  /**
   * 可供选择的选项数组
   */
  options: OptionObject[]
  /**
   * 当前选中的租户 ID
   */
  selectedTenantID: number | string | undefined
  /**
   * 在切换租户时阻止页面刷新
   *
   * 如果在查看"全局"内容时不希望切换租户时刷新页面,请设置为 true
   */
  setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
  /**
   * 设置选中的租户 ID
   *
   * @param args.id - 要选择的租户 ID
   * @param args.refresh - 是否在切换租户后刷新页面
   */
  setTenant: (args: {
    id: number | string | undefined
    refresh?: boolean
  }) => void
}

示例

示例目录中还包含一个官方的多租户示例。