mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
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
104 lines
3.0 KiB
Go
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"
|
|
}
|
|
}
|