跳到正文

参考

Tenancy(多租户)

共享 schema + tenant_id:tenancy.WithTenant 与 Middleware、UniqueScope、分区键,以及 Circle 多商户映射示例。

Tenancy(多租户)

包:tenancy

go-sequent 用共享 schema + tenant_id:只有一套表;Command、事件、Idempotency 键(同一业务键重复提交只生效一次)和读模型都带租户,查询按租户过滤。没有内置的 schema-per-tenant 或 database-per-tenant。

设置租户

网关或应用层根据 API key、子域名或已验证的 token 解析出租户,再放进 context:

ctx = tenancy.WithTenant(ctx, tenantID)
err := app.Service.Execute(ctx, cmd)

tenancy.Middleware 把它抄到 Meta.TenantID:

  • Meta.TenantID 为空时,用 context 中的租户填充。
  • 两者都已设置但不一致时,命令被拒绝:tenancy: command tenant ... != context tenant ...。
  • context 里没有租户时,命令原样通过。

把它放在链的最前面,后面的中间件才能看到租户:

app.Service.Middleware = append(
    []command.Middleware{tenancy.Middleware},
    app.Service.Middleware...,
)

其他辅助函数:tenancy.FromContext(ctx) 与 tenancy.Require(ctx),后者在没有租户时返回错误。

辅助函数

函数 用途
PartitionKey(tenantID) 租户 id 字符串;nil UUID 时为空。
PartitionKeyHashed(tenantID, shard) tenantID:shard,用于更细的分区,仍是同一个逻辑租户。
UniqueScope(tenantID, scope) tenant:{uuid}:scope,用于 Unique keys。
MatchAggregateTenant(cmdTenant, partitionKey) 聚合的分区不属于命令租户时返回错误。
tenancy.UniqueScope(tenantID, "email")
tenancy.PartitionKeyHashed(tenantID, "shard-a")
tenancy.MatchAggregateTenant(cmdTenant, agg.PartitionKey())

MatchAggregateTenant 是纵深防御。当聚合必须属于调用方租户时,在 Handler 里 Load 之后调用它。

哪些东西带着租户

  • Command。 Meta.TenantID,随命令记录存储。
  • Aggregate。 PartitionKey(),通常是 tenancy.PartitionKey(tenantID),在创建聚合时设置。
  • Unique keys。 用 UniqueScope 把租户带进 scope。
  • Idempotency。 键空间是 (TenantID, IdempotencyKey)。
  • Projection。 加 tenant_id 列,用 WHERE tenant_id = $1 过滤。
  • Outbox。 消息携带 TenantID。

分区

events 表存有 partition_key。表变得很大时,可以按租户或租户加哈希分区,仍在共享 schema 内。见 分区。

行级安全

基于 tenant_id 的行级安全(RLS)是可选的,库本身不提供。如果你自己加上,仍应在 Repository 与 Handler 里保留租户检查。

业务示例:Circle 多商户

当你为多商户支付或钱包产品集成 Circle API 时,租户边界是商户,而不是终端买家。

业务概念 go-sequent 映射
商户 Tenant:Meta.TenantID、行上的 tenant_id
买家、运营人员、后台操作者 User:Meta.UserID(先认证,再做 AuthZ)
Payment、Wallet、Settlement Aggregate,每个事件流各自 OCC
商户余额、对账、可提现金额 账本读模型,经 SyncInTxn(在写事务里同步更新读模型),按 tenant_id 过滤

不要让每个终端用户成为一个租户。终端用户可以作为事件载荷里的 buyer_id,或在发起命令时作为 UserID 出现;隔离与计费仍以商户为单位。

应用层映射:

列 作用
merchant_id 外部业务键
tenant_id(UUID) 写入 Meta.TenantID,用于读模型过滤
Circle 侧 id 取决于下面的配置模式

支付主流程

  1. 网关根据 API key、子域名或 JWT 中的 merchant_id 解析租户,调用 tenancy.WithTenant(ctx, tenantID)。已认证主体写入 Meta.UserID。永远不要只凭客户端提供的 user id 授权。
  2. CommandService.Execute 执行例如 CapturePayment。在写事务内:先做租户级 AuthZ;Handler 在 Payment 与 Wallet 聚合上应用事件(载荷里可含 Circle 的 payment_id 或 transfer_id);EventStore 追加时带上 Idempotency (tenant_id, key) 和 tenancy.UniqueScope(tenantID, "merchant_order") 之类的 Unique keys;账本读模型经 SyncInTxn 以 ... WHERE tenant_id = $1 更新。调用方满足 read-your-writes:提交返回后立刻能读到自己刚写的数据。
  3. 对 Circle 的 HTTP 调用,优先在提交之后通过带 AfterCommit 钩子的 Workflow 或 Outbox 完成,让外部延迟与重试远离账本事务。如果 Handler 必须调用 Circle,只做「创建意图、获取外部 id」这类短调用,余额事实仍交给 SyncInTxn Projector。

反模式:每个买家一个租户

给每个买家一个 TenantID,会把 Idempotency、Unique keys、AuthZ 和读模型过滤切得很碎,也不符合已入驻商户的计费边界。

两种 Circle 凭据模式

  1. 单个平台 API key。 宿主持有一个 Circle 凭据,每次调用在 Circle 的 metadata 或客户端引用里标注 merchant_id 或 TenantID。适合平台结算、代收。
  2. 每商户独立 Circle 凭据。 密钥按 TenantID 索引,存放在 KMS 或密钥表里,对外调用前根据命令的 TenantID 查出凭据。适合拥有自己 Circle 账户的商户。

两种模式都不改变「租户 = 商户」,变的只是凭据归属。

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。