Merge PR #541: 添加账户订阅到期时间管理功能 + 修复核心过期检查逻辑

## 原PR功能
-  后端添加subscriptionExpiresAt字段支持
-  前端提供到期时间设置界面(快捷选项 + 自定义日期)
-  账户列表显示到期状态(已过期🔴/即将过期🟠/永不过期)
-  新增AccountExpiryEditModal.vue编辑弹窗组件
-  支持创建和更新账户时设置到期时间
-  完整支持暗黑模式

## 🔧 关键修复(本次提交)
原PR缺少核心过期检查逻辑,过期账户仍会被调度使用。本次合并时添加了:

1. **新增isAccountNotExpired()方法**:
   - 检查账户subscriptionExpiresAt字段
   - 未设置过期时间视为永不过期
   - 添加debug日志记录过期账户

2. **在selectAvailableAccount()中添加过期检查**:
   - 过滤逻辑中集成this.isAccountNotExpired(account)
   - 确保过期账户不被选择

3. **在selectAccountForApiKey()中添加过期检查**:
   - 绑定账户检查中添加过期验证
   - 共享池过滤中添加过期验证

## 🗑️ 清理工作
- 移除了不应提交的account_expire_feature.md评审文档(756行)

## 技术细节
- API层使用expiresAt,存储层使用subscriptionExpiresAt
- 存储格式:ISO 8601 (UTC)
- 空值表示:null表示永不过期
- 时区处理:后端UTC,前端自动转换本地时区

作者: mrlitong (原PR) + Claude Code (修复)
PR: https://github.com/Wei-Shaw/claude-relay-service/pull/541
This commit is contained in:
shaw
2025-10-12 13:42:57 +08:00
5 changed files with 728 additions and 10 deletions

View File

@@ -2266,7 +2266,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
autoStopOnWarning,
useUnifiedUserAgent,
useUnifiedClientId,
unifiedClientId
unifiedClientId,
expiresAt
} = req.body
if (!name) {
@@ -2309,7 +2310,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
autoStopOnWarning: autoStopOnWarning === true, // 默认为false
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
unifiedClientId: unifiedClientId || '' // 统一的客户端标识
unifiedClientId: unifiedClientId || '', // 统一的客户端标识
expiresAt: expiresAt || null // 账户订阅到期时间
})
// 如果是分组类型,将账户添加到分组
@@ -2396,7 +2398,14 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
}
}
await claudeAccountService.updateAccount(accountId, updates)
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates }
if ('expiresAt' in mappedUpdates) {
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
delete mappedUpdates.expiresAt
}
await claudeAccountService.updateAccount(accountId, mappedUpdates)
logger.success(`📝 Admin updated Claude account: ${accountId}`)
return res.json({ success: true, message: 'Claude account updated successfully' })

View File

@@ -73,7 +73,8 @@ class ClaudeAccountService {
autoStopOnWarning = false, // 5小时使用量接近限制时自动停止调度
useUnifiedUserAgent = false, // 是否使用统一Claude Code版本的User-Agent
useUnifiedClientId = false, // 是否使用统一的客户端标识
unifiedClientId = '' // 统一的客户端标识
unifiedClientId = '', // 统一的客户端标识
expiresAt = null // 账户订阅到期时间
} = options
const accountId = uuidv4()
@@ -113,7 +114,9 @@ class ClaudeAccountService {
? JSON.stringify(subscriptionInfo)
: claudeAiOauth.subscriptionInfo
? JSON.stringify(claudeAiOauth.subscriptionInfo)
: ''
: '',
// 账户订阅到期时间
subscriptionExpiresAt: expiresAt || ''
}
} else {
// 兼容旧格式
@@ -141,7 +144,9 @@ class ClaudeAccountService {
autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度
useUnifiedUserAgent: useUnifiedUserAgent.toString(), // 是否使用统一Claude Code版本的User-Agent
// 手动设置的订阅信息
subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : ''
subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : '',
// 账户订阅到期时间
subscriptionExpiresAt: expiresAt || ''
}
}
@@ -486,7 +491,7 @@ class ClaudeAccountService {
createdAt: account.createdAt,
lastUsedAt: account.lastUsedAt,
lastRefreshAt: account.lastRefreshAt,
expiresAt: account.expiresAt,
expiresAt: account.subscriptionExpiresAt || null, // 账户订阅到期时间
// 添加 scopes 字段用于判断认证方式
// 处理空字符串的情况,避免返回 ['']
scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [],
@@ -618,7 +623,8 @@ class ClaudeAccountService {
'autoStopOnWarning',
'useUnifiedUserAgent',
'useUnifiedClientId',
'unifiedClientId'
'unifiedClientId',
'subscriptionExpiresAt'
]
const updatedData = { ...accountData }
let shouldClearAutoStopFields = false
@@ -637,6 +643,9 @@ class ClaudeAccountService {
} else if (field === 'subscriptionInfo') {
// 处理订阅信息更新
updatedData[field] = typeof value === 'string' ? value : JSON.stringify(value)
} else if (field === 'subscriptionExpiresAt') {
// 处理订阅到期时间,允许 null 值(永不过期)
updatedData[field] = value ? value.toString() : ''
} else if (field === 'claudeAiOauth') {
// 更新 Claude AI OAuth 数据
if (value) {
@@ -650,7 +659,7 @@ class ClaudeAccountService {
updatedData.lastRefreshAt = new Date().toISOString()
}
} else {
updatedData[field] = value.toString()
updatedData[field] = value !== null && value !== undefined ? value.toString() : ''
}
}
}