From 36a86e9ab4e7a726d580b6d6a5a7871f2e7912c7 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:03:37 +0800 Subject: [PATCH 01/10] =?UTF-8?q?perf(backend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9F=A5=E8=AF=A2=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并多个独立查询为单个SQL查询 - 减少数据库往返次数 - 提升仪表板统计数据获取效率 --- backend/internal/repository/usage_log_repo.go | 182 ++++++++++++++---- 1 file changed, 140 insertions(+), 42 deletions(-) diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index dd912c5a..3b8f7e1a 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -129,51 +129,67 @@ type DashboardStats = usagestats.DashboardStats func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) { var stats DashboardStats today := timezone.Today() + now := time.Now() - // 总用户数 - r.db.WithContext(ctx).Model(&userModel{}).Count(&stats.TotalUsers) + // 合并用户统计查询 + var userStats struct { + TotalUsers int64 `gorm:"column:total_users"` + TodayNewUsers int64 `gorm:"column:today_new_users"` + ActiveUsers int64 `gorm:"column:active_users"` + } + if err := r.db.WithContext(ctx).Raw(` + SELECT + COUNT(*) as total_users, + COUNT(CASE WHEN created_at >= ? THEN 1 END) as today_new_users, + (SELECT COUNT(DISTINCT user_id) FROM usage_logs WHERE created_at >= ?) as active_users + FROM users + `, today, today).Scan(&userStats).Error; err != nil { + return nil, err + } + stats.TotalUsers = userStats.TotalUsers + stats.TodayNewUsers = userStats.TodayNewUsers + stats.ActiveUsers = userStats.ActiveUsers - // 今日新增用户数 - r.db.WithContext(ctx).Model(&userModel{}). - Where("created_at >= ?", today). - Count(&stats.TodayNewUsers) + // 合并API Key统计查询 + var apiKeyStats struct { + TotalApiKeys int64 `gorm:"column:total_api_keys"` + ActiveApiKeys int64 `gorm:"column:active_api_keys"` + } + if err := r.db.WithContext(ctx).Raw(` + SELECT + COUNT(*) as total_api_keys, + COUNT(CASE WHEN status = ? THEN 1 END) as active_api_keys + FROM api_keys + `, service.StatusActive).Scan(&apiKeyStats).Error; err != nil { + return nil, err + } + stats.TotalApiKeys = apiKeyStats.TotalApiKeys + stats.ActiveApiKeys = apiKeyStats.ActiveApiKeys - // 今日活跃用户数 (今日有请求的用户) - r.db.WithContext(ctx).Model(&usageLogModel{}). - Distinct("user_id"). - Where("created_at >= ?", today). - Count(&stats.ActiveUsers) - - // 总 API Key 数 - r.db.WithContext(ctx).Model(&apiKeyModel{}).Count(&stats.TotalApiKeys) - - // 活跃 API Key 数 - r.db.WithContext(ctx).Model(&apiKeyModel{}). - Where("status = ?", service.StatusActive). - Count(&stats.ActiveApiKeys) - - // 总账户数 - r.db.WithContext(ctx).Model(&accountModel{}).Count(&stats.TotalAccounts) - - // 正常账户数 (schedulable=true, status=active) - r.db.WithContext(ctx).Model(&accountModel{}). - Where("status = ? AND schedulable = ?", service.StatusActive, true). - Count(&stats.NormalAccounts) - - // 异常账户数 (status=error) - r.db.WithContext(ctx).Model(&accountModel{}). - Where("status = ?", service.StatusError). - Count(&stats.ErrorAccounts) - - // 限流账户数 - r.db.WithContext(ctx).Model(&accountModel{}). - Where("rate_limited_at IS NOT NULL AND rate_limit_reset_at > ?", time.Now()). - Count(&stats.RateLimitAccounts) - - // 过载账户数 - r.db.WithContext(ctx).Model(&accountModel{}). - Where("overload_until IS NOT NULL AND overload_until > ?", time.Now()). - Count(&stats.OverloadAccounts) + // 合并账户统计查询 + var accountStats struct { + TotalAccounts int64 `gorm:"column:total_accounts"` + NormalAccounts int64 `gorm:"column:normal_accounts"` + ErrorAccounts int64 `gorm:"column:error_accounts"` + RateLimitAccounts int64 `gorm:"column:ratelimit_accounts"` + OverloadAccounts int64 `gorm:"column:overload_accounts"` + } + if err := r.db.WithContext(ctx).Raw(` + SELECT + COUNT(*) as total_accounts, + COUNT(CASE WHEN status = ? AND schedulable = true THEN 1 END) as normal_accounts, + COUNT(CASE WHEN status = ? THEN 1 END) as error_accounts, + COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts, + COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts + FROM accounts + `, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil { + return nil, err + } + stats.TotalAccounts = accountStats.TotalAccounts + stats.NormalAccounts = accountStats.NormalAccounts + stats.ErrorAccounts = accountStats.ErrorAccounts + stats.RateLimitAccounts = accountStats.RateLimitAccounts + stats.OverloadAccounts = accountStats.OverloadAccounts // 累计 Token 统计 var totalStats struct { @@ -273,6 +289,88 @@ func (r *usageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID return usageLogModelsToService(logs), nil, err } +// GetUserStatsAggregated returns aggregated usage statistics for a user using database-level aggregation +func (r *usageLogRepository) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { + var stats struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalInputTokens int64 `gorm:"column:total_input_tokens"` + TotalOutputTokens int64 `gorm:"column:total_output_tokens"` + TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` + TotalCost float64 `gorm:"column:total_cost"` + TotalActualCost float64 `gorm:"column:total_actual_cost"` + AverageDurationMs float64 `gorm:"column:avg_duration_ms"` + } + + err := r.db.WithContext(ctx).Model(&usageLogModel{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms + `). + Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). + Scan(&stats).Error + + if err != nil { + return nil, err + } + + return &usagestats.UsageStats{ + TotalRequests: stats.TotalRequests, + TotalInputTokens: stats.TotalInputTokens, + TotalOutputTokens: stats.TotalOutputTokens, + TotalCacheTokens: stats.TotalCacheTokens, + TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, + TotalCost: stats.TotalCost, + TotalActualCost: stats.TotalActualCost, + AverageDurationMs: stats.AverageDurationMs, + }, nil +} + +// GetApiKeyStatsAggregated returns aggregated usage statistics for an API key using database-level aggregation +func (r *usageLogRepository) GetApiKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { + var stats struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalInputTokens int64 `gorm:"column:total_input_tokens"` + TotalOutputTokens int64 `gorm:"column:total_output_tokens"` + TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` + TotalCost float64 `gorm:"column:total_cost"` + TotalActualCost float64 `gorm:"column:total_actual_cost"` + AverageDurationMs float64 `gorm:"column:avg_duration_ms"` + } + + err := r.db.WithContext(ctx).Model(&usageLogModel{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(COALESCE(duration_ms, 0)), 0) as avg_duration_ms + `). + Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime). + Scan(&stats).Error + + if err != nil { + return nil, err + } + + return &usagestats.UsageStats{ + TotalRequests: stats.TotalRequests, + TotalInputTokens: stats.TotalInputTokens, + TotalOutputTokens: stats.TotalOutputTokens, + TotalCacheTokens: stats.TotalCacheTokens, + TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, + TotalCost: stats.TotalCost, + TotalActualCost: stats.TotalActualCost, + AverageDurationMs: stats.AverageDurationMs, + }, nil +} + func (r *usageLogRepository) ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { var logs []usageLogModel err := r.db.WithContext(ctx). From 227d506c53965eda7827a022acba9d3685454f92 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:03:57 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(backend):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=BB=9F=E8=AE=A1=E5=92=8CAPI=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化使用统计处理逻辑 - 增强API密钥仓储层功能 - 改进账户使用服务 - 完善API契约测试覆盖 --- backend/internal/handler/usage_handler.go | 22 +++++--------- backend/internal/repository/api_key_repo.go | 16 ++++++++++ backend/internal/server/api_contract_test.go | 19 ++++++++++++ .../internal/service/account_usage_service.go | 4 +++ backend/internal/service/api_key_service.go | 13 ++++++++ backend/internal/service/usage_service.go | 30 +++++++++++++++---- 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/backend/internal/handler/usage_handler.go b/backend/internal/handler/usage_handler.go index 15b30bbb..a0cf9f2c 100644 --- a/backend/internal/handler/usage_handler.go +++ b/backend/internal/handler/usage_handler.go @@ -371,24 +371,16 @@ func (h *UsageHandler) DashboardApiKeysUsage(c *gin.Context) { return } - // Verify ownership of all requested API keys - userApiKeys, _, err := h.apiKeyService.List(c.Request.Context(), subject.UserID, pagination.PaginationParams{Page: 1, PageSize: 1000}) - if err != nil { - response.ErrorFrom(c, err) + // Limit the number of API key IDs to prevent SQL parameter overflow + if len(req.ApiKeyIDs) > 100 { + response.BadRequest(c, "Too many API key IDs (maximum 100 allowed)") return } - userApiKeyIDs := make(map[int64]bool) - for _, key := range userApiKeys { - userApiKeyIDs[key.ID] = true - } - - // Filter to only include user's own API keys - validApiKeyIDs := make([]int64, 0) - for _, id := range req.ApiKeyIDs { - if userApiKeyIDs[id] { - validApiKeyIDs = append(validApiKeyIDs, id) - } + validApiKeyIDs, err := h.apiKeyService.VerifyOwnership(c.Request.Context(), subject.UserID, req.ApiKeyIDs) + if err != nil { + response.ErrorFrom(c, err) + return } if len(validApiKeyIDs) == 0 { diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 718bef33..a6001ecc 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -81,6 +81,22 @@ func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, param return outKeys, paginationResultFromTotal(total, params), nil } +func (r *apiKeyRepository) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) { + if len(apiKeyIDs) == 0 { + return []int64{}, nil + } + + ids := make([]int64, 0, len(apiKeyIDs)) + err := r.db.WithContext(ctx). + Model(&apiKeyModel{}). + Where("user_id = ? AND id IN ?", userID, apiKeyIDs). + Pluck("id", &ids).Error + if err != nil { + return nil, err + } + return ids, nil +} + func (r *apiKeyRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) { var count int64 err := r.db.WithContext(ctx).Model(&apiKeyModel{}).Where("user_id = ?", userID).Count(&count).Error diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 1aeedf8d..55f83afa 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -788,6 +788,25 @@ func (r *stubApiKeyRepo) ListByUserID(ctx context.Context, userID int64, params }, nil } +func (r *stubApiKeyRepo) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) { + if len(apiKeyIDs) == 0 { + return []int64{}, nil + } + seen := make(map[int64]struct{}, len(apiKeyIDs)) + out := make([]int64, 0, len(apiKeyIDs)) + for _, id := range apiKeyIDs { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + key, ok := r.byID[id] + if ok && key.UserID == userID { + out = append(out, id) + } + } + return out, nil +} + func (r *stubApiKeyRepo) CountByUserID(ctx context.Context, userID int64) (int64, error) { var count int64 for _, key := range r.byID { diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 642c8e09..575e72b1 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -48,6 +48,10 @@ type UsageLogRepository interface { // Account stats GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) + + // Aggregated stats (optimized) + GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) + GetApiKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) } // usageCache 用于缓存usage数据 diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 4ab50fb5..e6234382 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -34,6 +34,7 @@ type ApiKeyRepository interface { Delete(ctx context.Context, id int64) error ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams) ([]ApiKey, *pagination.PaginationResult, error) + VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) CountByUserID(ctx context.Context, userID int64) (int64, error) ExistsByKey(ctx context.Context, key string) (bool, error) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]ApiKey, *pagination.PaginationResult, error) @@ -256,6 +257,18 @@ func (s *ApiKeyService) List(ctx context.Context, userID int64, params paginatio return keys, pagination, nil } +func (s *ApiKeyService) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) { + if len(apiKeyIDs) == 0 { + return []int64{}, nil + } + + validIDs, err := s.apiKeyRepo.VerifyOwnership(ctx, userID, apiKeyIDs) + if err != nil { + return nil, fmt.Errorf("verify api key ownership: %w", err) + } + return validIDs, nil +} + // GetByID 根据ID获取API Key func (s *ApiKeyService) GetByID(ctx context.Context, id int64) (*ApiKey, error) { apiKey, err := s.apiKeyRepo.GetByID(ctx, id) diff --git a/backend/internal/service/usage_service.go b/backend/internal/service/usage_service.go index 2ccad4ff..0df8a0de 100644 --- a/backend/internal/service/usage_service.go +++ b/backend/internal/service/usage_service.go @@ -148,22 +148,40 @@ func (s *UsageService) ListByAccount(ctx context.Context, accountID int64, param // GetStatsByUser 获取用户的使用统计 func (s *UsageService) GetStatsByUser(ctx context.Context, userID int64, startTime, endTime time.Time) (*UsageStats, error) { - logs, _, err := s.usageRepo.ListByUserAndTimeRange(ctx, userID, startTime, endTime) + stats, err := s.usageRepo.GetUserStatsAggregated(ctx, userID, startTime, endTime) if err != nil { - return nil, fmt.Errorf("list usage logs: %w", err) + return nil, fmt.Errorf("get user stats: %w", err) } - return s.calculateStats(logs), nil + return &UsageStats{ + TotalRequests: stats.TotalRequests, + TotalInputTokens: stats.TotalInputTokens, + TotalOutputTokens: stats.TotalOutputTokens, + TotalCacheTokens: stats.TotalCacheTokens, + TotalTokens: stats.TotalTokens, + TotalCost: stats.TotalCost, + TotalActualCost: stats.TotalActualCost, + AverageDurationMs: stats.AverageDurationMs, + }, nil } // GetStatsByApiKey 获取API Key的使用统计 func (s *UsageService) GetStatsByApiKey(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*UsageStats, error) { - logs, _, err := s.usageRepo.ListByApiKeyAndTimeRange(ctx, apiKeyID, startTime, endTime) + stats, err := s.usageRepo.GetApiKeyStatsAggregated(ctx, apiKeyID, startTime, endTime) if err != nil { - return nil, fmt.Errorf("list usage logs: %w", err) + return nil, fmt.Errorf("get api key stats: %w", err) } - return s.calculateStats(logs), nil + return &UsageStats{ + TotalRequests: stats.TotalRequests, + TotalInputTokens: stats.TotalInputTokens, + TotalOutputTokens: stats.TotalOutputTokens, + TotalCacheTokens: stats.TotalCacheTokens, + TotalTokens: stats.TotalTokens, + TotalCost: stats.TotalCost, + TotalActualCost: stats.TotalActualCost, + AverageDurationMs: stats.AverageDurationMs, + }, nil } // GetStatsByAccount 获取账号的使用统计 From c615a4264d3fa1ec5d68e9b3e5e89da43884d023 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:04:16 +0800 Subject: [PATCH 03/10] =?UTF-8?q?refactor(frontend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进ConfirmDialog对话框组件 - 增强DataTable表格组件功能和响应式布局 - 优化EmptyState空状态组件 - 完善SubscriptionProgressMini订阅进度组件 --- .../src/components/common/ConfirmDialog.vue | 11 +++-- frontend/src/components/common/DataTable.vue | 44 +++++++++++++++++-- frontend/src/components/common/EmptyState.vue | 11 +++-- .../common/SubscriptionProgressMini.vue | 2 +- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/common/ConfirmDialog.vue b/frontend/src/components/common/ConfirmDialog.vue index 267e397c..8de77837 100644 --- a/frontend/src/components/common/ConfirmDialog.vue +++ b/frontend/src/components/common/ConfirmDialog.vue @@ -31,8 +31,12 @@ diff --git a/frontend/src/components/common/SubscriptionProgressMini.vue b/frontend/src/components/common/SubscriptionProgressMini.vue index aefbf6fe..180ffed7 100644 --- a/frontend/src/components/common/SubscriptionProgressMini.vue +++ b/frontend/src/components/common/SubscriptionProgressMini.vue @@ -246,7 +246,7 @@ function formatDaysRemaining(expiresAt: string): string { const diff = expires.getTime() - now.getTime() if (diff < 0) return t('subscriptionProgress.expired') const days = Math.ceil(diff / (1000 * 60 * 60 * 24)) - if (days === 0) return t('subscriptionProgress.expirestoday') + if (days === 0) return t('subscriptionProgress.expiresToday') if (days === 1) return t('subscriptionProgress.expiresTomorrow') return t('subscriptionProgress.daysRemaining', { days }) } From 8d387886727b9088de18504e588c65b7c5fffb2c Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:04:35 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat(frontend):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增英文翻译条目 - 完善中文翻译内容 - 改进多语言支持 --- frontend/src/i18n/locales/en.ts | 25 ++++++++++++++++--- frontend/src/i18n/locales/zh.ts | 43 +++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 269f1a1a..3c3e2a93 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -52,6 +52,7 @@ export default { password: 'Password', databaseName: 'Database Name', sslMode: 'SSL Mode', + passwordPlaceholder: 'Password', ssl: { disable: 'Disable', require: 'Require', @@ -64,13 +65,17 @@ export default { host: 'Host', port: 'Port', password: 'Password (optional)', - database: 'Database' + database: 'Database', + passwordPlaceholder: 'Password' }, admin: { title: 'Admin Account', email: 'Email', password: 'Password', - confirmPassword: 'Confirm Password' + confirmPassword: 'Confirm Password', + passwordPlaceholder: 'Min 6 characters', + confirmPasswordPlaceholder: 'Confirm password', + passwordMismatch: 'Passwords do not match' }, ready: { title: 'Ready to Install', @@ -127,7 +132,14 @@ export default { searchPlaceholder: 'Search...', noOptionsFound: 'No options found', saving: 'Saving...', - refresh: 'Refresh' + refresh: 'Refresh', + time: { + never: 'Never', + justNow: 'Just now', + minutesAgo: '{n}m ago', + hoursAgo: '{n}h ago', + daysAgo: '{n}d ago' + } }, // Navigation @@ -263,7 +275,7 @@ export default { created: 'Created', copyToClipboard: 'Copy to clipboard', copied: 'Copied!', - importToCcSwitch: 'Import to CC Switch', + importToCcSwitch: 'Import to CCS', enable: 'Enable', disable: 'Disable', nameLabel: 'Name', @@ -517,6 +529,7 @@ export default { actual: 'Actual', standard: 'Standard', noDataAvailable: 'No data available', + recentUsage: 'Recent Usage', failedToLoad: 'Failed to load dashboard statistics' }, @@ -569,9 +582,13 @@ export default { noSubscription: 'No subscription', daysRemaining: '{days}d', expired: 'Expired', + disable: 'Disable', + enable: 'Enable', disableUser: 'Disable User', enableUser: 'Enable User', viewApiKeys: 'View API Keys', + groups: 'Groups', + apiKeys: 'API Keys', userApiKeys: 'User API Keys', noApiKeys: 'This user has no API keys', group: 'Group', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index baf851ce..50aa911b 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -49,6 +49,7 @@ export default { password: '密码', databaseName: '数据库名称', sslMode: 'SSL 模式', + passwordPlaceholder: '密码', ssl: { disable: '禁用', require: '要求', @@ -61,13 +62,17 @@ export default { host: '主机', port: '端口', password: '密码(可选)', - database: '数据库' + database: '数据库', + passwordPlaceholder: '密码' }, admin: { title: '管理员账户', email: '邮箱', password: '密码', - confirmPassword: '确认密码' + confirmPassword: '确认密码', + passwordPlaceholder: '至少 6 个字符', + confirmPasswordPlaceholder: '确认密码', + passwordMismatch: '密码不匹配' }, ready: { title: '准备安装', @@ -124,7 +129,14 @@ export default { searchPlaceholder: '搜索...', noOptionsFound: '无匹配选项', saving: '保存中...', - refresh: '刷新' + refresh: '刷新', + time: { + never: '从未', + justNow: '刚刚', + minutesAgo: '{n}分钟前', + hoursAgo: '{n}小时前', + daysAgo: '{n}天前' + } }, // Navigation @@ -260,7 +272,7 @@ export default { created: '创建时间', copyToClipboard: '复制到剪贴板', copied: '已复制!', - importToCcSwitch: '导入到 CC Switch', + importToCcSwitch: '导入到 CCS', enable: '启用', disable: '禁用', nameLabel: '名称', @@ -589,9 +601,13 @@ export default { noSubscription: '暂无订阅', daysRemaining: '{days}天', expired: '已过期', + disable: '禁用', + enable: '启用', disableUser: '禁用用户', enableUser: '启用用户', viewApiKeys: '查看 API 密钥', + groups: '分组', + apiKeys: 'API密钥', userApiKeys: '用户 API 密钥', noApiKeys: '此用户暂无 API 密钥', group: '分组', @@ -727,10 +743,13 @@ export default { priorityHint: '数值越高优先级越高,用于账号调度', statusLabel: '状态' }, - exclusive: { + exclusiveObj: { yes: '是', no: '否' }, + exclusive: '独占', + exclusiveHint: '启用后,此分组的用户将独占使用分配的账号', + rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', platforms: { all: '全部平台', claude: 'Claude', @@ -876,6 +895,7 @@ export default { deleteConfirmMessage: "确定要删除账号 '{name}' 吗?", refreshCookie: '刷新 Cookie', testAccount: '测试账号', + searchAccounts: '搜索账号...', // Filter options allPlatforms: '全部平台', allTypes: '全部类型', @@ -903,6 +923,19 @@ export default { lastUsed: '最近使用', actions: '操作' }, + clearRateLimit: '清除速率限制', + testConnection: '测试连接', + reAuthorize: '重新授权', + refreshToken: '刷新令牌', + noAccountsYet: '暂无账号', + createFirstAccount: '添加 AI 平台账号以开始使用 API 网关。', + tokenRefreshed: 'Token 刷新成功', + accountDeleted: '账号删除成功', + rateLimitCleared: '速率限制已清除', + setupToken: 'Setup Token', + apiKey: 'API Key', + deleteConfirm: "确定要删除账号 '{name}' 吗?此操作无法撤销。", + failedToClearRateLimit: '清除速率限制失败', platforms: { claude: 'Claude', openai: 'OpenAI', From 35b1bc37534bde14de820e90acfa515de0bf00eb Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:04:56 +0800 Subject: [PATCH 05/10] =?UTF-8?q?refactor(frontend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进数据格式化逻辑 - 增强工具函数可读性 --- frontend/src/utils/format.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts index bd545d42..aec7c863 100644 --- a/frontend/src/utils/format.ts +++ b/frontend/src/utils/format.ts @@ -3,30 +3,32 @@ * 参考 CRS 项目的 format.js 实现 */ +import { i18n } from '@/i18n' + /** * 格式化相对时间 * @param date 日期字符串或 Date 对象 * @returns 相对时间字符串,如 "5m ago", "2h ago", "3d ago" */ export function formatRelativeTime(date: string | Date | null | undefined): string { - if (!date) return 'Never' + if (!date) return i18n.global.t('common.time.never') const now = new Date() const past = new Date(date) const diffMs = now.getTime() - past.getTime() // 处理未来时间或无效日期 - if (diffMs < 0 || isNaN(diffMs)) return 'Never' + if (diffMs < 0 || isNaN(diffMs)) return i18n.global.t('common.time.never') const diffSecs = Math.floor(diffMs / 1000) const diffMins = Math.floor(diffSecs / 60) const diffHours = Math.floor(diffMins / 60) const diffDays = Math.floor(diffHours / 24) - if (diffDays > 0) return `${diffDays}d ago` - if (diffHours > 0) return `${diffHours}h ago` - if (diffMins > 0) return `${diffMins}m ago` - return 'Just now' + if (diffDays > 0) return i18n.global.t('common.time.daysAgo', { n: diffDays }) + if (diffHours > 0) return i18n.global.t('common.time.hoursAgo', { n: diffHours }) + if (diffMins > 0) return i18n.global.t('common.time.minutesAgo', { n: diffMins }) + return i18n.global.t('common.time.justNow') } /** From 7a0de1765ff9f217366c0a94316b05b372d358ed Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:05:16 +0800 Subject: [PATCH 06/10] =?UTF-8?q?refactor(frontend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进账户管理视图 - 优化分组管理界面 - 完善代理管理功能 - 增强兑换码管理 - 改进订阅管理视图 - 优化使用统计展示 - 完善用户管理界面 --- frontend/src/views/admin/AccountsView.vue | 32 ++-- frontend/src/views/admin/GroupsView.vue | 8 +- frontend/src/views/admin/ProxiesView.vue | 12 +- frontend/src/views/admin/RedeemView.vue | 4 +- .../src/views/admin/SubscriptionsView.vue | 8 +- frontend/src/views/admin/UsageView.vue | 141 +++++++++++------- frontend/src/views/admin/UsersView.vue | 34 ++--- 7 files changed, 133 insertions(+), 106 deletions(-) diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 567ffb52..2c389d5c 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -280,8 +280,7 @@ @@ -323,8 +323,7 @@ diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 75ff9570..a0d34fa3 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -166,8 +166,7 @@
diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue index 68a58d01..f5d39ef2 100644 --- a/frontend/src/views/admin/ProxiesView.vue +++ b/frontend/src/views/admin/ProxiesView.vue @@ -112,8 +112,7 @@ diff --git a/frontend/src/views/admin/RedeemView.vue b/frontend/src/views/admin/RedeemView.vue index 37799b3c..9ca0976c 100644 --- a/frontend/src/views/admin/RedeemView.vue +++ b/frontend/src/views/admin/RedeemView.vue @@ -161,8 +161,7 @@ - diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index 5f58e1a2..9496f0dc 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -257,8 +257,7 @@ diff --git a/frontend/src/views/admin/UsageView.vue b/frontend/src/views/admin/UsageView.vue index 69de978c..bdd9f68f 100644 --- a/frontend/src/views/admin/UsageView.vue +++ b/frontend/src/views/admin/UsageView.vue @@ -455,7 +455,11 @@ ${{ row.actual_cost.toFixed(6) }} -
+
@@ -471,60 +475,6 @@ />
- -
@@ -587,6 +537,66 @@ /> + + + +
+
+
+ +
+
成本明细
+
+ {{ t('admin.usage.inputCost') }} + ${{ tooltipData.input_cost.toFixed(6) }} +
+
+ {{ t('admin.usage.outputCost') }} + ${{ tooltipData.output_cost.toFixed(6) }} +
+
+ {{ t('admin.usage.cacheCreationCost') }} + ${{ tooltipData.cache_creation_cost.toFixed(6) }} +
+
+ {{ t('admin.usage.cacheReadCost') }} + ${{ tooltipData.cache_read_cost.toFixed(6) }} +
+
+ +
+ {{ t('usage.rate') }} + {{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x +
+
+ {{ t('usage.original') }} + ${{ tooltipData?.total_cost.toFixed(6) }} +
+
+ {{ t('usage.billed') }} + ${{ tooltipData?.actual_cost.toFixed(6) }} +
+
+ + +
+
+