Tamagui 订阅系统架构全解析:Stripe + Supabase 双轨计费、团队席位与 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 开源仓库(code/tamagui.dev)中订阅系统的产品体系、支付流程、数据库设计与权限联动机制。你将理解 PRO 循环订阅与一次性支付的差异、团队席位(Team Seats)如何通过subscription_items挂载到主订阅、Chat/Support 订阅为何采用独立月付周期,以及 GitHub 仓库访问与 Discord 频道权限如何由 Stripe Webhook 事件驱动自动开通与回收。全篇以 subcriptions.md 为骨架,并逐一对齐仓库中真实 API 路由与 SQL 迁移文件。
一、订阅产品体系总览
Tamagui 的付费体系分为四个产品线,对应 Stripe 中独立的产品与价格(Price),并在 Supabase 中以subscriptions、subscription_items、products、prices四张核心表建模。以下产品定义与价格 ID 均可在 products.ts 中查到(STRIPE_PRODUCTS常量按测试/生产环境切换)。
1.1 PRO 计划($240/年循环订阅)
数据库表:subscriptions、subscription_items、products、prices
- 循环订阅(Annual):$240/年
- 私有 Takeout GitHub 仓库访问权
- Bento 组件下载 + 私有 Bento 源码仓库访问
- 私有社区 Discord 聊天频道(#takeout-general)
- 所有代码与资产拥有终身使用权(订阅过期后仍可继续使用)
- 支持添加团队成员席位
从源码看,循环订阅对应STRIPE_PRODUCTS.PRO_SUBSCRIPTION(V1 遗留产品,在 products.ts 中被标记为@deprecated);当前新用户走 V2 计费(见下文 1.5)。
1.2 PRO 一次性支付($400/年访问权)
数据库表:subscriptions、subscription_items、products、prices
- 与循环订阅权益一致,但存在两条设计上的限制:
- ❌ 无 Discord takeout 频道访问权
- ❌ 无法添加团队席位(团队席位仅限循环订阅)
- 自动续费为
false(auto renewal false) - 支付产生的是发票(invoice)记录而非订阅
一次性支付对应STRIPE_PRODUCTS.PRO_ONE_TIME(V1,@deprecated),实现上通过stripe.invoiceItems.create()+stripe.invoices.create()+stripe.invoices.pay()完成,见 create-subscription+api.ts。
1.3 团队席位(Team Seats,$100/席/年)
数据库表:team_subscriptions、team_members、subscription_items
- 价格:$100/席/年,仅可与 PRO 循环订阅搭配
- 每席位权益:
- 团队成员获得完整 PRO 计划访问权限
- 访问 Discord #takeout-general 频道
- 仓库协作邀请(GitHub collaboration invite)
- 限制:
- 仅团队所有者(owner)可管理 Discord 访问
- 不可与一次性 PRO 计划一同购买
- 通过
subscription_items关联到主 PRO 订阅
对应的 Stripe 价格为PRO_TEAM_SEATS(订阅)与PRO_TEAM_SEATS_ONE_TIME(一次性),见 products.ts。
1.4 聊天支持(Chat Support,$200/月)与支持等级(Support Tiers 1-3)
数据库表:subscriptions、subscription_items、discord_invites
- Chat Support:$200/月
- 专属私有 Discord 房间
- 含 2 个 Discord 邀请名额
- 响应优先级高于社区频道
- 可与其他任何计划组合
- Support Tiers(1-3):$800 / $1,600 / $2,400 每月三档
- 每月 4 小时开发时间支持
- 更快的响应速度
- 每个等级额外 4 个私有 Discord 聊天邀请名额
- 可与 Chat Support 叠加
两者实现上各自创建独立的月度订阅(upgrade-subscription+api.ts),与 PRO 的计费周期相互独立。STRIPE_PRODUCTS.CHAT与STRIPE_PRODUCTS.SUPPORT同样是 V1 遗留产品;V2 已演进为SUPPORT_DIRECT($500/月)与SUPPORT_SPONSOR($2,000/月),并支持chat/direct/sponsor三档字符串类型,见 upgrade-subscription+api.ts。
1.5 当前在售的 V2 产品线(源码补充)
需要说明的是,本文主体文档 subcriptions.md 描述的 V1 计费模型在仓库中已标记为遗留(@deprecated)。当前新购流程以 V2 为主,相关产品定义在同一份 products.ts 中:
| 产品 | 价格 | 说明 |
|---|---|---|
| PRO_V2_LICENSE | $250 一次性 | 每项目授权,含全部模板、1 年更新、基础聊天支持、无限团队成员 |
| PRO_V2_UPGRADE | $100/年 | 购买后自动订阅,1 年后开始扣费,延续更新访问权 |
| SUPPORT_DIRECT | $500/月 | 每年 5 个 bug 修复、2 个工作日响应、问题优先处理 |
| SUPPORT_SPONSOR | $2,000/月 | 无限优先修复、1 天响应、每月视频会议 |
V2 购买由 create-v2-subscription+api.ts 负责,包含幂等键防重复扣费、部分失败回滚与 3DS 认证处理;支付成功后由 Webhook 创建项目记录。
二、核心实现流程详解
2.1 PRO 循环订阅流程
User Purchase → create-subscription+api.ts → Stripe Subscription Creation数据库流向:
- API:create-subscription+api.ts
- Stripe:以
PRO_SUBSCRIPTION_PRICE_ID创建订阅 - Webhook:webhook+api.ts 处理
customer.subscription.created - 数据库:
- 插入
subscriptions记录 - 插入
subscription_items记录 - 通过
subscription_items.subscription_id关联两者
- 插入
关键代码(create-subscription+api.ts):
// create-subscription+api.ts const subscription = await stripe.subscriptions.create({ customer: stripeCustomerId, items, payment_behavior: 'default_incomplete', payment_settings: { save_default_payment_method: 'on_subscription' }, expand: ['latest_invoice.payment_intent'], coupon: couponId || undefined, default_payment_method: paymentMethodId, }) const latestInvoice = subscription.latest_invoice as Stripe.Invoice const amountDue = latestInvoice?.amount_due || 0 const clientSecret = await getClientSecret(subscription) return Response.json({ id: subscription.id, clientSecret, amount_due: amountDue, })需要注意的几个实现细节(从源码可验证):
payment_behavior: 'default_incomplete'配合expand: ['latest_invoice.payment_intent'],目的是在订阅首次付款完成前保持incomplete状态,由前端拿到clientSecret后调用stripe.confirmPayment()完成 3DS 验证;getClientSecret()会先从展开的payment_intent取client_secret,取不到再回退到stripe.invoices.retrieve()二次查询(create-subscription+api.ts)。- 服务端会对客户端提交的优惠券做前置校验
assertValidCoupon(),防止绕过限制随意折扣 PRO 费用。 - Webhook 侧
manageSubscriptionStatusChange()会把 Stripe 订阅的完整快照(status、current_period_start/end、cancel_at、trial_*等)upsert 进subscriptions表,并同步subscription_items——先插入新 items,再删除与最新 Stripe 数据不一致的旧记录(supabaseAdmin.ts)。
2.2 PRO 一次性支付流程
User Purchase → create-subscription+api.ts → Stripe Invoice Creation数据库流向:
- API:create-subscription+api.ts(请求体携带
disableAutoRenew: true) - Stripe:以
PRO_ONE_TIME_PRICE_ID创建发票 - Webhook:处理
invoice.paid - 数据库:
- 以
cancel_at_period_end: true插入subscriptions - 将
cancel_at设为一周年后 - 插入
subscription_items
- 以
关键代码(webhook+api.ts 中的manageOneTimePayment()):
// webhook+api.ts - manageOneTimePayment() await supabaseAdmin.from('subscriptions').upsert({ id: invoice.id, user_id: uuid, metadata: invoice.metadata, status: 'active', cancel_at: oneYearFromNow.toISOString(), cancel_at_period_end: true, current_period_start: new Date().toISOString(), current_period_end: oneYearFromNow.toISOString(), created: new Date().toISOString(), ended_at: null, canceled_at: null, trial_start: null, trial_end: null, })一次性支付的关键区别在于:Webhook 只在invoice.subscription === null时进入manageOneTimePayment()(webhook+api.ts),因为一次性购买在 Stripe 侧不存在订阅对象。随后syncInvoiceLinePrices()会把发票行中的价格/产品反向同步进prices/products表,保证subscription_items引用完整。该分支同时会调用createTeamInvoice()处理一次性团队席位发票(见 supabaseAdmin.ts)。
2.3 团队席位购买流程
User Purchase → create-subscription+api.ts → Update Existing PRO Subscription数据库流向:
- API:create-subscription+api.ts(请求体携带
teamSeats > 0) - Stripe:向已有订阅追加
TEAM_SEATS_SUBSCRIPTION_PRICE_IDitem - Webhook:处理
customer.subscription.updated - 数据库:
- 更新
subscription_items加入团队席位 item - 通过
createTeamSubscription()创建team_subscriptions记录 - 在
team_subscriptions.total_seats中记录席位数量
- 更新
关键代码(create-subscription+api.ts):
// create-subscription+api.ts let items: Stripe.SubscriptionCreateParams.Item[] = [{ price: PRO_SUBSCRIPTION_PRICE_ID }] if (teamSeatCount > 0) { items.push({ price: TEAM_SEATS_SUBSCRIPTION_PRICE_ID, quantity: teamSeatCount }) }席位数量与到期时间的落库由 Webhook 触发的createTeamSubscription()完成(supabaseAdmin.ts):
export const createTeamSubscription = async (sub: Stripe.Subscription) => { const teamItem = sub.items.data.find( (item) => item.price.id === STRIPE_PRODUCTS.PRO_TEAM_SEATS.priceId ) // if there is no team item, return if (!teamItem) return // ... await supabaseAdmin.from('team_subscriptions').insert({ owner_id: userId, total_seats: teamItem.quantity || 1, expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), }) }如果teamItem不存在则直接返回,保证非团队订阅不会产生脏数据;owner_id唯一约束(见下方数据库章节)确保一个用户只持有一份团队订阅。后续追加席位走独立的 add-team-seats+api.ts:循环订阅场景下调用stripe.subscriptions.update()修改既有 team seats item 的quantity(存在则累加、不存在则新建),一次性场景则走 invoiceItems + invoice 通道。
2.4 团队成员管理流程
数据库表:team_members、team_subscriptions、users
添加成员流程(team-seat+api.ts):
- API:
team-seat+api.ts的 POST 端点 - 数据库:插入
team_members记录(status: 'active') - GitHub:通过
resend-github-invite+api.ts邀请加入仓库 - Discord:通过 Discord 面板手动邀请
服务端有明确的容量与去重校验(源码可验证):
activeSeats >= teamSub.total_seats时返回 403「No seats available. Add more seats to invite members.」——从不超过已付费席位上限;- 已存在
active状态的同成员时返回 409「User is already a team member」; - 被邀请用户必须真实存在于
users表,否则 404。
移除成员流程:
- API:
team-seat+api.ts的 DELETE 端点 - 数据库:删除
team_members记录 - GitHub:移除仓库访问权
- Discord:通过 Discord 面板手动移除
移除时同样先校验team_subscriptions.owner_id === user.id,防止越权操作其他团队的成员(team-seat+api.ts)。
席位查询(GET):会联合查询team_subscriptions及其team_members,将status === 'active'的成员数计为used_seats,并从users表补充成员头像与姓名,返回total_seats / used_seats / expires_at等字段,供前端展示座位余量。
2.5 Discord 席位计算
逻辑位置:ensureSubscription.ts 与 Discord API 端点。
文档中的核心算法如下:
// Discord seats calculation logic const baseSeats = subscription.quantity || 1 // PRO plan base seats const teamSeats = teamSubscription?.total_seats || 0 const totalDiscordSeats = baseSeats + teamSeats在实际实现中,通用频道的席位计算位于 channel+api.ts:先由ensureSubscription()得出基础席位,再探测用户是否持有team_subscriptions(先查 owner,再查team_members反向定位),若teamSubscription.total_seats > discordSeats,则取total_seats + 1(+1 代表 owner 本人)作为最终席位。该文件还保留了一段重要 TODO 注释,说明当前实现的局限:discord_invites表只记录subscription_id,无法按用户追踪席位占用,因此团队所有成员共享同一个 Discord 席位池(channel+api.ts)。
而私有支持频道的席位计算在 support+api.ts:遍历用户所有活跃订阅,Chat 产品贡献 2 个席位,Support tier 按subscription.quantity(tier 等级)× 4 累加,从而支持「Chat + 多个 Support tier」叠加的场景。
Legacy 支持:旧版 takeout 价格无独立产品元数据,席位通过解析价格描述(price description)计算——「Team (10-20 seats)」给 4 席、「Team (+20 seats)」给 8 席,解析逻辑位于 getProductInfo.tsx;而 V2 PRO 直接固定返回discordSeats: 2, licenseSeats: 2, githubSeats: 2(团队无限、按项目授权)。
2.6 聊天支持与支持等级实现
User Purchase → upgrade-subscription+api.ts → Separate Monthly Subscription数据库流向:
- API:upgrade-subscription+api.ts
- Stripe:创建独立的月度订阅
- Webhook:webhook+api.ts 处理订阅事件
- 数据库:
- 在
subscriptions表插入独立记录 - 计费周期与 PRO 计划不同
- 在
关键代码(upgrade-subscription+api.ts):
// upgrade-subscription+api.ts const items: Array<{ price: string; quantity?: number }> = [] if (chatSupport) { items.push({ price: STRIPE_PRODUCTS.CHAT.priceId }) } if (supportTier > 0) { items.push({ price: STRIPE_PRODUCTS.SUPPORT.priceId, quantity: supportTier }) }该路由同时兼容 V1 数值型 tier(用quantity表达等级)与 V2 字符串型 tier(chat/direct/sponsor,chat在 V2 中免费包含、不创建额外订阅),见getSupportTierPriceId()(upgrade-subscription+api.ts)。
2.7 Discord 集成与频道生命周期
数据库表:subscriptions(metadata 字段)、discord_invites
频道创建:
- 通用频道:面向 PRO 用户(#takeout-general)
- 支持频道:面向 Chat / Support tier 用户(私有频道)
通用频道在 channel+api.ts 中创建:首次访问时在 guild 中查找名为TAKEOUT_GENERAL_CHANNEL的频道,并把频道 ID 写入subscriptions.metadata;支持频道在 support+api.ts 中按用户邮箱前缀命名(非法字符替换为_),命名规则为${userName}-${chat | tier-N | chat-tier-N},频道 topic 中附带订阅 ID 便于追溯,同时以permission_overwrites对DEFAULT_ROLE_IDdenyVIEW_CHANNEL(bitfield1024)实现私有化。
元数据存储:
{ "discord_channel": "1132001717215559691" }成员管理:discord_invites表记录频道成员与邀请状态,用于追踪哪些用户被邀请进了哪些频道、防止重复邀请。POST 添加成员前会校验currentlyOccupiedSeats >= discordSeats(满员返回 403),并检查discord_user_id是否已存在;添加成功后为用户打上TAKEOUT_ROLE_ID角色(support+api.ts)。Discord 成员搜索由 search-member+api.ts 提供,且被 Pro 权限保护(ensureAccess校验,无 Pro 返回 403)。
重置功能:
- UI 重置按钮:删除整个 Discord 频道
- API:discord/support+api.ts 与 discord/channel+api.ts 的 DELETE 端点
- 影响:所有成员失去访问权,频道必须重建
DELETE 分支会依次:删除 Discord 频道 → 对每个discord_invites成员移除TAKEOUT_ROLE_ID角色 → 清空discord_invites表 → 清空subscriptions.metadata中的频道 ID。
2.8 订阅取消流程(源码补充)
doc/subcriptions.md 的 API 汇总中列出了/api/cancel-subscription。其实现位于 cancel-subscription+api.tsx:先校验subscriptions.user_id === user.id,然后区分两种取消策略(cancelSubscription.ts):
- 立即取消:订阅处于
past_due/unpaid,或存在billing_reason === 'subscription_cycle'且状态为draft/open的待续费发票时,先停止发票收款(draft 发票关闭auto_advance,open 发票 void),再stripe.subscriptions.cancel(),避免用户取消后仍被 Stripe 反复重试扣费; - 周期结束取消:健康订阅仅设置
cancel_at_period_end: true,保留到本周期结束的访问权益。
两种路径都会向用户发送取消确认邮件。
三、GitHub 仓库访问系统
3.1 GitHub 集成
数据库表:claims
流程:
- 用户操作:点击「Takeout 1」或「Takeout 2」直接打开仓库,或点击「Resend Invite」发送/重发 GitHub 团队邀请
- API:resend-github-invite+api.tsx 处理邀请请求
- GitHub 检查:helpers.ts 中的
checkIfUserIsTeamMember() - 响应处理:
- 已是成员:返回成功消息(含团队访问链接)
- 新邀请:发送 GitHub 团队邀请,返回成功消息
源码层面的校验链条如下:
- 该端点要求
subscription_id与product_id均为字符串(400 校验),并通过getActiveSubscriptions()验证订阅归属; - 校验订阅中是否包含合法的 Pro 产品 ID(V1 的
prod_RlRd2DVrG0frHe、prod_Rxu0x7jR0nWJSv,V2 的prod_TneqayKPO32G63、prod_TsDjQ6tmdFy7M6、prod_TsDjG5QpL21tT1); - 从
users_private表读取用户的 GitHub 用户名(缺失则提示重新用 GitHub 登录); - 调用
checkIfUserIsTeamMember('early-access', username)查询当前成员状态(active/pending/ 非成员),随后通过addUserToTeam()(GitHub RESTPUT /orgs/{org}/teams/{team}/memberships/{username})幂等重发邀请。
checkIfUserIsTeamMember与addUserToTeam/removeUserFromTeam三个函数均基于 GitHub REST API(X-GitHub-Api-Version: 2022-11-28),使用服务端GITHUB_ADMIN_TOKEN鉴权(github/helpers.ts)。此外github/helpers.ts还维护了whitelistGithubUsernames、whitelistBentoUsernames与 sponsor 白名单,供旧的 GitHub Sponsor 体系兼容使用。
3.2 访问判定总入口
前端/服务端判定用户是否拥有 Pro 权益时,统一走 hasProAccess.ts 的hasProAccess(userId):并行执行「活跃订阅检查 + 遗留产品所有权检查 + 白名单检查」三条路径,任一命中即视为有权限。其中hasLegacyAccess()会查询product_ownership表,检查 Bento 直购产品或metadata.is_lifetime === '1'的终身授权。
四、数据库 Schema 汇总
以下结构均来自 supabase/migrations 目录中的真实迁移文件。
4.1 核心表
subscriptions:主订阅记录。以 Stripe 订阅/发票 ID 为主键,含user_id、status(枚举:trialing/active/canceled/incomplete/incomplete_expired/past_due/unpaid,见 subscription.ts)、metadata(JSONB,存 Discord 频道 ID 等)、quantity、cancel_at_period_end、cancel_at、current_period_start/end、trial_*等字段,建表于 20230529071500_init.sqlsubscription_items:订阅与产品/价格的关联表(subscription_id+price_id+id),外键级联删除,建表于 20230620111753_add_subscription_items_table.sqlproducts:产品定义(PRO、Team Seats、Chat、Support)prices:每个产品的定价信息(含unit_amount、currency、interval、description,其中 description 承担了遗留席位解析的职责)customers:Stripe customer ID 与 Supabase 用户 UUID 的映射表(id(uuid)+stripe_customer_id)
4.2 团队管理
team_subscriptions:团队订阅元数据,含owner_id(唯一约束)、total_seats(check (total_seats > 0))、expires_at,建表于 20250326091322_create_team_subscriptions.sqlteam_members:团队成员关系(team_subscription_id级联删除、member_id、status枚举pending/active/removed,(team_subscription_id, member_id)唯一约束防重复)team_invoices:团队专项发票追踪
4.3 访问控制
claims:仓库与服务访问声明(product_id、subscription_id外键级联)product_ownership:遗留一次性购买追踪(关联price_id与user_id),建表于 20240129094306_create_product_ownership_table.sql,并在 20240129094632_update_claims_for_product_ownership.sql 中为claims增加product_ownership_id关联users_private:GitHub token 与私有数据
4.4 Discord 集成
subscriptions:metadata 中存储频道 ID(JSON 形式)discord_invites:Discord 成员邀请与邀请状态,含subscription_id(外键级联)、discord_user_id、discord_channel_id,建表于 20230724050018_add_discord_invites_table.sql
五、遗留产品与迁移
5.1 产品所有权系统(product_ownership)
数据库表:product_ownership
目的:
- 追踪订阅系统上线前的一次性购买
- 为遗留用户提供 Bento 访问
- 处理数据迁移问题
使用方式(源码对应hasProAccess.ts的 legacy 分支):
// Check legacy 🍱 Bento access const { data: ownership } = await supabase .from('product_ownership') .select('*') .eq('user_id', userId) .eq('product_id', BENTO_PRODUCT_ID)实际实现中hasLegacyAccess()采用 join 查询product_ownership → prices → products,判定「直接拥有 Bento 产品」或「prices.metadata.is_lifetime === '1'的终身授权」两种情形(hasProAccess.ts)。
5.2 遗留 Discord 席位计算
位置:ensureSubscription.ts
旧版 takeout 订阅通过解析价格描述计算 Discord 席位:
// Legacy price description parsing const description = price.description || '' const seatsMatch = description.match(/(\d+)\s*seats?/i) const seats = seatsMatch ? parseInt(seatsMatch[1]) : 1仓库中对应的完整解析实现在 getProductInfo.tsx(getTakeoutPriceInfo()),它同时兼容「hobby 首档无私有频道」「10-20 seats → 4 席」「+20 seats → 8 席」等旧价格描述规则。另外ensureSubscription.ts还会校验订阅的subscription_items中是否包含合法的 Pro 产品(V1 的Tamagui Pro、旧Takeout Stack,V2 的Tamagui Pro V2、Tamagui Pro V2 Upgrade、Tamagui Support Direct、Tamagui Support Sponsor),不满足则抛出 401,从源头阻断非 Pro 用户访问 takeout 频道。
5.3 迁移脚本
目的:向product_ownership添加用户记录以实现数据恢复。
使用方式:当用户在系统迁移后丢失访问权限时,手动添加记录恢复其权益。仓库中还保留了更细的运维脚本:grant-free-subscription.mjs 通过「零金额一次性发票 + 100% 折扣优惠券」为既有用户赠送一年 V2 Pro 授权,并轮询等待invoice.paidWebhook 将访问记录同步进 Supabase(幂等,防止重复赠送);README-subscription-analysis.md 记录了 2026 年 2 月生产环境订阅状态分析(active/trialing/past_due/canceled 等各状态分布)与优惠券修复记录。
六、已知问题与改进计划
6.1 已知问题
多个支持订阅的复杂性
- 用户可能同时持有 Chat 与 Support tier 订阅
- 造成计费复杂性与多个 Discord 频道并存
- 跨多个订阅的席位计算变得复杂(现有实现已在 support+api.ts 中对全部活跃订阅做聚合,但频道与角色仍是「一订阅一频道」)
Discord 重置限制
- 重置按钮会删除整个频道
- 重置后难以添加新成员
- 缺少细粒度的单个成员移除能力
一次性支付限制
- 一次性 PRO 购买无 Discord 访问权
- 一次性购买无法添加团队席位
大额折扣下的 Stripe 支付确认问题
- 问题:当发票因大额折扣(低于 Stripe $0.50 最低限额)被自动支付时,Stripe 不会创建 payment intent,因此没有
clientSecret可供stripe.confirmPayment()使用 - 解决方案:检查订阅/发票是否已支付,跳过支付确认步骤
- 实现:使用
data.amount_due && data.amount_due > 0 && data.clientSecret条件再调用stripe.confirmPayment() - 受影响场景:99.9% 折扣码使总额低于 $0.50 USD 的场景
- 源码佐证:服务端在 create-subscription+api.ts 返回
amount_due,前端可据此判断金额为零时无需走支付确认;getClientSecret()对拿不到 payment intent 的情况返回null,正是为这类场景兜底
- 问题:当发票因大额折扣(低于 Stripe $0.50 最低限额)被自动支付时,Stripe 不会创建 payment intent,因此没有
6.2 计划中的改进
合并 Chat + Support 订阅
// TODO: When user has Chat and purchases Support tier, // upgrade existing Chat subscription instead of creating new one // Benefits: Single Discord channel, unified billing, easier management增强 Discord 管理
- 单个成员移除
- 批量成员操作
- 无需整体重置的频道重建
改进团队席位管理
- 更好的席位利用率追踪
- 非活跃成员的自动化席位清理
七、测试与开发
7.1 用户模拟工具(User Impersonation Tool)
测试用户访问与订阅时,使用 Supabase SSR User Impersonate Tool:
npm install cp .env.example .env # Fill in Supabase credentials node main.mjs --email <user-email>该工具允许开发者模拟任意用户,以测试订阅流程、Discord 访问与仓库 claims。
7.2 关键测试场景
- PRO 订阅:同时测试循环与一次性两种流程
- 团队席位:测试成员添加/移除与 Discord 访问
- 支持等级:测试私有频道创建与席位计算
- 仓库 Claims:测试 GitHub 协作邀请
- 遗留迁移:测试 product ownership 访问
仓库中可直接参考的测试基建:
- test-subscription-states.mjs:基于
STRIPE_SECRET_KEY_TEST(测试环境)统计active/trialing/past_due/canceled/incomplete_expired/incomplete各状态的订阅分布; - subscriptionFilters.test.ts 与 subscriptionFilters.ts:通过
isManageableSubscription/isExpiredSubscription/isPastDueSubscription三个谓词函数对订阅状态分类。注意past_due/unpaid被有意纳入「可管理」集合——若隐藏它们,用户在 Stripe 重试扣费窗口期内会看到「无订阅」,却仍被扣费且无法停止,这是状态机设计上值得借鉴的细节; - 环境变量:测试模式由
STRIPE_TEST_MODE === 'true'或NODE_ENV === 'development'触发,此时 products.ts 切换到测试产品 ID 集合。
八、API 端点汇总
订阅管理
/api/create-subscription:创建 PRO + 团队席位订阅(循环或一次性由disableAutoRenew区分),实现见 create-subscription+api.ts/api/create-v2-subscription:V2 授权购买($250 一次性 + $100/年升级订阅 + 可选支持等级),见 create-v2-subscription+api.ts/api/upgrade-subscription:添加 Chat / Support tier 订阅(独立月付),见 upgrade-subscription+api.ts/api/add-team-seats:向已有订阅追加团队席位,见 add-team-seats+api.ts/api/cancel-subscription:取消订阅(支持周期结束取消与立即取消两条路径),见 cancel-subscription+api.tsx
团队管理
/api/team-seat:GET(列表 + 已用席位)、POST(邀请)、DELETE(移除)团队成员,见 team-seat+api.ts
Discord 集成
/api/discord/channel:管理通用 Discord 频道访问(#takeout-general),见 channel+api.ts/api/discord/support:管理私有支持频道访问,见 support+api.ts/api/discord/search-member:按关键字搜索 Discord 成员用于邀请,见 search-member+api.ts
Webhooks
/api/stripe/webhook:处理全部 Stripe 订阅生命周期事件,见 webhook+api.ts
事件处理矩阵(源码可验证):product.created/updated/deleted与price.created/updated/deleted同步产品目录;invoice.upcoming发送续费提醒(V1 发升级引导邮件、V2 发普通续费提醒);invoice.payment_failed发送支付失败邮件;invoice.paid(无 subscription)走一次性支付落库;customer.subscription.created/updated/deleted同步订阅快照并维护团队订阅,且在canceled/unpaid/incomplete_expired时回收 Discord/GitHub 访问权限(unclaimSubscription,其中past_due被特意排除——Stripe 仍在重试扣费)。
小结
Tamagui 的订阅系统是一个典型的「Stripe 计费 + Supabase 数据 + 外部权益联动」三层架构:Stripe 负责资金流与订阅状态机,Webhook 把事件单向同步进 Supabase 的subscriptions/subscription_items/team_subscriptions等表,再以此为事实来源驱动 GitHub 仓库邀请、Discord 频道权限与 Bento/Takeout 访问判定。理解其「按订阅挂载 item、按 metadata 存频道 ID、按状态机区分可管理/已过期订阅」的设计,对自建 SaaS 计费与权益系统具有直接参考价值;而 V1 到 V2 的迁移路径(@deprecated标记、product_ownership兼容层、getTakeoutPriceInfo遗留解析)则演示了如何在不破坏老用户权益的前提下平滑演进计费模型。
【免费下载链接】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),仅供参考