mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: add payment gateway and billing plans
This commit is contained in:
@@ -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
236
frontend/src/api/billing.ts
Normal 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
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user