feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
This commit is contained in:
@@ -26,6 +26,7 @@ import scheduledTestsAPI from './scheduledTests'
|
||||
import backupAPI from './backup'
|
||||
import tlsFingerprintProfileAPI from './tlsFingerprintProfile'
|
||||
import channelsAPI from './channels'
|
||||
import adminPaymentAPI from './payment'
|
||||
|
||||
/**
|
||||
* Unified admin API object for convenient access
|
||||
@@ -53,7 +54,8 @@ export const adminAPI = {
|
||||
scheduledTests: scheduledTestsAPI,
|
||||
backup: backupAPI,
|
||||
tlsFingerprintProfiles: tlsFingerprintProfileAPI,
|
||||
channels: channelsAPI
|
||||
channels: channelsAPI,
|
||||
payment: adminPaymentAPI
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -79,7 +81,8 @@ export {
|
||||
scheduledTestsAPI,
|
||||
backupAPI,
|
||||
tlsFingerprintProfileAPI,
|
||||
channelsAPI
|
||||
channelsAPI,
|
||||
adminPaymentAPI
|
||||
}
|
||||
|
||||
export default adminAPI
|
||||
|
||||
176
frontend/src/api/admin/payment.ts
Normal file
176
frontend/src/api/admin/payment.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Admin Payment API endpoints
|
||||
* Handles payment management operations for administrators
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import type {
|
||||
DashboardStats,
|
||||
PaymentOrder,
|
||||
PaymentChannel,
|
||||
SubscriptionPlan,
|
||||
ProviderInstance
|
||||
} from '@/types/payment'
|
||||
import type { BasePaginationResponse } from '@/types'
|
||||
|
||||
/** Admin-facing payment config returned by GET /admin/payment/config */
|
||||
export interface AdminPaymentConfig {
|
||||
enabled: boolean
|
||||
min_amount: number
|
||||
max_amount: number
|
||||
daily_limit: number
|
||||
order_timeout_minutes: number
|
||||
max_pending_orders: number
|
||||
enabled_payment_types: string[]
|
||||
balance_disabled: boolean
|
||||
load_balance_strategy: string
|
||||
product_name_prefix: string
|
||||
product_name_suffix: string
|
||||
help_image_url: string
|
||||
help_text: string
|
||||
}
|
||||
|
||||
/** Fields accepted by PUT /admin/payment/config (all optional via pointer semantics) */
|
||||
export interface UpdatePaymentConfigRequest {
|
||||
enabled?: boolean
|
||||
min_amount?: number
|
||||
max_amount?: number
|
||||
daily_limit?: number
|
||||
order_timeout_minutes?: number
|
||||
max_pending_orders?: number
|
||||
enabled_payment_types?: string[]
|
||||
balance_disabled?: boolean
|
||||
load_balance_strategy?: string
|
||||
product_name_prefix?: string
|
||||
product_name_suffix?: string
|
||||
help_image_url?: string
|
||||
help_text?: string
|
||||
}
|
||||
|
||||
export const adminPaymentAPI = {
|
||||
// ==================== Config ====================
|
||||
|
||||
/** Get payment configuration (admin view) */
|
||||
getConfig() {
|
||||
return apiClient.get<AdminPaymentConfig>('/admin/payment/config')
|
||||
},
|
||||
|
||||
/** Update payment configuration */
|
||||
updateConfig(data: UpdatePaymentConfigRequest) {
|
||||
return apiClient.put('/admin/payment/config', data)
|
||||
},
|
||||
|
||||
// ==================== Dashboard ====================
|
||||
|
||||
/** Get payment dashboard statistics */
|
||||
getDashboard(days?: number) {
|
||||
return apiClient.get<DashboardStats>('/admin/payment/dashboard', {
|
||||
params: days ? { days } : undefined
|
||||
})
|
||||
},
|
||||
|
||||
// ==================== Orders ====================
|
||||
|
||||
/** Get all orders (paginated, with filters) */
|
||||
getOrders(params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: string
|
||||
payment_type?: string
|
||||
user_id?: number
|
||||
keyword?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
order_type?: string
|
||||
}) {
|
||||
return apiClient.get<BasePaginationResponse<PaymentOrder>>('/admin/payment/orders', { params })
|
||||
},
|
||||
|
||||
/** Get a specific order by ID */
|
||||
getOrder(id: number) {
|
||||
return apiClient.get<PaymentOrder>(`/admin/payment/orders/${id}`)
|
||||
},
|
||||
|
||||
/** Cancel an order (admin) */
|
||||
cancelOrder(id: number) {
|
||||
return apiClient.post(`/admin/payment/orders/${id}/cancel`)
|
||||
},
|
||||
|
||||
/** Retry recharge for a failed order */
|
||||
retryRecharge(id: number) {
|
||||
return apiClient.post(`/admin/payment/orders/${id}/retry`)
|
||||
},
|
||||
|
||||
/** Process a refund */
|
||||
refundOrder(id: number, data: { amount: number; reason: string; deduct_balance?: boolean; force?: boolean }) {
|
||||
return apiClient.post(`/admin/payment/orders/${id}/refund`, data)
|
||||
},
|
||||
|
||||
// ==================== Channels ====================
|
||||
|
||||
/** Get all payment channels */
|
||||
getChannels() {
|
||||
return apiClient.get<PaymentChannel[]>('/admin/payment/channels')
|
||||
},
|
||||
|
||||
/** Create a payment channel */
|
||||
createChannel(data: Partial<PaymentChannel>) {
|
||||
return apiClient.post<PaymentChannel>('/admin/payment/channels', data)
|
||||
},
|
||||
|
||||
/** Update a payment channel */
|
||||
updateChannel(id: number, data: Partial<PaymentChannel>) {
|
||||
return apiClient.put<PaymentChannel>(`/admin/payment/channels/${id}`, data)
|
||||
},
|
||||
|
||||
/** Delete a payment channel */
|
||||
deleteChannel(id: number) {
|
||||
return apiClient.delete(`/admin/payment/channels/${id}`)
|
||||
},
|
||||
|
||||
// ==================== Subscription Plans ====================
|
||||
|
||||
/** Get all subscription plans */
|
||||
getPlans() {
|
||||
return apiClient.get<SubscriptionPlan[]>('/admin/payment/plans')
|
||||
},
|
||||
|
||||
/** Create a subscription plan */
|
||||
createPlan(data: Record<string, unknown>) {
|
||||
return apiClient.post<SubscriptionPlan>('/admin/payment/plans', data)
|
||||
},
|
||||
|
||||
/** Update a subscription plan */
|
||||
updatePlan(id: number, data: Record<string, unknown>) {
|
||||
return apiClient.put<SubscriptionPlan>(`/admin/payment/plans/${id}`, data)
|
||||
},
|
||||
|
||||
/** Delete a subscription plan */
|
||||
deletePlan(id: number) {
|
||||
return apiClient.delete(`/admin/payment/plans/${id}`)
|
||||
},
|
||||
|
||||
// ==================== Provider Instances ====================
|
||||
|
||||
/** Get all provider instances */
|
||||
getProviders() {
|
||||
return apiClient.get<ProviderInstance[]>('/admin/payment/providers')
|
||||
},
|
||||
|
||||
/** Create a provider instance */
|
||||
createProvider(data: Partial<ProviderInstance>) {
|
||||
return apiClient.post<ProviderInstance>('/admin/payment/providers', data)
|
||||
},
|
||||
|
||||
/** Update a provider instance */
|
||||
updateProvider(id: number, data: Partial<ProviderInstance>) {
|
||||
return apiClient.put<ProviderInstance>(`/admin/payment/providers/${id}`, data)
|
||||
},
|
||||
|
||||
/** Delete a provider instance */
|
||||
deleteProvider(id: number) {
|
||||
return apiClient.delete(`/admin/payment/providers/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default adminPaymentAPI
|
||||
@@ -38,8 +38,7 @@ export interface SystemSettings {
|
||||
doc_url: string
|
||||
home_content: string
|
||||
hide_ccs_import_button: boolean
|
||||
purchase_subscription_enabled: boolean
|
||||
purchase_subscription_url: string
|
||||
sora_client_enabled: boolean
|
||||
backend_mode_enabled: boolean
|
||||
custom_menu_items: CustomMenuItem[]
|
||||
custom_endpoints: CustomEndpoint[]
|
||||
@@ -114,6 +113,26 @@ export interface SystemSettings {
|
||||
enable_fingerprint_unification: boolean
|
||||
enable_metadata_passthrough: boolean
|
||||
enable_cch_signing: boolean
|
||||
|
||||
// Payment configuration
|
||||
payment_enabled: boolean
|
||||
payment_min_amount: number
|
||||
payment_max_amount: number
|
||||
payment_daily_limit: number
|
||||
payment_order_timeout_minutes: number
|
||||
payment_max_pending_orders: number
|
||||
payment_enabled_types: string[]
|
||||
payment_balance_disabled: boolean
|
||||
payment_load_balance_strategy: string
|
||||
payment_product_name_prefix: string
|
||||
payment_product_name_suffix: string
|
||||
payment_help_image_url: string
|
||||
payment_help_text: string
|
||||
payment_cancel_rate_limit_enabled: boolean
|
||||
payment_cancel_rate_limit_max: number
|
||||
payment_cancel_rate_limit_window: number
|
||||
payment_cancel_rate_limit_unit: string
|
||||
payment_cancel_rate_limit_window_mode: string
|
||||
}
|
||||
|
||||
export interface UpdateSettingsRequest {
|
||||
@@ -136,8 +155,6 @@ export interface UpdateSettingsRequest {
|
||||
doc_url?: string
|
||||
home_content?: string
|
||||
hide_ccs_import_button?: boolean
|
||||
purchase_subscription_enabled?: boolean
|
||||
purchase_subscription_url?: string
|
||||
backend_mode_enabled?: boolean
|
||||
custom_menu_items?: CustomMenuItem[]
|
||||
custom_endpoints?: CustomEndpoint[]
|
||||
@@ -194,6 +211,25 @@ export interface UpdateSettingsRequest {
|
||||
enable_fingerprint_unification?: boolean
|
||||
enable_metadata_passthrough?: boolean
|
||||
enable_cch_signing?: boolean
|
||||
// Payment configuration
|
||||
payment_enabled?: boolean
|
||||
payment_min_amount?: number
|
||||
payment_max_amount?: number
|
||||
payment_daily_limit?: number
|
||||
payment_order_timeout_minutes?: number
|
||||
payment_max_pending_orders?: number
|
||||
payment_enabled_types?: string[]
|
||||
payment_balance_disabled?: boolean
|
||||
payment_load_balance_strategy?: string
|
||||
payment_product_name_prefix?: string
|
||||
payment_product_name_suffix?: string
|
||||
payment_help_image_url?: string
|
||||
payment_help_text?: string
|
||||
payment_cancel_rate_limit_enabled?: boolean
|
||||
payment_cancel_rate_limit_max?: number
|
||||
payment_cancel_rate_limit_window?: number
|
||||
payment_cancel_rate_limit_unit?: string
|
||||
payment_cancel_rate_limit_window_mode?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ export { keysAPI } from './keys'
|
||||
export { usageAPI } from './usage'
|
||||
export { userAPI } from './user'
|
||||
export { redeemAPI, type RedeemHistoryItem } from './redeem'
|
||||
export { paymentAPI } from './payment'
|
||||
export { userGroupsAPI } from './groups'
|
||||
export { totpAPI } from './totp'
|
||||
export { default as announcementsAPI } from './announcements'
|
||||
|
||||
79
frontend/src/api/payment.ts
Normal file
79
frontend/src/api/payment.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* User Payment API endpoints
|
||||
* Handles payment operations for regular users
|
||||
*/
|
||||
|
||||
import { apiClient } from './client'
|
||||
import type {
|
||||
PaymentConfig,
|
||||
SubscriptionPlan,
|
||||
PaymentChannel,
|
||||
MethodLimitsResponse,
|
||||
CheckoutInfoResponse,
|
||||
CreateOrderRequest,
|
||||
CreateOrderResult,
|
||||
PaymentOrder
|
||||
} from '@/types/payment'
|
||||
import type { BasePaginationResponse } from '@/types'
|
||||
|
||||
export const paymentAPI = {
|
||||
/** Get payment configuration (enabled types, limits, etc.) */
|
||||
getConfig() {
|
||||
return apiClient.get<PaymentConfig>('/payment/config')
|
||||
},
|
||||
|
||||
/** Get available subscription plans */
|
||||
getPlans() {
|
||||
return apiClient.get<SubscriptionPlan[]>('/payment/plans')
|
||||
},
|
||||
|
||||
/** Get available payment channels */
|
||||
getChannels() {
|
||||
return apiClient.get<PaymentChannel[]>('/payment/channels')
|
||||
},
|
||||
|
||||
/** Get all checkout page data in a single call */
|
||||
getCheckoutInfo() {
|
||||
return apiClient.get<CheckoutInfoResponse>('/payment/checkout-info')
|
||||
},
|
||||
|
||||
/** Get payment method limits and fee rates */
|
||||
getLimits() {
|
||||
return apiClient.get<MethodLimitsResponse>('/payment/limits')
|
||||
},
|
||||
|
||||
/** Create a new payment order */
|
||||
createOrder(data: CreateOrderRequest) {
|
||||
return apiClient.post<CreateOrderResult>('/payment/orders', data)
|
||||
},
|
||||
|
||||
/** Get current user's orders */
|
||||
getMyOrders(params?: { page?: number; page_size?: number; status?: string }) {
|
||||
return apiClient.get<BasePaginationResponse<PaymentOrder>>('/payment/orders/my', { params })
|
||||
},
|
||||
|
||||
/** Get a specific order by ID */
|
||||
getOrder(id: number) {
|
||||
return apiClient.get<PaymentOrder>(`/payment/orders/${id}`)
|
||||
},
|
||||
|
||||
/** Cancel a pending order */
|
||||
cancelOrder(id: number) {
|
||||
return apiClient.post(`/payment/orders/${id}/cancel`)
|
||||
},
|
||||
|
||||
/** Verify order payment status with upstream provider */
|
||||
verifyOrder(outTradeNo: string) {
|
||||
return apiClient.post<PaymentOrder>('/payment/orders/verify', { out_trade_no: outTradeNo })
|
||||
},
|
||||
|
||||
/** Verify order payment status without auth (public endpoint for result page) */
|
||||
verifyOrderPublic(outTradeNo: string) {
|
||||
return apiClient.post<PaymentOrder>('/payment/public/orders/verify', { out_trade_no: outTradeNo })
|
||||
},
|
||||
|
||||
/** Request a refund for a completed order */
|
||||
requestRefund(id: number, data: { reason: string }) {
|
||||
return apiClient.post(`/payment/orders/${id}/refund-request`, data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user