feat: add payment gateway and billing plans

This commit is contained in:
Entropy.Xu
2026-05-13 01:18:38 +08:00
parent 0fa97595bf
commit 10285c5eb9
97 changed files with 14797 additions and 404 deletions

View File

@@ -1,6 +1,6 @@
import apiClient from './client'
import { buildCacheKey, cachedRequest } from '@/utils/cache'
import type { RefundRequest, WalletSummary, WalletTransaction } from './wallet'
import type { RefundRequest, WalletDailyQuotaSummary, WalletSummary, WalletTransaction } from './wallet'
export interface AdminWallet extends WalletSummary {
user_id: string | null
@@ -8,6 +8,11 @@ export interface AdminWallet extends WalletSummary {
owner_type: 'user' | 'api_key'
owner_name: string | null
created_at: string
wallet_balance?: number | null
package_balance?: number | null
total_available_balance?: number | null
daily_quota?: WalletDailyQuotaSummary | null
deduction_order?: string[]
}
export interface AdminWalletListResponse {

236
frontend/src/api/billing.ts Normal file
View File

@@ -0,0 +1,236 @@
import apiClient from './client'
import type { PaymentOrder } from './wallet'
export type BillingDurationUnit = 'day' | 'month' | 'year' | 'custom'
export type BillingPurchaseLimitScope = 'active_period' | 'lifetime' | 'unlimited'
export type WalletCreditBucket = 'recharge' | 'gift'
export interface EpayChannelConfig {
channel: string
display_name: string
}
export interface EpayGatewayConfig {
provider: 'epay'
enabled: boolean
endpoint_url?: string | null
callback_base_url?: string | null
merchant_id?: string | null
has_secret: boolean
pay_currency?: string | null
usd_exchange_rate?: number | null
min_recharge_usd?: number | null
channels?: EpayChannelConfig[]
created_at?: number | null
updated_at?: number | null
}
export interface UpdateEpayGatewayConfigRequest {
enabled: boolean
endpoint_url: string
callback_base_url?: string | null
merchant_id: string
merchant_key?: string
pay_currency: string
usd_exchange_rate: number
min_recharge_usd: number
channels: EpayChannelConfig[]
}
export interface GatewayTestResponse {
ok: boolean
provider: string
}
export interface WalletCreditEntitlement {
type: 'wallet_credit'
amount_usd: number
balance_bucket?: WalletCreditBucket
}
export interface DailyQuotaEntitlement {
type: 'daily_quota'
daily_quota_usd: number
reset_timezone?: string
carry_over?: boolean
allow_wallet_overage?: boolean
}
export interface MembershipGroupEntitlement {
type: 'membership_group'
grant_user_groups: string[]
}
export type BillingEntitlement =
| WalletCreditEntitlement
| DailyQuotaEntitlement
| MembershipGroupEntitlement
export interface BillingPlan {
id: string
title: string
description?: string | null
price_amount: number
price_currency: string
duration_unit: BillingDurationUnit
duration_value: number
enabled: boolean
sort_order: number
max_active_per_user: number
purchase_limit_scope: BillingPurchaseLimitScope
entitlements: BillingEntitlement[]
created_at?: number | null
updated_at?: number | null
}
export interface BillingPlanWriteRequest {
title: string
description?: string | null
price_amount: number
price_currency: string
duration_unit: BillingDurationUnit
duration_value: number
enabled: boolean
sort_order: number
max_active_per_user: number
purchase_limit_scope: BillingPurchaseLimitScope
entitlements: BillingEntitlement[]
}
export interface BillingPlanListResponse {
items: BillingPlan[]
total: number
}
export interface BillingCheckoutRequest {
payment_method?: string
payment_provider?: string
payment_channel?: string
}
export interface BillingCheckoutResponse {
order: PaymentOrder & {
order_kind?: string
product_id?: string | null
product?: BillingPlan | null
}
payment_instructions: Record<string, unknown>
}
export interface UserPlanEntitlement {
id: string
user_id: string
plan_id: string
payment_order_id: string
status: string
starts_at: string | null
expires_at: string | null
entitlements: BillingEntitlement[]
active?: boolean
created_at?: string | null
updated_at?: string | null
}
export interface UserPlanEntitlementsResponse {
items: UserPlanEntitlement[]
total: number
}
function normalizeChannels(channels: EpayGatewayConfig['channels']): EpayChannelConfig[] {
return Array.isArray(channels)
? channels
.map((item) => {
const raw = item as EpayChannelConfig & { type?: string }
const channel = String(raw.channel || raw.type || '').trim()
return {
channel,
display_name: String(raw.display_name || channel).trim(),
}
})
.filter((item) => item.channel && item.display_name)
: []
}
function normalizeGatewayConfig(config: EpayGatewayConfig): EpayGatewayConfig {
return {
provider: 'epay',
enabled: Boolean(config.enabled),
endpoint_url: config.endpoint_url ?? '',
callback_base_url: config.callback_base_url ?? '',
merchant_id: config.merchant_id ?? '',
has_secret: Boolean(config.has_secret),
pay_currency: config.pay_currency ?? 'CNY',
usd_exchange_rate: Number(config.usd_exchange_rate ?? 7.2),
min_recharge_usd: Number(config.min_recharge_usd ?? 1),
channels: normalizeChannels(config.channels),
created_at: config.created_at ?? null,
updated_at: config.updated_at ?? null,
}
}
export const epayGatewayApi = {
async get(): Promise<EpayGatewayConfig> {
const response = await apiClient.get<EpayGatewayConfig>('/api/admin/payments/gateways/epay')
return normalizeGatewayConfig(response.data)
},
async update(payload: UpdateEpayGatewayConfigRequest): Promise<EpayGatewayConfig> {
const request: UpdateEpayGatewayConfigRequest = {
...payload,
channels: normalizeChannels(payload.channels),
}
const response = await apiClient.put<EpayGatewayConfig>('/api/admin/payments/gateways/epay', request)
return normalizeGatewayConfig(response.data)
},
async test(): Promise<GatewayTestResponse> {
const response = await apiClient.post<GatewayTestResponse>('/api/admin/payments/gateways/epay/test', {})
return response.data
},
}
export const adminBillingPlansApi = {
async list(): Promise<BillingPlanListResponse> {
const response = await apiClient.get<BillingPlanListResponse>('/api/admin/billing/plans')
return response.data
},
async create(payload: BillingPlanWriteRequest): Promise<BillingPlan> {
const response = await apiClient.post<BillingPlan>('/api/admin/billing/plans', payload)
return response.data
},
async update(planId: string, payload: BillingPlanWriteRequest): Promise<BillingPlan> {
const response = await apiClient.put<BillingPlan>(`/api/admin/billing/plans/${planId}`, payload)
return response.data
},
async setStatus(planId: string, enabled: boolean): Promise<BillingPlan> {
const response = await apiClient.patch<BillingPlan>(`/api/admin/billing/plans/${planId}/status`, { enabled })
return response.data
},
async delete(planId: string): Promise<void> {
await apiClient.delete(`/api/admin/billing/plans/${planId}`)
},
}
export const billingApi = {
async listPlans(): Promise<BillingPlanListResponse> {
const response = await apiClient.get<BillingPlanListResponse>('/api/billing/plans')
return response.data
},
async checkout(planId: string, payload: BillingCheckoutRequest): Promise<BillingCheckoutResponse> {
const response = await apiClient.post<BillingCheckoutResponse>(
`/api/billing/plans/${planId}/checkout`,
payload
)
return response.data
},
async listEntitlements(): Promise<UserPlanEntitlementsResponse> {
const response = await apiClient.get<UserPlanEntitlementsResponse>('/api/billing/entitlements')
return response.data
},
}

View File

@@ -1,6 +1,7 @@
import apiClient from './client'
import { cachedRequest } from '@/utils/cache'
import type { UserSession as SessionRecord } from '@/types/session'
import type { BillingPlan, UserPlanEntitlement } from './billing'
export type UserRole = 'admin' | 'user'
export type ListPolicyMode = 'inherit' | 'unrestricted' | 'specific' | 'deny_all'
@@ -227,6 +228,26 @@ export interface UpsertUserApiKeyRequest {
export type UserSession = SessionRecord
export interface AdminUserPlanEntitlement extends UserPlanEntitlement {
plan_title?: string | null
plan?: BillingPlan | null
}
export interface AdminUserPlanEntitlementsResponse {
items: AdminUserPlanEntitlement[]
total: number
}
export interface GrantUserPlanRequest {
plan_id: string
reason?: string | null
}
export interface GrantUserPlanResponse extends AdminUserPlanEntitlementsResponse {
order?: Record<string, unknown>
credited?: boolean
}
export interface GetAllUsersOptions {
search?: string
role?: UserRole
@@ -361,6 +382,24 @@ export const usersApi = {
return response.data
},
async listUserPlanEntitlements(userId: string): Promise<AdminUserPlanEntitlementsResponse> {
const response = await apiClient.get<AdminUserPlanEntitlementsResponse>(
`/api/admin/users/${userId}/billing/entitlements`
)
return response.data
},
async grantUserPlan(
userId: string,
payload: GrantUserPlanRequest
): Promise<GrantUserPlanResponse> {
const response = await apiClient.post<GrantUserPlanResponse>(
`/api/admin/users/${userId}/billing/grant-plan`,
payload
)
return response.data
},
async revokeUserSession(userId: string, sessionId: string): Promise<{ message: string }> {
const response = await apiClient.delete<{ message: string }>(`/api/admin/users/${userId}/sessions/${sessionId}`)
return response.data

View File

@@ -2,7 +2,7 @@ import apiClient from './client'
export interface WalletSummary {
id: string
// balance = 可用余额(充值余额 + 赠款余额)
// balance = 钱包可用余额(充值余额 + 赠款余额),不包含套餐每日额度
balance: number
recharge_balance: number
gift_balance: number
@@ -18,15 +18,28 @@ export interface WalletSummary {
updated_at: string
}
export interface WalletDailyQuotaSummary {
has_active: boolean
total_usd: number
used_usd: number
remaining_usd: number
allow_wallet_overage: boolean
}
export interface WalletBalanceResponse {
wallet: WalletSummary | null
unlimited: boolean
limit_mode: 'finite' | 'unlimited'
// balance = 可用余额(充值余额 + 赠款余额)
// balance = 钱包可用余额(充值余额 + 赠款余额),不包含套餐每日额度
balance: number | null
recharge_balance?: number | null
gift_balance?: number | null
refundable_balance?: number | null
wallet_balance?: number | null
package_balance?: number | null
total_available_balance?: number | null
daily_quota?: WalletDailyQuotaSummary | null
deduction_order?: string[]
currency: string
pending_refund_count?: number
}
@@ -102,6 +115,13 @@ export interface PaymentOrder {
refunded_amount_usd: number
refundable_amount_usd: number
payment_method: string
payment_provider?: string | null
payment_channel?: string | null
order_kind?: 'wallet_recharge' | 'plan_purchase' | string
product_id?: string | null
product_snapshot?: Record<string, unknown> | null
fulfillment_status?: string | null
fulfillment_error?: string | null
gateway_order_id: string | null
gateway_response: Record<string, unknown> | null
status: string
@@ -135,11 +155,24 @@ export interface RefundRequest {
export interface WalletRechargeCreateRequest {
amount_usd: number
payment_method: string
payment_provider?: string
payment_channel?: string
pay_amount?: number
pay_currency?: string
exchange_rate?: number
}
export interface WalletRechargeOption {
payment_method: string
display_name: string
provider?: string
payment_provider?: string
payment_channel?: string
pay_currency?: string
usd_exchange_rate?: number
min_recharge_usd?: number
}
export interface WalletRefundCreateRequest {
amount_usd: number
payment_order_id?: string
@@ -190,6 +223,11 @@ export const walletApi = {
return response.data
},
async listRechargeOptions(): Promise<{ items: WalletRechargeOption[] }> {
const response = await apiClient.get<{ items: WalletRechargeOption[] }>('/api/wallet/recharge/options')
return response.data
},
async listRechargeOrders(params?: { limit?: number; offset?: number }): Promise<{
items: PaymentOrder[]
total: number