ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Tamagui 订阅系统架构全解析:Stripe + Supabase 双轨计费、团队席位与 Discord 权限实现

Tamagui 订阅系统架构全解析:Stripe + Supabase 双轨计费、团队席位与 Discord 权限实现 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_SUBSCRIPTIONV1 遗留产品在 products.ts 中被标记为deprecated当前新用户走 V2 计费见下文 1.5。1.2 PRO 一次性支付$400/年访问权数据库表subscriptions、subscription_items、products、prices与循环订阅权益一致但存在两条设计上的限制❌ 无 Discord takeout 频道访问权❌ 无法添加团队席位团队席位仅限循环订阅自动续费为falseauto renewal false支付产生的是发票invoice记录而非订阅一次性支付对应STRIPE_PRODUCTS.PRO_ONE_TIMEV1deprecated实现上通过stripe.invoiceItems.create()stripe.invoices.create()stripe.invoices.pay()完成见 create-subscriptionapi.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_invitesChat Support$200/月专属私有 Discord 房间含 2 个 Discord 邀请名额响应优先级高于社区频道可与其他任何计划组合Support Tiers1-3$800 / $1,600 / $2,400 每月三档每月 4 小时开发时间支持更快的响应速度每个等级额外 4 个私有 Discord 聊天邀请名额可与 Chat Support 叠加两者实现上各自创建独立的月度订阅upgrade-subscriptionapi.ts与 PRO 的计费周期相互独立。STRIPE_PRODUCTS.CHAT与STRIPE_PRODUCTS.SUPPORT同样是 V1 遗留产品V2 已演进为SUPPORT_DIRECT$500/月与SUPPORT_SPONSOR$2,000/月并支持chat/direct/sponsor三档字符串类型见 upgrade-subscriptionapi.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-subscriptionapi.ts 负责包含幂等键防重复扣费、部分失败回滚与 3DS 认证处理支付成功后由 Webhook 创建项目记录。二、核心实现流程详解2.1 PRO 循环订阅流程User Purchase → create-subscriptionapi.ts → Stripe Subscription Creation数据库流向APIcreate-subscriptionapi.tsStripe以PRO_SUBSCRIPTION_PRICE_ID创建订阅Webhookwebhookapi.ts 处理customer.subscription.created数据库插入subscriptions记录插入subscription_items记录通过subscription_items.subscription_id关联两者关键代码create-subscriptionapi.ts// create-subscriptionapi.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-subscriptionapi.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-subscriptionapi.ts → Stripe Invoice Creation数据库流向APIcreate-subscriptionapi.ts请求体携带disableAutoRenew: trueStripe以PRO_ONE_TIME_PRICE_ID创建发票Webhook处理invoice.paid数据库以cancel_at_period_end: true插入subscriptions将cancel_at设为一周年后插入subscription_items关键代码webhookapi.ts 中的manageOneTimePayment()// webhookapi.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()webhookapi.ts因为一次性购买在 Stripe 侧不存在订阅对象。随后syncInvoiceLinePrices()会把发票行中的价格/产品反向同步进prices/products表保证subscription_items引用完整。该分支同时会调用createTeamInvoice()处理一次性团队席位发票见 supabaseAdmin.ts。2.3 团队席位购买流程User Purchase → create-subscriptionapi.ts → Update Existing PRO Subscription数据库流向APIcreate-subscriptionapi.ts请求体携带teamSeats 0Stripe向已有订阅追加TEAM_SEATS_SUBSCRIPTION_PRICE_IDitemWebhook处理customer.subscription.updated数据库更新subscription_items加入团队席位 item通过createTeamSubscription()创建team_subscriptions记录在team_subscriptions.total_seats中记录席位数量关键代码create-subscriptionapi.ts// create-subscriptionapi.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.tsexport 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-seatsapi.ts循环订阅场景下调用stripe.subscriptions.update()修改既有 team seats item 的quantity存在则累加、不存在则新建一次性场景则走 invoiceItems invoice 通道。2.4 团队成员管理流程数据库表team_members、team_subscriptions、users添加成员流程team-seatapi.tsAPIteam-seatapi.ts的 POST 端点数据库插入team_members记录status: activeGitHub通过resend-github-inviteapi.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。移除成员流程APIteam-seatapi.ts的 DELETE 端点数据库删除team_members记录GitHub移除仓库访问权Discord通过 Discord 面板手动移除移除时同样先校验team_subscriptions.owner_id user.id防止越权操作其他团队的成员team-seatapi.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在实际实现中通用频道的席位计算位于 channelapi.ts先由ensureSubscription()得出基础席位再探测用户是否持有team_subscriptions先查 owner再查team_members反向定位若teamSubscription.total_seats discordSeats则取total_seats 11 代表 owner 本人作为最终席位。该文件还保留了一段重要 TODO 注释说明当前实现的局限discord_invites表只记录subscription_id无法按用户追踪席位占用因此团队所有成员共享同一个 Discord 席位池channelapi.ts。而私有支持频道的席位计算在 supportapi.ts遍历用户所有活跃订阅Chat 产品贡献 2 个席位Support tier 按subscription.quantitytier 等级× 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-subscriptionapi.ts → Separate Monthly Subscription数据库流向APIupgrade-subscriptionapi.tsStripe创建独立的月度订阅Webhookwebhookapi.ts 处理订阅事件数据库在subscriptions表插入独立记录计费周期与 PRO 计划不同关键代码upgrade-subscriptionapi.ts// upgrade-subscriptionapi.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 字符串型 tierchat/direct/sponsorchat在 V2 中免费包含、不创建额外订阅见getSupportTierPriceId()upgrade-subscriptionapi.ts。2.7 Discord 集成与频道生命周期数据库表subscriptionsmetadata 字段、discord_invites频道创建通用频道面向 PRO 用户#takeout-general支持频道面向 Chat / Support tier 用户私有频道通用频道在 channelapi.ts 中创建首次访问时在 guild 中查找名为TAKEOUT_GENERAL_CHANNEL的频道并把频道 ID 写入subscriptions.metadata支持频道在 supportapi.ts 中按用户邮箱前缀命名非法字符替换为_命名规则为${userName}-${chat | tier-N | chat-tier-N}频道 topic 中附带订阅 ID 便于追溯同时以permission_overwrites对DEFAULT_ROLE_IDdenyVIEW_CHANNELbitfield1024实现私有化。元数据存储{ discord_channel: 1132001717215559691 }成员管理discord_invites表记录频道成员与邀请状态用于追踪哪些用户被邀请进了哪些频道、防止重复邀请。POST 添加成员前会校验currentlyOccupiedSeats discordSeats满员返回 403并检查discord_user_id是否已存在添加成功后为用户打上TAKEOUT_ROLE_ID角色supportapi.ts。Discord 成员搜索由 search-memberapi.ts 提供且被 Pro 权限保护ensureAccess校验无 Pro 返回 403。重置功能UI 重置按钮删除整个 Discord 频道APIdiscord/supportapi.ts 与 discord/channelapi.ts 的 DELETE 端点影响所有成员失去访问权频道必须重建DELETE 分支会依次删除 Discord 频道 → 对每个discord_invites成员移除TAKEOUT_ROLE_ID角色 → 清空discord_invites表 → 清空subscriptions.metadata中的频道 ID。2.8 订阅取消流程源码补充doc/subcriptions.md 的 API 汇总中列出了/api/cancel-subscription。其实现位于 cancel-subscriptionapi.tsx先校验subscriptions.user_id user.id然后区分两种取消策略cancelSubscription.ts立即取消订阅处于past_due/unpaid或存在billing_reason subscription_cycle且状态为draft/open的待续费发票时先停止发票收款draft 发票关闭auto_advanceopen 发票 void再stripe.subscriptions.cancel()避免用户取消后仍被 Stripe 反复重试扣费周期结束取消健康订阅仅设置cancel_at_period_end: true保留到本周期结束的访问权益。两种路径都会向用户发送取消确认邮件。三、GitHub 仓库访问系统3.1 GitHub 集成数据库表claims流程用户操作点击「Takeout 1」或「Takeout 2」直接打开仓库或点击「Resend Invite」发送/重发 GitHub 团队邀请APIresend-github-inviteapi.tsx 处理邀请请求GitHub 检查helpers.ts 中的checkIfUserIsTeamMember()响应处理已是成员返回成功消息含团队访问链接新邀请发送 GitHub 团队邀请返回成功消息源码层面的校验链条如下该端点要求subscription_id与product_id均为字符串400 校验并通过getActiveSubscriptions()验证订阅归属校验订阅中是否包含合法的 Pro 产品 IDV1 的prod_RlRd2DVrG0frHe、prod_Rxu0x7jR0nWJSvV2 的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 APIX-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、metadataJSONB存 Discord 频道 ID 等、quantity、cancel_at_period_end、cancel_at、current_period_start/end、trial_*等字段建表于 20230529071500_init.sqlsubscription_items订阅与产品/价格的关联表subscription_idprice_idid外键级联删除建表于 20230620111753_add_subscription_items_table.sqlproducts产品定义PRO、Team Seats、Chat、Supportprices每个产品的定价信息含unit_amount、currency、interval、description其中 description 承担了遗留席位解析的职责customersStripe customer ID 与 Supabase 用户 UUID 的映射表iduuidstripe_customer_id4.2 团队管理team_subscriptions团队订阅元数据含owner_id唯一约束、total_seatscheck (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_privateGitHub token 与私有数据4.4 Discord 集成subscriptionsmetadata 中存储频道 IDJSON 形式discord_invitesDiscord 成员邀请与邀请状态含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.tsxgetTakeoutPriceInfo()它同时兼容「hobby 首档无私有频道」「10-20 seats → 4 席」「20 seats → 8 席」等旧价格描述规则。另外ensureSubscription.ts还会校验订阅的subscription_items中是否包含合法的 Pro 产品V1 的Tamagui Pro、旧Takeout StackV2 的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 频道并存跨多个订阅的席位计算变得复杂现有实现已在 supportapi.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-subscriptionapi.ts 返回amount_due前端可据此判断金额为零时无需走支付确认getClientSecret()对拿不到 payment intent 的情况返回null正是为这类场景兜底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 Toolnpm 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-subscriptionapi.ts/api/create-v2-subscriptionV2 授权购买$250 一次性 $100/年升级订阅 可选支持等级见 create-v2-subscriptionapi.ts/api/upgrade-subscription添加 Chat / Support tier 订阅独立月付见 upgrade-subscriptionapi.ts/api/add-team-seats向已有订阅追加团队席位见 add-team-seatsapi.ts/api/cancel-subscription取消订阅支持周期结束取消与立即取消两条路径见 cancel-subscriptionapi.tsx团队管理/api/team-seatGET列表 已用席位、POST邀请、DELETE移除团队成员见 team-seatapi.tsDiscord 集成/api/discord/channel管理通用 Discord 频道访问#takeout-general见 channelapi.ts/api/discord/support管理私有支持频道访问见 supportapi.ts/api/discord/search-member按关键字搜索 Discord 成员用于邀请见 search-memberapi.tsWebhooks/api/stripe/webhook处理全部 Stripe 订阅生命周期事件见 webhookapi.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),仅供参考
返回列表