mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
RawQuery from GET callbacks can be double-URL-encoded by upstream proxies. Use Query().Encode() to rebuild from fully decoded params, ensuring VerifyNotification computes the signature on correct values.
116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/payment"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// PaymentWebhookHandler handles payment provider webhook callbacks.
|
|
type PaymentWebhookHandler struct {
|
|
paymentService *service.PaymentService
|
|
registry *payment.Registry
|
|
}
|
|
|
|
// maxWebhookBodySize is the maximum allowed webhook request body size (1 MB).
|
|
const maxWebhookBodySize = 1 << 20
|
|
|
|
// NewPaymentWebhookHandler creates a new PaymentWebhookHandler.
|
|
func NewPaymentWebhookHandler(paymentService *service.PaymentService, registry *payment.Registry) *PaymentWebhookHandler {
|
|
return &PaymentWebhookHandler{
|
|
paymentService: paymentService,
|
|
registry: registry,
|
|
}
|
|
}
|
|
|
|
// EasyPayNotify handles EasyPay payment notifications.
|
|
// POST /api/v1/payment/webhook/easypay
|
|
func (h *PaymentWebhookHandler) EasyPayNotify(c *gin.Context) {
|
|
h.handleNotify(c, "easypay")
|
|
}
|
|
|
|
// AlipayNotify handles Alipay payment notifications.
|
|
// POST /api/v1/payment/webhook/alipay
|
|
func (h *PaymentWebhookHandler) AlipayNotify(c *gin.Context) {
|
|
h.handleNotify(c, "alipay")
|
|
}
|
|
|
|
// WxpayNotify handles WeChat Pay payment notifications.
|
|
// POST /api/v1/payment/webhook/wxpay
|
|
func (h *PaymentWebhookHandler) WxpayNotify(c *gin.Context) {
|
|
h.handleNotify(c, "wxpay")
|
|
}
|
|
|
|
// StripeWebhook handles Stripe webhook events.
|
|
// POST /api/v1/payment/webhook/stripe
|
|
func (h *PaymentWebhookHandler) StripeWebhook(c *gin.Context) {
|
|
h.handleNotify(c, "stripe")
|
|
}
|
|
|
|
// handleNotify is the shared logic for all provider webhook handlers.
|
|
func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) {
|
|
var rawBody string
|
|
if c.Request.Method == http.MethodGet {
|
|
// GET callbacks (e.g. EasyPay): RawQuery may be double-encoded by
|
|
// upstream proxies, so rebuild from the already-decoded Query().
|
|
rawBody = c.Request.URL.Query().Encode()
|
|
} else {
|
|
body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxWebhookBodySize))
|
|
if err != nil {
|
|
slog.Error("[Payment Webhook] failed to read body", "provider", providerKey, "error", err)
|
|
c.String(http.StatusBadRequest, "failed to read body")
|
|
return
|
|
}
|
|
rawBody = string(body)
|
|
}
|
|
|
|
provider, err := h.registry.GetProviderByKey(providerKey)
|
|
if err != nil {
|
|
slog.Warn("[Payment Webhook] provider not registered", "provider", providerKey, "error", err)
|
|
c.String(http.StatusOK, successResponse(providerKey))
|
|
return
|
|
}
|
|
|
|
headers := make(map[string]string)
|
|
for k := range c.Request.Header {
|
|
headers[strings.ToLower(k)] = c.GetHeader(k)
|
|
}
|
|
|
|
notification, err := provider.VerifyNotification(c.Request.Context(), rawBody, headers)
|
|
if err != nil {
|
|
slog.Error("[Payment Webhook] verify failed", "provider", providerKey, "error", err, "method", c.Request.Method, "rawBody", rawBody)
|
|
c.String(http.StatusBadRequest, "verify failed")
|
|
return
|
|
}
|
|
|
|
// nil notification means irrelevant event (e.g. Stripe non-payment event); return success.
|
|
if notification == nil {
|
|
c.String(http.StatusOK, successResponse(providerKey))
|
|
return
|
|
}
|
|
|
|
if err := h.paymentService.HandlePaymentNotification(c.Request.Context(), notification, providerKey); err != nil {
|
|
slog.Error("[Payment Webhook] handle notification failed", "provider", providerKey, "error", err)
|
|
c.String(http.StatusInternalServerError, "handle failed")
|
|
return
|
|
}
|
|
|
|
c.String(http.StatusOK, successResponse(providerKey))
|
|
}
|
|
|
|
// successResponse returns the provider-specific success response string.
|
|
func successResponse(providerKey string) string {
|
|
switch providerKey {
|
|
case "stripe":
|
|
return ""
|
|
default:
|
|
return "success"
|
|
}
|
|
}
|