关于Stripe Checkout.Session.Completed事件触发条件的技术咨询
核心问题解答
当仅接受信用卡支付时,Checkout.Session.Completed事件确实只会在支付成功后触发。用户输入卡号点击「支付」后,Stripe会完成信用卡授权/扣费流程,只有确认支付成功(包括3D验证通过、即时扣费成功等场景),才会向你的WebHook端点发送该事件。支付失败(如卡号无效、余额不足、3D验证未通过)时,不会触发此事件,用户会留在Checkout页面看到错误提示,不会进入完成流程。
代码实现的优化建议
你给出的WebHook处理逻辑方向正确,这里提供几个实际优化点:
增加支付状态二次校验:
虽然事件触发时支付已成功,但极端场景下(比如Stripe内部事件重试的边缘情况),建议从事件中取出Session对象,确认payment_status为paid后再执行业务逻辑,避免意外:var session = stripeEvent.Data.Object as Session; if (session?.PaymentStatus == "paid") { // 执行保存CustomerId、升级套餐等操作 }区分异常类型:
不要直接捕获所有异常,Stripe的EventUtility.ConstructEvent会抛出SignatureVerificationException,单独捕获该异常返回400,其他异常返回500。这样Stripe会自动重试服务器错误(500)导致的失败事件,而签名错误(400)不会重试:catch (SignatureVerificationException sigEx) { return BadRequest("无效签名"); } catch (Exception ex) { // 记录异常日志 return StatusCode(500); }避免硬编码订阅周期:
在InvoicePaid事件中,不要固定设置32天有效期,建议从发票关联的订阅对象中获取实际计费周期:var invoice = stripeEvent.Data.Object as Invoice; var subscription = await _stripe.Subscriptions.GetAsync(invoice.SubscriptionId); var nextBillingDate = subscription.CurrentPeriodEnd; // 用nextBillingDate更新用户有效期保存订阅ID:
如果高级套餐是订阅模式(非单次付款),Checkout.Session.Completed事件的Session对象会包含subscription字段,建议同时保存该ID,后续关联发票、订阅变更事件会更便捷。
优化后的完整代码示例
[HttpPost("webhook")] [AllowAnonymous] public async Task<ActionResult> WebHook([FromServices] IOptions<StripeKeys> stripeOptions, [FromServices] StripeService stripeService) { var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], stripeOptions.Value.WebHookSecret); switch (stripeEvent.Type) { case Events.CheckoutSessionCompleted: { var session = stripeEvent.Data.Object as Session; if (session?.PaymentStatus == "paid") { // 保存CustomerId和SubscriptionId到数据库 // 将用户套餐升级为高级版 // 发送支付成功邮件 } break; } case Events.InvoicePaid: { var invoice = stripeEvent.Data.Object as Invoice; var subscription = await stripeService.Subscriptions.GetAsync(invoice.SubscriptionId); // 根据订阅的CurrentPeriodEnd更新用户有效期 // 发送收据邮件 break; } case Events.InvoicePaymentFailed: { // 将用户套餐降级为基础版 // 发送支付失败提醒邮件 break; } case Events.InvoiceUpcoming: { // 发送续费提醒邮件 break; } case Events.CustomerSubscriptionDeleted: { // 将用户套餐降级为基础版 // 发送订阅取消邮件 break; } default: break; } return Ok(); } catch (SignatureVerificationException) { return BadRequest("无效的WebHook签名"); } catch (Exception ex) { // 记录异常日志 return StatusCode(500); } }
内容的提问来源于stack exchange,提问作者MarsS

