refactor: 统一账户过期时间字段映射和检查逻辑

主要改进:
1. 创建 mapExpiryField() 工具函数统一处理前后端字段映射(expiresAt -> subscriptionExpiresAt)
2. 统一 subscriptionExpiresAt 初始值为 null(替代空字符串)
3. 规范过期检查方法名为 isSubscriptionExpired(),返回 true 表示已过期
4. 优化过期检查条件判断,只检查 null 而非空字符串
5. 补充 OpenAI-Responses 和调度器中缺失的过期检查逻辑
6. 添加代码评审文档记录未修复问题

影响范围:
- 所有 9 种账户服务的过期字段处理
- admin.js 中所有账户更新路由
- 统一调度器的过期账户过滤逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mrlitong
2025-10-14 08:04:05 +00:00
parent ba60a2dcbb
commit cbc3a83f11
13 changed files with 410 additions and 82 deletions

View File

@@ -32,6 +32,26 @@ const ProxyHelper = require('../utils/proxyHelper')
const router = express.Router()
// 🛠️ 工具函数:映射前端字段名到后端字段名
/**
* 映射前端的 expiresAt 字段到后端的 subscriptionExpiresAt 字段
* @param {Object} updates - 更新对象
* @param {string} accountType - 账户类型 (如 'Claude', 'OpenAI' 等)
* @param {string} accountId - 账户 ID
* @returns {Object} 映射后的更新对象
*/
function mapExpiryField(updates, accountType, accountId) {
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(
`Mapping expiresAt to subscriptionExpiresAt for ${accountType} account ${accountId}`
)
}
return mappedUpdates
}
// 👥 用户管理
// 获取所有用户列表用于API Key分配
@@ -2399,11 +2419,7 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
}
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
}
const mappedUpdates = mapExpiryField(updates, 'Claude', accountId)
await claudeAccountService.updateAccount(accountId, mappedUpdates)
@@ -2746,14 +2762,7 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(
`Mapping expiresAt to subscriptionExpiresAt for Claude Console account ${accountId}`
)
}
const mappedUpdates = mapExpiryField(updates, 'Claude Console', accountId)
// 验证priority的有效性1-100
if (
@@ -3172,12 +3181,7 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for CCR account ${accountId}`)
}
const mappedUpdates = mapExpiryField(updates, 'CCR', accountId)
// 验证priority的有效性1-100
if (
@@ -3575,12 +3579,7 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) =
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for Bedrock account ${accountId}`)
}
const mappedUpdates = mapExpiryField(updates, 'Bedrock', accountId)
// 验证priority的有效性1-100
if (
@@ -4047,12 +4046,7 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
}
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for Gemini account ${accountId}`)
}
const mappedUpdates = mapExpiryField(updates, 'Gemini', accountId)
// 处理分组的变更
if (mappedUpdates.accountType !== undefined) {
@@ -7430,12 +7424,7 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for OpenAI account ${id}`)
}
const mappedUpdates = mapExpiryField(updates, 'OpenAI', id)
const { needsImmediateRefresh, requireRefreshSuccess } = mappedUpdates
@@ -7988,12 +7977,7 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) =>
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for Azure OpenAI account ${id}`)
}
const mappedUpdates = mapExpiryField(updates, 'Azure OpenAI', id)
const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates)
@@ -8357,12 +8341,7 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for OpenAI-Responses account ${id}`)
}
const mappedUpdates = mapExpiryField(updates, 'OpenAI-Responses', id)
// 验证priority的有效性1-100
if (mappedUpdates.priority !== undefined) {
@@ -8838,12 +8817,7 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
const updates = { ...req.body }
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
logger.info(`Mapping expiresAt to subscriptionExpiresAt for Droid account ${id}`)
}
const mappedUpdates = mapExpiryField(updates, 'Droid', id)
const { accountType: rawAccountType, groupId, groupIds } = mappedUpdates

View File

@@ -132,7 +132,7 @@ async function createAccount(accountData) {
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意Azure OpenAI 使用 API Key 认证,没有 OAuth token因此没有 expiresAt
subscriptionExpiresAt: accountData.subscriptionExpiresAt || '',
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
// 状态字段
isActive: accountData.isActive !== false ? 'true' : 'false',
@@ -317,7 +317,8 @@ async function getAllAccounts() {
schedulable: accountData.schedulable !== 'false',
// ✅ 前端显示订阅过期时间(业务字段)
expiresAt: accountData.subscriptionExpiresAt || null
expiresAt: accountData.subscriptionExpiresAt || null,
platform: 'azure-openai'
})
}
}
@@ -351,7 +352,7 @@ async function getSharedAccounts() {
* @returns {boolean} - true: 已过期, false: 未过期
*/
function isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)

View File

@@ -59,7 +59,7 @@ class BedrockAccountService {
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意Bedrock 使用 AWS 凭证,没有 OAuth token因此没有 expiresAt
subscriptionExpiresAt: options.subscriptionExpiresAt || '',
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -154,6 +154,7 @@ class BedrockAccountService {
createdAt: account.createdAt,
updatedAt: account.updatedAt,
type: 'bedrock',
platform: 'bedrock',
hasCredentials: !!account.awsCredentials
})
}
@@ -299,7 +300,7 @@ class BedrockAccountService {
const availableAccounts = accountsResult.data.filter((account) => {
// ✅ 检查账户订阅是否过期
if (this._isSubscriptionExpired(account)) {
if (this.isSubscriptionExpired(account)) {
logger.debug(
`⏰ Skipping expired Bedrock account: ${account.name}, expired at ${account.subscriptionExpiresAt || account.expiresAt}`
)
@@ -380,8 +381,8 @@ class BedrockAccountService {
* @param {Object} account - 账户对象
* @returns {boolean} - true: 已过期, false: 未过期
*/
_isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') {
isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)

View File

@@ -79,7 +79,7 @@ class CcrAccountService {
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意CCR 使用 API Key 认证,没有 OAuth token因此没有 expiresAt
subscriptionExpiresAt: options.subscriptionExpiresAt || '',
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
createdAt: new Date().toISOString(),
lastUsedAt: '',

View File

@@ -787,13 +787,13 @@ class ClaudeAccountService {
}
/**
* 检查账户是否过期
* 检查账户订阅是否过期
* @param {Object} account - 账户对象
* @returns {boolean} - 如果未设置过期时间或未过期返回 true
* @returns {boolean} - true: 已过期, false: 未过期
*/
isAccountNotExpired(account) {
isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt) {
return true // 未设置过期时间,视为永不过期
return false // 未设置过期时间,视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)
@@ -803,10 +803,10 @@ class ClaudeAccountService {
logger.debug(
`⏰ Account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
)
return false
return true
}
return true
return false
}
// 🎯 智能选择可用账户支持sticky会话和模型过滤
@@ -819,7 +819,7 @@ class ClaudeAccountService {
account.isActive === 'true' &&
account.status !== 'error' &&
account.schedulable !== 'false' &&
this.isAccountNotExpired(account)
!this.isSubscriptionExpired(account)
)
// 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
@@ -915,7 +915,7 @@ class ClaudeAccountService {
boundAccount.isActive === 'true' &&
boundAccount.status !== 'error' &&
boundAccount.schedulable !== 'false' &&
this.isAccountNotExpired(boundAccount)
!this.isSubscriptionExpired(boundAccount)
) {
logger.info(
`🎯 Using bound dedicated account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
@@ -937,7 +937,7 @@ class ClaudeAccountService {
account.status !== 'error' &&
account.schedulable !== 'false' &&
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
this.isAccountNotExpired(account)
!this.isSubscriptionExpired(account)
)
// 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号

View File

@@ -86,7 +86,7 @@ class ClaudeConsoleAccountService {
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意Claude Console 没有 OAuth token因此没有 expiresAttoken过期
subscriptionExpiresAt: options.subscriptionExpiresAt || '',
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
// 限流相关
rateLimitedAt: '',

View File

@@ -738,7 +738,7 @@ class DroidAccountService {
expiresAt: normalizedExpiresAt || '', // OAuth Token 过期时间(技术字段,自动刷新)
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
subscriptionExpiresAt: options.subscriptionExpiresAt || '',
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
@@ -828,6 +828,7 @@ class DroidAccountService {
// ✅ 前端显示订阅过期时间(业务字段)
expiresAt: account.subscriptionExpiresAt || null,
platform: account.platform || 'droid',
apiKeyCount: (() => {
const parsedCount = this._parseApiKeyEntries(account.apiKeys).length
@@ -1276,8 +1277,8 @@ class DroidAccountService {
* @param {Object} account - 账户对象
* @returns {boolean} - true: 已过期, false: 未过期
*/
_isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') {
isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)
@@ -1330,7 +1331,7 @@ class DroidAccountService {
const status = typeof account.status === 'string' ? account.status.toLowerCase() : ''
// ✅ 检查账户订阅是否过期
if (this._isSubscriptionExpired(account)) {
if (this.isSubscriptionExpired(account)) {
logger.debug(
`⏰ Skipping expired Droid account: ${account.name}, expired at ${account.subscriptionExpiresAt}`
)

View File

@@ -389,7 +389,7 @@ async function createAccount(accountData) {
scopes: accountData.geminiOauth ? accountData.scopes || OAUTH_SCOPES.join(' ') : '',
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
subscriptionExpiresAt: accountData.subscriptionExpiresAt || '',
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
// 代理设置
proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
@@ -814,7 +814,7 @@ function isTokenExpired(account) {
* @returns {boolean} - true: 已过期, false: 未过期
*/
function isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)

View File

@@ -340,7 +340,7 @@ function isTokenExpired(account) {
* @returns {boolean} - true: 已过期, false: 未过期
*/
function isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)
@@ -571,7 +571,7 @@ async function createAccount(accountData) {
: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // OAuth Token 过期时间(技术字段)
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
subscriptionExpiresAt: accountData.subscriptionExpiresAt || '',
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
// 状态字段
isActive: accountData.isActive !== false ? 'true' : 'false',

View File

@@ -78,7 +78,7 @@ class OpenAIResponsesAccountService {
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意OpenAI-Responses 使用 API Key 认证,没有 OAuth token因此没有 expiresAt
subscriptionExpiresAt: options.subscriptionExpiresAt || '',
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
createdAt: new Date().toISOString(),
lastUsedAt: '',
@@ -225,6 +225,7 @@ class OpenAIResponsesAccountService {
// ✅ 前端显示订阅过期时间(业务字段)
account.expiresAt = account.subscriptionExpiresAt || null
account.platform = account.platform || 'openai-responses'
accounts.push(account)
}
@@ -274,6 +275,7 @@ class OpenAIResponsesAccountService {
// ✅ 前端显示订阅过期时间(业务字段)
accountData.expiresAt = accountData.subscriptionExpiresAt || null
accountData.platform = accountData.platform || 'openai-responses'
accounts.push(accountData)
}
@@ -521,6 +523,25 @@ class OpenAIResponsesAccountService {
return { success: true, message: 'Account status reset successfully' }
}
// ⏰ 检查账户订阅是否已过期
isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt) {
return false // 未设置过期时间,视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ OpenAI-Responses Account ${account.name} (${account.id}) subscription expired at ${account.subscriptionExpiresAt}`
)
return true
}
return false
}
// 获取限流信息
_getRateLimitInfo(accountData) {
if (accountData.rateLimitStatus !== 'limited') {

View File

@@ -545,6 +545,18 @@ class UnifiedClaudeScheduler {
continue
}
// 检查订阅是否过期
if (account.subscriptionExpiresAt) {
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ Claude Console account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
)
continue
}
}
// 主动触发一次额度检查,确保状态即时生效
try {
await claudeConsoleAccountService.checkQuotaUsage(account.id)
@@ -642,6 +654,18 @@ class UnifiedClaudeScheduler {
continue
}
// 检查订阅是否过期
if (account.subscriptionExpiresAt) {
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ CCR account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
)
continue
}
}
// 检查是否被限流
const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
@@ -774,6 +798,17 @@ class UnifiedClaudeScheduler {
) {
return false
}
// 检查订阅是否过期
if (account.subscriptionExpiresAt) {
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ Claude Console account ${account.name} (${accountId}) expired at ${account.subscriptionExpiresAt} (session check)`
)
return false
}
}
// 检查是否超额
try {
await claudeConsoleAccountService.checkQuotaUsage(accountId)
@@ -832,6 +867,17 @@ class UnifiedClaudeScheduler {
if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel, 'in session check')) {
return false
}
// 检查订阅是否过期
if (account.subscriptionExpiresAt) {
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ CCR account ${account.name} (${accountId}) expired at ${account.subscriptionExpiresAt} (session check)`
)
return false
}
}
// 检查是否超额
try {
await ccrAccountService.checkQuotaUsage(accountId)
@@ -1353,6 +1399,18 @@ class UnifiedClaudeScheduler {
continue
}
// 检查订阅是否过期
if (account.subscriptionExpiresAt) {
const expiryDate = new Date(account.subscriptionExpiresAt)
const now = new Date()
if (expiryDate <= now) {
logger.debug(
`⏰ CCR account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
)
continue
}
}
// 检查是否被限流或超额
const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)

View File

@@ -211,6 +211,15 @@ class UnifiedOpenAIScheduler {
error.statusCode = 403 // Forbidden - 调度被禁止
throw error
}
// ⏰ 检查 OpenAI-Responses 专属账户订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(boundAccount)) {
const errorMsg = `Dedicated account ${boundAccount.name} subscription has expired`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 403 // Forbidden - 订阅已过期
throw error
}
}
// 专属账户可选的模型检查只有明确配置了supportedModels且不为空才检查
@@ -461,6 +470,14 @@ class UnifiedOpenAIScheduler {
}
}
// ⏰ 检查订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(account)) {
logger.debug(
`⏭️ Skipping OpenAI-Responses account ${account.name} - subscription expired`
)
continue
}
// OpenAI-Responses 账户默认支持所有模型
// 因为它们是第三方兼容 API模型支持由第三方决定
@@ -536,6 +553,11 @@ class UnifiedOpenAIScheduler {
logger.info(`🚫 OpenAI-Responses account ${accountId} is not schedulable`)
return false
}
// ⏰ 检查订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(account)) {
logger.info(`🚫 OpenAI-Responses account ${accountId} subscription expired`)
return false
}
// 检查并清除过期的限流状态
const isRateLimitCleared =
await openaiResponsesAccountService.checkAndClearRateLimit(accountId)