Jelajahi Sumber

feat: enhance Stripe webhook handling for async payment events

CaIon 3 minggu lalu
induk
melakukan
b2a40d3381
1 mengubah file dengan 70 tambahan dan 3 penghapusan
  1. 70 3
      controller/topup_stripe.go

+ 70 - 3
controller/topup_stripe.go

@@ -146,6 +146,12 @@ func RequestStripePay(c *gin.Context) {
 }
 }
 
 
 func StripeWebhook(c *gin.Context) {
 func StripeWebhook(c *gin.Context) {
+	if setting.StripeWebhookSecret == "" {
+		log.Println("Stripe Webhook Secret 未配置,拒绝处理")
+		c.AbortWithStatus(http.StatusForbidden)
+		return
+	}
+
 	payload, err := io.ReadAll(c.Request.Body)
 	payload, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 	if err != nil {
 		log.Printf("解析Stripe Webhook参数失败: %v\n", err)
 		log.Printf("解析Stripe Webhook参数失败: %v\n", err)
@@ -154,8 +160,7 @@ func StripeWebhook(c *gin.Context) {
 	}
 	}
 
 
 	signature := c.GetHeader("Stripe-Signature")
 	signature := c.GetHeader("Stripe-Signature")
-	endpointSecret := setting.StripeWebhookSecret
-	event, err := webhook.ConstructEventWithOptions(payload, signature, endpointSecret, webhook.ConstructEventOptions{
+	event, err := webhook.ConstructEventWithOptions(payload, signature, setting.StripeWebhookSecret, webhook.ConstructEventOptions{
 		IgnoreAPIVersionMismatch: true,
 		IgnoreAPIVersionMismatch: true,
 	})
 	})
 
 
@@ -170,6 +175,10 @@ func StripeWebhook(c *gin.Context) {
 		sessionCompleted(event)
 		sessionCompleted(event)
 	case stripe.EventTypeCheckoutSessionExpired:
 	case stripe.EventTypeCheckoutSessionExpired:
 		sessionExpired(event)
 		sessionExpired(event)
+	case stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded:
+		sessionAsyncPaymentSucceeded(event)
+	case stripe.EventTypeCheckoutSessionAsyncPaymentFailed:
+		sessionAsyncPaymentFailed(event)
 	default:
 	default:
 		log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
 		log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
 	}
 	}
@@ -186,7 +195,65 @@ func sessionCompleted(event stripe.Event) {
 		return
 		return
 	}
 	}
 
 
-	// Try complete subscription order first
+	paymentStatus := event.GetObjectValue("payment_status")
+	if paymentStatus != "paid" {
+		log.Printf("Stripe Checkout 支付尚未完成,payment_status: %s, ref: %s(等待异步支付结果)", paymentStatus, referenceId)
+		return
+	}
+
+	fulfillOrder(event, referenceId, customerId)
+}
+
+// sessionAsyncPaymentSucceeded handles delayed payment methods (bank transfer, SEPA, etc.)
+// that confirm payment after the checkout session completes.
+func sessionAsyncPaymentSucceeded(event stripe.Event) {
+	customerId := event.GetObjectValue("customer")
+	referenceId := event.GetObjectValue("client_reference_id")
+	log.Printf("Stripe 异步支付成功: %s", referenceId)
+
+	fulfillOrder(event, referenceId, customerId)
+}
+
+// sessionAsyncPaymentFailed marks orders as failed when delayed payment methods
+// ultimately fail (e.g. bank transfer not received, SEPA rejected).
+func sessionAsyncPaymentFailed(event stripe.Event) {
+	referenceId := event.GetObjectValue("client_reference_id")
+	log.Printf("Stripe 异步支付失败: %s", referenceId)
+
+	if len(referenceId) == 0 {
+		log.Println("异步支付失败事件未提供支付单号")
+		return
+	}
+
+	LockOrder(referenceId)
+	defer UnlockOrder(referenceId)
+
+	topUp := model.GetTopUpByTradeNo(referenceId)
+	if topUp == nil {
+		log.Println("异步支付失败,充值订单不存在:", referenceId)
+		return
+	}
+
+	if topUp.Status != common.TopUpStatusPending {
+		log.Printf("异步支付失败,订单状态非pending: %s, ref: %s", topUp.Status, referenceId)
+		return
+	}
+
+	topUp.Status = common.TopUpStatusFailed
+	if err := topUp.Update(); err != nil {
+		log.Printf("标记充值订单失败出错: %v, ref: %s", err, referenceId)
+		return
+	}
+	log.Printf("充值订单已标记为失败: %s", referenceId)
+}
+
+// fulfillOrder is the shared logic for crediting quota after payment is confirmed.
+func fulfillOrder(event stripe.Event, referenceId string, customerId string) {
+	if len(referenceId) == 0 {
+		log.Println("未提供支付单号")
+		return
+	}
+
 	LockOrder(referenceId)
 	LockOrder(referenceId)
 	defer UnlockOrder(referenceId)
 	defer UnlockOrder(referenceId)
 	payload := map[string]any{
 	payload := map[string]any{