diff --git a/account_expire_feature.md b/account_expire_feature.md
new file mode 100644
index 00000000..ab4ba588
--- /dev/null
+++ b/account_expire_feature.md
@@ -0,0 +1,756 @@
+# 账户过期功能代码评审报告
+
+**评审日期**: 2025-10-11
+**评审范围**: 账户到期时间管理功能
+**评审人**: Claude Code
+**文档版本**: 1.0
+
+---
+
+## 📌 功能概述
+
+本次功能新增为 Claude 账户管理系统添加了订阅到期时间管理能力,包括:
+
+- ✅ 后端支持存储和管理账户的 `subscriptionExpiresAt` 字段
+- ✅ 前端提供到期时间设置界面(快捷选项 + 自定义日期)
+- ✅ 账户列表显示到期状态(已过期/即将过期/永不过期)
+- ✅ 独立的到期时间编辑弹窗组件
+- ❌ **账户选择逻辑未集成过期检查(核心缺陷)**
+
+---
+
+## 📂 涉及的代码变更
+
+| 文件路径 | 变更类型 | 主要内容 |
+|---------|---------|---------|
+| `src/routes/admin.js` | 修改 | POST/PUT 端点支持 `expiresAt` 字段 |
+| `src/services/claudeAccountService.js` | 修改 | 存储/读取/更新 `subscriptionExpiresAt` |
+| `web/admin-spa/src/views/AccountsView.vue` | 修改 | 列表显示到期状态、编辑功能 |
+| `web/admin-spa/src/components/accounts/AccountForm.vue` | 修改 | 创建/编辑表单添加到期时间选择器 |
+| `web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue` | 新增 | 到期时间编辑弹窗组件 |
+
+---
+
+## 🚨 严重问题(P0 - 必须立即修复)
+
+### 问题 #1: 账户选择逻辑完全缺少过期检查
+
+**严重程度**: 🔴 Critical
+**影响范围**: 核心业务逻辑
+**功能影响**: **过期账户仍会被调度使用,功能完全失效**
+
+#### 问题定位
+
+**文件**: `src/services/claudeAccountService.js`
+**位置**:
+- 第 786-791 行 (`selectAvailableAccount` 方法)
+- 第 901-906 行 (`selectAccountForApiKey` 方法)
+
+#### 当前代码
+
+```javascript
+// selectAvailableAccount 方法
+let activeAccounts = accounts.filter(
+ (account) =>
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false'
+)
+
+// selectAccountForApiKey 方法
+let sharedAccounts = accounts.filter(
+ (account) =>
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false' &&
+ (account.accountType === 'shared' || !account.accountType)
+)
+```
+
+#### 问题分析
+
+虽然后端存储了 `subscriptionExpiresAt` 字段,前端也能编辑和显示,但在**账户调度的核心逻辑中完全没有检查此字段**。这意味着:
+
+1. 即使账户已过期,只要 `isActive === 'true'`,账户仍会被选中
+2. 用户设置的到期时间完全不起作用
+3. 过期的 Claude Max/Pro 账户仍会接收请求并可能导致 API 错误
+
+#### 修复方案
+
+在两个账户选择方法中都添加过期检查:
+
+```javascript
+// selectAvailableAccount 方法(第 786-791 行)
+let activeAccounts = accounts.filter((account) => {
+ // 基础状态检查
+ const isBasicActive =
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false'
+
+ // 过期检查
+ const isNotExpired =
+ !account.subscriptionExpiresAt ||
+ new Date(account.subscriptionExpiresAt) > new Date()
+
+ return isBasicActive && isNotExpired
+})
+
+// selectAccountForApiKey 方法(第 901-906 行)
+let sharedAccounts = accounts.filter((account) => {
+ // 基础状态检查
+ const isBasicActive =
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false' &&
+ (account.accountType === 'shared' || !account.accountType)
+
+ // 过期检查
+ const isNotExpired =
+ !account.subscriptionExpiresAt ||
+ new Date(account.subscriptionExpiresAt) > new Date()
+
+ return isBasicActive && isNotExpired
+})
+```
+
+#### 推荐优化
+
+为了提高代码可维护性,建议抽取为独立方法:
+
+```javascript
+// 添加辅助方法(在 ClaudeAccountService 类中)
+/**
+ * 检查账户是否未过期
+ * @param {Object} account - 账户对象
+ * @returns {boolean} - 如果未设置过期时间或未过期返回 true
+ */
+isAccountNotExpired(account) {
+ if (!account.subscriptionExpiresAt) {
+ return true // 未设置过期时间,视为永不过期
+ }
+ return new Date(account.subscriptionExpiresAt) > new Date()
+}
+
+// 使用示例
+let activeAccounts = accounts.filter(
+ (account) =>
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false' &&
+ this.isAccountNotExpired(account)
+)
+```
+
+#### 额外建议
+
+考虑添加日志记录,方便调试:
+
+```javascript
+if (account.subscriptionExpiresAt && !this.isAccountNotExpired(account)) {
+ logger.warn(
+ `⚠️ Account ${account.name} (${account.id}) is expired (${account.subscriptionExpiresAt}), skipping...`
+ )
+}
+```
+
+---
+
+## ⚠️ 主要问题(P1 - 高优先级修复)
+
+### 问题 #2: 前后端字段命名不一致
+
+**严重程度**: 🟠 Major
+**影响范围**: 数据一致性
+**潜在风险**: POST 创建账户时数据丢失、代码可读性差
+
+#### 问题定位
+
+- **前端**: 统一使用 `expiresAt`
+- **后端存储**: 使用 `subscriptionExpiresAt`
+- **API 端点**: POST 和 PUT 处理方式不一致
+
+#### 代码对比
+
+**POST 端点** (`src/routes/admin.js:2243-2290`):
+```javascript
+const { expiresAt } = req.body
+// ...
+await claudeAccountService.createAccount({
+ // ...
+ expiresAt: expiresAt || null // ❌ 直接传递,字段名不匹配
+})
+```
+
+**PUT 端点** (`src/routes/admin.js:2376-2382`):
+```javascript
+// ✅ 有映射逻辑
+const mappedUpdates = { ...updates }
+if ('expiresAt' in mappedUpdates) {
+ mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
+ delete mappedUpdates.expiresAt
+}
+```
+
+#### 问题分析
+
+1. POST 创建时传递的 `expiresAt` 在 `createAccount` 方法中被正确接收并映射为 `subscriptionExpiresAt`(第 76 行参数、第 118 行存储)
+2. 但这种隐式映射增加了代码理解难度
+3. PUT 和 POST 的处理逻辑不一致
+
+#### 修复方案
+
+**选项 A(推荐)**: 统一在路由层映射
+
+```javascript
+// POST 端点也添加映射逻辑
+router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
+ const { expiresAt, ...otherFields } = req.body
+
+ // 统一映射字段
+ const accountData = {
+ ...otherFields,
+ subscriptionExpiresAt: expiresAt || null
+ }
+
+ const result = await claudeAccountService.createAccount(accountData)
+ // ...
+})
+```
+
+**选项 B**: 前后端统一字段名
+
+如果修改成本可控,建议后端也使用 `expiresAt`,避免混淆。
+
+---
+
+### 问题 #3: null 与空字符串混用
+
+**严重程度**: 🟠 Major
+**影响范围**: 逻辑判断
+**潜在风险**: 前端判断 `if (account.expiresAt)` 可能失效
+
+#### 问题定位
+
+**文件**: `src/services/claudeAccountService.js`
+
+```javascript
+// 创建时(第 118 行)
+subscriptionExpiresAt: expiresAt || '' // ❌ 空字符串
+
+// 返回时(第 494 行)
+expiresAt: account.subscriptionExpiresAt || null // ✅ null
+
+// 更新时(第 646 行)
+updatedData[field] = value ? value.toString() : '' // ❌ 空字符串
+```
+
+#### 问题分析
+
+JavaScript 中:
+- `null` 是 falsy 值
+- `''`(空字符串)也是 falsy 值
+- 但在 JSON 序列化和 Redis 存储中可能表现不同
+
+前端判断时:
+```javascript
+if (account.expiresAt) { // 如果是空字符串,这个判断仍然为 false
+ // 显示过期时间
+}
+```
+
+虽然当前可能不会立即出错,但混用会导致未来维护困难。
+
+#### 修复方案
+
+统一使用 `null` 表示"永不过期":
+
+```javascript
+// 创建时
+subscriptionExpiresAt: expiresAt || null
+
+// 更新时
+if (field === 'subscriptionExpiresAt') {
+ updatedData[field] = value || null
+}
+```
+
+如果 Redis 存储要求字符串,则统一在存储层转换:
+
+```javascript
+// 存储前转换
+subscriptionExpiresAt: expiresAt ? expiresAt.toString() : ''
+
+// 读取后转换
+expiresAt: account.subscriptionExpiresAt ? account.subscriptionExpiresAt : null
+```
+
+---
+
+### 问题 #4: POST 端点缺少字段映射
+
+见问题 #2 的修复方案。
+
+---
+
+## 💡 次要问题(P2 - 建议修复)
+
+### 问题 #5: AccountForm.vue 存在大量重复代码
+
+**文件**: `web/admin-spa/src/components/accounts/AccountForm.vue`
+**位置**: 第 644-685 行 和 第 2069-2110 行
+
+两处几乎完全相同的"到期时间"选择器 UI 代码。
+
+**建议**: 抽取为独立子组件 `ExpirySelector.vue`:
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**使用**:
+```vue
+
+```
+
+---
+
+### 问题 #6: 前端验证不足
+
+**文件**: `web/admin-spa/src/components/accounts/AccountForm.vue`
+**位置**: 第 5071-5127 行
+
+虽然 `` 设置了 `:min` 属性,但缺少 JavaScript 验证。
+
+**建议**: 在 `updateAccountCustomExpireAt()` 中添加验证:
+
+```javascript
+const updateAccountCustomExpireAt = () => {
+ if (form.value.customExpireDate) {
+ const selectedDate = new Date(form.value.customExpireDate)
+ const now = new Date()
+
+ // 验证日期是否在未来
+ if (selectedDate <= now) {
+ showToast('到期时间必须是未来的日期', 'error')
+ form.value.customExpireDate = ''
+ form.value.expiresAt = null
+ return
+ }
+
+ form.value.expiresAt = selectedDate.toISOString()
+ }
+}
+```
+
+---
+
+### 问题 #7: 时区处理需要明确
+
+**影响范围**: 全局
+**问题**:
+- 后端存储 UTC 时间(ISO 8601)
+- 前端显示使用 `toLocaleString('zh-CN')` 转换为本地时间
+- `` 使用浏览器本地时区
+
+**建议**:
+1. 在 UI 上明确标注时区,如"到期时间(北京时间)"
+2. 或在保存前提示用户确认时区
+3. 添加注释说明时区转换逻辑
+
+---
+
+### 问题 #8: 错误处理可以更详细
+
+**文件**: `web/admin-spa/src/views/AccountsView.vue`
+**位置**: 第 3545-3562 行
+
+```javascript
+catch (error) {
+ showToast('更新失败', 'error')
+ // ...
+}
+```
+
+**建议**: 区分错误类型并提供有用的信息:
+
+```javascript
+catch (error) {
+ let errorMessage = '更新失败'
+
+ if (error.response) {
+ // HTTP 错误
+ if (error.response.status === 400) {
+ errorMessage = '输入数据无效,请检查日期格式'
+ } else if (error.response.status === 403) {
+ errorMessage = '权限不足,无法修改账户'
+ } else if (error.response.status === 404) {
+ errorMessage = '账户不存在'
+ } else {
+ errorMessage = error.response.data?.message || '服务器错误'
+ }
+ } else if (error.request) {
+ errorMessage = '网络连接失败,请检查网络'
+ }
+
+ showToast(errorMessage, 'error')
+ // ...
+}
+```
+
+---
+
+### 问题 #9: 数据同步问题
+
+**文件**: `web/admin-spa/src/views/AccountsView.vue`
+**位置**: 第 3547-3552 行
+
+```javascript
+// 保存成功后直接修改本地数据
+const account = accounts.value.find((acc) => acc.id === accountId)
+if (account) {
+ account.expiresAt = expiresAt || null
+}
+```
+
+**风险**: 本地数据可能与服务端不一致。
+
+**建议**: 重新获取数据或要求后端返回完整的更新后对象:
+
+```javascript
+// 方案 A: 重新加载
+if (data.success) {
+ showToast('账户到期时间已更新', 'success')
+ await loadAccounts() // 重新获取完整数据
+ closeAccountExpiryEdit()
+}
+
+// 方案 B: 使用后端返回的数据
+if (data.success && data.account) {
+ showToast('账户到期时间已更新', 'success')
+ const account = accounts.value.find((acc) => acc.id === accountId)
+ if (account) {
+ Object.assign(account, data.account)
+ }
+ closeAccountExpiryEdit()
+}
+```
+
+---
+
+### 问题 #10: 后端输入验证缺失
+
+**文件**: `src/routes/admin.js`
+**位置**: POST 和 PUT 端点
+
+**问题**: 没有验证 `expiresAt` 的格式和有效性。
+
+**建议**: 添加验证逻辑:
+
+```javascript
+// 验证到期时间
+if (expiresAt) {
+ // 检查是否为有效的 ISO 8601 日期
+ const expiryDate = new Date(expiresAt)
+ if (isNaN(expiryDate.getTime())) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid expiry date format. Expected ISO 8601 format.'
+ })
+ }
+
+ // 检查是否为未来的日期
+ if (expiryDate <= new Date()) {
+ return res.status(400).json({
+ success: false,
+ message: 'Expiry date must be in the future.'
+ })
+ }
+}
+```
+
+---
+
+## ✅ 优点和亮点
+
+1. **优秀的 UI/UX 设计**
+ - 快捷选项(30天、90天、180天等)降低用户操作成本
+ - 自定义日期选择提供灵活性
+ - 状态显示清晰直观(已过期🔴 / 即将过期🟠 / 永不过期⚪)
+
+2. **独立的编辑弹窗组件**
+ - `AccountExpiryEditModal.vue` 设计合理,职责单一
+ - 提供预览功能,用户体验好
+ - 支持快速修改,无需进入完整的编辑表单
+
+3. **暗黑模式完全兼容**
+ - 所有新增 UI 都正确使用了 `dark:` 前缀
+ - 符合项目的主题系统设计
+
+4. **代码风格一致**
+ - 遵循项目现有的编码规范
+ - 使用了项目已有的工具函数和组件
+
+5. **功能相对完整**
+ - 创建、编辑、显示的完整流程
+ - 表单验证(HTML5 层面)
+ - 错误处理框架
+
+---
+
+## 📋 修复清单(按优先级排序)
+
+### P0 - 立即修复(阻塞发布)
+
+- [ ] **#1** 在 `selectAvailableAccount()` 中添加过期检查逻辑
+- [ ] **#1** 在 `selectAccountForApiKey()` 中添加过期检查逻辑
+- [ ] **#1** 添加过期账户跳过的日志记录
+
+### P1 - 高优先级(本周内修复)
+
+- [ ] **#2** 统一 POST 端点的字段映射逻辑
+- [ ] **#3** 统一使用 `null` 表示"永不过期",避免空字符串
+- [ ] **#4** 确保 `createAccount` 方法正确接收 `subscriptionExpiresAt`
+
+### P2 - 中优先级(下周修复)
+
+- [ ] **#5** 抽取 `ExpirySelector.vue` 组件,消除重复代码
+- [ ] **#6** 添加前端日期验证(确保选择未来的日期)
+- [ ] **#8** 增强错误处理,区分不同错误类型
+- [ ] **#9** 修复数据同步问题(保存后重新加载或使用后端返回)
+
+### P3 - 低优先级(有时间再优化)
+
+- [ ] **#7** 在 UI 上标注时区信息
+- [ ] **#10** 添加后端输入验证(日期格式、未来日期)
+- [ ] 添加单元测试覆盖过期检查逻辑
+- [ ] 添加集成测试验证完整流程
+
+---
+
+## 🧪 测试建议
+
+### 单元测试
+
+```javascript
+// 测试账户过期检查逻辑
+describe('ClaudeAccountService - Expiry Check', () => {
+ it('should exclude expired accounts', async () => {
+ const expiredDate = new Date(Date.now() - 86400000).toISOString() // 昨天
+ const account = {
+ id: 'test-1',
+ isActive: 'true',
+ status: 'active',
+ schedulable: 'true',
+ subscriptionExpiresAt: expiredDate
+ }
+
+ const result = service.isAccountNotExpired(account)
+ expect(result).toBe(false)
+ })
+
+ it('should include non-expired accounts', async () => {
+ const futureDate = new Date(Date.now() + 86400000).toISOString() // 明天
+ const account = {
+ id: 'test-2',
+ isActive: 'true',
+ status: 'active',
+ schedulable: 'true',
+ subscriptionExpiresAt: futureDate
+ }
+
+ const result = service.isAccountNotExpired(account)
+ expect(result).toBe(true)
+ })
+
+ it('should include accounts without expiry date', async () => {
+ const account = {
+ id: 'test-3',
+ isActive: 'true',
+ status: 'active',
+ schedulable: 'true',
+ subscriptionExpiresAt: null
+ }
+
+ const result = service.isAccountNotExpired(account)
+ expect(result).toBe(true)
+ })
+})
+```
+
+### 集成测试要点
+
+1. **创建账户并设置过期时间**
+ - 创建账户时设置未来的过期时间
+ - 验证 Redis 中存储的字段名和值
+
+2. **过期账户不被调度**
+ - 创建已过期的账户
+ - 发送 API 请求
+ - 验证该账户未被选择
+
+3. **前端显示正确**
+ - 访问账户列表页面
+ - 验证已过期账户显示红色警告
+ - 验证即将过期账户显示橙色提示
+
+4. **编辑功能正常**
+ - 打开编辑弹窗
+ - 修改过期时间
+ - 验证保存成功并正确显示
+
+### 手动测试步骤
+
+1. 创建测试账户,设置 30 天后过期
+2. 验证账户列表显示正确的过期日期
+3. 修改系统时间(或修改数据库)使账户过期
+4. 发送 API 请求,确认过期账户未被选择
+5. 使用编辑弹窗延长过期时间
+6. 再次发送请求,确认账户恢复可用
+
+---
+
+## 📊 影响评估
+
+| 评估维度 | 评分 | 说明 |
+|---------|------|------|
+| 功能完整性 | ⭐⭐⭐☆☆ (3/5) | UI 完整,但核心逻辑缺失 |
+| 代码质量 | ⭐⭐⭐☆☆ (3/5) | 结构合理,但存在不一致 |
+| UI/UX | ⭐⭐⭐⭐☆ (4/5) | 设计优秀,体验良好 |
+| 健壮性 | ⭐⭐☆☆☆ (2/5) | 缺少验证和错误处理 |
+| 可维护性 | ⭐⭐⭐☆☆ (3/5) | 存在重复代码,需优化 |
+
+**总体评价**: 本次功能的 UI 设计和用户体验非常优秀,但**核心的过期检查逻辑未实现**,导致功能实际不可用。修复 P0 问题后,功能即可正常工作。建议按照优先级清单依次修复问题。
+
+---
+
+## 🔧 快速修复指南
+
+### Step 1: 修复核心逻辑(P0)
+
+**文件**: `src/services/claudeAccountService.js`
+
+在 `ClaudeAccountService` 类中添加辅助方法:
+
+```javascript
+/**
+ * 检查账户是否未过期
+ * @param {Object} account - 账户对象
+ * @returns {boolean} - 如果未设置过期时间或未过期返回 true
+ */
+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} (${account.id}) expired at ${account.subscriptionExpiresAt}`
+ )
+ return false
+ }
+
+ return true
+}
+```
+
+然后在两处修改过滤逻辑:
+
+**位置 1**: 第 786 行附近
+```javascript
+let activeAccounts = accounts.filter(
+ (account) =>
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false' &&
+ this.isAccountNotExpired(account) // 添加这一行
+)
+```
+
+**位置 2**: 第 901 行附近
+```javascript
+let sharedAccounts = accounts.filter(
+ (account) =>
+ account.isActive === 'true' &&
+ account.status !== 'error' &&
+ account.schedulable !== 'false' &&
+ (account.accountType === 'shared' || !account.accountType) &&
+ this.isAccountNotExpired(account) // 添加这一行
+)
+```
+
+### Step 2: 修复 POST 端点映射(P1)
+
+**文件**: `src/routes/admin.js`
+
+在第 2243 行附近的 POST 端点中:
+
+```javascript
+router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
+ const {
+ name,
+ // ... 其他字段
+ expiresAt
+ } = req.body
+
+ // 映射字段名
+ const subscriptionExpiresAt = expiresAt || null
+
+ const result = await claudeAccountService.createAccount({
+ name,
+ // ... 其他字段
+ expiresAt: subscriptionExpiresAt // 使用映射后的变量
+ })
+
+ // ...
+})
+```
+
+### Step 3: 统一 null 处理(P1)
+
+**文件**: `src/services/claudeAccountService.js`
+
+将所有 `|| ''` 改为 `|| null`(第 118、147、646 行附近)
+
+---
+
+## 📞 联系信息
+
+如有任何疑问或需要进一步澄清,请联系代码审查团队。
+
+**文档维护**: Claude Code
+**最后更新**: 2025-10-11
diff --git a/src/routes/admin.js b/src/routes/admin.js
index ff64da54..bad940ba 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -2243,7 +2243,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
autoStopOnWarning,
useUnifiedUserAgent,
useUnifiedClientId,
- unifiedClientId
+ unifiedClientId,
+ expiresAt
} = req.body
if (!name) {
@@ -2286,7 +2287,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 // 账户订阅到期时间
})
// 如果是分组类型,将账户添加到分组
@@ -2373,7 +2375,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' })
diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js
index ec9e5b11..c796e7c9 100644
--- a/src/services/claudeAccountService.js
+++ b/src/services/claudeAccountService.js
@@ -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() : ''
}
}
}
diff --git a/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue b/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue
new file mode 100644
index 00000000..046c3332
--- /dev/null
+++ b/web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue
@@ -0,0 +1,416 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
修改到期时间
+
+ 为 "{{ account.name || 'Account' }}" 设置新的到期时间
+
+
+
+
+
+
+
+
+
+
+
+
当前状态
+
+
+
+ {{ formatFullExpireDate(account.expiresAt) }}
+
+ ({{ getExpiryStatus(account.expiresAt).text }})
+
+
+
+
+
+ 永不过期
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择一个未来的日期和时间作为到期时间
+
+
+
+
+
+
+
+
+
+ 新的到期时间
+
+
+
+ {{ formatFullExpireDate(localForm.expiresAt) }}
+
+ ({{ getExpiryStatus(localForm.expiresAt).text }})
+
+
+
+
+ 永不过期
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue
index 5739fc4a..fc817ca5 100644
--- a/web/admin-spa/src/components/accounts/AccountForm.vue
+++ b/web/admin-spa/src/components/accounts/AccountForm.vue
@@ -641,6 +641,49 @@
+
+
+
+
+
+
+
+
+
+
+ 将于 {{ formatExpireDate(form.expiresAt) }} 过期
+
+
+
+ 账户永不过期
+
+
+
+ 设置 Claude Max/Pro 订阅的到期时间,到期后将停止调度此账户
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将于 {{ formatExpireDate(form.expiresAt) }} 过期
+
+
+
+ 账户永不过期
+
+
+
+ 设置 Claude Max/Pro 订阅的到期时间,到期后将停止调度此账户
+
+
+