数据模型
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 映射为权限。