Backend fixes: - #1: doSub subscription idempotency via audit log check - #2: markFailed only when status=RECHARGING (prevents overwriting COMPLETED) - #3: ExpireTimedOutOrders checks upstream payment before expiring - #4: Public verify endpoint for payment result page (no auth required) - #5: EasyPay QueryOrder returns amount, confirmPayment handles zero amount - #6: WxPay notifyUrl priority: request-first, config-fallback - #7: EasyPay remove double URL decode in VerifyNotification - #8: checkPaid/cancelUpstreamPayment use order's provider instance - #9: Amount NaN/Inf/negative validation in order creation and refund - #10: Refund amount comparison uses tolerance instead of float64 == - #11: Skip balance deduction on retry when previous rollback failed - #12: checkPaid logs fulfillment errors instead of silently ignoring - #13: WxPay certSerial added to required config fields Frontend fixes: - Payment result page no longer requires authentication - Public verify API fallback for expired sessions
101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
package routes
|
|
|
|
import (
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RegisterPaymentRoutes registers all payment-related routes:
|
|
// user-facing endpoints, webhook endpoints, and admin endpoints.
|
|
func RegisterPaymentRoutes(
|
|
v1 *gin.RouterGroup,
|
|
paymentHandler *handler.PaymentHandler,
|
|
webhookHandler *handler.PaymentWebhookHandler,
|
|
adminPaymentHandler *admin.PaymentHandler,
|
|
jwtAuth middleware.JWTAuthMiddleware,
|
|
adminAuth middleware.AdminAuthMiddleware,
|
|
settingService *service.SettingService,
|
|
) {
|
|
// --- User-facing payment endpoints (authenticated) ---
|
|
authenticated := v1.Group("/payment")
|
|
authenticated.Use(gin.HandlerFunc(jwtAuth))
|
|
authenticated.Use(middleware.BackendModeUserGuard(settingService))
|
|
{
|
|
authenticated.GET("/config", paymentHandler.GetPaymentConfig)
|
|
authenticated.GET("/plans", paymentHandler.GetPlans)
|
|
authenticated.GET("/channels", paymentHandler.GetChannels)
|
|
authenticated.GET("/limits", paymentHandler.GetLimits)
|
|
|
|
orders := authenticated.Group("/orders")
|
|
{
|
|
orders.POST("", paymentHandler.CreateOrder)
|
|
orders.GET("/my", paymentHandler.GetMyOrders)
|
|
orders.GET("/:id", paymentHandler.GetOrder)
|
|
orders.POST("/:id/cancel", paymentHandler.CancelOrder)
|
|
orders.POST("/:id/refund-request", paymentHandler.RequestRefund)
|
|
}
|
|
}
|
|
|
|
// --- Public payment endpoints (no auth) ---
|
|
// Payment result page needs to verify order status without login
|
|
// (user session may have expired during provider redirect).
|
|
public := v1.Group("/payment/public")
|
|
{
|
|
public.POST("/orders/verify", paymentHandler.VerifyOrderPublic)
|
|
}
|
|
|
|
// --- Webhook endpoints (no auth) ---
|
|
webhook := v1.Group("/payment/webhook")
|
|
{
|
|
webhook.POST("/easypay", webhookHandler.EasyPayNotify)
|
|
webhook.POST("/alipay", webhookHandler.AlipayNotify)
|
|
webhook.POST("/wxpay", webhookHandler.WxpayNotify)
|
|
webhook.POST("/stripe", webhookHandler.StripeWebhook)
|
|
}
|
|
|
|
// --- Admin payment endpoints (admin auth) ---
|
|
adminGroup := v1.Group("/admin/payment")
|
|
adminGroup.Use(gin.HandlerFunc(adminAuth))
|
|
{
|
|
// Dashboard
|
|
adminGroup.GET("/dashboard", adminPaymentHandler.GetDashboard)
|
|
|
|
// Config
|
|
adminGroup.GET("/config", adminPaymentHandler.GetConfig)
|
|
adminGroup.PUT("/config", adminPaymentHandler.UpdateConfig)
|
|
|
|
// Orders
|
|
adminOrders := adminGroup.Group("/orders")
|
|
{
|
|
adminOrders.GET("", adminPaymentHandler.ListOrders)
|
|
adminOrders.GET("/:id", adminPaymentHandler.GetOrderDetail)
|
|
adminOrders.POST("/:id/cancel", adminPaymentHandler.CancelOrder)
|
|
adminOrders.POST("/:id/retry", adminPaymentHandler.RetryFulfillment)
|
|
adminOrders.POST("/:id/refund", adminPaymentHandler.ProcessRefund)
|
|
}
|
|
|
|
|
|
// Subscription Plans
|
|
plans := adminGroup.Group("/plans")
|
|
{
|
|
plans.GET("", adminPaymentHandler.ListPlans)
|
|
plans.POST("", adminPaymentHandler.CreatePlan)
|
|
plans.PUT("/:id", adminPaymentHandler.UpdatePlan)
|
|
plans.DELETE("/:id", adminPaymentHandler.DeletePlan)
|
|
}
|
|
|
|
// Provider Instances
|
|
providers := adminGroup.Group("/providers")
|
|
{
|
|
providers.GET("", adminPaymentHandler.ListProviders)
|
|
providers.POST("", adminPaymentHandler.CreateProvider)
|
|
providers.PUT("/:id", adminPaymentHandler.UpdateProvider)
|
|
providers.DELETE("/:id", adminPaymentHandler.DeleteProvider)
|
|
}
|
|
}
|
|
}
|