Tamagui Takeout 订阅权限同步体系实战:Stripe、GitHub、Discord 三方数据一致性运维指南
2026/9/15 2:24:04 网站建设 项目流程

Tamagui Takeout 订阅权限同步体系实战:Stripe、GitHub、Discord 三方数据一致性运维指南

【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui

本指南以 Tamagui 开源仓库中的 scripts/takeout/README.md 为核心,系统讲解 Takeout 商业化产品(Tamagui Pro、Takeout Stack、Team Seats)在 Stripe 订阅、Supabase 数据库、GitHub 团队授权与 Discord 角色授权四者之间的同步架构、巡检脚本、清理流程与历史故障复盘。读完本文,你将掌握如何用仓库内提供的 6 个 TypeScript 运维脚本完成三方数据一致性审计、Webhook 故障排查、月度过期 Claim 清理与 Discord 角色回收,并理解底层权限同步链路在code/tamagui.dev中的真实实现。

一、体系架构:订阅 → Claim → GitHub/Discord 的同步链路

Takeout 是 Tamagui 的商业化产品,其权限发放不再依赖人工操作,而是由一套「Stripe 订阅 + Supabase 数据库 + GitHub 团队 + Discord 角色」的自动化链路驱动。文档给出了四个核心步骤:

  1. 用户订阅:客户在 Stripe 中订阅 Tamagui Pro 或 Takeout Stack。
  2. Webhook 创建 Claim:Stripe 事件触发 Webhook,在数据库claims表中创建一条领取记录,并写入team_slug: 'early-access'
  3. 用户领取权限:用户通过官网(tamagui.dev)点击 "Claim Access",系统将其加入 GitHub 的early-access团队。
  4. 订阅取消:Stripe Webhook 将对应 Claim 标记为未领取(unclaimed_at),同时从 GitHub 团队移除用户,并回收 Discord Takeout 角色。

这一流程对应的真实实现位于 code/tamagui.dev/app/api/stripe/webhook+api.ts,它通过stripe.webhooks.constructEvent()校验签名后分发product.*price.*invoice.*customer.subscription.*等事件类型;取消/续费类事件最终会调用 code/tamagui.dev/features/api/unclaimProduct.ts 中的unclaimSubscription()完成 GitHub 与 Discord 的双向回收。

授予 early-access 的三个产品

文档明确列出以下三个 Stripe 产品会授予 early-access 权限(产品 ID 与 scripts/takeout/check-discord-sync.ts 中的EARLY_ACCESS_PRODUCT_IDS常量一一对应):

产品Stripe 产品 ID权限说明
Tamagui Proprod_RlRd2DVrG0frHe包含 takeout 访问权限
Takeout Stackprod_NzLEazaqBgoKnC独立的 takeout 产品
Tamagui Pro Team Seatsprod_Rxu0x7jR0nWJSv团队订阅席位

值得注意的差异:check-subscription-sync.ts判断 takeout 订阅时采用的是「名称匹配 + ID 兜底 + metadata 关键字」的复合策略(nametakeout/tamagui pro,或metadata.type === 'repo'metadata.includes_takeout === 'true'metadata.repository_nametakeout/unistack等),而check-discord-sync.ts仅依据三个硬编码产品 ID 精确过滤。前者覆盖面更广,适合做总量审计;后者更严格,适合做 Discord 角色对账。

涉及的数据库表

整套体系依赖 Supabase 中的三张核心表(README「Related Files」一节列出):

  • claims:权限领取记录,核心字段包括subscription_idproduct_iddata(JSON,内含user_githubteam_slugrepository_name等)、created_atunclaimed_at
  • discord_invites:Discord 用户与订阅的映射(discord_user_idsubscription_id);
  • subscriptions:本地订阅镜像表,用于通过user_id反查订阅归属用户。

二、运行前置条件与环境变量

所有脚本均为 TypeScript 源码(scripts/takeout/ 目录下共 6 个),以#!/usr/bin/env tsx作为 shebang,运行时依赖tsx@supabase/supabase-jsstripe@discordjs/*(core、rest、ws)等包。执行方式统一为:

npx tsx scripts/takeout/<脚本名>.ts [参数]

按 README 说明,脚本所需的环境变量会自动从code/tamagui.dev/.env加载;从源码看,脚本本身直接读取process.env.*并在缺失时打印错误、process.exit(1)退出(例如 check-subscription-sync.ts)。因此运行前需确认对应环境已注入下列变量:

变量用途需要该变量的脚本
NEXT_PUBLIC_SUPABASE_URLSupabase 项目 URL全部(除 check-webhook-events)
SUPABASE_SERVICE_ROLE_KEYSupabase 服务端密钥(service key)全部(除 check-webhook-events)
STRIPE_SECRET_KEYStripe 密钥全部
GITHUB_ADMIN_TOKEN具备 org/team 管理权限的 GitHub PATcheck-subscription-sync
DISCORD_BOT_TOKENDiscord Bot Tokencheck-discord-sync、remove-discord-users

三、健康巡检:三个「只读审计」脚本

1. check-subscription-sync.ts:Stripe 与 GitHub 团队的对账

这是最核心的同步健康检查脚本,运行方式:

npx tsx scripts/takeout/check-subscription-sync.ts

它并行拉取三类数据后交叉比对(源码见 check-subscription-sync.ts):

  • 数据库活跃 Claim:查询claims表中unclaimed_at IS NULL的记录(最多 1 万条),再按data.repository_name === 'takeout'过滤出 takeout 相关 Claim;
  • Stripe 活跃订阅:分页拉取所有status: 'active'的订阅,按 takeout 产品集合过滤;
  • GitHub 团队成员:通过 GitHub REST API 分页拉取https://api.github.com/orgs/tamagui/teams/early-access/members(每页 100),并统计待处理邀请.../invitations

最终输出摘要与差异分析:对比「数据库 Claims vs Stripe 订阅」「GitHub 团队 vs 数据库 Claims」两个差值,当任一差值绝对值超过5时输出 ⚠️ 警告(提示可能存在的订阅无 Claim、已取消订阅残留 Claim、迁移不完整、手动增删成员等情况),并列出「数据库有而 Stripe 无」「Stripe 有而数据库无」的订阅 ID 前 10 条。脚本最后返回结构化摘要对象,便于程序化消费。

典型应用场景:怀疑同步异常、迁移前后验证、或例行巡检。

2. check-discord-sync.ts:Discord Takeout 角色对账

npx tsx scripts/takeout/check-discord-sync.ts

该脚本将「Discord 中持有 Takeout 角色(角色 ID1131082605052301403,所属 Tamagui 服务器 ID909986013848412191)」的用户与「Stripe 活跃订阅」进行比对(源码见 check-discord-sync.ts):

  • 通过@discordjs/ws网关连接 Discord,拉取服务器内最多 1000 名成员并筛选出持有 Takeout 角色的用户;
  • 通过 Supabasediscord_invites表查询这些用户的subscription_id映射;
  • 逐一判断:无映射记录的用户归入「⚠️ 无 Supabase 映射(可能是旧账号/测试账号)」;有映射但订阅全部不活跃的用户归入「❌ 应移除」;至少有一个活跃订阅的用户为「✅ 有效」。

脚本会将应移除用户写入tmp/discord-users-to-remove-<时间戳>.json(源码中使用process.cwd()下的tmp/目录,mkdirSync(tmp, { recursive: true })自动创建),文件包含generated_atcountusers数组(含discord_idusernameglobal_nameinactive_subscription_ids),供后续移除脚本消费。

典型应用场景:审计 Discord 访问、找出已取消订阅却仍保留角色的过期用户。

3. check-webhook-events.ts:Webhook 事件排障

npx tsx scripts/takeout/check-webhook-events.ts [--limit 100]

该脚本是纯 Stripe 侧诊断工具(源码见 check-webhook-events.ts),输出三部分信息:

  1. 事件类型分布:调用stripe.events.list({ limit })拉取最近 N 条事件(--limit默认 100),按event.type分组计数排序展示;
  2. 订阅相关事件明细:过滤customer.subscription.*前缀的事件,最多展示最近 20 条,包含事件时间(ISO 格式)、订阅 ID、状态,并对customer.subscription.deleted标注「该订阅已取消」;
  3. Webhook 端点状态:调用stripe.webhookEndpoints.list()列出已配置端点,展示 URL、enabled状态与启用的事件列表,若未配置任何端点则直接警告。

典型应用场景:排查 Webhook 是否收到事件、订阅创建/取消事件是否按时到达、端点配置是否正确。

四、月度清理:分析—复核—执行三段式脚本

清理类操作统一遵循「先分析出文件 → 人工复核 → dry-run 试跑 → 正式执行」的安全流程,避免误删有效权限。

1. analyze-stale-claims.ts:过期 Claim 安全分析(只读)

npx tsx scripts/takeout/analyze-stale-claims.ts

该脚本不删除任何数据,只做只读分析(源码注释明确标注 "Does NOT delete anything")。它并行拉取「Stripe 全部活跃订阅」与「Supabase 全部活跃 Claim(unclaimed_at IS NULL,最多 1 万条)」,然后逐条判定:

  • 订阅 ID 不是sub_/in_开头 → 归入「无效订阅 ID」;
  • 订阅 ID 存在于活跃订阅集合 → 归入「有效 Claim(保留)」;
  • 否则 → 归入「过期 Claim(清理候选)」。

分析结果写入tmp/(README 记为/tmp/,源码实际为仓库工作目录下的tmp/)目录,共生成 3~4 个文件:

文件内容
active-subscriptions-<ts>.json去重后的全部活跃 Stripe 订阅 ID 列表
valid-claims-<ts>.json应保留的 Claim(含 id、subscription_id、product_id、GitHub 用户名、repository_name、created_at)
stale-claims-<ts>.json待清理 Claim 候选(含team_slug字段,并带 "REVIEW CAREFULLY BEFORE CLEANING UP" 警告)
invalid-claims-<ts>.json仅当存在无效订阅 ID 时才生成
cleanup-summary-<ts>.txt汇总报告:总数、按 product_id / repository_name 分组的过期 Claim 分布、前 20 条样本、文件清单与后续步骤

汇总报告中特别强调安全原则:未删除/修改任何数据、全部数据已备份为 JSON、可与 Stripe 后台交叉核验、执行清理前必须复核。

典型应用场景:定位已取消订阅的残留 Claim,为清理做准备。

2. cleanup-stale-claims.ts:过期 Claim 批量标记

# 先试跑 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json --dry-run # 确认无误后正式执行 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json

脚本读取分析阶段产出的stale-claims-*.json,将其中的 Claim ID 按每批 100 条分批执行supabase.from('claims').update({ unclaimed_at: new Date().toISOString() }).in('id', batch)(源码见 cleanup-stale-claims.ts)。关键安全机制:

  • --dry-run只打印「Would update N claims」与样本 ID,不写库;
  • 非 dry-run 时启动前有3 秒倒计时Ctrl+C可取消),给操作者最后的反悔窗口;
  • 分批执行并统计成功/失败数量,失败批次会打印具体错误。

典型应用场景:对已复核的过期 Claim 执行批量回收,将其标记为unclaimed

3. remove-discord-users.ts:Discord 角色批量移除

# 先试跑 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json --dry-run # 确认后正式移除 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json

脚本读取check-discord-sync.ts产出的discord-users-to-remove-*.json,逐一调用 Discord REST API(discordClient.api.guilds.removeRoleFromMember)移除用户的 Takeout 角色(源码见 remove-discord-users.ts)。两个工程化细节值得关注:

  • 限速保护DELAY_MS = 1000,每处理一个用户后 sleep 1 秒,规避 Discord 速率限制;
  • 3 秒倒计时:与 cleanup 脚本一致,非 dry-run 正式执行前同样有确认缓冲期。

典型应用场景:复核discord-users-to-remove文件后,批量回收已取消订阅用户的 Discord 访问权。

五、标准运维流程速查

README 提供了四组可直接照搬的运维流程:

检查同步是否健康

npx tsx scripts/takeout/check-subscription-sync.ts npx tsx scripts/takeout/check-discord-sync.ts

调试 Webhook 问题

npx tsx scripts/takeout/check-webhook-events.ts

月度过期 Claim 清理

# 1. 分析 npx tsx scripts/takeout/analyze-stale-claims.ts # 2. 人工复核 tmp/ 下的分析文件 # 3. 清理 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json

月度 Discord 清理

# 1. 检查 Discord 同步 npx tsx scripts/takeout/check-discord-sync.ts # 2. 人工复核 tmp/discord-users-to-remove-*.json # 3. 移除已取消订阅的用户 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json

六、历史故障复盘:unclaimProduct.ts 的「WHERE 缺失」事故

README 记录了 2024 年 11 月修复的一个关键线上 Bug,这是理解本套体系为何需要「巡检 + 清理」双保险的最佳案例。

问题:订阅取消时,Webhook 更新 Claim 记录却遗漏了 WHERE 子句,导致更新波及全表:

// BEFORE (BROKEN) await supabaseAdmin.from('claims').update({ unclaimed_at: Number(new Date()).toString(), }) // 这会更新数据库中 ALL 的 claims 记录!

修复:为更新语句补充id条件,并修正时间戳格式为 ISO 字符串:

// AFTER (FIXED) await supabaseAdmin .from('claims') .update({ unclaimed_at: new Date().toISOString(), }) .eq('id', claim.id) // ← 补上的 WHERE 子句

对照当前源码 code/tamagui.dev/features/api/unclaimProduct.ts,该修复已稳定落地:循环遍历claimRes.data,逐条update(...).eq('id', claim.id)

同批附加修复(README 完整列出):

  • unclaimRepoAccess()调用前补上await,避免未等待的异步回收;
  • 为没有team_slug的旧 Claim 增加回退逻辑,默认按early-access团队移除(源码 unclaimProduct.ts 中可见:team_slug非字符串时console.warn并调用removeUserFromTeam('early-access', login));
  • 日期格式统一改为 ISO 字符串;
  • 新增 Discord 角色移除:订阅取消时同步回收 Takeout 角色。当前实现中unclaimDiscordAccess()会查询该订阅的discord_invitesPromise.allSettled并行移除所有关联用户的角色,随后删除discord_invites记录(见 unclaimProduct.ts)。

七、架构演进:从「仓库协作者」到「团队授权」

2024 年 11 月的另一项重要变更是授权模型整体迁移:

迁移前(Before)

  • 用户被逐个添加为单个仓库的协作者(collaborator)
  • Claim 记录中带有repository_name: 'takeout''unistack'

迁移后(After)

  • 用户被加入统一的early-accessGitHub 团队
  • 团队统一持有所有相关仓库的访问权;
  • Claim 记录写入team_slug: 'early-access'

这一迁移的收益显而易见:新增/回收权限只需增删团队成员即可,不必逐仓库维护协作者列表,也避免了订阅频繁变动时的仓库级权限漂移。脚本中的TEAM_SLUG = 'early-access'ORG_NAME = 'tamagui'常量(check-subscription-sync.ts)即是该模型在当前运维工具中的直接体现。

迁移统计(README 记录)

  • 邀请 197 名活跃订阅者加入 GitHub 团队;
  • 移除 40 名(已取消订阅)用户的 Discord Takeout 角色;
  • 清理 590 条来自已取消订阅的过期 Claim;
  • 修复 Webhook Bug,防止后续再次产生过期 Claim 与 Discord 清理缺口。

八、当前状态基线(2024 年 11 月快照)

README 记录的时点数据可作为对账基线与容量参考:

指标数值
活跃 Stripe 订阅~370
GitHub 团队成员~350(活跃 + 待接受邀请)
Discord Takeout 角色~27(仅活跃订阅)
Webhooks正常工作
Claim 创建正确写入team_slug
Claim 清理取消时正确标记unclaimed
Discord 清理取消时移除角色并删除discord_invites

说明:以上为 README 记录的时点快照,随着业务推进数值会持续变化,建议以check-subscription-sync.ts/check-discord-sync.ts的实时输出为准。

九、故障排查手册

「有订阅但没有 Claim」

可能原因:

  • 用户尚未在官网点击 "Claim Access";
  • 订阅创建时 Webhook 调用失败;
  • 用户在 Claim 体系上线之前订阅。

解决方案:提示用户访问 tamagui.dev 的账户页面手动领取权限。

「用户不在 GitHub 团队」

排查步骤:

  1. 确认用户是否已在官网完成领取(claim);
  2. 运行check-subscription-sync.ts定位缺失用户;
  3. check-webhook-events.ts验证 Webhook 是否正常工作。

「过期 Claim 持续堆积」

排查步骤:

  1. 确认 unclaimProduct.ts 中的 Bug 修复已部署;
  2. 检查 Webhook 事件是否持续收到(用check-webhook-events.ts);
  3. 查看 Webhook 日志中的报错。

十、相关文件索引

为便于继续深入阅读仓库源码,以下是本体系的关键实现文件:

  • scripts/takeout/README.md:本指南所依据的运维文档;
  • code/tamagui.dev/app/api/stripe/webhook+api.ts:Stripe Webhook 处理器(事件校验、签名验证、事件分发);
  • code/tamagui.dev/features/api/unclaimProduct.ts:取消授权逻辑(GitHub 团队移除 + Discord 角色回收);
  • code/tamagui.dev/features/user/claim-product.ts:Claim 创建逻辑;
  • code/tamagui.dev/features/github/helpers.ts:GitHub 团队管理(含removeUserFromTeam等);
  • code/tamagui.dev/features/discord/helpers.ts:Discord 客户端与常量(Takeout 角色 ID、服务器 ID);
  • code/tamagui.dev/app/api/discord/channel+api.ts:Discord 频道管理;
  • 数据库表:claimsdiscord_invitessubscriptions

综上,Tamagui 的 Takeout 权限体系是一个以「Stripe 为计费事实源、Supabase 为状态存储、GitHub 团队与 Discord 角色为交付载体」的闭环。运维人员只需掌握scripts/takeout/下的六个脚本,即可独立完成从日常健康巡检到月度权限回收的全部工作;而理解unclaimProduct.ts的历史 Bug 与修复,则能帮助你在未来遇到同类同步问题时快速定位根因。

【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询