From 9af4a53fe9db17cc97a4e8cdd3c53254c00db419 Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 5 Apr 2026 22:25:51 +0800 Subject: [PATCH] fix: structured errors, slog logging, snake_case JSON tags - Registry uses infraerrors.NotFound instead of fmt.Errorf - Webhook handler uses slog instead of log.Printf - PaymentConfigService JSON tags unified to snake_case - PaymentOrderExpiryService uses slog - Remove duplicate declarations in registry.go --- .../handler/payment_webhook_handler.go | 10 ++--- backend/internal/payment/registry.go | 17 +++++--- .../service/payment_config_service.go | 42 +++++++++---------- .../service/payment_order_expiry_service.go | 6 +-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/backend/internal/handler/payment_webhook_handler.go b/backend/internal/handler/payment_webhook_handler.go index 66eae259ad..09650081b7 100644 --- a/backend/internal/handler/payment_webhook_handler.go +++ b/backend/internal/handler/payment_webhook_handler.go @@ -2,7 +2,7 @@ package handler import ( "io" - "log" + "log/slog" "net/http" "github.com/Wei-Shaw/sub2api/internal/payment" @@ -53,14 +53,14 @@ func (h *PaymentWebhookHandler) StripeWebhook(c *gin.Context) { func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) // 1MB limit if err != nil { - log.Printf("[Payment Webhook] failed to read body for %s: %v", providerKey, err) + slog.Error("[Payment Webhook] failed to read body", "provider", providerKey, "error", err) c.String(http.StatusBadRequest, "failed to read body") return } provider, err := h.registry.GetProviderByKey(providerKey) if err != nil { - log.Printf("[Payment Webhook] provider %s not registered: %v", providerKey, err) + slog.Warn("[Payment Webhook] provider not registered", "provider", providerKey, "error", err) c.String(http.StatusOK, successResponse(providerKey)) return } @@ -72,7 +72,7 @@ func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) notification, err := provider.VerifyNotification(c.Request.Context(), string(body), headers) if err != nil { - log.Printf("[Payment Webhook] %s verify failed: %v", providerKey, err) + slog.Error("[Payment Webhook] verify failed", "provider", providerKey, "error", err) c.String(http.StatusBadRequest, "verify failed") return } @@ -84,7 +84,7 @@ func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) } if err := h.paymentService.HandlePaymentNotification(c.Request.Context(), notification, providerKey); err != nil { - log.Printf("[Payment Webhook] %s handle notification failed: %v", providerKey, err) + slog.Error("[Payment Webhook] handle notification failed", "provider", providerKey, "error", err) c.String(http.StatusInternalServerError, "handle failed") return } diff --git a/backend/internal/payment/registry.go b/backend/internal/payment/registry.go index 9c27d6d619..3a2ba427fa 100644 --- a/backend/internal/payment/registry.go +++ b/backend/internal/payment/registry.go @@ -1,8 +1,9 @@ package payment import ( - "fmt" "sync" + + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) // Registry is a thread-safe registry mapping PaymentType to Provider. @@ -11,6 +12,10 @@ type Registry struct { providers map[PaymentType]Provider } +// ErrProviderNotFound is returned when a requested payment provider is not registered. +var ErrProviderNotFound = infraerrors.NotFound("PROVIDER_NOT_FOUND", "payment provider not registered") + + // NewRegistry creates a new empty provider registry. func NewRegistry() *Registry { return &Registry{ @@ -34,7 +39,7 @@ func (r *Registry) GetProvider(t PaymentType) (Provider, error) { defer r.mu.RUnlock() p, ok := r.providers[t] if !ok { - return nil, fmt.Errorf("no payment provider registered for type: %s", t) + return nil, ErrProviderNotFound } return p, nil } @@ -51,7 +56,7 @@ func (r *Registry) GetProviderByKey(key string) (Provider, error) { } seen[k] = true } - return nil, fmt.Errorf("no payment provider registered with key: %s", key) + return nil, ErrProviderNotFound } // GetProviderKey returns the provider key for the given payment type, @@ -81,7 +86,7 @@ func (r *Registry) SupportedTypes() []PaymentType { func (r *Registry) Clear() { r.mu.Lock() defer r.mu.Unlock() - for k := range r.providers { - delete(r.providers, k) - } + r.providers = make(map[PaymentType]Provider) } + +// Clear removes all registered providers. diff --git a/backend/internal/service/payment_config_service.go b/backend/internal/service/payment_config_service.go index abb8d4e176..d617563b49 100644 --- a/backend/internal/service/payment_config_service.go +++ b/backend/internal/service/payment_config_service.go @@ -37,38 +37,38 @@ const ( // PaymentConfig holds the payment system configuration. type PaymentConfig struct { Enabled bool `json:"enabled"` - MinAmount float64 `json:"minAmount"` - MaxAmount float64 `json:"maxAmount"` - DailyLimit float64 `json:"dailyLimit"` - OrderTimeoutMin int `json:"orderTimeoutMinutes"` - MaxPendingOrders int `json:"maxPendingOrders"` - EnabledTypes []string `json:"enabledTypes"` - BalanceDisabled bool `json:"balanceDisabled"` - LoadBalanceStrategy string `json:"loadBalanceStrategy"` - ProductNamePrefix string `json:"productNamePrefix"` - ProductNameSuffix string `json:"productNameSuffix"` + MinAmount float64 `json:"min_amount"` + MaxAmount float64 `json:"max_amount"` + DailyLimit float64 `json:"daily_limit"` + OrderTimeoutMin int `json:"order_timeout_minutes"` + MaxPendingOrders int `json:"max_pending_orders"` + EnabledTypes []string `json:"enabled_payment_types"` + BalanceDisabled bool `json:"balance_disabled"` + LoadBalanceStrategy string `json:"load_balance_strategy"` + ProductNamePrefix string `json:"product_name_prefix"` + ProductNameSuffix string `json:"product_name_suffix"` } // UpdatePaymentConfigRequest contains fields to update payment configuration. type UpdatePaymentConfigRequest struct { Enabled *bool `json:"enabled"` - MinAmount *float64 `json:"minAmount"` - MaxAmount *float64 `json:"maxAmount"` - DailyLimit *float64 `json:"dailyLimit"` - OrderTimeoutMin *int `json:"orderTimeoutMinutes"` - MaxPendingOrders *int `json:"maxPendingOrders"` - EnabledTypes []string `json:"enabledTypes"` - BalanceDisabled *bool `json:"balanceDisabled"` - LoadBalanceStrategy *string `json:"loadBalanceStrategy"` - ProductNamePrefix *string `json:"productNamePrefix"` - ProductNameSuffix *string `json:"productNameSuffix"` + MinAmount *float64 `json:"min_amount"` + MaxAmount *float64 `json:"max_amount"` + DailyLimit *float64 `json:"daily_limit"` + OrderTimeoutMin *int `json:"order_timeout_minutes"` + MaxPendingOrders *int `json:"max_pending_orders"` + EnabledTypes []string `json:"enabled_payment_types"` + BalanceDisabled *bool `json:"balance_disabled"` + LoadBalanceStrategy *string `json:"load_balance_strategy"` + ProductNamePrefix *string `json:"product_name_prefix"` + ProductNameSuffix *string `json:"product_name_suffix"` } // MethodLimits holds per-payment-type limits. type MethodLimits struct { PaymentType string `json:"paymentType"` FeeRate float64 `json:"feeRate"` - DailyLimit float64 `json:"dailyLimit"` + DailyLimit float64 `json:"daily_limit"` SingleMin float64 `json:"singleMin"` SingleMax float64 `json:"singleMax"` } diff --git a/backend/internal/service/payment_order_expiry_service.go b/backend/internal/service/payment_order_expiry_service.go index 2e0001b042..850d8f737d 100644 --- a/backend/internal/service/payment_order_expiry_service.go +++ b/backend/internal/service/payment_order_expiry_service.go @@ -2,7 +2,7 @@ package service import ( "context" - "log" + "log/slog" "sync" "time" ) @@ -62,10 +62,10 @@ func (s *PaymentOrderExpiryService) runOnce() { expired, err := s.paymentSvc.ExpireTimedOutOrders(ctx) if err != nil { - log.Printf("[PaymentOrderExpiry] failed to expire orders: %v", err) + slog.Error("[PaymentOrderExpiry] failed to expire orders", "error", err) return } if expired > 0 { - log.Printf("[PaymentOrderExpiry] expired %d timed-out orders", expired) + slog.Info("[PaymentOrderExpiry] expired timed-out orders", "count", expired) } }