前置条件
- 一个能跑的 payment provider(看 /docs/payments/overview)。
license表已经创建(pnpm db:push时根据src/db/license.schema.ts建)。LICENSE_DOWNLOAD_URL已配(看步骤 6)——没配的话端点会返回一个纯文本 “联系客服” 兜底。
完整链路
- 客户付款。Stripe(或任何 provider)触发
checkout.completed。webhook handler 把事件归一化,调src/payment/handlers/core.ts里的processNormalizedEvent。 - 派发到 license。
processNormalizedEvent插入payment行,然后调dispatchLicense(paymentId, p)。这个 helper 检查p.scene === siteConfig.product.id(也就是说买家买的是 vibestrap 本身, 不是 credit pack),匹配才调issueLicense。 - 签发 license。
issueLicense({userId, productId, paymentId})在(userId, paymentId)上幂等——webhook 重试安全。key 是lic_<nanoid>(约 22 字符),存进license.key上有 unique 索引。 - 客户看到 license。买家访问
/settings/licenses能看到 key、下载次数、 再下载按钮。 - 买家点下载。按钮链到
/api/license/download?key=lic_xxx。handler 校验 session、确认 key 属于当前用户、调consumeKey()(自增downloadCount、 写lastDownloadAt),再 302 跳到LICENSE_DOWNLOAD_URL。 - 占位符替换。
LICENSE_DOWNLOAD_URL里的{key}字符串会被替换成 encoded 后的 license key,上游就能识别买家。
下载链接的几种玩法
在.env.local 把 LICENSE_DOWNLOAD_URL 设成下面之一:
{key} 占位符走了 encodeURIComponent,放在 URL 哪个位置都安全。
验证生效
- 在 Stripe 测试模式跑一笔测试购买。
- 看 Postgres 日志 / 用
pnpm db:studio确认license表里多了一行:key = lic_...、userId对、paymentId匹配。 - 用买家身份访问
/settings/licenses——列表里应该有这个 license。 - 点 “下载”。应该跳到
LICENSE_DOWNLOAD_URL指向的地方(如果没配就看到 纯文本兜底信息)。 - 刷新 settings 页面——
downloadCount现在是 1。 - 重发同一个 Stripe webhook。确认没创建重复 license(幂等检查)。
常见坑
LICENSE_DOWNLOAD_URL没设。端点会返回纯文本让买家联系客服。dev 时 方便,生产丢人。收真钱前一定要配上。productId跟siteConfig.product.id对不上。dispatchLicense在 scene 不是siteConfig.product.id(或老的'vibestrap-lifetime')时会 短路。如果你在 config 里改了 product slug,要么补刷历史 payment,要么在dispatchLicense里加个别名。- 退款不会自动撤销 license。Stripe 的
charge.refunded不会删 license 行。要在业务逻辑里处理——软删 license、过期 key、或者相信客户(大部分 独立开发者都这么做)。 - 忘了
{key}占位符。URL 里没{key}上游就无法识别买家。永远带上, 除非你用的是一个所有人共享的下载链接(那为什么要发 key 呢)。 - 两个用户都登录时 key 能共享。端点查
row.userId === session.user.id, 所以 key 只能给原买家用。但账号共享就破防了——接受这个已知缺陷,或者 叠加 IP / 设备指纹。
官方文档
- 支付 provider 抽象:/docs/payments/overview
- Stripe webhooks:stripe.com/docs/webhooks
- GitHub release 下载:docs.github.com/en/rest/releases
- Cloudflare R2 签名 URL:developers.cloudflare.com/r2/api/s3/presigned-urls
- AWS S3 预签名 URL:docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html