From 9b38e44e1d084b93ada34bc29583983e430dc7d9 Mon Sep 17 00:00:00 2001 From: erio Date: Fri, 10 Apr 2026 14:27:44 +0800 Subject: [PATCH] fix(payment): restore double URL decode for EasyPay webhook values EasyPay callbacks arrive with double-encoded query values (e.g. %25E5 instead of %E5) due to redirect chains. url.ParseQuery decodes once; decodeURLValue applies a second safe decode so the sign matches what EasyPay computed. Also removes temporary debug logging. --- backend/internal/handler/payment_webhook_handler.go | 3 ++- backend/internal/payment/provider/easypay.go | 9 ++++----- backend/internal/service/payment_service.go | 7 +------ 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/backend/internal/handler/payment_webhook_handler.go b/backend/internal/handler/payment_webhook_handler.go index b923a2a9a2..6d8540d80b 100644 --- a/backend/internal/handler/payment_webhook_handler.go +++ b/backend/internal/handler/payment_webhook_handler.go @@ -95,7 +95,8 @@ func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) if len(truncatedBody) > webhookLogTruncateLen { truncatedBody = truncatedBody[:webhookLogTruncateLen] + "...(truncated)" } - slog.Error("[Payment Webhook] verify failed", "provider", providerKey, "error", err, "method", c.Request.Method, "bodyLen", len(rawBody), "rawBody", truncatedBody) + slog.Error("[Payment Webhook] verify failed", "provider", providerKey, "error", err, "method", c.Request.Method, "bodyLen", len(rawBody)) + slog.Debug("[Payment Webhook] verify failed body", "provider", providerKey, "rawBody", truncatedBody) c.String(http.StatusBadRequest, "verify failed") return } diff --git a/backend/internal/payment/provider/easypay.go b/backend/internal/payment/provider/easypay.go index a203aaf615..5d3c4b8bfb 100644 --- a/backend/internal/payment/provider/easypay.go +++ b/backend/internal/payment/provider/easypay.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "io" - "log/slog" "net/http" "net/url" "sort" @@ -177,18 +176,18 @@ func (e *EasyPay) VerifyNotification(_ context.Context, rawBody string, _ map[st if err != nil { return nil, fmt.Errorf("parse notify: %w", err) } - // url.ParseQuery already decodes values — no additional decode needed. + // EasyPay callbacks may arrive with double-encoded values (e.g. via redirect + // chains), so url.ParseQuery decodes once, then decodeURLValue decodes again. + // If a value is NOT double-encoded, QueryUnescape returns it unchanged. params := make(map[string]string) for k := range values { - params[k] = values.Get(k) + params[k] = decodeURLValue(values.Get(k)) } sign := params["sign"] if sign == "" { return nil, fmt.Errorf("missing sign") } if !easyPayVerifySign(params, e.config["pkey"], sign) { - expected := easyPaySign(params, e.config["pkey"]) - slog.Error("[EasyPay] sign mismatch", "expected", expected, "got", sign, "pkeyLen", len(e.config["pkey"]), "instanceID", e.instanceID, "paramCount", len(params)) return nil, fmt.Errorf("invalid signature") } status := payment.ProviderStatusFailed diff --git a/backend/internal/service/payment_service.go b/backend/internal/service/payment_service.go index cb78111790..25ec15f748 100644 --- a/backend/internal/service/payment_service.go +++ b/backend/internal/service/payment_service.go @@ -229,15 +229,10 @@ func (s *PaymentService) GetWebhookProvider(ctx context.Context, providerKey, ou if err == nil { p, pErr := s.getOrderProvider(ctx, order) if pErr == nil { - slog.Info("[Webhook] using order's provider instance", "outTradeNo", outTradeNo, "instanceID", order.ProviderInstanceID, "providerKey", p.ProviderKey()) return p, nil } - slog.Warn("[Webhook] order provider creation failed, falling back to registry", "outTradeNo", outTradeNo, "instanceID", order.ProviderInstanceID, "error", pErr) - } else { - slog.Warn("[Webhook] order lookup failed", "outTradeNo", outTradeNo, "error", err) + slog.Warn("[Webhook] order provider creation failed, falling back to registry", "outTradeNo", outTradeNo, "error", pErr) } - } else { - slog.Info("[Webhook] no out_trade_no extracted, using registry", "providerKey", providerKey) } s.EnsureProviders(ctx) return s.registry.GetProviderByKey(providerKey)