diff --git a/account_expire_bugfix.md b/account_expire_bugfix.md deleted file mode 100644 index 34f3afe1..00000000 --- a/account_expire_bugfix.md +++ /dev/null @@ -1,684 +0,0 @@ -# 账号过期功能代码评审报告 - -**评审日期**: 2025-10-12 -**评审范围**: 账号订阅过期时间管理功能 -**代码状态**: 部分完成,存在严重缺陷 -**评审结论**: ❌ 不建议合并,需要修复核心缺陷 - ---- - -## 📋 执行摘要 - -本次功能开发实现了为所有 9 个账户平台添加订阅过期时间管理功能。在数据存储和前端展示层面,实现**完整且准确**。但在**核心调度逻辑层存在严重缺陷**: - -**✅ 已完成**: -- 前端路由层:所有平台支持过期时间编辑 -- 后端数据层:所有服务完整存储 `subscriptionExpiresAt` 字段 -- 字段映射层:路由层正确处理 `expiresAt` → `subscriptionExpiresAt` 映射 - -**❌ 严重缺陷**: -- **调度逻辑缺失**:除 Claude 外的所有平台(Gemini、OpenAI、Droid 等)未在账号选择时检查订阅过期时间,导致过期账号仍会被正常调度使用 - -**影响评估**: 该缺陷导致功能对大部分平台实际无效,用户设置的过期时间不会生效。 - ---- - -## ✅ 已完成部分(质量优秀) - -### 1. 前端路由修复 ⭐⭐⭐⭐⭐ - -**文件**: `web/admin-spa/src/views/AccountsView.vue` -**位置**: 第 3730-3790 行 - -**实现内容**: -```javascript -const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => { - const account = accounts.value.find((acc) => acc.id === accountId) - - // 根据平台类型动态选择正确的 API 端点 - let endpoint = '' - switch (account.platform) { - case 'claude': - case 'claude-oauth': - endpoint = `/admin/claude-accounts/${accountId}` - break - case 'gemini': - endpoint = `/admin/gemini-accounts/${accountId}` - break - // ... 其他 7 个平台 - } - - await apiClient.put(endpoint, { expiresAt: expiresAt || null }) -} -``` - -**覆盖平台**: 所有 9 个平台(claude, gemini, claude-console, bedrock, ccr, openai, droid, azure_openai, openai-responses) - ---- - -### 2. 后端路由层字段映射 ⭐⭐⭐⭐⭐ - -**文件**: `src/routes/admin.js` -**覆盖路由**: 8 个 PUT 端点 - -**统一实现**: -```javascript -// 所有路由统一添加字段映射逻辑 -const mappedUpdates = { ...updates } -if ('expiresAt' in mappedUpdates) { - mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt - delete mappedUpdates.expiresAt - logger.info(`Mapping expiresAt to subscriptionExpiresAt...`) -} -``` - -**覆盖位置**: -- ✅ Claude Console: admin.js:2748 -- ✅ CCR: admin.js:3174 -- ✅ Bedrock: admin.js:3577 -- ✅ Gemini: admin.js:4047 -- ✅ OpenAI: admin.js:7429 -- ✅ Azure OpenAI: admin.js:7987 -- ✅ OpenAI-Responses: admin.js:8357 -- ✅ Droid: admin.js:8837 - ---- - -### 3. 后端数据层字段存储 ⭐⭐⭐⭐⭐ - -**涉及服务**: 全部 9 个 AccountService - -**三层完整实现**: - -#### 存储层 (createAccount) -```javascript -subscriptionExpiresAt: accountData.subscriptionExpiresAt || '', -``` - -#### 查询层 (getAllAccounts) -```javascript -// 映射给前端 -expiresAt: accountData.subscriptionExpiresAt || null, -``` - -#### 更新层 (updateAccount) -```javascript -if (updates.subscriptionExpiresAt !== undefined) { - // 直接保存,不做调整 -} -``` - -**字段独立性**: -- `expiresAt` - OAuth Token 过期时间(技术字段,自动刷新) -- `subscriptionExpiresAt` - 账户订阅到期时间(业务字段,手动管理) -- 两字段完全独立,Token 刷新不会覆盖订阅过期时间 ✅ - ---- - -## ❌ 严重缺陷(阻塞发布) - -### 核心问题:调度逻辑缺失订阅过期时间检查 - -**严重性**: 🔴 **P0 - 阻塞发布** -**影响范围**: Gemini、OpenAI、Droid、及其他 6 个平台(除 Claude 外所有平台) -**影响**: 过期账号仍会被正常调度,导致功能实际无效 - ---- - -### 缺陷 1: Gemini 账号 ❌ - -**文件**: `src/services/geminiAccountService.js` -**位置**: 第 285-290 行 -**方法**: `selectAvailableAccount()` - -**问题代码**: -```javascript -for (const accountId of sharedAccountIds) { - const account = await getAccount(accountId) - if (account && account.isActive === 'true' && !isRateLimited(account)) { - availableAccounts.push(account) // ❌ 未检查 subscriptionExpiresAt - } -} -``` - -**修复方案**: -```javascript -function isSubscriptionExpired(account) { - if (!account.subscriptionExpiresAt) return false - return new Date(account.subscriptionExpiresAt) <= new Date() -} - -for (const accountId of sharedAccountIds) { - const account = await getAccount(accountId) - if ( - account && - account.isActive === 'true' && - !isRateLimited(account) && - !isSubscriptionExpired(account) // ✅ 添加过期检查 - ) { - availableAccounts.push(account) - } -} -``` - ---- - -### 缺陷 2: OpenAI 账号 ❌ - -**文件**: `src/services/openaiAccountService.js` -**位置**: 第 917-922 行 -**方法**: `selectAvailableAccount()` - -**问题代码**: -```javascript -for (const accountId of sharedAccountIds) { - const account = await getAccount(accountId) - if (account && account.isActive === 'true' && !isRateLimited(account)) { - availableAccounts.push(account) // ❌ 未检查 subscriptionExpiresAt - } -} -``` - -**修复方案**: 与 Gemini 相同,添加过期检查 - ---- - -### 缺陷 3: Droid 账号 ❌ - -**文件**: `src/services/droidAccountService.js` -**位置**: 第 914-939 行 -**方法**: `getSchedulableAccounts()` - -**问题代码**: -```javascript -return allAccounts.filter((account) => { - const isActive = this._isTruthy(account.isActive) - const isSchedulable = this._isTruthy(account.schedulable) - const status = typeof account.status === 'string' ? account.status.toLowerCase() : '' - - if (!isActive || !isSchedulable || status !== 'active') { - return false // ❌ 只检查了这些条件,未检查 subscriptionExpiresAt - } - // ... -}) -``` - -**修复方案**: -```javascript -_isSubscriptionExpired(account) { - if (!account.subscriptionExpiresAt) return false - return new Date(account.subscriptionExpiresAt) <= new Date() -} - -return allAccounts.filter((account) => { - const isActive = this._isTruthy(account.isActive) - const isSchedulable = this._isTruthy(account.schedulable) - const status = typeof account.status === 'string' ? account.status.toLowerCase() : '' - const expired = this._isSubscriptionExpired(account) // ✅ 添加过期检查 - - if (!isActive || !isSchedulable || status !== 'active' || expired) { - return false - } - // ... -}) -``` - ---- - -### 缺陷 4-9: 其他平台 ⚠️ - -**平台**: CCR、Claude Console、Bedrock、Azure OpenAI、OpenAI-Responses - -**状态**: 未发现独立的账号选择方法 - -**分析**: -- 这些平台可能通过 Claude 的统一调度逻辑 -- 或采用简单的轮询/随机选择 -- **需要全面测试确认** - -**建议**: 修复前 3 个平台后,进行全量测试 - ---- - -### 参考实现: Claude 账号 ✅ - -**文件**: `src/services/claudeAccountService.js` -**位置**: 第 786-814 行 - -**正确实现**: -```javascript -isAccountNotExpired(account) { - if (!account.subscriptionExpiresAt) { - return true // 未设置视为永不过期 - } - - const expiryDate = new Date(account.subscriptionExpiresAt) - const now = new Date() - - if (expiryDate <= now) { - logger.debug( - `⏰ Account ${account.name} expired at ${account.subscriptionExpiresAt}` - ) - return false - } - return true -} - -// 在账号筛选时使用 -activeAccounts = activeAccounts.filter(account => - account.isActive === 'true' && - account.status !== 'error' && - account.schedulable !== 'false' && - this.isAccountNotExpired(account) // ✅ 正确检查 -) -``` - -**质量**: ⭐⭐⭐⭐⭐ 可作为其他平台的参考 - ---- - -## 🔧 修复方案 - -### 推荐方案:各服务添加独立过期检查函数 - -**优点**: -- 保持服务独立性 -- 可定制日志格式 -- 不引入额外依赖 - -### 修复步骤 - -#### 步骤 1: 添加辅助函数 - -在各服务中添加: -```javascript -/** - * 检查账户订阅是否过期 - * @param {Object} account - 账户对象 - * @returns {boolean} - true: 已过期, false: 未过期 - */ -function isSubscriptionExpired(account) { - if (!account.subscriptionExpiresAt || account.subscriptionExpiresAt === '') { - return false // 未设置视为永不过期 - } - const expiryDate = new Date(account.subscriptionExpiresAt) - return expiryDate <= new Date() -} -``` - -#### 步骤 2: 在筛选逻辑中调用 - -**Gemini**: -```javascript -if ( - account && - account.isActive === 'true' && - !isRateLimited(account) && - !isSubscriptionExpired(account) // ✅ -) { - availableAccounts.push(account) -} else if (account && isSubscriptionExpired(account)) { - logger.debug( - `⏰ Skipping expired Gemini account: ${account.name}, expired at ${account.subscriptionExpiresAt}` - ) -} -``` - -**OpenAI**: 同上 - -**Droid**: -```javascript -const expired = this._isSubscriptionExpired(account) - -if (!isActive || !isSchedulable || status !== 'active' || expired) { - if (expired) { - logger.debug(`⏰ Skipping expired Droid account: ${account.name}`) - } - return false -} -``` - ---- - -## ✅ 测试方案 - -### 1. 功能测试 - -#### TC-1.1: Gemini 账号过期检查 -```yaml -前提条件: - - 2 个 Gemini 共享账号 - - Account A: subscriptionExpiresAt = "2025-01-01" (未过期) - - Account B: subscriptionExpiresAt = "2024-01-01" (已过期) - -步骤: - 1. 调用 /api/v1/messages 发送 Gemini 请求 - 2. 检查日志查看选中的账号 - -预期: - - ✅ 只有 Account A 被选中 - - ✅ 日志显示 Account B 因过期被跳过 - - ✅ 请求正常完成 -``` - -#### TC-1.2: OpenAI 账号过期检查 -```yaml -前提条件: - - 2 个 OpenAI 共享账号 - - Account C: subscriptionExpiresAt = "2025-01-01" (未过期) - - Account D: subscriptionExpiresAt = "2024-01-01" (已过期) - -步骤: - 1. 调用 OpenAI API - 2. 检查日志 - -预期: - - ✅ 只有 Account C 被选中 - - ✅ 请求正常完成 -``` - -#### TC-1.3: Droid 账号过期检查 -```yaml -前提条件: - - 2 个 Droid 共享账号 - - Account E: subscriptionExpiresAt = "2025-01-01" (未过期) - - Account F: subscriptionExpiresAt = "2024-01-01" (已过期) - -步骤: - 1. 调用 Droid API - 2. 检查日志 - -预期: - - ✅ 只有 Account E 被选中 -``` - -#### TC-1.4: 全部过期无可用账号 -```yaml -前提条件: - - 某平台所有账号都已过期 - -步骤: - 1. 调用该平台 API - -预期: - - ✅ 返回错误: "No available {platform} accounts" - - ✅ 日志显示所有账号因过期被跳过 -``` - ---- - -### 2. 边界测试 - -#### TC-2.1: 未设置过期时间 -```yaml -前提条件: - - subscriptionExpiresAt = null 或 "" - -步骤: - 1. 调用 API - -预期: - - ✅ 账号正常被选中(永不过期) -``` - -#### TC-2.2: 过期时间边界 -```yaml -前提条件: - - 当前时间: 2025-10-12 10:00:00 - - subscriptionExpiresAt = "2025-10-12T10:00:00Z" - -步骤: - 1. 在 10:00:00 时调用 API - 2. 在 10:00:01 时调用 API - -预期: - - ✅ 两次调用账号都因过期被跳过(<= 判断) -``` - -#### TC-2.3: 无效日期格式 -```yaml -前提条件: - - subscriptionExpiresAt = "invalid-date" - -步骤: - 1. 调用 API - -预期: - - ✅ new Date() 返回 Invalid Date - - ✅ 比较结果为 false,账号视为未过期(容错) -``` - ---- - -### 3. 回归测试 - -#### TC-3.1: 现有条件不受影响 -```yaml -验证点: - - isActive = 'false' 仍被跳过 - - schedulable = 'false' 仍被跳过 - - status = 'error' 仍被跳过 - - 限流账号仍被跳过 - -预期: - - ✅ 所有原有过滤条件正常工作 -``` - -#### TC-3.2: Token 刷新不影响订阅时间 -```yaml -前提条件: - - subscriptionExpiresAt = "2026-01-01" - - OAuth token 即将过期 - -步骤: - 1. 触发 token 自动刷新 - 2. 检查 subscriptionExpiresAt - -预期: - - ✅ expiresAt (OAuth) 被更新 - - ✅ subscriptionExpiresAt 保持不变 -``` - -#### TC-3.3: 字段独立性 -```yaml -步骤: - 1. 通过 Web 界面更新订阅过期时间 - -预期: - - ✅ subscriptionExpiresAt 更新 - - ✅ expiresAt (OAuth) 不变 -``` - ---- - -### 4. 集成测试 - -#### TC-4.1: 多平台混合场景 -```yaml -场景: - - 3 个 Claude: 1 过期, 2 正常 - - 2 个 Gemini: 1 过期, 1 正常 - - 2 个 OpenAI: 全部过期 - -步骤: - 1. 调用 10 次 Claude API - 2. 调用 10 次 Gemini API - 3. 调用 1 次 OpenAI API - -预期: - - ✅ Claude 分配到 2 个正常账号 - - ✅ Gemini 分配到 1 个正常账号 - - ✅ OpenAI 返回错误 -``` - -#### TC-4.2: Web 界面端到端 -```yaml -步骤: - 1. 登录 Web 管理界面 - 2. 编辑账号过期时间为昨天 - 3. 保存并刷新 - 4. 调用 API 验证 - -预期: - - ✅ 过期时间成功保存 - - ✅ 界面正确显示 - - ✅ API 调用时账号被跳过 -``` - ---- - -### 5. 性能测试 - -#### TC-5.1: 大量过期账号性能 -```yaml -场景: - - 100 个账号,95 个过期,5 个正常 - -步骤: - 1. 并发 100 次 API 调用 - 2. 测量响应时间 - -预期: - - ✅ 过期检查耗时 <5ms - - ✅ 无性能告警 -``` - ---- - -## 📊 测试覆盖率目标 - -| 测试类型 | 目标覆盖率 | 优先级 | -|---------|-----------|--------| -| 功能测试 | 100% | P0 | -| 边界测试 | 100% | P0 | -| 回归测试 | 100% | P0 | -| 集成测试 | 80% | P1 | -| 性能测试 | - | P1 | - ---- - -## 🎯 修复优先级 - -### P0 - 必须修复(阻塞发布) - -1. ❌ **Gemini** - geminiAccountService.js:285 -2. ❌ **OpenAI** - openaiAccountService.js:917 -3. ❌ **Droid** - droidAccountService.js:914 - -### P1 - 高优先级(建议修复) - -4. ⚠️ **确认其他 5 平台** - CCR, Claude Console, Bedrock, Azure OpenAI, OpenAI-Responses -5. 📝 **统一日志格式** - -### P2 - 中优先级(可选) - -6. 🔔 **WebHook 通知** - 账号即将过期提醒 -7. 🎨 **前端视觉提示** - 高亮即将过期账号 - ---- - -## 📝 修复检查清单 - -完成修复后,请逐项确认: - -### 代码修改 -- [ ] Gemini 添加 `isSubscriptionExpired()` 函数 -- [ ] Gemini 在 `selectAvailableAccount()` 中调用检查 -- [ ] OpenAI 添加 `isSubscriptionExpired()` 函数 -- [ ] OpenAI 在 `selectAvailableAccount()` 中调用检查 -- [ ] Droid 添加 `_isSubscriptionExpired()` 函数 -- [ ] Droid 在 `getSchedulableAccounts()` 中调用检查 -- [ ] 所有检查都记录调试日志 - -### 代码质量 -- [ ] ESLint 检查通过 -- [ ] Prettier 格式化完成 -- [ ] 添加必要注释 -- [ ] 函数命名符合规范 - -### 测试验证 -- [ ] 通过所有功能测试(TC-1.1 ~ 1.4) -- [ ] 通过所有边界测试(TC-2.1 ~ 2.3) -- [ ] 通过所有回归测试(TC-3.1 ~ 3.3) -- [ ] 通过集成测试(TC-4.1 ~ 4.2) -- [ ] 性能测试无退化(TC-5.1) - -### 文档更新 -- [ ] 更新 CLAUDE.md -- [ ] 记录 commit 信息 -- [ ] 更新本报告状态 - ---- - -## 🚀 发布建议 - -### ⛔ 不建议当前版本发布 - -**原因**: -1. **功能不完整**: 核心调度逻辑缺失,大部分平台功能无效 -2. **用户体验差**: 用户设置过期时间但实际不生效 -3. **潜在风险**: 过期账号继续使用可能导致 API 失败 - -### ✅ 发布前必须完成 - -1. 修复 P0 级别的 3 个缺陷 -2. 通过所有 P0 测试用例 -3. 进行充分回归测试 - -### 📅 推荐发布流程 - -| 阶段 | 任务 | 工期 | -|-----|------|------| -| 阶段 1 | 修复代码 + 单元测试 | 1-2 天 | -| 阶段 2 | 集成测试 + 回归测试 | 1 天 | -| 阶段 3 | 测试环境验证 | 1-2 天 | -| 阶段 4 | 生产环境部署 | 0.5 天 | - -**预计完成**: 3-5 个工作日 - ---- - -## 📞 评审信息 - -- **评审人员**: Claude Code -- **评审日期**: 2025-10-12 -- **项目名称**: Claude Relay Service -- **功能名称**: 账号订阅过期时间管理 - ---- - -## 附录 A: 相关文件清单 - -### 前端 -- `web/admin-spa/src/views/AccountsView.vue` ✅ - -### 后端路由 -- `src/routes/admin.js` ✅ - -### 后端服务 -- `src/services/claudeAccountService.js` ✅ (参考实现) -- `src/services/geminiAccountService.js` ❌ 需修复 -- `src/services/openaiAccountService.js` ❌ 需修复 -- `src/services/droidAccountService.js` ❌ 需修复 -- `src/services/claudeConsoleAccountService.js` ✅ -- `src/services/bedrockAccountService.js` ✅ -- `src/services/ccrAccountService.js` ✅ -- `src/services/azureOpenaiAccountService.js` ✅ -- `src/services/openaiResponsesAccountService.js` ✅ - ---- - -## 附录 B: 技术债务 - -发现以下技术债务(不影响本次功能): - -1. **缺少单元测试**: 所有 AccountService 缺少测试 -2. **代码重复**: 9 个服务逻辑高度相似 -3. **日志不统一**: 不同服务日志格式差异大 -4. **错误处理简单**: 部分服务错误处理不完善 - -**建议**: 后续迭代逐步改进 - ---- - -**报告结束**