InsForge Razorpay 支付接入指南:订单、订阅、Webhook 与履约触发器实战
2026/9/15 17:03:33 网站建设 项目流程

InsForge Razorpay 支付接入指南:订单、订阅、Webhook 与履约触发器实战

【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge

导读

本篇技术指南以 InsForge 开源后端平台的 Razorpay 支付模块为核心,系统讲解如何在该平台上接入印度主流支付网关 Razorpay:从一次性订单(Razorpay Checkout)到订阅(Plans/Subscriptions)、再到手工 Webhook 配置与基于 PostgreSQL 触发器的履约(Fulfillment)链路。读完本文,你将掌握 InsForge Payments API 中 Razorpay 的完整调用方式、签名验证原理、RLS 权限模型,以及如何用一条安全的 SQL 触发器把"已支付"变成"已履约"。

一、Razorpay 在 InsForge 中的适用边界

Razorpay 与 Stripe 是两套并行的支付提供商,InsForge 的 Payments 模块为二者提供了独立但同构的 API 表面。在 Razorpay 流程中,你应当使用:

  • Razorpay Orders + Razorpay Checkout:处理一次性付款;
  • Razorpay Subscriptions + Checkout 授权:处理周期性订阅;
  • Razorpay Items 与 Plans:管理商品目录与订阅计划;
  • 手动配置的 Razorpay Webhook:接收支付、退款、订阅状态事件;
  • 后端路由:完成订阅的取消(cancel)、暂停(pause)、恢复(resume)。

需要特别注意的是:不要在 Razorpay 流程中使用 Stripe Checkout、Stripe Prices 或 Billing Portal 概念。Razorpay Checkout 是在应用内通过checkout.js运行的,它不会返回一个托管式 Checkout URL——这一点与 Stripe 的托管支付页有本质区别,集成时最容易踩坑。

二、动手前的前置检查

按照 InsForge 的 Agent 文档要求,接入 Razorpay 前必须逐条确认以下六项:

  1. 默认使用environment: "test",除非用户明确批准切换 live 环境;
  2. 确认目标环境(test/live)的Key ID 和 Key Secret 都已配置
  3. 订阅流程要求Items 和 Plans 已存在于同一环境
  4. Webhook 必须在 Razorpay Dashboard 手工配置,并指向公网 HTTPS URL;
  5. Checkout 回调校验只代表"客户端回跳是真实的",持久化履约必须依赖 Webhook;
  6. 一次性商品优先创建 Razorpay Items(Orders 虽可只传金额,但 Items 能在同步后保持目录可见;Orders 本质只是支付尝试记录)。

底层支撑:这些前置校验不是文档口号,而是被源码强制的。在 razorpay.provider.ts 中,validateRazorpayKey会校验 Key ID 必须以rzp_test_(test 环境)或rzp_live_(live 环境)开头,否则直接抛出RazorpayKeyValidationError;在 config.service.ts 中,setRazorpayKeys会调用retrieveAccount()发起一次真实的 Orders 探测请求来验证密钥有效性,并把密钥以加密形式写入system.secrets,同时把连接状态快照写入payments.provider_connections。如果更换了 Key 指向的 Razorpay 账号(provider_account_id发生变化),平台会自动清理该环境下的历史支付数据,避免数据串号。

三、Webhook 手工配置

Razorpay 的 Webhook 是手动管理的,InsForge 不会自动注册 endpoint。你需要在Razorpay Dashboard → Payments → Settings → Webhooks中生成/查看 Webhook URL 与 Secret。

平台为管理员提供了两个 Webhook 运维路由:

GET /api/payments/razorpay/test/webhook POST /api/payments/razorpay/test/webhook/rotate-secret

GET .../webhook返回该环境的 webhook URL 与 Secret(若未设置会自动生成 32 字节随机 token 并加密存储);POST .../webhook/rotate-secret则强制轮换 Secret。这两个路由实现在 config.routes.ts 中,webhook URL 的形态由getWebhookUrl拼装:${API_BASE_URL}/api/webhooks/razorpay/{environment}(见 config.service.ts),环境名只能是testlive

推荐启用的事件清单

以下 19 个事件是 InsForge 的 Webhook 处理器实际会消费的事件(未在清单内的事件会被标记为ignored)。请在 Dashboard 中勾选订阅:

类别事件
支付payment.authorizedpayment.capturedpayment.failed
订单order.paid
发票invoice.paidinvoice.expired
退款refund.createdrefund.processedrefund.failed
订阅subscription.createdsubscription.activatedsubscription.chargedsubscription.updatedsubscription.cancelledsubscription.pausedsubscription.resumedsubscription.haltedsubscription.completedsubscription.expired

Razorpay 只能向公网 HTTPS URL 投递 Webhook。

Webhook 接收端的实现原理

InsForge 的 Webhook 接收端位于/api/webhooks/razorpay/:environment(razorpay.routes.ts),处理链路在 webhook.service.ts:

  1. 读取x-razorpay-signature请求头,缺失直接 401;
  2. 使用 HMAC-SHA256 对原始请求体字节Buffer)计算签名并与头值做timingSafeEqual恒时比较(见 razorpay.provider.ts)——注意必须对未解码的原始字节做哈希,任何 JSON 重序列化都会导致验签失败;
  3. 事件以provider_event_id(优先使用x-razorpay-event-id头,否则由account_id.event.entity_id.created_at拼装)作为幂等键,先记录payments.webhook_eventspending状态,重复事件直接跳过,保证 at-least-once 投递下的幂等性;
  4. 按事件类型分发到 payment/refund/subscription/invoice/order 处理器,统一写入payments.transactions镜像表,并把订单/订阅状态同步到各自的 provider 镜像表。

四、一次性订单:从创建到验证

4.1 创建 Razorpay Order

正确姿势是:先在应用侧创建一笔"待支付"的内部订单(pending order),再通过 provider-scoped SDK 创建 Razorpay Order

const { data, error } = await insforge.payments.razorpay.createOrder('test', { amount: 50000, // 最小货币单位(INR 的 paise),这里是 ₹500.00 currency: 'INR', receipt: 'order_123', subject: { type: 'team', id: 'team_123' }, // 账单归属主体 customerEmail: 'buyer@example.com', notes: { order_id: 'order_123' } // 履约触发器依赖此键 }); if (error) throw error;

如果履约触发器读取notes.order_id,创建 Order(或 Subscription)时必须传入notes: { order_id: ... }

从源码看,这一流程比表面上更严谨:order.service.ts 的createOrder会先在payments.razorpay_orders插入一条status='initialized'的记录(同时生成 receipt),再把 InsForge 内部记录 ID 以保留键insforge_order_id写入 notes 一并传给 Razorpay;Razorpay 创建成功后回写order_id/amount/status;若调用失败,该内部记录会被标记为failed并记录last_error,而不是静默消失。这保证了应用侧始终有一份可对账的订单记录。

4.2 打开 Checkout 并验证回调

在前端用data.checkoutOptions打开 Razorpay Checkout。Checkout 回调会返回三个值:razorpay_order_idrazorpay_payment_idrazorpay_signature。随后通过 SDK 验证:

await insforge.payments.razorpay.verifyOrder('test', { orderId: response.razorpay_order_id, paymentId: response.razorpay_payment_id, signature: response.razorpay_signature });

验证的底层逻辑是:对"${orderId}|${paymentId}"字符串用该环境的 Key Secret 做 HMAC-SHA256,与签名做恒时比较(见 razorpay.provider.ts 与verifyCheckoutSignature)。验证通过后,内部订单状态会被更新为attempted(若已是paid则保持),并记录verified_payment_idverified_at(见 order.service.ts)。

关键认知:验证成功只证明"客户端回跳是真实的、签名有效",不能据此把订单标记为已支付或授予访问权限。持久的履约必须来自经过验证的 Razorpay Webhook 事件。

五、订阅:Plans、创建与管理

5.1 Plan 是订阅的基础

Razorpay 订阅使用Plans(不是 Stripe 的 Prices)。一个 Plan 是围绕一个 Razorpay Item 的周期性定价定义,字段包含perioddaily/weekly/monthly/yearly)、interval以及内嵌的 item(名称、金额、币种)。Plans 与 Items 由RazorpayCatalogService管理(catalog.service.ts),创建 Plan 时会同步落库到payments.razorpay_plans,同时把 Plan 内嵌的 item 同步到payments.razorpay_items,并通过 advisory lock(payments_razorpay_environment_{env})防止并发目录操作互相覆盖。

5.2 创建订阅

先创建/同步 Plan,再通过 provider-scoped SDK 创建订阅:

const { data, error } = await insforge.payments.razorpay.createSubscription('test', { planId: 'plan_123', totalCount: 12, // 总扣款周期数 subject: { type: 'team', id: 'team_123' }, customerEmail: 'buyer@example.com' }); if (error) throw error;

创建成功后,前端用data.checkoutOptions.subscription_id打开 Checkout;回调拿到订阅支付签名后验证:

await insforge.payments.razorpay.verifySubscription('test', { subscriptionId: response.razorpay_subscription_id, paymentId: response.razorpay_payment_id, signature: response.razorpay_signature });

注意订阅签名的拼接顺序与订单不同:订阅是对"${paymentId}|${subscriptionId}"做 HMAC(见 razorpay.provider.ts),混用订单的orderId|paymentId顺序必然验签失败。验证通过后订阅状态由created变为authenticated,并记录authorization_payment_id

5.3 订阅生命周期管理

// 取消:cancelAtCycleEnd=false 表示立即取消 await insforge.payments.razorpay.cancelSubscription('test', 'sub_123', { cancelAtCycleEnd: false }); // 暂停 / 恢复 await insforge.payments.razorpay.pauseSubscription('test', 'sub_123'); await insforge.payments.razorpay.resumeSubscription('test', 'sub_123');

5.4 底层权限模型:RLS 探测

订阅相关操作不是"谁都能干"的。源码在 subscription.service.ts 中实现了两层防护:

  • 创建订阅会先以当前用户上下文执行一次INSERT探测(写入一条sub_rls_probe_*的临时订阅记录),借由payments.razorpay_subscriptions表上的 RLS 策略判断该用户是否有权为这个 billing subject 建订阅,随后ROLLBACK TO SAVEPOINT回滚探测写入;
  • 取消/暂停/恢复会先执行UPDATE ... RETURNING subject_type, subject_id探测,验证用户对该订阅有UPDATE权限,并顺带取回账单归属主体(subject)用于回写镜像记录;
  • PostgreSQL 还会对INSERT/UPDATE ... RETURNING返回的行施加SELECT策略,因此当策略探测需要返回行时,必须为同一 billing subject 配置匹配的SELECT可见性。

同时,不要让用户提交任意的 subject——应用必须自行校验当前用户确实能管理该账单主体,否则任何人都可能给别人的 team 开订阅。这正对应"常见故障"表中的最后一行:User can start a subscription for another team → Add RLS or server-side membership checks

六、履约(Fulfillment):用 Webhook 驱动业务动作

履约是支付接入最核心的业务环节,原则只有一条:Checkout 回调验证不能作为标记已支付/授予访问的依据,必须以验证过的 Webhook 事件为准。同时,不要把履约触发器挂到payments.razorpay_subscriptions这类 provider 镜像表上——镜像表由平台写入,业务逻辑应挂在自己应用的表或payments.webhook_events上。

6.1 一次性订单履约触发器

以下触发器监听payments.webhook_events,当 Razorpay 的payment.capturedorder.paidinvoice.paid事件处理完成(processing_status='processed')时,从 payload 的 notes 中解析order_id,把应用侧public.orders中对应pending订单置为paid

CREATE OR REPLACE FUNCTION public.fulfill_razorpay_order() RETURNS TRIGGER AS $$ BEGIN IF NEW.provider = 'razorpay' AND NEW.event_type IN ('payment.captured', 'order.paid', 'invoice.paid') AND NEW.processing_status = 'processed' AND COALESCE( NEW.payload -> 'payload' -> 'payment' -> 'entity' -> 'notes' ->> 'order_id', NEW.payload -> 'payload' -> 'invoice' -> 'entity' -> 'notes' ->> 'order_id' ) IS NOT NULL THEN UPDATE public.orders SET status = 'paid', paid_at = COALESCE(NEW.processed_at, NOW()) WHERE id::text = COALESCE( NEW.payload -> 'payload' -> 'payment' -> 'entity' -> 'notes' ->> 'order_id', NEW.payload -> 'payload' -> 'invoice' -> 'entity' -> 'notes' ->> 'order_id' ) AND status = 'pending'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER fulfill_razorpay_order_from_webhook AFTER INSERT OR UPDATE ON payments.webhook_events FOR EACH ROW EXECUTE FUNCTION public.fulfill_razorpay_order();

6.2 订阅履约与撤销

订阅场景需要从订阅实体的 notes 中解析账单归属主体。InsForge 在创建订阅时会把insforge_subject_typeinsforge_subject_id写入 notes,并同时创建payments.customer_mappings,因此客户映射(customer mapping)也是安全的兜底方案:

CREATE OR REPLACE FUNCTION public.grant_razorpay_subscription_access() RETURNS TRIGGER AS $$ DECLARE v_subject_type TEXT; v_subject_id TEXT; BEGIN IF NEW.provider = 'razorpay' AND NEW.event_type = 'subscription.charged' AND NEW.processing_status = 'processed' THEN v_subject_type := NEW.payload -> 'payload' -> 'subscription' -> 'entity' -> 'notes' ->> 'insforge_subject_type'; v_subject_id := NEW.payload -> 'payload' -> 'subscription' -> 'entity' -> 'notes' ->> 'insforge_subject_id'; IF v_subject_id IS NULL THEN SELECT m.subject_type, m.subject_id INTO v_subject_type, v_subject_id FROM payments.customer_mappings m WHERE m.provider = NEW.provider AND m.environment = NEW.environment AND m.provider_customer_id = NEW.payload -> 'payload' -> 'subscription' -> 'entity' ->> 'customer_id'; END IF; IF v_subject_id IS NULL THEN RAISE WARNING 'Razorpay event % has no resolvable billing subject', NEW.provider_event_id; RETURN NEW; END IF; -- Branch on the subject type sent at creation; team_id is a UUID here, -- so the type check also guards the cast. IF v_subject_type = 'team' THEN INSERT INTO public.team_entitlements (team_id, plan, active, updated_at) VALUES (v_subject_id::uuid, 'pro', true, NOW()) ON CONFLICT (team_id) DO UPDATE SET plan = EXCLUDED.plan, active = true, updated_at = NOW(); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER grant_razorpay_subscription_access_from_webhook AFTER INSERT OR UPDATE ON payments.webhook_events FOR EACH ROW EXECUTE FUNCTION public.grant_razorpay_subscription_access();

撤销访问用同样的方式从subscription.cancelledsubscription.haltedsubscription.expired事件处理(把active置为 false 或按业务删除权益)。

最后,请按应用自身 schema 和事件形态调整 payload 路径,为应用自有的账单表启用 RLS,payments.transactions只用于 Dashboard 与报表展示,不要把它当作业务状态源。

七、安全清单

  • 对共享主体(shared subject)暴露订单/订阅流程前,必须添加 RLS 或服务端成员关系校验;
  • 考虑为payments.razorpay_orderspayments.razorpay_subscriptions启用 RLS;
  • 不要payments.customerspayments.transactionspayments.razorpay_subscriptions直接暴露给终端用户;
  • 不要直接写 provider 托管的支付表,一切写入走 Payments API、Razorpay Webhook 或应用自有触发器的目标表;
  • notes 中insforge_前缀的键是保留键(如insforge_subject_typeinsforge_subject_idinsforge_order_id),应用侧不可占用。

八、调试:五条诊断 SQL

怀疑支付链路出问题时,按顺序检查这几张表:

最近的 Razorpay 订单尝试:

SELECT id, environment, status, subject_type, subject_id, order_id, receipt, amount, currency, verified_payment_id, last_error, created_at, updated_at FROM payments.razorpay_orders ORDER BY created_at DESC LIMIT 20;

最近的 Razorpay 订阅:

SELECT environment, subscription_id, plan_id, customer_id, status, subject_type, subject_id, authorization_payment_id, current_start, current_end, created_at, updated_at FROM payments.razorpay_subscriptions ORDER BY created_at DESC LIMIT 20;

客户映射(确认 subject ↔ customer 关联):

SELECT provider, environment, subject_type, subject_id, provider_customer_id, created_at, updated_at FROM payments.customer_mappings WHERE provider = 'razorpay' ORDER BY updated_at DESC LIMIT 20;

Razorpay 交易流水(报表/对账用):

SELECT provider, environment, type, status, subject_type, subject_id, provider_object_type, provider_object_id, amount, currency, paid_at, failed_at, refunded_at, created_at FROM payments.transactions WHERE provider = 'razorpay' ORDER BY created_at DESC LIMIT 20;

Webhook 失败与待处理事件(排查履约不生效的第一站):

SELECT provider, environment, provider_event_id, event_type, processing_status, attempt_count, last_error, received_at, processed_at FROM payments.webhook_events WHERE provider = 'razorpay' AND processing_status IN ('failed', 'pending') ORDER BY received_at DESC LIMIT 20;

九、常见故障排查表

症状排查方向
订单创建失败确认该环境的 Key ID / Key Secret 已配置,且金额是最小货币单位(如 INR 的 paise)
Checkout 打不开确认https://checkout.razorpay.com/v1/checkout.js已加载,且checkoutOptions.key存在
签名验证失败必须原样传入 Razorpay Checkout 返回的 order/subscription ID、payment ID 与 signature,注意订单与订阅的签名拼接顺序不同
Webhook 签名无效确认 Razorpay Dashboard 的 webhook secret 与同一test/live环境的配置一致,且 URL 以/api/webhooks/razorpay/{environment}结尾
订阅创建失败确认 Plan 存在于同一 Razorpay 环境且关联了有效的 Item
Razorpay 已扣款但 InsForge 无记录检查手工 Webhook 配置是否完成,并查询payments.webhook_events中该事件的状态
用户能给别的 team 开订阅为账单主体补充 RLS 或服务端成员关系校验

十、小结

InsForge 的 Razorpay 集成可以概括为一条完整链路:配置密钥(system.secrets+ 连接快照)→ 目录同步(Items/Plans)→ 创建内部订单/订阅并打上insforge_*notes → 前端 Checkout → 回调验签(仅证明回跳真实)→ 手工 Webhook(HMAC 验签 + 幂等落库)→ 触发器履约(订单/订阅)→ 诊断 SQL 兜底。把握住"Checkout 回调只验证、Webhook 才履约"这一原则,再结合 RLS 权限探测与调试 SQL,就能在生产环境中稳定、安全地运行 Razorpay 支付。

相关源码可进一步研读:razorpay.provider.ts(底层 SDK 封装与签名)、order.service.ts 与 subscription.service.ts(业务流程与 RLS 探测)、webhook.service.ts(事件分发与幂等)、config.service.ts(密钥与 Webhook 配置)、catalog.service.ts(Items/Plans 同步)。

【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询