mcpField
S
spree-multi-tenant

spree-multi-tenant

github.com/spree开发10101 次安装4 周前更新
代码仓库

关于 spree-multi-tenant

面向开发者的 Spree 6 多租户 SaaS 指南,使用需授权的 Enterprise Gems。内容涵盖数据隔离、请求中的租户解析、安装配置及租户感知代码。

主要功能

  1. 01行级租户数据隔离
  2. 02基于主机名解析租户
  3. 03店铺与客户的租户模型
  4. 04租户范围内的任务与代码
  5. 05Enterprise 安装说明

适用场景

  1. 01为独立商家构建 SaaS
  2. 02隔离不同租户的客户数据
  3. 03配置租户店铺及默认资源
  4. 04为每个租户运行定时任务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/spree/agent-skills --skill spree-multi-tenant

安装后在 Claude Code 中输入/spree-multi-tenant确认已加载。

详细介绍

多租户(SaaS)Spree

仅限企业版。 多租户功能通过获得许可的 spree_enterprise + spree_multi_tenant gems(私有 Keygen gem 源)提供。它不属于开源 Spree。在据此规划之前,请向 Spree 确认哪个 gem 版本支持你的 Spree 6 版本。

多租户、多店铺与市场平台

需求使用
许多彼此独立的商家,数据完全隔离,单次部署(类似 Shopify 的 SaaS)多租户(本技能)
一个商家运营多个共享商品目录/客户的店面多个 Spree::Store(跨店共享可使用 spree_multi_store)——参见 spree-data-model
许多卖家在同一个共享店面中发布商品市场平台——参见 spree-marketplace

隔离机制

  • Spree::Tenant 是顶层记录。创建租户时会创建其店铺,并运行 Spree 的种子数据(国家、区域、税种类别……)。
  • 行级多租户:每个多租户表都有一个 tenant_id 列。该 gem 基于 acts_as_tenant 构建。
  • 该 gem 将 Spree.base_class 设为 SpreeMultiTenant::Base,因此所有继承自 Spree.base_class 的 Spree 模型都会自动启用多租户。SpreeMultiTenant::Base 还会让 spree_base_uniqueness_scope 返回 [:tenant_id],因此唯一性校验(slug、代码、SKU)按租户分别进行。
  • 当前租户生效时,所有读取操作都会自动添加 WHERE tenant_id = ?,所有写入操作都会自动设置 tenant_id。
  • 当前租户会根据每个请求的主机名解析:租户子域名(store1.example.com)或自定义域名 → 店铺 → store.tenant_id。在控制器中可通过 current_tenant 获取,在其他位置可通过 SpreeMultiTenant.current_tenant 获取。
  • 一个租户可以拥有多个店铺。Spree::Store.default 返回第一个租户的默认店铺——面向租户的代码绝不能依赖它。
  • 员工(管理员用户)是全局的,可受邀加入多个租户。客户按租户区分——同一邮箱可以在两个租户分别注册。
  • 隔离同样适用于 Admin API、Store API 和控制面板。

安装(摘要)

  1. 在服务器环境(以及 CI、预发布、生产构建环境)中设置 KEYGEN_ACCOUNT_ID 和 KEYGEN_LICENSE_KEY。

  2. 如果使用 Docker CLI 流程,请运行 spree eject(必须将 gems 安装到你自己的镜像中)。

  3. 添加到 server/Gemfile:

    source "https://license:#{ENV['KEYGEN_LICENSE_KEY']}@rubygems.pkg.keygen.sh/#{ENV['KEYGEN_ACCOUNT_ID']}" do
      gem 'spree_enterprise'
      gem 'spree_multi_tenant'
    end
    
  4. 仅限 Docker:在 server/Dockerfile 中将 Keygen 值作为 BuildKit 密钥传递给每一条 bundle install(RUN --mount=type=secret,id=keygen_license_key,env=KEYGEN_LICENSE_KEY ...),并在两个 compose 文件中声明这些密钥。绝不要使用 ARG/ENV——这会把许可证写入镜像层。

  5. 运行 spree bundle install(或 bundle install),然后:

    spree generate spree_enterprise:install && spree generate spree_multi_tenant:install
    # classic: bin/rails g spree_enterprise:install && bin/rails g spree_multi_tenant:install
    

    这会复制并运行迁移,在 config/routes.rb 中为 Spree 引擎的挂载添加域名约束(并在应用子域名上挂载租户应用),在客户模型中包含 SpreeMultiTenant::CustomerUserConcern,并将管理员用户的父类改为 Spree::Base。

安装后配置

# server/config/initializers/spree.rb
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'lvh.me')
# server/.env
SPREE_ROOT_DOMAIN=lvh.me   # local dev — NOT localhost (browsers reject localhost as a cookie domain)
COOKIE_TLD_LENGTH=2        # labels in the root domain: example.com=2, shop.example.co.uk=3
  • COOKIE_TLD_LENGTH 会将会话 Cookie 限定到父域名,使员工在 app.<root> 和租户子域名之间切换时保持登录。配置错误时的表现:在子域名之间切换会退出登录,并出现 ActionController::InvalidAuthenticityToken。

  • 主机授权:允许根域名,否则请求会因“Blocked hosts”而失败:

    # server/config/environments/development.rb (and production equivalent)
    if (root_domain = ENV['SPREE_ROOT_DOMAIN']).present?
      config.hosts << ".#{root_domain}"
    end
    
  • 客户模型:生成器添加 SpreeMultiTenant::CustomerUserConcern 后,从客户模型中移除 Devise 的 :validatable——它会强制邮箱全局唯一;该 concern 会按租户重新校验。不要添加其他 Devise 模块。管理员用户模型保留 :validatable(员工邮箱是全局唯一的)。

  • config/initializers/spree_multi_tenant.rb:

    SpreeMultiTenant::Config[:app_subdomain] = 'app'   # app.example.com — signup at /tenants/new
    SpreeMultiTenant::Config[:mail_from_name] = ENV.fetch('MAIL_FROM_NAME', 'Your SaaS')
    SpreeMultiTenant::Config[:mail_from_address] = ENV.fetch('MAIL_FROM_ADDRESS', "support@#{Spree.root_domain}")
    

    mail_from_* 用于发送面向员工的邮件(邀请、导出)。客户邮件使用租户店铺的名称,并将店铺邮箱设为 Reply-To。

编写面向租户的代码

新模型

租户模型继承自 SpreeMultiTenant::Base(如果使用 Spree.base_class,则会自动继承),并且需要 tenant_id 列。不要声明 belongs_to :tenant——该 gem 会处理。

spree generate model Spree::Brand tenant:references name:string --parent SpreeMultiTenant::Base

如果使用 spree generate api_resource / spree:model,请自行添加 tenant_id 列和索引,并在任何唯一数据库索引中包含 tenant_id([:tenant_id, :slug])。

强制设置 / 退出租户上下文
SpreeMultiTenant.with_tenant(tenant) do
  # everything here is scoped to tenant
end

SpreeMultiTenant.without_tenant do
  # cross-tenant: platform billing, global reports, maintenance
end

控制台 / rails runner / rake 任务启动时没有租户:

SpreeMultiTenant.current_tenant = Spree::Store.find_by(code: 'my_store').tenant
Spree::Current.store = Spree::Store.find_by(code: 'my_store')  # Spree 6 code also expects a current store
为新商家的商店配置资源

在租户上下文中使用 Spree::Seeds::StoreResources.call(store: store) 为其填充数据(每个商店的种子数据——频道、角色、支付方式、API 密钥等——不会影响其他商店),然后使用 Spree::Stores::ProvisionDefaults.call(store:, country:) 配置适用于该国家/地区的默认项(市场、仓库、配送区域、自提)。可选演示数据:Spree::SampleData::LoadJob.perform_later(store.id)。参见 spree-data-model。

后台任务

在当前租户上下文中入队的任务会在该租户的上下文中运行。在租户上下文之外入队的任务(cron/周期性任务、without_tenant 代码块)必须显式遍历租户:

Spree::Tenant.find_each do |tenant|
  SpreeMultiTenant.with_tenant(tenant) { MyApp::NightlySyncJob.perform_later }
end

注意事项

  • insert_all / upsert_all / 原始 SQL 会绕过租户隔离——请自行在每一行设置 tenant_id,否则会创建孤立的/跨租户的记录。
  • 在租户代码中使用 Spree::Store.default / Spree::Store.first 会返回错误租户的商店——请使用 current_store / Spree::Current.store。
  • 唯一数据库索引中未包含 tenant_id,会导致第二个租户使用相同的 slug/SKU 时在数据库层面失败,即使验证通过也是如此。
  • 本地开发时使用 localhost 会导致 cookie 出问题;请使用 lvh.me 或 localtest.me。
  • 如果 Docker 构建步骤中(不仅是运行时)缺少 Keygen 凭证,bundle install 会返回 401 错误。
  • 租户记录(包括 webhook 端点)只能在所属租户内访问;执行跨租户操作的订阅者或任务必须谨慎使用 without_tenant。
  • 使用 insert_all 写入数据或定义自有基类的第三方扩展可能需要 tenant_id 迁移和装饰器——请使用两个租户测试每个扩展。

延伸阅读

  • node_modules/@spree/docs/dist/developer/multi-tenant/quickstart.md
  • node_modules/@spree/docs/dist/developer/multi-tenant/core-concepts.md
  • node_modules/@spree/docs/dist/developer/multi-tenant/configuration.md
  • https://spreecommerce.org/pricing(Enterprise 许可证)
  • 相关技能:spree-data-model(商店、频道)、spree-marketplace、spree-deployment、spree-security

评论

支持 Markdown