diff --git a/src/routes/admin.js b/src/routes/admin.js index f0a3035e..bf61bd26 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2795,7 +2795,14 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, } } - await claudeConsoleAccountService.updateAccount(accountId, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + await claudeConsoleAccountService.updateAccount(accountId, mappedUpdates) logger.success(`📝 Admin updated Claude Console account: ${accountId}`) return res.json({ success: true, message: 'Claude Console account updated successfully' }) @@ -3205,7 +3212,14 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => { } } - await ccrAccountService.updateAccount(accountId, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + await ccrAccountService.updateAccount(accountId, mappedUpdates) logger.success(`📝 Admin updated CCR account: ${accountId}`) return res.json({ success: true, message: 'CCR account updated successfully' }) @@ -3566,7 +3580,14 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) = }) } - const result = await bedrockAccountService.updateAccount(accountId, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + const result = await bedrockAccountService.updateAccount(accountId, mappedUpdates) if (!result.success) { return res @@ -3891,6 +3912,8 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { return { ...account, + // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt + expiresAt: account.subscriptionExpiresAt || null, groupInfos, usage: { daily: usageStats.daily, @@ -3908,6 +3931,8 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, + // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt + expiresAt: account.subscriptionExpiresAt || null, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, @@ -4032,7 +4057,14 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) => } } - const updatedAccount = await geminiAccountService.updateAccount(accountId, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates) logger.success(`📝 Admin updated Gemini account: ${accountId}`) return res.json({ success: true, data: updatedAccount }) @@ -7539,6 +7571,13 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => { : currentAccount.emailVerified } + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt (订阅过期时间) + // 注意:这里不影响上面 OAuth token 的 expiresAt 字段 + if ('expiresAt' in updates && !updates.openaiOauth?.expires_in) { + updateData.subscriptionExpiresAt = updates.expiresAt + delete updateData.expiresAt + } + const updatedAccount = await openaiAccountService.updateAccount(id, updateData) // 如果需要刷新但不强制成功(非关键更新) @@ -7925,7 +7964,14 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => const { id } = req.params const updates = req.body - const account = await azureOpenaiAccountService.updateAccount(id, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates) res.json({ success: true, @@ -8298,7 +8344,14 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) updates.priority = priority.toString() } - const result = await openaiResponsesAccountService.updateAccount(id, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + const result = await openaiResponsesAccountService.updateAccount(id, mappedUpdates) if (!result.success) { return res.status(400).json(result) @@ -8664,6 +8717,9 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => { return { ...account, + // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt + // OAuth token 的原始 expiresAt 保留在内部使用 + expiresAt: account.subscriptionExpiresAt || null, schedulable: account.schedulable === 'true', boundApiKeysCount, groupInfos, @@ -8677,6 +8733,8 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => { logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message) return { ...account, + // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt + expiresAt: account.subscriptionExpiresAt || null, boundApiKeysCount: 0, groupInfos: [], usage: { @@ -8791,7 +8849,14 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => { updates.accountType = targetAccountType } - const account = await droidAccountService.updateAccount(id, updates) + // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt + const mappedUpdates = { ...updates } + if ('expiresAt' in mappedUpdates) { + mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + delete mappedUpdates.expiresAt + } + + const account = await droidAccountService.updateAccount(id, mappedUpdates) try { if (currentAccount.accountType === 'group' && targetAccountType !== 'group') { diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 5f672f2a..aa7c261f 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -3724,20 +3724,55 @@ const closeAccountExpiryEdit = () => { editingExpiryAccount.value = null } +// 根据账户平台解析更新端点 +const resolveAccountUpdateEndpoint = (account) => { + switch (account.platform) { + case 'claude': + return `/admin/claude-accounts/${account.id}` + case 'claude-console': + return `/admin/claude-console-accounts/${account.id}` + case 'bedrock': + return `/admin/bedrock-accounts/${account.id}` + case 'openai': + return `/admin/openai-accounts/${account.id}` + case 'azure_openai': + return `/admin/azure-openai-accounts/${account.id}` + case 'openai-responses': + return `/admin/openai-responses-accounts/${account.id}` + case 'ccr': + return `/admin/ccr-accounts/${account.id}` + case 'gemini': + return `/admin/gemini-accounts/${account.id}` + case 'droid': + return `/admin/droid-accounts/${account.id}` + default: + throw new Error(`Unsupported platform: ${account.platform}`) + } +} + // 保存账户过期时间 const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => { try { - const data = await apiClient.put(`/admin/claude-accounts/${accountId}`, { + // 找到对应的账户以获取平台信息 + const account = accounts.value.find((acc) => acc.id === accountId) + if (!account) { + showToast('账户不存在', 'error') + if (expiryEditModalRef.value) { + expiryEditModalRef.value.resetSaving() + } + return + } + + // 根据平台动态选择端点 + const endpoint = resolveAccountUpdateEndpoint(account) + const data = await apiClient.put(endpoint, { expiresAt: expiresAt || null }) if (data.success) { showToast('账户到期时间已更新', 'success') // 更新本地数据 - const account = accounts.value.find((acc) => acc.id === accountId) - if (account) { - account.expiresAt = expiresAt || null - } + account.expiresAt = expiresAt || null closeAccountExpiryEdit() } else { showToast(data.message || '更新失败', 'error') @@ -3747,7 +3782,7 @@ const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => { } } } catch (error) { - showToast('更新失败', 'error') + showToast(error.message || '更新失败', 'error') // 重置保存状态 if (expiryEditModalRef.value) { expiryEditModalRef.value.resetSaving()