diff --git a/app/templates/[id]/form.tsx b/app/templates/[id]/form.tsx index 38c7a31..32179af 100644 --- a/app/templates/[id]/form.tsx +++ b/app/templates/[id]/form.tsx @@ -20,7 +20,7 @@ export const unstable_settings = { import { BackButton } from '@/components/ui/back-button'; import { DynamicFormField } from '@/components/forms/dynamic-form-field'; import { getTemplateById } from '@/lib/api/templates'; -import { getUserBalance, chargeUser } from '@/lib/api/balance'; +import { recordTokenUsage } from '@/lib/api/balance'; import { runTemplate } from '@/lib/api/template-runs'; import { Template, TemplateGraphNode } from '@/lib/types/template'; @@ -105,47 +105,23 @@ export default function TemplateFormScreen() { setIsSubmitting(true); try { - // 1. 检查余额 - const balanceResponse = await getUserBalance(); - - if (!balanceResponse.success) { - Alert.alert('错误', '无法获取账户余额'); - return; - } - - const balance = balanceResponse.data.balance; + // 1. 扣费(已集成余额检查和错误处理) const requiredAmount = template.costPrice || 0; - - if (balance < requiredAmount) { - Alert.alert( - '余额不足', - `当前余额: ${balance}\n所需费用: ${requiredAmount}\n请先充值`, - [ - { text: '取消', style: 'cancel' }, - { text: '去充值', onPress: () => router.push('/recharge') }, - ] - ); - return; - } - - // 2. 扣费 - const chargeResponse = await chargeUser({ - amount: requiredAmount, - description: `生成视频 - ${template.title}`, + const usageResponse = await recordTokenUsage({ + price: requiredAmount, + name: `生成视频 - ${template.title}`, metadata: { templateId: template.id, templateTitle: template.title, }, }); - if (!chargeResponse.success) { - Alert.alert('错误', '扣费失败,请稍后重试'); + if (!usageResponse.success) { + // recordTokenUsage 已经显示了错误提示,这里不需要重复 return; } - const transactionId = chargeResponse.data.id; - - // 3. 调用 template run + // 2. 调用 template run const runResponse = await runTemplate(id, formData); if (!runResponse.success) { @@ -155,7 +131,7 @@ export default function TemplateFormScreen() { const generationId = runResponse.data; - // 4. 跳转到结果页面 + // 3. 跳转到结果页面 Alert.alert('成功', '视频生成任务已创建', [ { text: '查看结果', diff --git a/lib/api/balance.ts b/lib/api/balance.ts index f792ac9..616aa89 100644 --- a/lib/api/balance.ts +++ b/lib/api/balance.ts @@ -1,50 +1,214 @@ -import { apiClient } from './client'; +import { Alert } from 'react-native'; +import { router } from 'expo-router'; +import { authClient } from '../auth/client'; export interface UserBalance { - userId: string; - balance: number; - currency: string; + remainingTokenBalance: number; + totalTokenBalance: number; + usedTokenBalance: number; } export interface BalanceResponse { success: boolean; data: UserBalance; + message?: string; } -export interface ChargeRequest { - amount: number; - description?: string; +export interface TokenUsageRequest { + price: number; + name: string; metadata?: Record; } -export interface ChargeTransaction { - id: string; - userId: string; - amount: number; - type: 'charge' | 'refund'; - description?: string; - metadata?: Record; - createdAt: string; -} - -export interface ChargeResponse { +export interface TokenUsageResponse { success: boolean; - data: ChargeTransaction; + data?: { + identifier: string; + remainingBalance: number; + }; + message?: string; +} + +export interface BalanceCheckResult { + hasEnough: boolean; + currentBalance: number; + isLoading: boolean; + message?: string; } /** * 获取用户余额 + * 从 metered 类型的订阅中获取 creditBalance */ export async function getUserBalance(): Promise { - return apiClient('/api/users/balance'); + try { + const { data, error } = await authClient.subscription.list({}); + + if (error) { + return { + success: false, + data: { + remainingTokenBalance: 0, + totalTokenBalance: 0, + usedTokenBalance: 0, + }, + message: error.message || '获取余额失败', + }; + } + + // 找到 metered 类型的订阅 + const meteredSubscriptions = data?.filter((sub: any) => sub.type === 'metered') || []; + + if (meteredSubscriptions.length === 0) { + return { + success: false, + data: { + remainingTokenBalance: 0, + totalTokenBalance: 0, + usedTokenBalance: 0, + }, + message: '未找到计费订阅', + }; + } + + const creditBalance = (meteredSubscriptions[0] as any)?.creditBalance || {}; + + return { + success: true, + data: { + remainingTokenBalance: creditBalance.remainingTokenBalance || 0, + totalTokenBalance: creditBalance.totalTokenBalance || 0, + usedTokenBalance: creditBalance.usedTokenBalance || 0, + }, + }; + } catch (error) { + console.error('Failed to get user balance:', error); + return { + success: false, + data: { + remainingTokenBalance: 0, + totalTokenBalance: 0, + usedTokenBalance: 0, + }, + message: '网络错误,请稍后重试', + }; + } } /** - * 扣费 + * 检查用户余额是否足够 */ -export async function chargeUser(data: ChargeRequest): Promise { - return apiClient('/api/users/charge', { - method: 'POST', - body: JSON.stringify(data), - }); +export async function checkTokenBalance(tokens: number): Promise { + const balanceResponse = await getUserBalance(); + + if (!balanceResponse.success) { + return { + hasEnough: false, + currentBalance: 0, + isLoading: false, + message: balanceResponse.message || '无法获取余额', + }; + } + + const currentBalance = balanceResponse.data.remainingTokenBalance; + const hasEnough = currentBalance >= tokens; + + return { + hasEnough, + currentBalance, + isLoading: false, + message: hasEnough + ? undefined + : `余额不足,当前: ${currentBalance.toLocaleString()}, 需要: ${tokens.toLocaleString()}`, + }; +} + +/** + * 扣除 Token 使用量 + * 参考 usePricing 中的 recordTokenUsage 方法 + */ +export async function recordTokenUsage( + request: TokenUsageRequest +): Promise { + const { price, name, metadata } = request; + + // price 的单位就是 token + const tokens = Math.ceil(price); + + try { + // 1. 检查余额是否足够 + const balanceCheck = await checkTokenBalance(tokens); + + if (balanceCheck.isLoading) { + return { + success: false, + message: '正在检查余额...', + }; + } + + if (!balanceCheck.hasEnough) { + // 余额不足,提示用户并引导充值 + Alert.alert( + '余额不足', + `当前余额: ${balanceCheck.currentBalance}\n需要费用: ${tokens}\n请先充值`, + [ + { text: '取消', style: 'cancel' }, + { + text: '去充值', + onPress: () => { + router.push('/(tabs)' as any); + }, + }, + ] + ); + + return { + success: false, + message: balanceCheck.message || '余额不足', + }; + } + + // 2. 余额充足,执行消费操作 + const { data, error } = await authClient.subscription.meterEvent({ + event_name: 'token_usage', + payload: { + value: tokens.toString(), + ...(metadata && { metadata }), + } as any, + }); + + if (error) { + Alert.alert('扣费失败', error.message || '请稍后重试'); + return { + success: false, + message: error.message || '扣费失败', + }; + } + + // 3. 扣费成功,返回结果 + const balanceAfter = await getUserBalance(); + return { + success: true, + data: { + identifier: data?.identifier || '', + remainingBalance: balanceAfter.data.remainingTokenBalance, + }, + message: '扣费成功', + }; + } catch (error) { + console.error('Failed to record token usage:', error); + const errorMessage = error instanceof Error ? error.message : '扣费失败,请稍后重试'; + Alert.alert('错误', errorMessage); + return { + success: false, + message: errorMessage, + }; + } +} + +/** + * 跳转到充值页面 + */ +export function redirectToPricePage() { + router.push('/(tabs)' as any); }