Files
李建琦 6d655c9903
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
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

504 lines
16 KiB
Go

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"})
}