拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

Cal.diy 集成 Stripe 支付从密钥配置、Connect OAuth 到预约收款与订阅扣费的完整实战指南【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diyCal.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.tsslug: stripe、type: stripe_payment在系统中以stripe_payment作为支付类型标识isOAuth: true该应用通过 Stripe Connect OAuth 完成账号授权而不是简单地填写 API KeyextendsFeature: EventType应用能力直接挂在「事件类型」上可在创建/编辑预约类型时启用按次收费installed字段由三个环境变量共同决定STRIPE_CLIENT_ID、NEXT_PUBLIC_STRIPE_PUBLIC_KEY、STRIPE_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 keypk_...用于浏览器端加载 Stripe.jsNEXT_PUBLIC_STRIPE_PUBLIC_KEY私有密钥Secret keysk_...用于服务端所有 API 调用STRIPE_PRIVATE_KEYConnect 客户端 IDca_...用于 OAuth 授权流程STRIPE_CLIENT_IDWebhook 签名密钥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-27const 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 用户名月付价格 IDNEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRODUCT_IDPremium 套餐产品 IDNEXT_PUBLIC_STRIPE_TEAM_MONTHLY_PRICE_ID团队按座位per-seat月付价格 IDSTRIPE_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 与 WebhookREADME 的第 38 步全部在 Stripe Dashboard 完成是连接 Cal.diy 与 Stripe 的关键环节。开启 Connect OAuthStandard Accounts进入 Stripe Connect Settings为Standard Accounts激活 OAuth。Standard 模式允许每个 Cal.diy 用户用自己的 Stripe 账户独立收款——平台自身持有STRIPE_PRIVATE_KEY而每个接入的用户通过 OAuth 获得独立的stripe_user_id交易在其名下结算。随后将以下地址登记为 OAuth redirect URLREADME 中写作CALENDSO URL占位符实际为部署实例的根地址即代码中的WEBAPP_URLWEBAPP_URL/api/integrations/stripepayment/callback从源码看这个回调端点有三处会生成跳转api/add.ts 使用client_id与scope: read_write、response_type: code构造https://connect.stripe.com/oauth/authorize?授权链接并将当前用户的邮箱、姓名预填进stripe_user参数pages/setup/_getServerSideProps.ts 在应用安装页Setup执行服务端重定向把returnTo、onErrorReturnTo、fromApp等信息编码进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/webhookREADME 要求为 Webhook选择所有payment_intent事件即payment_intent.succeeded、payment_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首项usdpaymentOptionON_BOOKING/HOLD支付时机见下文enabledboolean是否对该事件类型启用收费refundPolicyenum退款策略来自calcom/lib/payment/typesrefundDaysCountnumber退款天数窗口refundCountCalendarDaysboolean退款天数按自然日还是工作日计算autoChargeNoShowFeeIfCancelledboolean取消时是否自动收取爽约费autoChargeNoShowFeeTimeValue/autoChargeNoShowFeeTimeUnitnumber / enum爽约费计费窗口与单位minutes/hours/days其中paymentOption的合法取值定义在 lib/constants.tsexport 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_BOOKINGcreate()流程先通过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里写入了bookingId、calAccountId、bookerEmail等业务信息便于在 Stripe Dashboard 中反查订单来源。创建成功后模块会在本地Payment表中落一条记录externalId指向 PaymentIntent ID并保存stripe_publishable_key与stripeAccount供前端展示支付界面使用。collectCard chargeCard先留存后扣款HOLDHOLD 模式分为两步collectCard()创建 SetupIntent仅收集卡信息payment_method_types: [card]对应 Payment 记录以 SetupIntent ID 作为externalIdchargeCard()在需要扣款时如确认爽约先校验 Stripe Customer 与支付方式仍存在再创建off_session: true、confirm: true的 PaymentIntent 完成后台扣款成功后把 Payment 记录标记为success: true并合并 PaymentIntent 数据。chargeCard()对常见扣款失败做了用户友好化映射例如 your card was declined 会转换为内部错误码your_card_was_declined供前端展示本地化文案。refund 与 deletePaymentrefund()基于payment.externalIdPaymentIntent ID调用stripe.refunds.create()仅在支付成功且未退款时执行退款成功后更新本地记录refunded: truedeletePayment()用于预约被取消时清理先列出并过期所有关联的 Checkout Session再取消 PaymentIntent保证不会出现「预约已取消但待支付订单仍有效」的脏状态。支付链接与前端加载预约者付款时Cal.diy 会生成一个独立的支付落地页链接其构造逻辑在 lib/client/createPaymentLink.tsexport 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对应一个公开支付页 URLname、date、email作为查询参数预填。前端 Stripe.js 的加载则由 lib/client/getStripe.ts 完成它使用loadStripe并采用单例模式stripePromise只初始化一次避免在 SPA 中重复加载 SDK。订阅场景Premium 用户名与团队计费除事件类型收费外Stripe 还支撑平台级订阅业务。Premium 用户名订阅api/subscription.ts 处理 Premium 用户名购买校验用户已存在 Stripe Customer 后创建mode: subscription的 Checkout Sessionline_items引用getPremiumMonthlyPlanPriceId()返回的价格 ID并开启allow_promotion_codes。Session 的success_url与cancel_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 个步骤整理一份最终核对表创建/复用 Stripe 账户测试阶段开启 Test-Mode从 API Keys 页面复制pk_...→NEXT_PUBLIC_STRIPE_PUBLIC_KEYsk_...→STRIPE_PRIVATE_KEY在 Stripe Connect Settings 激活 Standard Accounts 的 OAuth将WEBAPP_URL/api/integrations/stripepayment/callback添加为 redirect URL复制客户端 IDca_...→STRIPE_CLIENT_ID在 Webhooks 页面添加WEBAPP_URL/api/integrations/stripepayment/webhook为 Webhook 勾选全部payment_intent事件复制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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门