跳转到内容

数据模型

Kasskeep 按 租户 → App → 用户 / 交易 的层级存储所有数据。你的账号就是一个租户,一个租户下可以有多个 App。

租户是一个组织:一家公司、一个团队或一位独立开发者。它拥有 App、成员和审计日志。

  • 每张业务表都带 tenant_id。查询按它过滤,Postgres 行级安全作为第二道防线强制执行,所以即使代码里漏了过滤条件,也读不到其他租户的数据。
  • 商店凭据和回调签名密钥用 AES-256-GCM 加密。每个租户的密钥由主密钥经 HKDF-SHA256 派生,并把租户 id 绑定为附加认证数据,所以密文复制到其他租户的行里也无法解密。
  • 后台任务(回调投递、商店确认、对账)在租户之间轮转调度,每轮有上限,一个租户积压不会饿死其他租户。
  • 面向 App 的接口按租户限流,超限返回 429 rate_limited。被暂停的租户会收到 403 tenant_suspended。

租户 id 形如 ten_...。

App 是一个卖东西的产品,比如一款同时上架 iOS、Android 和网页的游戏。一个 App 包括:

  • 商店标识(ios_bundle_id、android_package、App Store 的 app id);
  • 渠道凭据:Apple、Google、Stripe 和广告网络;
  • webhook_url,以及密钥与权限中介绍的三个密钥;
  • 各种开关,比如测试商店和测试广告。

App id 形如 app_...,出现在每个商店通知地址里。

用户由你自己的 app_user_id 标识。Kasskeep 不创建账号,只记录你的哪个用户拥有哪些购买。

  • 服务端接受 1 到 200 个字符的任意字符串。React Native SDK 要求 UUID,因为 Apple 的 appAccountToken 必须是 UUID。
  • 在 iOS 上,SDK 把 appAccountToken 设为用户 id;在 Android 上设的是 obfuscatedAccountId。交易携带的 id 不一致时,以 403 app_account_token_mismatch 拒绝;交易已绑定到其他用户时,以 403 transaction_belongs_to_another_user 拒绝。

GET /v1/customers/:app_user_id 返回该用户的订阅和购买。见客户。

交易是商店的一次扣款:一次性购买,或订阅的一个周期。它在 App、平台、环境和商店交易 id 的组合内唯一,同一笔购买提交两次也只记一次。

订阅是一条续订链的当前状态,以原始交易 id 为键。它的 status 取以下值之一:

状态 含义 是否有权限
active 在当前周期内,会自动续订 有
canceled 在当前周期内,已关闭自动续订 有,到周期结束为止
grace_period 扣款失败,处于宽限期内 有
billing_retry 扣款失败,商店仍在重试 无
expired 已结束 无
revoked 已退款,或家人共享被撤销 无
paused 仅 Android,用户已暂停 无

一次性购买的 status 为 owned、refunded 或 revoked。

Kasskeep 获知的每一次变化都会成为一个事件,比如 purchase、subscription.renewed 或 refund。事件会签名后 POST 到你的回调地址,也可以通过 GET /v1/events 按顺序读取。事件 id 由商店事件本身派生,所以同一次续订,无论来自通知、客户端提交还是对账,id 都相同。见事件类型。

商品用商店的商品 id(product_id)标识。Stripe 没有这种意义上的商品 id,所以要用商品映射把你的商品 id 对应到 Stripe 价格,这样同一个 product_id 就能同时在 App Store、Google Play 和网页上售卖。

规划中 权益:把 “pro” 这类权限定义一次,再映射到各渠道的商品,这已列入路线图。目前由你的后端把 product_id 映射为权限。