Stripe

Stripe 插件,用于 Better Auth,管理订阅和支付。

Stripe 插件将 Stripe 的支付与订阅功能集成到 Better Auth 中。由于支付和认证通常紧密相关,此插件简化了在应用中集成 Stripe 的流程,负责客户创建、订阅管理和 webhook 处理。

功能

  • 用户注册时自动创建 Stripe 客户
  • 管理订阅计划和价格
  • 处理订阅生命周期事件(创建、更新、取消)
  • 安全处理 Stripe webhook,带签名验证
  • 向应用暴露订阅数据
  • 支持试用期和订阅升级
  • 自动防止试用滥用 — 用户在所有计划中每个账户只可获一次试用
  • 灵活的引用系统,关联订阅与用户或组织
  • 支持团队订阅和座位管理

安装

安装插件

首先,安装插件:

npm install @better-auth/stripe

如果你的项目采用客户端和服务器分离,确保在两端都安装该插件。

安装 Stripe SDK

接下来,在服务器端安装 Stripe SDK:

npm install stripe@^20.0.0

在 auth 配置中添加插件

auth.ts
import { betterAuth } from "better-auth"
import { stripe } from "@better-auth/stripe"
import Stripe from "stripe"

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: "2025-11-17.clover", // Stripe SDK v20.0.0 支持的最新 API 版本
})

export const auth = betterAuth({
    // ... 你已有的配置
    plugins: [
        stripe({
            stripeClient,
            stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
            createCustomerOnSignUp: true,
        })
    ]
})

从 Stripe v18 升级? 版本 19 采用了异步的 webhook 签名验证(constructEventAsync),插件内部已处理,无需你改动代码!

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"

export const authClient = createAuthClient({
    // ... 你已有的配置
    plugins: [
        stripeClient({
            subscription: true // 如果启用订阅管理
        })
    ]
})

迁移数据库

运行迁移或生成 schema 以向数据库添加所需表:

npx auth migrate
npx auth generate

你也可以查看 Schema 手动添加表。

设置 Stripe webhook

在 Stripe 仪表盘创建 webhook,指向:

https://your-domain.com/api/auth/stripe/webhook

/api/auth 是认证服务器的默认路径。

确保至少选择以下事件:

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

保存 Stripe 提供的 webhook 签名密钥,作为环境变量 STRIPE_WEBHOOK_SECRET

使用

客户管理

你可以仅使用此插件进行客户管理,而不启用订阅。适合你仅想关联 Stripe 客户和用户的场景。

当设置 createCustomerOnSignUp: true 时,用户注册时会自动创建 Stripe 客户,并与数据库中的用户关联。你也可定制客户创建流程:

auth.ts
stripe({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
        // 处理新创建的客户
        console.log(`客户 ${stripeCustomer.id} 已为用户 ${user.id} 创建`);
    },
    getCustomerCreateParams: async (user, ctx) => {
        // 定制 Stripe 客户创建参数
        return {
            metadata: {
                referralSource: user.metadata?.referralSource
            }
        };
    }
})

订阅管理

定义计划

你可以静态定义订阅计划,也可动态获取:

auth.ts
// 静态定义
subscription: {
    enabled: true,
    plans: [
        {
            name: "basic", // 计划名称,存入数据库时自动小写
            priceId: "price_1234567890", // Stripe 价格 ID
            annualDiscountPriceId: "price_1234567890", // (可选)年付折扣价格 ID
            limits: {
                projects: 5,
                storage: 10
            }
        },
        {
            name: "pro",
            priceId: "price_0987654321",
            limits: {
                projects: 20,
                storage: 50
            },
            freeTrial: {
                days: 14,
            }
        }
    ]
}

// 动态定义(数据库或接口获取)
subscription: {
    enabled: true,
    plans: async () => {
        const plans = await db.query("SELECT * FROM plans");
        return plans.map(plan => ({
            name: plan.name,
            priceId: plan.stripe_price_id,
            limits: JSON.parse(plan.limits)
        }));
    }
}

详情见 计划配置

创建订阅

通过调用 subscription.upgrade 方法创建订阅:

POST/subscription/upgrade
const { data, error } = await authClient.subscription.upgrade({    plan: "pro", // required    annual: true,    referenceId: "123",    subscriptionId: "sub_123",    metadata,    customerType,    seats: 1,    locale,    successUrl, // required    cancelUrl, // required    returnUrl,    disableRedirect: false, // required    scheduleAtPeriodEnd: false,});
Parameters
planstringrequired

要升级的计划名称。

annualboolean

是否升级为年付计划。

referenceIdstring

订阅的引用 ID。默认根据 customerType 赋值。

subscriptionIdstring

要升级的订阅 ID。

metadataRecord<string, any>

附加元数据,存储于订阅。

customerType"user" | "organization"

计费的客户类型。(默认:"user")

seatsnumber

要升级的座位数(如适用)。

localestring

显示 Checkout 的 IETF 语言标签。未提供或设为 auto 时使用浏览器语言。

successUrlstringrequired

支付或设置完成后 Stripe 重定向的 URL。

cancelUrlstringrequired

若设置,Checkout 显示返回按钮,取消支付时重定向此处。

returnUrlstring

从账单门户返回的 URL(用于升级现有订阅)

disableRedirectbooleanrequired

成功订阅后禁用自动重定向。

scheduleAtPeriodEndboolean

在当前计费周期结束时调度计划变更,而不是即时生效。

简单示例:

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    annual: true, // 可选:升级为年付计划
    referenceId: "org_123", // 可选:默认为 customerType 的值
    seats: 5, // 可选:团队计划座位数
    locale: "en" // 可选:用英文显示 Checkout
});

这会创建一个 Checkout 会话,并将用户重定向至 Stripe Checkout 页面。

插件只支持每个引用 ID(用户或组织)最多一个活跃或试用订阅,不支持同一引用并发多订阅。

如果用户已有活跃订阅,切记升级时必须提供 subscriptionId 参数,否则可能同时创建新订阅,导致重复计费。

重要提示: successUrl 参数会在内部被修改,以处理结账完成与 webhook 处理之间的竞态条件。插件会创建跳转中转页面,确保订阅状态被正确更新后再跳转到你指定的成功页。

const { error } = await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
if(error) {
    alert(error.message);
}

切换计划

想切换订阅计划,可以调用 subscription.upgrade

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    subscriptionId: "sub_123", // 当前用户计划的 Stripe 订阅 ID
});

确保用户仅支付新计划,不会同时支付两个计划费用。

在周期结束时调度计划更改

默认计划调整立即生效,且自动按比例计费。若希望用户继续使用当前计划直到结算周期结束,再切换计划,可以这样:

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    returnUrl: "/billing",
    scheduleAtPeriodEnd: true, // 默认 false
});

此方案利用 Stripe 订阅调度 API,创建两阶段执行:当前计划持续至周期结束,然后自动启动新计划,且不会按比例计费。

scheduleAtPeriodEndtrue 时:

  • 订阅计划保持不变直到周期结束,仅存储 stripeScheduleId 以供客户端检测待切换状态
  • 不会跳转到 Stripe Checkout 或账单门户,调整由服务端执行
  • 计费周期结束后 Stripe 会触发 customer.subscription.updated webhook,自动更新订阅记录
  • 若周期前请求新的升级或调度,先释放已有挂起调度

列出活跃订阅

获取用户的活跃订阅:

GET/subscription/list
const { data: subscriptions, error } = await authClient.subscription.list({    query: {        referenceId: '123',        customerType,    },});// 获取活跃订阅const activeSubscription = subscriptions.find(    sub => sub.status === "active" || sub.status === "trialing");// 检查订阅限制const projectLimit = subscriptions?.limits?.projects || 0;
Parameters
referenceIdstring

要列出的订阅引用 ID。

customerType"user" | "organization"

计费客户类型。(默认:"user")

确保插件配置中加入了 authorizeReference 以授权引用 ID:

auth.ts
stripe({
    // ... 其他选项
    subscription: {
        // ... 其他订阅配置
        authorizeReference: async ({ user, session, referenceId, action }) => {
            if(action === "list-subscription") {
                const org = await db.member.findFirst({
                    where: {
                        organizationId: referenceId,
                        userId: user.id
                    }   
                });
                return org?.role === "owner"
            }
            // 检查用户是否有权限列出此引用的订阅
            return true;
        }
    }
})

取消订阅

取消订阅:

POST/subscription/cancel
const { data, error } = await authClient.subscription.cancel({    referenceId: 'org_123',    customerType,    subscriptionId: 'sub_123',    returnUrl: '/account', // required});
Parameters
referenceIdstring

要取消的订阅引用 ID。默认根据 customerType 赋值。

customerType"user" | "organization"

计费客户类型。(默认:"user")

subscriptionIdstring

要取消的订阅 ID。

returnUrlstringrequired

用户点击账单门户中的链接返回你网站时的跳转 URL。

将会重定向用户进入 Stripe 账单门户,让用户自行取消订阅。

取消状态说明

Stripe 支持多种取消状态,插件会跟踪它们:

字段说明
cancelAtPeriodEnd若订阅状态为 active,表示将在当前计费期结束时取消;若状态为 canceled,表示已在期末取消。
cancelAt订阅计划被取消时生效的时间点(若已排期取消)。
canceledAt已取消订阅的请求时间。注意:若通过 cancelAtPeriodEnd 取消,此时间为取消请求时间,不是实际结束时间。
endedAt订阅真正结束的日期。
status仅当订阅真正结束后,才会切换为 "canceled" 状态。

恢复订阅

注意: 只支持活动订阅且处于待取消或待调度变更状态的恢复。已结束的订阅(status: "canceled"endedAt 已设)无法恢复。

用户取消或排期变更后改变主意,可以恢复订阅:

POST/subscription/restore
const { data, error } = await authClient.subscription.restore({    referenceId: '123',    customerType,    subscriptionId: 'sub_123',});
Parameters
referenceIdstring

要恢复的订阅引用 ID。默认根据 customerType 赋值。

customerType"user" | "organization"

计费客户类型。(默认:"user")

subscriptionIdstring

要恢复的订阅 ID。

此接口处理两种情况:

  • 待取消:取消期末生效的订阅,设置 cancelAtPeriodEndfalse,清除 cancelAtcanceledAt,保证订阅续期继续。
  • 待计划变更(通过 scheduleAtPeriodEnd):释放 Stripe 订阅调度,清除 stripeScheduleId,保持当前计划不变。

创建账单门户会话

创建 Stripe 账单门户会话,让客户管理订阅、支付方式和账单历史:

POST/subscription/billing-portal
const { data, error } = await authClient.subscription.billingPortal({    locale,    referenceId: "123",    customerType,    returnUrl,    disableRedirect: false,});
Parameters
localestring

显示账单门户的 IETF 语言标签。未提供或 auto 时使用浏览器语言。

referenceIdstring

订阅引用 ID。

customerType"user" | "organization"

计费客户类型。(默认:"user")

returnUrlstring

退出账单门户后跳转的 URL。

disableRedirectboolean

禁用自动重定向到账单页面。 @default false

支持的语言标签请查看 IETF 语言标签文档

该接口会创建 Stripe 账单门户会话,响应中以 data.url 返回,可以重定向用户至该地址进行订阅和支付管理。

引用系统

默认订阅与用户 ID 关联。你也可以自定义引用 ID,将订阅关联到其他实体,如组织:

client.ts
// 为组织创建订阅
await authClient.subscription.upgrade({
    plan: "pro",
    referenceId: "org_123456",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 5 // 团队计划座位数
});

// 列出组织的订阅
const { data: subscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123456"
    }
});

团队订阅与座位数

团队或组织计划可以指定座位数:

await authClient.subscription.upgrade({
    plan: "team",
    referenceId: "org_123456",
    seats: 10, // 10 个团队成员
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

seats 参数会传给 Stripe 作为订阅品项数量,可用于应用逻辑中限制团队成员数。

要授权引用 ID,请实现 authorizeReference

auth.ts
subscription: {
    // ... 其他选项
    authorizeReference: async ({ user, session, referenceId, action }) => {
        // 检查用户是否有权限管理此引用的订阅
        if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
            const org = await db.member.findFirst({
                where: {
                    organizationId: referenceId,
                    userId: user.id
                }   
            });
            return org?.role === "owner"
        }
        return true;
    }
}

Webhook 处理

插件自动处理常见 webhook 事件:

  • checkout.session.completed:结账完成后更新订阅状态
  • customer.subscription.created:在非结账流程创建订阅时触发
  • customer.subscription.updated:订阅更新时触发
  • customer.subscription.deleted:标记订阅已取消

你也可以响应自定义事件:

auth.ts
stripe({
    // ... 其他选项
    onEvent: async (event) => {
        // 处理任何 Stripe 事件
        switch (event.type) {
            case "invoice.paid":
                // 处理已付款发票
                break;
            case "payment_intent.succeeded":
                // 处理支付成功
                break;
        }
    }
})

订阅生命周期钩子

你可以挂载订阅各种生命周期事件回调:

auth.ts
subscription: {
    // ... 其他选项
    onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
        // 结账方式成功创建订阅时调用
        await sendWelcomeEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionCreated: async ({ event, subscription, stripeSubscription, plan }) => {
        // 订阅在结账外创建时调用(比如后台仪表盘)
        await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionUpdate: async ({ event, subscription }) => {
        // 订阅更新时调用
        console.log(`订阅 ${subscription.id} 已更新`);
    },
    onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
        // 订阅取消时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
        // 订阅删除时调用
        console.log(`订阅 ${subscription.id} 已删除`);
    }
}

试用期

你可以为计划配置试用期:

auth.ts
{
    name: "pro",
    priceId: "price_0987654321",
    freeTrial: {
        days: 14,
        onTrialStart: async (subscription) => {
            // 试用期开始时调用
            await sendTrialStartEmail(subscription.referenceId);
        },
        onTrialEnd: async ({ subscription }, ctx) => {
            // 试用期结束时调用
            await sendTrialEndEmail(subscription.referenceId);
        },
        onTrialExpired: async (subscription, ctx) => {
            // 试用到期未转换时调用
            await sendTrialExpiredEmail(subscription.referenceId);
        }
    }
}

Schema

Stripe 插件会向数据库添加以下表:

User

表名:user

Table
字段
类型
描述
stripeCustomerId
string
?
Stripe 客户 ID

Organization

表名:organization (仅当 organization.enabledtrue 时)

Table
字段
类型
描述
stripeCustomerId
string
?
组织的 Stripe 客户 ID

Subscription

表名:subscription

Table
字段
类型
描述
id
string
pk
订阅唯一标识
plan
string
订阅计划名称
referenceId
string
订阅关联 ID(默认用户 ID)。**不**应为唯一字段,允许用户取消后重新订阅。
stripeCustomerId
string
?
Stripe 客户 ID
stripeSubscriptionId
string
?
Stripe 订阅 ID
status
string
订阅状态(active、canceled 等)
periodStart
Date
?
当前计费周期起始时间
periodEnd
Date
?
当前计费周期结束时间
cancelAtPeriodEnd
boolean
?
是否在计费结束时取消订阅
cancelAt
Date
?
订阅被计划取消的时间点
canceledAt
Date
?
订阅取消请求时间。注意:`cancelAtPeriodEnd` 取消时此处为请求时间,不是结束时间。
endedAt
Date
?
订阅实际结束时间
seats
number
?
团队计划座位数
trialStart
Date
?
试用期起始时间
trialEnd
Date
?
试用期结束时间
billingInterval
string
?
订阅计费间隔(如 'month', 'year')
stripeScheduleId
string
?
Stripe 订阅调度 ID,存在则说明有待处理计划变更

自定义 Schema

若需要更改表名或字段,可传入 schema 选项至 Stripe 插件:

auth.ts
stripe({
    // ... 其他选项
    schema: {
        subscription: {
            modelName: "stripeSubscriptions", // 将订阅表映射为 stripeSubscriptions
            fields: {
                plan: "planName" // 将 plan 字段映射为 planName
            }
        }
    }
})

选项

选项类型说明
stripeClientStripeStripe 客户端实例。必需
stripeWebhookSecretstringStripe webhook 签名密钥。必需
createCustomerOnSignUpboolean用户注册时是否自动创建 Stripe 客户。默认:false
onCustomerCreatefunction客户创建后回调,参数 { stripeCustomer, user } 和上下文。
getCustomerCreateParamsfunction自定义 Stripe 客户创建参数,参数是 user 和上下文。
onEventfunction收到任何 Stripe webhook 事件时回调,参数为 Stripe.Event
subscriptionobject订阅配置。详见下文 订阅选项
organizationobject启用组织客户支持。详见下文 组织选项
schemaobject自定义 Stripe 插件的数据库 schema。

订阅选项

选项类型说明
enabledboolean是否启用订阅功能。必需
plansStripePlan[]function订阅计划数组或异步返回计划的函数。启用时必需
requireEmailVerificationboolean是否要求用户验证邮箱后才允许订阅升级。默认:false
authorizeReferencefunction授权引用 ID。参数 { user, session, referenceId, action } 和上下文。
getCheckoutSessionParamsfunction自定义 Stripe Checkout 会话参数。参数 { user, session, plan, subscription },请求和上下文。
onSubscriptionCompletefunction通过结账创建订阅时调用。参数 { event, stripeSubscription, subscription, plan } 和上下文。
onSubscriptionCreatedfunction在非结账流程创建订阅时调用。参数 { event, stripeSubscription, subscription, plan }
onSubscriptionUpdatefunction订阅更新时调用。参数 { event, subscription }
onSubscriptionCancelfunction订阅取消时调用。参数 { event, subscription, stripeSubscription, cancellationDetails }
onSubscriptionDeletedfunction订阅被删除时调用。参数 { event, stripeSubscription, subscription }

计划配置

选项类型说明
namestring计划名称。必需
priceIdstringStripe 价格 ID。除非使用 lookupKey,否则必需。
lookupKeystringStripe 价格查找键。可替代 priceId
annualDiscountPriceIdstring年付计费价格 ID。
annualDiscountLookupKeystring年付计费价格查找键。
limitsobject计划限制(如 { projects: 10, storage: 5 })。
groupstring计划分组名称。
seatPriceIdstring按座位计费价格 ID,需启用 organization 插件。
lineItemsLineItem[]额外的结账会话商品条目。
freeTrialobject试用配置,详见下文 免费试用配置

Stripe Checkout 会话不支持 混合周期订阅。结账中所有商品条目必须使用相同计费间隔(如全部月付或全部年付)。若周期不同,Stripe API 会拒绝请求。

免费试用配置

选项类型说明
daysnumber试用天数。必需
onTrialStartfunction试用开始时调用,参数 subscription
onTrialEndfunction试用结束时调用,参数 { subscription } 和上下文。
onTrialExpiredfunction试用过期且未转化时调用,参数 subscription 和上下文。

组织选项

选项类型说明
enabledboolean启用组织客户支持。必需
getCustomerCreateParamsfunction自定义组织的 Stripe 客户创建参数,参数为 organization 和上下文。
onCustomerCreatefunction组织客户创建后调用,参数 { stripeCustomer, organization } 和上下文。

高级用法

与组织插件一起使用

Stripe 插件集成了 organization 插件,支持组织作为 Stripe 客户。订阅的计费对象由个人用户转为组织,适合 B2B 场景。

启用组织客户时:

  • 组织首次订阅时自动创建 Stripe 客户
  • 同步组织名称更改到 Stripe 客户
  • 有活跃订阅的组织无法被删除

启用组织客户

设置 organization.enabledtrue,并确保已安装组织插件:

auth.ts
plugins: [
    organization(),
    stripe({
        // ... 其他选项
        subscription: {
            enabled: true,
            plans: [...],
        },
        organization: { 
            enabled: true
        } 
    })
]

创建组织订阅

即使启用了组织客户,用户订阅仍可用且为默认。若需组织作为计费客户,传入 customerType: "organization"

client.ts
await authClient.subscription.upgrade({
    plan: "team",
    referenceId: activeOrg.id,
    customerType: "organization", 
    seats: 10,
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

授权

请实现 authorizeReference,确认用户有权管理组织订阅:

auth.ts
subscription: {
    // ... 其他订阅选项
    authorizeReference: async ({ user, referenceId, action }) => {
        const member = await db.members.findFirst({
            where: {
                userId: user.id,
                organizationId: referenceId
            }
        });

        return member?.role === "owner" || member?.role === "admin";
    }
}

组织计费邮箱

组织没有唯一邮箱,账单邮箱不会自动同步。组织一般使用专门账单邮箱,和用户账户不同。 你可在结账后通过 Stripe 仪表盘修改邮箱,或用 stripeClient 自定义:

await stripeClient.customers.update(organization.stripeCustomerId, {
    email: "billing@company.com"
});

自定义结账会话参数

你可以传递额外参数给 Stripe Checkout:

auth.ts
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
    return {
        params: {
            allow_promotion_codes: true,
            tax_id_collection: {
                enabled: true
            },
            billing_address_collection: "required",
            custom_text: {
                submit: {
                    message: "我们将立即为您启用订阅"
                }
            },
            metadata: {
                planType: "business",
                referralCode: user.metadata?.referralCode
            }
        },
        options: {
            idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
        }
    };
}

税务收集

开启税务 ID 收集:

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                tax_id_collection: {
                    enabled: true
                }
            }
        };
    }
}

自动税费计算

启用自动税费计算(按客户位置),设置 automatic_tax

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                automatic_tax: {
                    enabled: true
                }
            }
        };
    }
}

注意需先在 Stripe 仪表盘设置税务注册和配置。

试用期管理

插件自动防止用户多次获取免费试用。用户在任一计划使用过试用后,其他计划将不再提供试用。

原理:

  • 系统跟踪用户所有计划的试用使用情况
  • 用户订阅含试用的计划时,查询历史是否曾试用
  • 若有试用记录(trialStarttrialEndtrialing 状态),则新订阅不提供试用
  • 防止用户反复取消订阅、重新订阅以骗取多次试用

示例:

  1. 用户订阅“Starter”计划,有 7 天试用
  2. 用户试用后取消订阅
  3. 用户尝试订阅“Premium”计划,试用被禁止
  4. 用户即刻支付 Premium 计划费用

此行为自动生效,无需额外配置,且不可通过配置覆盖。

故障排查

webhook 问题

若 webhook 未正确处理:

  1. 确认 Stripe 仪表盘配置的 webhook URL 是否正确
  2. 验证 webhook 签名密钥是否正确
  3. 检查是否勾选全部必需事件
  4. 查看服务器日志是否有处理错误

订阅状态异常

若订阅状态未正确更新:

  1. 确认 webhook 事件已接收并处理
  2. 确认 stripeCustomerIdstripeSubscriptionId 是否正确填写
  3. 核对应用中引用 ID 是否与 Stripe 一致

本地测试 webhook

可用 Stripe CLI 转发 webhook 至本地:

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

CLI 会输出本地使用的 webhook 签名密钥,配置至本地环境即可。