feat(wallet): 钱包系统替代配额系统,新增支付与退款机制

- 新增钱包余额管理、充值、扣费、退款完整流程
- 新增支付网关抽象层(支持手动/支付宝/微信)
- 用量计费从配额系统迁移到钱包余额扣费
- 新增管理员钱包管理与支付订单管理页面
- 新增用户钱包中心页面
- 移除独立 Key 锁定机制,统一由钱包余额控制
- 新增相关 API 路由、序列化器与数据库迁移
- 新增钱包、支付、退款相关测试
This commit is contained in:
LewisPen
2026-03-08 00:05:48 +08:00
committed by fawney19
parent 9cdcce1b5f
commit 783f654953
108 changed files with 13152 additions and 3372 deletions

View File

@@ -0,0 +1,93 @@
import apiClient from './client'
import type { PaymentOrder } from './wallet'
export interface PaymentCallbackRecord {
id: string
payment_order_id: string | null
payment_method: string
callback_key: string
order_no: string | null
gateway_order_id: string | null
payload_hash: string | null
signature_valid: boolean
status: string
payload: Record<string, unknown> | null
error_message: string | null
created_at: string
processed_at: string | null
}
export interface AdminPaymentOrderListResponse {
items: PaymentOrder[]
total: number
limit: number
offset: number
}
export interface AdminPaymentCallbacksResponse {
items: PaymentCallbackRecord[]
total: number
limit: number
offset: number
}
export interface AdminPaymentCreditRequest {
gateway_order_id?: string
pay_amount?: number
pay_currency?: string
exchange_rate?: number
gateway_response?: Record<string, unknown>
}
export const adminPaymentsApi = {
async listOrders(params?: {
status?: string
payment_method?: string
limit?: number
offset?: number
}): Promise<AdminPaymentOrderListResponse> {
const response = await apiClient.get<AdminPaymentOrderListResponse>('/api/admin/payments/orders', { params })
return response.data
},
async getOrder(orderId: string): Promise<{ order: PaymentOrder }> {
const response = await apiClient.get<{ order: PaymentOrder }>(`/api/admin/payments/orders/${orderId}`)
return response.data
},
async expireOrder(orderId: string): Promise<{ order: PaymentOrder; expired: boolean }> {
const response = await apiClient.post<{ order: PaymentOrder; expired: boolean }>(
`/api/admin/payments/orders/${orderId}/expire`,
{}
)
return response.data
},
async failOrder(orderId: string): Promise<{ order: PaymentOrder }> {
const response = await apiClient.post<{ order: PaymentOrder }>(
`/api/admin/payments/orders/${orderId}/fail`,
{}
)
return response.data
},
async creditOrder(
orderId: string,
payload: AdminPaymentCreditRequest
): Promise<{ order: PaymentOrder; credited: boolean }> {
const response = await apiClient.post<{ order: PaymentOrder; credited: boolean }>(
`/api/admin/payments/orders/${orderId}/credit`,
payload
)
return response.data
},
async listCallbacks(params?: {
payment_method?: string
limit?: number
offset?: number
}): Promise<AdminPaymentCallbacksResponse> {
const response = await apiClient.get<AdminPaymentCallbacksResponse>('/api/admin/payments/callbacks', { params })
return response.data
},
}

View File

@@ -0,0 +1,247 @@
import apiClient from './client'
import type { RefundRequest, WalletSummary, WalletTransaction } from './wallet'
export interface AdminWallet extends WalletSummary {
user_id: string | null
api_key_id: string | null
owner_type: 'user' | 'api_key'
owner_name: string | null
created_at: string
}
export interface AdminWalletListResponse {
items: AdminWallet[]
total: number
limit: number
offset: number
}
export interface AdminWalletDetailResponse extends AdminWallet {
pending_refund_count: number
}
export interface AdminWalletTransactionsResponse {
wallet: AdminWallet
items: WalletTransaction[]
total: number
limit: number
offset: number
}
export interface AdminWalletRefundsResponse {
wallet: AdminWallet
items: RefundRequest[]
total: number
limit: number
offset: number
}
export interface AdminLedgerTransaction extends WalletTransaction {
wallet_id: string
owner_type: 'user' | 'api_key'
owner_name: string | null
wallet_status?: string | null
}
export interface AdminGlobalRefund extends RefundRequest {
wallet_id: string
owner_type: 'user' | 'api_key'
owner_name: string | null
wallet_status?: string | null
}
export interface AdminLedgerResponse {
items: AdminLedgerTransaction[]
total: number
limit: number
offset: number
}
export interface AdminGlobalRefundsListResponse {
items: AdminGlobalRefund[]
total: number
limit: number
offset: number
}
export interface ManualRechargeRequest {
amount_usd: number
payment_method?: string
description?: string
}
export interface WalletAdjustRequest {
amount_usd: number
balance_type?: 'recharge' | 'gift'
description?: string
}
export interface RefundFailRequest {
reason: string
}
export interface RefundCompleteRequest {
gateway_refund_id?: string
payout_reference?: string
payout_proof?: Record<string, unknown>
}
export const adminWalletApi = {
async listWallets(params?: {
status?: string
limit?: number
offset?: number
}): Promise<AdminWalletListResponse> {
const response = await apiClient.get<AdminWalletListResponse>('/api/admin/wallets', { params })
return response.data
},
async listAllWallets(params?: {
status?: string
}): Promise<AdminWallet[]> {
const items: AdminWallet[] = []
const limit = 200
const maxPages = 200
let offset = 0
let page = 0
while (page < maxPages) {
const response = await apiClient.get<AdminWalletListResponse>('/api/admin/wallets', {
params: {
...params,
limit,
offset,
},
})
const data = response.data
items.push(...data.items)
if (items.length >= data.total || data.items.length < limit) {
break
}
const nextOffset = offset + data.items.length
if (nextOffset <= offset) {
throw new Error('分页游标未前进,终止全量钱包拉取以避免死循环')
}
offset = nextOffset
page += 1
}
if (page >= maxPages) {
throw new Error(`钱包列表分页超过最大页数 ${maxPages},已中止请求`)
}
return items
},
async getWalletDetail(walletId: string): Promise<AdminWalletDetailResponse> {
const response = await apiClient.get<AdminWalletDetailResponse>(`/api/admin/wallets/${walletId}`)
return response.data
},
async listLedger(params?: {
category?: string
reason_code?: string
owner_type?: string
limit?: number
offset?: number
}): Promise<AdminLedgerResponse> {
const response = await apiClient.get<AdminLedgerResponse>('/api/admin/wallets/ledger', { params })
return response.data
},
async listGlobalRefunds(params?: {
status?: string
owner_type?: string
limit?: number
offset?: number
}): Promise<AdminGlobalRefundsListResponse> {
const response = await apiClient.get<AdminGlobalRefundsListResponse>('/api/admin/wallets/refund-requests', {
params,
})
return response.data
},
async getWalletTransactions(
walletId: string,
params?: { limit?: number; offset?: number }
): Promise<AdminWalletTransactionsResponse> {
const response = await apiClient.get<AdminWalletTransactionsResponse>(
`/api/admin/wallets/${walletId}/transactions`,
{ params }
)
return response.data
},
async getWalletRefunds(
walletId: string,
params?: { limit?: number; offset?: number }
): Promise<AdminWalletRefundsResponse> {
const response = await apiClient.get<AdminWalletRefundsResponse>(
`/api/admin/wallets/${walletId}/refunds`,
{ params }
)
return response.data
},
async rechargeWallet(walletId: string, payload: ManualRechargeRequest): Promise<{
wallet: AdminWallet
payment_order: {
id: string
order_no: string
amount_usd: number
payment_method: string
status: string
created_at: string
credited_at: string | null
}
}> {
const response = await apiClient.post(`/api/admin/wallets/${walletId}/recharge`, payload)
return response.data
},
async adjustWallet(walletId: string, payload: WalletAdjustRequest): Promise<{
wallet: AdminWallet
transaction: WalletTransaction
}> {
const response = await apiClient.post(`/api/admin/wallets/${walletId}/adjust`, payload)
return response.data
},
async processRefund(walletId: string, refundId: string): Promise<{
wallet: AdminWallet
refund: RefundRequest
transaction: WalletTransaction
}> {
const response = await apiClient.post(
`/api/admin/wallets/${walletId}/refunds/${refundId}/process`,
{}
)
return response.data
},
async failRefund(walletId: string, refundId: string, payload: RefundFailRequest): Promise<{
wallet: AdminWallet
refund: RefundRequest
transaction: WalletTransaction | null
}> {
const response = await apiClient.post(
`/api/admin/wallets/${walletId}/refunds/${refundId}/fail`,
payload
)
return response.data
},
async completeRefund(
walletId: string,
refundId: string,
payload: RefundCompleteRequest
): Promise<{ refund: RefundRequest }> {
const response = await apiClient.post(
`/api/admin/wallets/${walletId}/refunds/${refundId}/complete`,
payload
)
return response.data
},
}

View File

@@ -1,5 +1,6 @@
import apiClient from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
// LDAP 配置导出结构
export interface LDAPConfigExport {
@@ -69,9 +70,8 @@ export interface UserExport {
allowed_api_formats?: string[] | null
allowed_models?: string[] | null
model_capability_settings?: Record<string, Record<string, boolean>>
quota_usd?: number | null
used_usd?: number
total_usd?: number
unlimited?: boolean
wallet?: BillingSummary | null
is_active: boolean
api_keys: UserApiKeyExport[]
}
@@ -82,8 +82,6 @@ export interface UserApiKeyExport {
key_encrypted?: string | null
name?: string | null
is_standalone: boolean
balance_used_usd?: number
current_balance_usd?: number | null
allowed_providers?: string[] | null
allowed_api_formats?: string[] | null
allowed_models?: string[] | null
@@ -329,10 +327,7 @@ export interface AdminApiKey {
name?: string
key_display?: string // 脱敏后的密钥显示
is_active: boolean
is_locked: boolean // 管理员锁定标志
is_standalone: boolean // 是否为独立余额Key
balance_used_usd?: number // 已使用余额仅独立Key
current_balance_usd?: number | null // 当前余额独立Key预付费模式null表示无限制
total_requests?: number
total_tokens?: number
total_cost_usd?: number
@@ -354,7 +349,8 @@ export interface CreateStandaloneApiKeyRequest {
allowed_models?: string[] | null
rate_limit?: number | null // null = 无限制
expires_at?: string | null // ISO 日期字符串,如 "2025-12-31"null = 永不过期
initial_balance_usd: number // 初始余额,必须设置
initial_balance_usd: number | null // 初始余额null = 无限制
unlimited_balance?: boolean | null // 编辑时仅切换额度模式,不调整余额数值
auto_delete_on_expiry?: boolean // 过期后是否自动删除
}
@@ -502,7 +498,10 @@ export const adminApi = {
},
// 更新独立余额Key
async updateApiKey(keyId: string, data: Partial<CreateStandaloneApiKeyRequest>): Promise<AdminApiKey & { message: string }> {
async updateApiKey(
keyId: string,
data: Partial<CreateStandaloneApiKeyRequest>
): Promise<AdminApiKey & { message: string }> {
const response = await apiClient.put<AdminApiKey & { message: string }>(
`/api/admin/api-keys/${keyId}`,
data
@@ -526,27 +525,10 @@ export const adminApi = {
return response.data
},
// 切换API密钥锁定状态(锁定/解锁)
async toggleLockApiKey(keyId: string): Promise<ApiKeyLockResponse> {
// 切换用户普通 API Key 锁定状态(锁定/解锁)
async toggleUserApiKeyLock(userId: string, keyId: string): Promise<ApiKeyLockResponse> {
const response = await apiClient.patch<ApiKeyLockResponse>(
`/api/admin/api-keys/${keyId}/lock`
)
return response.data
},
// 为独立余额Key调整余额
async addApiKeyBalance(keyId: string, amountUsd: number): Promise<AdminApiKey & { message: string }> {
const response = await apiClient.patch<AdminApiKey & { message: string }>(
`/api/admin/api-keys/${keyId}/balance`,
{ amount_usd: amountUsd }
)
return response.data
},
// 重置独立余额Key的已使用额度
async resetApiKeyUsage(keyId: string): Promise<AdminApiKey & { message: string }> {
const response = await apiClient.patch<AdminApiKey & { message: string }>(
`/api/admin/api-keys/${keyId}/reset-usage`
`/api/admin/users/${userId}/api-keys/${keyId}/lock`
)
return response.data
},

View File

@@ -89,15 +89,30 @@ export interface AuthSettingsResponse {
ldap_exclusive: boolean
}
export interface BillingSummary {
id?: string | null
balance: number
recharge_balance: number
gift_balance: number
refundable_balance: number
currency: string
status: string
limit_mode: 'finite' | 'unlimited'
unlimited: boolean
total_recharged: number
total_consumed: number
total_refunded: number
total_adjusted: number
updated_at?: string | null
}
export interface User {
id: string // UUID
username: string
email?: string
role: string // 'admin' or 'user'
is_active: boolean
quota_usd?: number | null
used_usd?: number
total_usd?: number
billing?: BillingSummary
allowed_providers?: string[] | null // 允许使用的提供商 ID 列表
allowed_api_formats?: string[] | null // 允许使用的 API 格式列表
allowed_models?: string[] | null // 允许使用的模型名称列表

View File

@@ -2,6 +2,7 @@ import apiClient from './client'
import type { ActivityHeatmap } from '@/types/activity'
import type { TieredPricingConfig } from './endpoints/types'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
export interface Profile {
id: string // UUID
@@ -9,9 +10,7 @@ export interface Profile {
username: string
role: string
is_active: boolean
quota_usd: number | null
used_usd: number
total_usd?: number // 累积消费总额
billing: BillingSummary
created_at: string
updated_at?: string
last_login_at?: string
@@ -103,8 +102,7 @@ export interface UsageResponse {
total_cost: number // 官方费率
total_actual_cost?: number // 倍率消耗(仅管理员可见)
avg_response_time: number
quota_usd: number | null
used_usd: number
billing: BillingSummary
summary_by_model: ModelSummary[]
summary_by_provider?: ProviderSummary[]
pagination?: {

View File

@@ -6,14 +6,13 @@ export interface User {
email: string
role: 'admin' | 'user'
is_active: boolean
quota_usd: number | null
used_usd: number
total_usd: number
unlimited: boolean
allowed_providers: string[] | null // 允许使用的提供商 ID 列表
allowed_api_formats: string[] | null // 允许使用的 API 格式列表
allowed_models: string[] | null // 允许使用的模型名称列表
created_at: string
updated_at?: string
last_login_at?: string | null
}
export interface CreateUserRequest {
@@ -21,7 +20,7 @@ export interface CreateUserRequest {
password: string
email: string
role?: 'admin' | 'user'
quota_usd?: number | null
initial_gift_usd?: number | null
unlimited?: boolean
allowed_providers?: string[] | null
allowed_api_formats?: string[] | null
@@ -32,7 +31,7 @@ export interface UpdateUserRequest {
email?: string
is_active?: boolean
role?: 'admin' | 'user'
quota_usd?: number | null
unlimited?: boolean
password?: string
allowed_providers?: string[] | null
allowed_api_formats?: string[] | null
@@ -50,8 +49,6 @@ export interface ApiKey {
is_active: boolean
is_locked: boolean // 管理员锁定标志
is_standalone: boolean // 是否为独立余额Key
balance_used_usd?: number // 已使用余额仅独立Key
current_balance_usd?: number | null // 当前余额独立Key预付费模式null表示无限制
rate_limit?: number // 速率限制(请求/分钟)
total_requests?: number // 总请求数
total_cost_usd?: number // 总费用
@@ -96,10 +93,12 @@ export const usersApi = {
await apiClient.delete(`/api/admin/users/${userId}/api-keys/${keyId}`)
},
async resetUserQuota(userId: string): Promise<void> {
await apiClient.patch(`/api/admin/users/${userId}/quota`)
async getFullApiKey(userId: string, keyId: string): Promise<{ key: string }> {
const response = await apiClient.get<{ key: string }>(
`/api/admin/users/${userId}/api-keys/${keyId}/full-key`
)
return response.data
},
// 管理员统计
async getUsageStats(): Promise<Record<string, unknown>> {
const response = await apiClient.get('/api/admin/usage/stats')

176
frontend/src/api/wallet.ts Normal file
View File

@@ -0,0 +1,176 @@
import apiClient from './client'
export interface WalletSummary {
id: string
// balance = 总可用余额(充值余额 + 赠款余额)
balance: number
recharge_balance: number
gift_balance: number
refundable_balance: number
currency: string
status: string
limit_mode?: 'finite' | 'unlimited'
unlimited?: boolean
total_recharged: number
total_consumed: number
total_refunded: number
total_adjusted: number
updated_at: string
}
export interface WalletBalanceResponse {
wallet: WalletSummary | null
unlimited: boolean
limit_mode: 'finite' | 'unlimited'
// balance = 总可用余额(充值余额 + 赠款余额)
balance: number | null
recharge_balance?: number | null
gift_balance?: number | null
refundable_balance?: number | null
currency: string
pending_refund_count?: number
}
export interface WalletTransaction {
id: string
category: string
reason_code: string
amount: number
// 总可用余额(充值+赠款)快照
balance_before: number
balance_after: number
// 分账户快照
recharge_balance_before: number
recharge_balance_after: number
gift_balance_before: number
gift_balance_after: number
link_type?: string | null
link_id?: string | null
operator_id?: string | null
operator_name?: string | null
operator_email?: string | null
description?: string | null
created_at: string
}
export interface WalletTransactionsResponse extends WalletBalanceResponse {
items: WalletTransaction[]
total: number
limit: number
offset: number
}
export interface PaymentOrder {
id: string
order_no: string
wallet_id: string
user_id: string | null
amount_usd: number
pay_amount: number | null
pay_currency: string | null
exchange_rate: number | null
refunded_amount_usd: number
refundable_amount_usd: number
payment_method: string
gateway_order_id: string | null
gateway_response: Record<string, unknown> | null
status: string
created_at: string
paid_at: string | null
credited_at: string | null
expires_at: string | null
}
export interface RefundRequest {
id: string
refund_no: string
payment_order_id: string | null
source_type: string
source_id: string | null
refund_mode: string
amount_usd: number
status: string
reason: string | null
failure_reason: string | null
gateway_refund_id: string | null
payout_method: string | null
payout_reference: string | null
payout_proof: Record<string, unknown> | null
created_at: string
updated_at: string
processed_at: string | null
completed_at: string | null
}
export interface WalletRechargeCreateRequest {
amount_usd: number
payment_method: string
pay_amount?: number
pay_currency?: string
exchange_rate?: number
}
export interface WalletRefundCreateRequest {
amount_usd: number
payment_order_id?: string
source_type?: string
source_id?: string
refund_mode?: string
reason?: string
idempotency_key?: string
}
export const walletApi = {
async getBalance(): Promise<WalletBalanceResponse> {
const response = await apiClient.get<WalletBalanceResponse>('/api/wallet/balance')
return response.data
},
async getTransactions(params?: { limit?: number; offset?: number }): Promise<WalletTransactionsResponse> {
const response = await apiClient.get<WalletTransactionsResponse>('/api/wallet/transactions', { params })
return response.data
},
async createRechargeOrder(payload: WalletRechargeCreateRequest): Promise<{
order: PaymentOrder
payment_instructions: Record<string, unknown>
}> {
const response = await apiClient.post('/api/wallet/recharge', payload)
return response.data
},
async listRechargeOrders(params?: { limit?: number; offset?: number }): Promise<{
items: PaymentOrder[]
total: number
limit: number
offset: number
}> {
const response = await apiClient.get('/api/wallet/recharge', { params })
return response.data
},
async getRechargeOrder(orderId: string): Promise<{ order: PaymentOrder }> {
const response = await apiClient.get(`/api/wallet/recharge/${orderId}`)
return response.data
},
async listRefunds(params?: { limit?: number; offset?: number }): Promise<{
items: RefundRequest[]
total: number
limit: number
offset: number
}> {
const response = await apiClient.get('/api/wallet/refunds', { params })
return response.data
},
async getRefund(refundId: string): Promise<RefundRequest> {
const response = await apiClient.get<RefundRequest>(`/api/wallet/refunds/${refundId}`)
return response.data
},
async createRefund(payload: WalletRefundCreateRequest): Promise<RefundRequest> {
const response = await apiClient.post<RefundRequest>('/api/wallet/refunds', payload)
return response.data
},
}