news 2026/9/10 20:42:33

Cal.diy 集成 Stripe 支付:从密钥配置、Connect OAuth 到预约收款与订阅扣费的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cal.diy 集成 Stripe 支付:从密钥配置、Connect OAuth 到预约收款与订阅扣费的完整实战指南

Cal.diy 集成 Stripe 支付:从密钥配置、Connect OAuth 到预约收款与订阅扣费的完整实战指南

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

Cal.diy 将 Stripe 作为其默认的支付基础设施,为「事件类型(Event Type)收款」「Premium 用户名订阅」「团队/组织计费」等场景提供统一支付能力。本指南以 packages/app-store/stripepayment/README.md 的 8 步配置流程为主线,结合仓库源码逐层拆解 Stripe 接入的完整链路——从密钥获取、环境变量设置、Connect OAuth 授权,到支付意图(PaymentIntent)、卡信息留存(SetupIntent)与 Webhook 通知的实际实现,帮助你在一套可复现的步骤内完成支付集成并理解其底层机制。

Stripe 支付集成模块概览

Stripe 应用位于仓库的 packages/app-store/stripepayment 目录,是 Cal.diy App Store 中category: "payment"variant: "payment"的标准应用。其元数据定义在 packages/app-store/stripepayment/_metadata.ts:

  • slug: "stripe"type: "stripe_payment",在系统中以stripe_payment作为支付类型标识;
  • isOAuth: true:该应用通过 Stripe Connect OAuth 完成账号授权,而不是简单地填写 API Key;
  • extendsFeature: "EventType":应用能力直接挂在「事件类型」上,可在创建/编辑预约类型时启用按次收费;
  • installed字段由三个环境变量共同决定:STRIPE_CLIENT_IDNEXT_PUBLIC_STRIPE_PUBLIC_KEYSTRIPE_PRIVATE_KEY全部存在时才认为应用已安装。

整个目录结构围绕三条主线组织:

  • 接入层:api/add.ts 生成 Connect 授权链接,api/callback.ts 处理 OAuth 回调并落库凭证;
  • 服务层:lib/PaymentService.ts 实现创建支付、扣款、退款等核心业务,lib/server.ts 封装 Stripe 官方 SDK;
  • 配置层:zod.ts 校验应用密钥格式,components/EventTypeAppSettingsInterface.tsx 提供事件类型设置界面。

前置准备:创建 Stripe 账户并开启测试模式

按照 README 的第一步,需要准备一个可用的 Stripe 账号。官方文档特别强调:进行功能验证时,应始终在 Dashboard 右上角的 Test-Mode 开关打开状态下操作。测试模式下产生的密钥(以pk_test_sk_test_开头)与生产密钥(以pk_live_sk_live_开头)隔离,测试数据不会影响真实交易,也不会产生实际扣款。

在 Cal.diy 中,测试模式还影响一处细节:从 pages/setup/_getServerSideProps.ts 可以看到,当环境变量NEXT_PUBLIC_IS_E2E被设置时,Connect OAuth 请求中会强制指定country: "US",注释表明这是为了让 E2E 测试在国际化环境下不失败。

获取 API 密钥并配置环境变量

在 Stripe Dashboard 的 API Keys 页面可以找到两类密钥:

密钥前缀说明应写入的环境变量
可发布密钥(Publishable key)pk_...用于浏览器端加载 Stripe.jsNEXT_PUBLIC_STRIPE_PUBLIC_KEY
私有密钥(Secret key)sk_...用于服务端所有 API 调用STRIPE_PRIVATE_KEY
Connect 客户端 IDca_...用于 OAuth 授权流程STRIPE_CLIENT_ID
Webhook 签名密钥whsec_...用于校验 Webhook 请求签名STRIPE_WEBHOOK_SECRET

将以上四项写入.env文件后,Stripe 应用即可出现在已安装列表。密钥格式校验定义在 zod.ts 的appKeysSchema中:

export const appKeysSchema = z.object({ client_id: z.string().startsWith("ca_").min(1), client_secret: z.string().startsWith("sk_").min(1), public_key: z.string().startsWith("pk_").min(1), webhook_secret: z.string().startsWith("whsec_").min(1), });

也就是说,服务启动时若密钥前缀不匹配(例如把sk_test_...写错位置),会直接触发 schema 校验失败,从源头避免配置错误。

服务端 SDK 的初始化位于 lib/server.ts,它使用STRIPE_PRIVATE_KEY创建 Stripe 实例,并将 API 版本固定为2020-08-27

const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY || ""; const stripe = new Stripe(stripePrivateKey, { apiVersion: "2020-08-27", });

订阅与 Premium 相关的扩展环境变量

除了上述四项基础密钥,模块还通过 lib/constants.ts 读取一批与订阅计费相关的环境变量:

环境变量用途
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLYPremium 用户名月付价格 ID
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRODUCT_IDPremium 套餐产品 ID
NEXT_PUBLIC_STRIPE_TEAM_MONTHLY_PRICE_ID团队按座位(per-seat)月付价格 ID
STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID电话号码月付价格 ID

这些变量由 lib/utils.ts 中的getPremiumMonthlyPlanPriceId()getPerSeatPlanPrice()getPhoneNumberMonthlyPriceId()等函数消费,分别服务于 Premium 用户名购买与团队订阅场景。其中getPhoneNumberMonthlyPriceId()在变量缺失时会主动抛错,提示STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID env var is not set

Stripe Dashboard 配置:Connect OAuth 与 Webhook

README 的第 3~8 步全部在 Stripe Dashboard 完成,是连接 Cal.diy 与 Stripe 的关键环节。

开启 Connect OAuth(Standard Accounts)

进入 Stripe Connect Settings,为Standard Accounts激活 OAuth。Standard 模式允许每个 Cal.diy 用户用自己的 Stripe 账户独立收款——平台自身持有STRIPE_PRIVATE_KEY,而每个接入的用户通过 OAuth 获得独立的stripe_user_id,交易在其名下结算。

随后将以下地址登记为 OAuth redirect URL(README 中写作<CALENDSO URL>占位符,实际为部署实例的根地址,即代码中的WEBAPP_URL):

<WEBAPP_URL>/api/integrations/stripepayment/callback

从源码看,这个回调端点有三处会生成跳转:

  • api/add.ts 使用client_idscope: "read_write"response_type: "code"构造https://connect.stripe.com/oauth/authorize?授权链接,并将当前用户的邮箱、姓名预填进stripe_user参数;
  • pages/setup/_getServerSideProps.ts 在应用安装页(Setup)执行服务端重定向,把returnToonErrorReturnTofromApp等信息编码进state,保证 OAuth 完成后能回到正确的页面;
  • api/callback.ts 接收授权码,调用stripe.oauth.token()换取访问令牌,再通过stripe.accounts.retrieve()获取账户默认币种(default_currency),最后调用createOAuthAppCredential{ appId: "stripe", type: "stripe_payment" }与令牌数据一并写入 Credential 表。

回调端点还处理了用户拒绝授权的场景:当 Stripe 返回access_denied时,会跳转到state.onErrorReturnTo(默认/apps/installed/payment),避免用户卡在死循环里。

创建 Webhook 并订阅 payment_intent 事件

在 Stripe Webhooks 页面添加端点:

<WEBAPP_URL>/api/integrations/stripepayment/webhook

README 要求为 Webhook选择所有payment_intent事件(即payment_intent.succeededpayment_intent.payment_failed等),因为预约收款的核心状态都体现在 PaymentIntent 上。创建完成后将whsec_...开头的签名密钥填入STRIPE_WEBHOOK_SECRET

需要注意一个仓库现状:社区版(Community Edition)的 apps/web/pages/api/integrations/stripepayment/webhook.ts 当前实现会直接返回 404,提示 "Payment webhooks are not available in community edition";而在 apps/web/playwright/fixtures/users.ts 的 E2E 流程中,会使用stripe.webhooks.generateTestHeaderString()构造合法的stripe-signature签名头,向该端点 POST 一个payment_intent.succeeded事件来验证「支付确认」流程是否被正确触发。这提示你:在社区版本地验证时,支付成功回调的端到端链路需要依赖 E2E 工具或自建 Webhook 消费,而不能依赖该端点返回业务结果。

事件类型级支付配置:价格、币种与支付选项

接入成功后,每个事件类型都可以独立启用 Stripe 收费。设置界面由 components/EventTypeAppSettingsInterface.tsx 提供,配置项的数据模型定义在 zod.ts 的appDataSchema中:

配置项类型说明
pricenumber收费金额(以最小货币单位存储)
currencystring币种,默认取currencyOptions首项usd
paymentOptionON_BOOKING/HOLD支付时机,见下文
enabledboolean是否对该事件类型启用收费
refundPolicyenum退款策略(来自@calcom/lib/payment/types
refundDaysCountnumber退款天数窗口
refundCountCalendarDaysboolean退款天数按自然日还是工作日计算
autoChargeNoShowFeeIfCancelledboolean取消时是否自动收取爽约费
autoChargeNoShowFeeTimeValue/autoChargeNoShowFeeTimeUnitnumber / enum爽约费计费窗口与单位(minutes/hours/days)

其中paymentOption的合法取值定义在 lib/constants.ts:

export const paymentOptions = [ { label: "on_booking_option", value: "ON_BOOKING" }, { label: "hold_option", value: "HOLD" }, ];
  • ON_BOOKING:预约创建时立即创建 PaymentIntent 并完成扣款(即「预订即支付」);
  • HOLD:预约时只通过 SetupIntent 留存卡信息、不扣款,之后在特定时机(如爽约)再真正chargeCard扣款。

设置界面还做了两项保护:启用支付时若未选择币种和支付选项会自动填入默认值(USD / ON_BOOKING),未选择退款策略时默认RefundPolicy.NEVER;若事件类型配置了重复(recurring)规则或开启按席位(seats)预订,界面会显示对应警告——重复事件每个实例都会被收费,这是需要提前告知预约者的事项。币种列表来自 lib/currencyOptions.ts,覆盖 AED、CNY、EUR、USD 等 130+ 种 Stripe 支持的币种。

服务端支付核心:PaymentIntent 与 SetupIntent

支付服务的完整实现在 lib/PaymentService.ts,通过BuildPaymentService(credentials)工厂函数对外暴露(工厂方式避免 Stripe SDK 类型泄漏到.d.ts产物中)。

create:预订即支付(ON_BOOKING)

create()流程:先通过retrieveOrCreateStripeCustomerByEmail按预约者邮箱在收款账号(stripe_user_id)下创建或复用 Stripe Customer,再调用stripe.paymentIntents.create(),关键参数如下:

const params: Stripe.PaymentIntentCreateParams = { amount: payment.amount, currency: payment.currency, customer: customer.id, automatic_payment_methods: { enabled: true }, metadata: { identifier: "cal.com", bookingId, calAccountId: userId, /* ... */ }, }; const paymentIntent = await this.stripe.paymentIntents.create(params, { stripeAccount: this.credentials.stripe_user_id, });

注意amount最小货币单位(如分),这与设置界面中convertToSmallestCurrencyUnit的换算一致。metadata里写入了bookingIdcalAccountIdbookerEmail等业务信息,便于在 Stripe Dashboard 中反查订单来源。创建成功后,模块会在本地Payment表中落一条记录,externalId指向 PaymentIntent ID,并保存stripe_publishable_keystripeAccount供前端展示支付界面使用。

collectCard + chargeCard:先留存后扣款(HOLD)

HOLD 模式分为两步:

  • collectCard():创建 SetupIntent(仅收集卡信息,payment_method_types: ["card"]),对应 Payment 记录以 SetupIntent ID 作为externalId
  • chargeCard():在需要扣款时(如确认爽约),先校验 Stripe Customer 与支付方式仍存在,再创建off_session: trueconfirm: true的 PaymentIntent 完成后台扣款,成功后把 Payment 记录标记为success: true并合并 PaymentIntent 数据。

chargeCard()对常见扣款失败做了用户友好化映射,例如 "your card was declined" 会转换为内部错误码your_card_was_declined,供前端展示本地化文案。

refund 与 deletePayment

  • refund()基于payment.externalId(PaymentIntent ID)调用stripe.refunds.create(),仅在支付成功且未退款时执行;退款成功后更新本地记录refunded: true
  • deletePayment()用于预约被取消时清理:先列出并过期所有关联的 Checkout Session,再取消 PaymentIntent,保证不会出现「预约已取消但待支付订单仍有效」的脏状态。

支付链接与前端加载

预约者付款时,Cal.diy 会生成一个独立的支付落地页链接,其构造逻辑在 lib/client/createPaymentLink.ts:

export function createPaymentLink(opts: { paymentUid, name?, date?, email?, absolute? }): string { let link = ""; if (absolute) link = WEBSITE_URL; const query = stringify({ date, name, email }); return `${link}/payment/${paymentUid}?${query}`; }

即每个 Payment 记录的唯一uid对应一个公开支付页 URL,namedateemail作为查询参数预填。前端 Stripe.js 的加载则由 lib/client/getStripe.ts 完成,它使用loadStripe并采用单例模式stripePromise只初始化一次),避免在 SPA 中重复加载 SDK。

订阅场景:Premium 用户名与团队计费

除事件类型收费外,Stripe 还支撑平台级订阅业务。

Premium 用户名订阅

api/subscription.ts 处理 Premium 用户名购买:校验用户已存在 Stripe Customer 后,创建mode: "subscription"的 Checkout Session,line_items引用getPremiumMonthlyPlanPriceId()返回的价格 ID,并开启allow_promotion_codes。Session 的success_urlcancel_url都指向:

<WEBAPP_URL>/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=...

支付结果回调

api/paymentCallback.ts 通过 lib/getCustomerAndCheckoutSession.ts 拉取 Checkout Session 与 Customer,再按「Stripe Customer 邮箱 → 用户 metadata 中的stripeCustomerId」两级策略定位平台用户。核心分支如下:

  • payment_status !== "paid":跳回回调页并携带paymentStatus参数,前端据此显示支付失败/未完成状态;
  • 支付成功:把目标用户名写入用户记录并将metadata.isPremium置为true;随后通过VerificationTokenService.create()生成有效期 1 天的验证令牌,并调用sendVerificationRequest向用户邮箱发送验证登录链接,完成「支付 → 领取 Premium 用户名」的闭环。

相关逻辑有配套单测覆盖:api/tests/paymentCallback.test.ts 与 lib/VerificationTokenService.test.ts。

测试与验证

仓库为 Stripe 集成提供了多层验证手段:

  • OAuth 安装流程:pages/setup/tests/_getServerSideProps.test.ts 验证未登录跳转、client_id缺失、授权链接构造等分支;
  • 支付回调流程:api/tests/paymentCallback.test.ts 覆盖用户定位与支付状态分支;api/tests/portal.test.ts 覆盖计费门户;
  • 验证令牌:lib/repositories/VerificationTokenRepository.test.ts 覆盖令牌存取;
  • E2E 支付确认:apps/web/playwright/fixtures/users.ts 模拟完整流程——支付完成后从 URL 提取payment_intent参数,构造payment_intent.succeeded事件并用stripe.webhooks.generateTestHeaderString()生成签名,POST 到 Webhook 端点,最后断言返回 200。

本地联调时可参考这套 E2E 的报文结构:事件类型为payment_intent.succeeded,事件对象携带{ id: paymentIntentId },并额外传入account字段标识收款账号。

配置清单速查

按 README 的 8 个步骤整理一份最终核对表:

  1. 创建/复用 Stripe 账户,测试阶段开启 Test-Mode;
  2. 从 API Keys 页面复制pk_...NEXT_PUBLIC_STRIPE_PUBLIC_KEYsk_...STRIPE_PRIVATE_KEY
  3. 在 Stripe Connect Settings 激活 Standard Accounts 的 OAuth;
  4. <WEBAPP_URL>/api/integrations/stripepayment/callback添加为 redirect URL;
  5. 复制客户端 IDca_...STRIPE_CLIENT_ID
  6. 在 Webhooks 页面添加<WEBAPP_URL>/api/integrations/stripepayment/webhook
  7. 为 Webhook 勾选全部payment_intent事件;
  8. 复制whsec_...STRIPE_WEBHOOK_SECRET

完成上述配置并重启服务后,即可在事件类型设置中启用「需要付款」,并为每个预约类型指定价格、币种、支付时机与退款/爽约策略。若你在社区版中遇到 Webhook 端点的 404 响应,请对照 apps/web/pages/api/integrations/stripepayment/webhook.ts 的现状确认版本行为,并以测试模式 + E2E 脚本先行验证支付主链路。

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 20:41:01

CANN/ge GE工具模块文档

GeUtils 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 20:40:29

Ricon组态系统在智能楼宇中的核心应用与优化

1. Ricon组态系统与智能楼宇的完美结合 第一次接触Ricon组态系统是在三年前的一个商业综合体项目中。当时业主方提出要实现整栋大楼的智能化管控&#xff0c;要求将空调、照明、安防等十几个子系统集成到一个平台上。经过多方对比&#xff0c;我们最终选择了Ricon组态系统作为核…

作者头像 李华
网站建设 2026/9/10 20:39:09

昇腾/ge自定义逻辑流分配Pass开发

使用自定义逻辑流分配Pass定制并发 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 P…

作者头像 李华