Files
sub2api/backend/internal/handler/payment_webhook_handler.go
T
erio 64c4495ae5 feat: integrate payment system (EasyPay, Alipay, WxPay, Stripe)
Replace iframe-based external payment with native implementation:

Backend (Go):
- 5 new Ent schemas: PaymentOrder, PaymentAuditLog, PaymentChannel,
  SubscriptionPlan, PaymentProviderInstance
- 5 SQL migrations (090-094)
- Payment provider interface + registry + load balancer
- 4 provider implementations (EasyPay, Alipay, WxPay, Stripe)
- PaymentService (order lifecycle, fulfillment, refund 3-phase)
- PaymentConfigService (settings, instance/channel/plan CRUD)
- 31 API endpoints (9 user + 4 webhook + 18 admin)
- Wire DI integration

Frontend (Vue3):
- Payment types, API modules, store, router changes
- Sidebar: payment_enabled controls visibility, new menu items
- User pages: PaymentView (replace iframe), QR payment, Stripe,
  result page, order list
- Admin pages: Order management (4 tabs), provider instance mgmt
- Settings: new payment tab, removed old purchase iframe config
- i18n: full payment namespace (en + zh)

Docs:
- PAYMENT_FRONTEND_MIGRATION_PLAN.md
- PAYMENT_BACKEND_MIGRATION_PLAN.md
- PAYMENT_IMPLEMENTATION_ROADMAP.md
2026-04-05 15:06:05 +08:00

104 lines
3.0 KiB
Go

package handler
import (
"io"
"log"
"net/http"
"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
}
// 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) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Printf("[Payment Webhook] failed to read body for %s: %v", providerKey, 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)
c.String(http.StatusOK, successResponse(providerKey))
return
}
headers := make(map[string]string)
for k := range c.Request.Header {
headers[k] = c.GetHeader(k)
}
notification, err := provider.VerifyNotification(c.Request.Context(), string(body), headers)
if err != nil {
log.Printf("[Payment Webhook] %s verify failed: %v", providerKey, err)
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 {
log.Printf("[Payment Webhook] %s handle notification failed: %v", providerKey, 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"
}
}