mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat(admin): 新增账户余额/配额查询与展示
- 新增 accountBalanceService 与多 Provider 适配(Claude/Claude Console/OpenAI Responses/通用) - Redis 增加余额查询结果与本地统计缓存读写 - 管理端新增 /admin/accounts/balance 相关接口与汇总接口,并在应用启动时注册 Provider - 后台前端新增余额组件与 Dashboard 余额/配额汇总、低余额/高使用提示 - 补充 accountBalanceService 单元测试
This commit is contained in:
133
src/services/balanceProviders/baseBalanceProvider.js
Normal file
133
src/services/balanceProviders/baseBalanceProvider.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const axios = require('axios')
|
||||
const logger = require('../../utils/logger')
|
||||
const ProxyHelper = require('../../utils/proxyHelper')
|
||||
|
||||
/**
|
||||
* Provider 抽象基类
|
||||
* 各平台 Provider 需继承并实现 queryBalance(account)
|
||||
*/
|
||||
class BaseBalanceProvider {
|
||||
constructor(platform) {
|
||||
this.platform = platform
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询余额(抽象方法)
|
||||
* @param {object} account - 账户对象
|
||||
* @returns {Promise<object>}
|
||||
* 形如:
|
||||
* {
|
||||
* balance: number|null,
|
||||
* currency?: string,
|
||||
* quota?: { daily, used, remaining, resetAt, percentage, unlimited? },
|
||||
* queryMethod?: 'api'|'field'|'local',
|
||||
* rawData?: any
|
||||
* }
|
||||
*/
|
||||
async queryBalance(_account) {
|
||||
throw new Error('queryBalance 方法必须由子类实现')
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 HTTP 请求方法(支持代理)
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
* @param {object} account
|
||||
*/
|
||||
async makeRequest(url, options = {}, account = {}) {
|
||||
const config = {
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 15000,
|
||||
data: options.data,
|
||||
params: options.params,
|
||||
responseType: options.responseType
|
||||
}
|
||||
|
||||
const proxyConfig = account.proxyConfig || account.proxy
|
||||
if (proxyConfig) {
|
||||
const agent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
if (agent) {
|
||||
config.httpAgent = agent
|
||||
config.httpsAgent = agent
|
||||
config.proxy = false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios(config)
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
status: response.status,
|
||||
headers: response.headers
|
||||
}
|
||||
} catch (error) {
|
||||
const status = error.response?.status
|
||||
const message = error.response?.data?.message || error.message || '请求失败'
|
||||
this.logger.debug(`余额 Provider HTTP 请求失败: ${url} (${this.platform})`, {
|
||||
status,
|
||||
message
|
||||
})
|
||||
return { success: false, status, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从账户字段读取 dailyQuota / dailyUsage(通用降级方案)
|
||||
* 注意:部分平台 dailyUsage 字段可能不是实时值,最终以 AccountBalanceService 的本地统计为准
|
||||
*/
|
||||
readQuotaFromFields(account) {
|
||||
const dailyQuota = Number(account?.dailyQuota || 0)
|
||||
const dailyUsage = Number(account?.dailyUsage || 0)
|
||||
|
||||
// 无限制
|
||||
if (!Number.isFinite(dailyQuota) || dailyQuota <= 0) {
|
||||
return {
|
||||
balance: null,
|
||||
currency: 'USD',
|
||||
quota: {
|
||||
daily: Infinity,
|
||||
used: Number.isFinite(dailyUsage) ? dailyUsage : 0,
|
||||
remaining: Infinity,
|
||||
percentage: 0,
|
||||
unlimited: true
|
||||
},
|
||||
queryMethod: 'field'
|
||||
}
|
||||
}
|
||||
|
||||
const used = Number.isFinite(dailyUsage) ? dailyUsage : 0
|
||||
const remaining = Math.max(0, dailyQuota - used)
|
||||
const percentage = dailyQuota > 0 ? (used / dailyQuota) * 100 : 0
|
||||
|
||||
return {
|
||||
balance: remaining,
|
||||
currency: 'USD',
|
||||
quota: {
|
||||
daily: dailyQuota,
|
||||
used,
|
||||
remaining,
|
||||
percentage: Math.round(percentage * 100) / 100
|
||||
},
|
||||
queryMethod: 'field'
|
||||
}
|
||||
}
|
||||
|
||||
parseCurrency(data) {
|
||||
return data?.currency || data?.Currency || 'USD'
|
||||
}
|
||||
|
||||
async safeExecute(fn, fallbackValue = null) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
this.logger.error(`余额 Provider 执行失败: ${this.platform}`, error)
|
||||
return fallbackValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseBalanceProvider
|
||||
Reference in New Issue
Block a user