Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PaymentHandler handles admin payment management.
|
||||
type PaymentHandler struct {
|
||||
paymentService *service.PaymentService
|
||||
configService *service.PaymentConfigService
|
||||
}
|
||||
|
||||
// NewPaymentHandler creates a new admin PaymentHandler.
|
||||
func NewPaymentHandler(paymentService *service.PaymentService, configService *service.PaymentConfigService) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
paymentService: paymentService,
|
||||
configService: configService,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
|
||||
// GetDashboard returns payment dashboard statistics.
|
||||
// GET /api/v1/admin/payment/dashboard
|
||||
func (h *PaymentHandler) GetDashboard(c *gin.Context) {
|
||||
days := 30
|
||||
if d := c.Query("days"); d != "" {
|
||||
if v, err := strconv.Atoi(d); err == nil && v > 0 {
|
||||
days = v
|
||||
}
|
||||
}
|
||||
stats, err := h.paymentService.GetDashboardStats(c.Request.Context(), days)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, stats)
|
||||
}
|
||||
|
||||
// --- Orders ---
|
||||
|
||||
// ListOrders returns a paginated list of all payment orders.
|
||||
// GET /api/v1/admin/payment/orders
|
||||
func (h *PaymentHandler) ListOrders(c *gin.Context) {
|
||||
page, pageSize := response.ParsePagination(c)
|
||||
var userID int64
|
||||
if uid := c.Query("user_id"); uid != "" {
|
||||
if v, err := strconv.ParseInt(uid, 10, 64); err == nil {
|
||||
userID = v
|
||||
}
|
||||
}
|
||||
orders, total, err := h.paymentService.AdminListOrders(c.Request.Context(), userID, service.OrderListParams{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: c.Query("status"),
|
||||
OrderType: c.Query("order_type"),
|
||||
PaymentType: c.Query("payment_type"),
|
||||
Keyword: c.Query("keyword"),
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Paginated(c, sanitizeAdminPaymentOrdersForResponse(orders), int64(total), page, pageSize)
|
||||
}
|
||||
|
||||
// GetOrderDetail returns detailed information about a single order.
|
||||
// GET /api/v1/admin/payment/orders/:id
|
||||
func (h *PaymentHandler) GetOrderDetail(c *gin.Context) {
|
||||
orderID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
order, err := h.paymentService.GetOrderByID(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
auditLogs, _ := h.paymentService.GetOrderAuditLogs(c.Request.Context(), orderID)
|
||||
response.Success(c, gin.H{"order": sanitizeAdminPaymentOrderForResponse(order), "auditLogs": auditLogs})
|
||||
}
|
||||
|
||||
// CancelOrder cancels a pending order (admin).
|
||||
// POST /api/v1/admin/payment/orders/:id/cancel
|
||||
func (h *PaymentHandler) CancelOrder(c *gin.Context) {
|
||||
orderID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
msg, err := h.paymentService.AdminCancelOrder(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"message": msg})
|
||||
}
|
||||
|
||||
// RetryFulfillment retries fulfillment for a paid order.
|
||||
// POST /api/v1/admin/payment/orders/:id/retry
|
||||
func (h *PaymentHandler) RetryFulfillment(c *gin.Context) {
|
||||
orderID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.paymentService.RetryFulfillment(c.Request.Context(), orderID); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"message": "fulfillment retried"})
|
||||
}
|
||||
|
||||
type AdminPaymentOrderResult struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
UserEmail string `json:"user_email,omitempty"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
UserNotes *string `json:"user_notes,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
PayAmount float64 `json:"pay_amount"`
|
||||
FeeRate float64 `json:"fee_rate"`
|
||||
Currency string `json:"currency"`
|
||||
RechargeCode string `json:"recharge_code,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
PaymentTradeNo string `json:"payment_trade_no,omitempty"`
|
||||
PayURL *string `json:"pay_url,omitempty"`
|
||||
QRCode *string `json:"qr_code,omitempty"`
|
||||
QRCodeImg *string `json:"qr_code_img,omitempty"`
|
||||
OrderType string `json:"order_type"`
|
||||
PlanID *int64 `json:"plan_id,omitempty"`
|
||||
SubscriptionGroupID *int64 `json:"subscription_group_id,omitempty"`
|
||||
SubscriptionDays *int `json:"subscription_days,omitempty"`
|
||||
ProviderInstanceID *string `json:"provider_instance_id,omitempty"`
|
||||
ProviderKey *string `json:"provider_key,omitempty"`
|
||||
Status string `json:"status"`
|
||||
RefundAmount float64 `json:"refund_amount"`
|
||||
RefundReason *string `json:"refund_reason,omitempty"`
|
||||
RefundAt *time.Time `json:"refund_at,omitempty"`
|
||||
ForceRefund bool `json:"force_refund,omitempty"`
|
||||
RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"`
|
||||
RefundRequestReason *string `json:"refund_request_reason,omitempty"`
|
||||
RefundRequestedBy *string `json:"refund_requested_by,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
FailedAt *time.Time `json:"failed_at,omitempty"`
|
||||
FailedReason *string `json:"failed_reason,omitempty"`
|
||||
ClientIP string `json:"client_ip,omitempty"`
|
||||
SrcHost string `json:"src_host,omitempty"`
|
||||
SrcURL *string `json:"src_url,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func sanitizeAdminPaymentOrdersForResponse(orders []*dbent.PaymentOrder) []*AdminPaymentOrderResult {
|
||||
out := make([]*AdminPaymentOrderResult, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
if item := sanitizeAdminPaymentOrderForResponse(order); item != nil {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sanitizeAdminPaymentOrderForResponse(order *dbent.PaymentOrder) *AdminPaymentOrderResult {
|
||||
if order == nil {
|
||||
return nil
|
||||
}
|
||||
return &AdminPaymentOrderResult{
|
||||
ID: order.ID,
|
||||
UserID: order.UserID,
|
||||
UserEmail: order.UserEmail,
|
||||
UserName: order.UserName,
|
||||
UserNotes: order.UserNotes,
|
||||
Amount: order.Amount,
|
||||
PayAmount: order.PayAmount,
|
||||
FeeRate: order.FeeRate,
|
||||
Currency: service.PaymentOrderCurrency(order),
|
||||
RechargeCode: order.RechargeCode,
|
||||
OutTradeNo: order.OutTradeNo,
|
||||
PaymentType: order.PaymentType,
|
||||
PaymentTradeNo: order.PaymentTradeNo,
|
||||
PayURL: order.PayURL,
|
||||
QRCode: order.QrCode,
|
||||
QRCodeImg: order.QrCodeImg,
|
||||
OrderType: order.OrderType,
|
||||
PlanID: order.PlanID,
|
||||
SubscriptionGroupID: order.SubscriptionGroupID,
|
||||
SubscriptionDays: order.SubscriptionDays,
|
||||
ProviderInstanceID: order.ProviderInstanceID,
|
||||
ProviderKey: order.ProviderKey,
|
||||
Status: order.Status,
|
||||
RefundAmount: order.RefundAmount,
|
||||
RefundReason: order.RefundReason,
|
||||
RefundAt: order.RefundAt,
|
||||
ForceRefund: order.ForceRefund,
|
||||
RefundRequestedAt: order.RefundRequestedAt,
|
||||
RefundRequestReason: order.RefundRequestReason,
|
||||
RefundRequestedBy: order.RefundRequestedBy,
|
||||
ExpiresAt: order.ExpiresAt,
|
||||
PaidAt: order.PaidAt,
|
||||
CompletedAt: order.CompletedAt,
|
||||
FailedAt: order.FailedAt,
|
||||
FailedReason: order.FailedReason,
|
||||
ClientIP: order.ClientIP,
|
||||
SrcHost: order.SrcHost,
|
||||
SrcURL: order.SrcURL,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// AdminProcessRefundRequest is the request body for admin refund processing.
|
||||
type AdminProcessRefundRequest struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Reason string `json:"reason"`
|
||||
Force bool `json:"force"`
|
||||
DeductBalance bool `json:"deduct_balance"`
|
||||
}
|
||||
|
||||
// ProcessRefund processes a refund for an order (admin).
|
||||
// POST /api/v1/admin/payment/orders/:id/refund
|
||||
func (h *PaymentHandler) ProcessRefund(c *gin.Context) {
|
||||
orderID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req AdminProcessRefundRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan, earlyResult, err := h.paymentService.PrepareRefund(c.Request.Context(), orderID, req.Amount, req.Reason, req.Force, req.DeductBalance)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if earlyResult != nil {
|
||||
response.Success(c, earlyResult)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.paymentService.ExecuteRefund(c.Request.Context(), plan)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// QueryAndFinalizeRefund queries the provider refund status and finalizes a pending refund.
|
||||
// POST /api/v1/admin/payment/orders/:id/refund/query
|
||||
func (h *PaymentHandler) QueryAndFinalizeRefund(c *gin.Context) {
|
||||
orderID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.paymentService.QueryAndFinalizeRefund(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// --- Subscription Plans ---
|
||||
|
||||
// ListPlans returns all subscription plans.
|
||||
// GET /api/v1/admin/payment/plans
|
||||
func (h *PaymentHandler) ListPlans(c *gin.Context) {
|
||||
plans, err := h.configService.ListPlans(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans)
|
||||
response.Success(c, adminSubscriptionPlansForResponse(plans, groupInfo))
|
||||
}
|
||||
|
||||
type AdminSubscriptionPlanResult struct {
|
||||
ID int64 `json:"id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
GroupPlatform string `json:"group_platform,omitempty"`
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
RateMultiplier float64 `json:"rate_multiplier,omitempty"`
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"`
|
||||
ModelScopes []string `json:"supported_model_scopes,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
OriginalPrice *float64 `json:"original_price,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
ValidityUnit string `json:"validity_unit"`
|
||||
Features string `json:"features"`
|
||||
ProductName string `json:"product_name"`
|
||||
ForSale bool `json:"for_sale"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func adminSubscriptionPlansForResponse(plans []*dbent.SubscriptionPlan, groupInfo map[int64]service.PlanGroupInfo) []AdminSubscriptionPlanResult {
|
||||
result := make([]AdminSubscriptionPlanResult, 0, len(plans))
|
||||
for _, p := range plans {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
gi := groupInfo[p.GroupID]
|
||||
result = append(result, AdminSubscriptionPlanResult{
|
||||
ID: int64(p.ID),
|
||||
GroupID: p.GroupID,
|
||||
GroupPlatform: gi.Platform,
|
||||
GroupName: gi.Name,
|
||||
RateMultiplier: gi.RateMultiplier,
|
||||
DailyLimitUSD: gi.DailyLimitUSD,
|
||||
WeeklyLimitUSD: gi.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: gi.MonthlyLimitUSD,
|
||||
ModelScopes: gi.ModelScopes,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Price: p.Price,
|
||||
OriginalPrice: p.OriginalPrice,
|
||||
Currency: p.Currency,
|
||||
ValidityDays: p.ValidityDays,
|
||||
ValidityUnit: p.ValidityUnit,
|
||||
Features: p.Features,
|
||||
ProductName: p.ProductName,
|
||||
ForSale: p.ForSale,
|
||||
SortOrder: p.SortOrder,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CreatePlan creates a new subscription plan.
|
||||
// POST /api/v1/admin/payment/plans
|
||||
func (h *PaymentHandler) CreatePlan(c *gin.Context) {
|
||||
var req service.CreatePlanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
plan, err := h.configService.CreatePlan(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Created(c, plan)
|
||||
}
|
||||
|
||||
// UpdatePlan updates an existing subscription plan.
|
||||
// PUT /api/v1/admin/payment/plans/:id
|
||||
func (h *PaymentHandler) UpdatePlan(c *gin.Context) {
|
||||
id, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req service.UpdatePlanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
plan, err := h.configService.UpdatePlan(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, plan)
|
||||
}
|
||||
|
||||
// DeletePlan deletes a subscription plan.
|
||||
// DELETE /api/v1/admin/payment/plans/:id
|
||||
func (h *PaymentHandler) DeletePlan(c *gin.Context) {
|
||||
id, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.configService.DeletePlan(c.Request.Context(), id); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// --- Provider Instances ---
|
||||
|
||||
// ListProviders returns all payment provider instances.
|
||||
// GET /api/v1/admin/payment/providers
|
||||
func (h *PaymentHandler) ListProviders(c *gin.Context) {
|
||||
providers, err := h.configService.ListProviderInstancesWithConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, providers)
|
||||
}
|
||||
|
||||
// CreateProvider creates a new payment provider instance.
|
||||
// POST /api/v1/admin/payment/providers
|
||||
func (h *PaymentHandler) CreateProvider(c *gin.Context) {
|
||||
var req service.CreateProviderInstanceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
inst, err := h.configService.CreateProviderInstance(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
h.paymentService.RefreshProviders(c.Request.Context())
|
||||
response.Created(c, inst)
|
||||
}
|
||||
|
||||
// UpdateProvider updates an existing payment provider instance.
|
||||
// PUT /api/v1/admin/payment/providers/:id
|
||||
func (h *PaymentHandler) UpdateProvider(c *gin.Context) {
|
||||
id, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req service.UpdateProviderInstanceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
inst, err := h.configService.UpdateProviderInstance(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
h.paymentService.RefreshProviders(c.Request.Context())
|
||||
response.Success(c, inst)
|
||||
}
|
||||
|
||||
// DeleteProvider deletes a payment provider instance.
|
||||
// DELETE /api/v1/admin/payment/providers/:id
|
||||
func (h *PaymentHandler) DeleteProvider(c *gin.Context) {
|
||||
id, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.configService.DeleteProviderInstance(c.Request.Context(), id); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
h.paymentService.RefreshProviders(c.Request.Context())
|
||||
response.Success(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// parseIDParam parses an int64 path parameter.
|
||||
// Returns the parsed ID and true on success; on failure it writes a BadRequest response and returns false.
|
||||
func parseIDParam(c *gin.Context, paramName string) (int64, bool) {
|
||||
id, err := strconv.ParseInt(c.Param(paramName), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid "+paramName)
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// --- Config ---
|
||||
|
||||
// GetConfig returns the payment configuration (admin view).
|
||||
// GET /api/v1/admin/payment/config
|
||||
func (h *PaymentHandler) GetConfig(c *gin.Context) {
|
||||
cfg, err := h.configService.GetPaymentConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, cfg)
|
||||
}
|
||||
|
||||
// UpdateConfig updates the payment configuration.
|
||||
// PUT /api/v1/admin/payment/config
|
||||
func (h *PaymentHandler) UpdateConfig(c *gin.Context) {
|
||||
var req service.UpdatePaymentConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.configService.UpdatePaymentConfig(c.Request.Context(), req); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"message": "updated"})
|
||||
}
|
||||
Reference in New Issue
Block a user