♻️ refactor: 重构余额系统 - 基于 usePricing 逻辑优化
主要改进: - 🔄 完全重构 balance.ts,参考 usePricing.md 的最佳实践 - ✅ 集成余额检查、扣费、错误处理到单一 API - 🎯 使用 authClient.subscription.meterEvent 替代自定义 API - 💡 添加 checkTokenBalance 和 redirectToPricePage 辅助方法 - 🧹 简化 form.tsx 中的扣费流程,减少60%代码量 技术细节: - 从 metered 订阅类型获取 creditBalance - 使用 React Native Alert 替代 Web toast - 余额不足时自动引导用户充值 - 扣费成功后自动刷新余额显示 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, any>;
|
||||
}
|
||||
|
||||
export interface ChargeTransaction {
|
||||
id: string;
|
||||
userId: string;
|
||||
amount: number;
|
||||
type: 'charge' | 'refund';
|
||||
description?: string;
|
||||
metadata?: Record<string, any>;
|
||||
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<BalanceResponse> {
|
||||
return apiClient<BalanceResponse>('/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<ChargeResponse> {
|
||||
return apiClient<ChargeResponse>('/api/users/charge', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
export async function checkTokenBalance(tokens: number): Promise<BalanceCheckResult> {
|
||||
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<TokenUsageResponse> {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user