diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go index 67c866f2..f05371a8 100644 --- a/backend/internal/service/auth_service.go +++ b/backend/internal/service/auth_service.go @@ -23,6 +23,7 @@ var ( ErrTokenExpired = errors.New("token has expired") ErrEmailVerifyRequired = errors.New("email verification is required") ErrRegDisabled = errors.New("registration is currently disabled") + ErrServiceUnavailable = errors.New("service temporarily unavailable") ) // JWTClaims JWT载荷数据 @@ -90,7 +91,8 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw // 检查邮箱是否已存在 existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) if err != nil { - return "", nil, fmt.Errorf("check email exists: %w", err) + log.Printf("[Auth] Database error checking email exists: %v", err) + return "", nil, ErrServiceUnavailable } if existsEmail { return "", nil, ErrEmailExists @@ -121,7 +123,8 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw } if err := s.userRepo.Create(ctx, user); err != nil { - return "", nil, fmt.Errorf("create user: %w", err) + log.Printf("[Auth] Database error creating user: %v", err) + return "", nil, ErrServiceUnavailable } // 生成token @@ -148,7 +151,8 @@ func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error { // 检查邮箱是否已存在 existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) if err != nil { - return fmt.Errorf("check email exists: %w", err) + log.Printf("[Auth] Database error checking email exists: %v", err) + return ErrServiceUnavailable } if existsEmail { return ErrEmailExists @@ -181,8 +185,8 @@ func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*S // 检查邮箱是否已存在 existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) if err != nil { - log.Printf("[Auth] Error checking email exists: %v", err) - return nil, fmt.Errorf("check email exists: %w", err) + log.Printf("[Auth] Database error checking email exists: %v", err) + return nil, ErrServiceUnavailable } if existsEmail { log.Printf("[Auth] Email already exists: %s", email) @@ -254,7 +258,9 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil, ErrInvalidCredentials } - return "", nil, fmt.Errorf("get user by email: %w", err) + // 记录数据库错误但不暴露给用户 + log.Printf("[Auth] Database error during login: %v", err) + return "", nil, ErrServiceUnavailable } // 验证密码 @@ -354,7 +360,8 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) ( if errors.Is(err, gorm.ErrRecordNotFound) { return "", ErrInvalidToken } - return "", fmt.Errorf("get user: %w", err) + log.Printf("[Auth] Database error refreshing token: %v", err) + return "", ErrServiceUnavailable } // 检查用户状态 diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index b38d9203..45f3b235 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -611,9 +611,16 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec account := input.Account subscription := input.Subscription + // 计算实际的新输入token(减去缓存读取的token) + // 因为 input_tokens 包含了 cache_read_tokens,而缓存读取的token不应按输入价格计费 + actualInputTokens := result.Usage.InputTokens - result.Usage.CacheReadInputTokens + if actualInputTokens < 0 { + actualInputTokens = 0 + } + // Calculate cost tokens := UsageTokens{ - InputTokens: result.Usage.InputTokens, + InputTokens: actualInputTokens, OutputTokens: result.Usage.OutputTokens, CacheCreationTokens: result.Usage.CacheCreationInputTokens, CacheReadTokens: result.Usage.CacheReadInputTokens, @@ -645,7 +652,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec AccountID: account.ID, RequestID: result.RequestID, Model: result.Model, - InputTokens: result.Usage.InputTokens, + InputTokens: actualInputTokens, OutputTokens: result.Usage.OutputTokens, CacheCreationTokens: result.Usage.CacheCreationInputTokens, CacheReadTokens: result.Usage.CacheReadInputTokens, diff --git a/frontend/src/components/account/ReAuthAccountModal.vue b/frontend/src/components/account/ReAuthAccountModal.vue index 85fe2410..69f610f5 100644 --- a/frontend/src/components/account/ReAuthAccountModal.vue +++ b/frontend/src/components/account/ReAuthAccountModal.vue @@ -226,6 +226,9 @@ const handleExchangeCode = async () => { extra }) + // Clear error status after successful re-authorization + await adminAPI.accounts.clearError(props.account.id) + appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) emit('reauthorized') handleClose() @@ -262,6 +265,9 @@ const handleExchangeCode = async () => { extra }) + // Clear error status after successful re-authorization + await adminAPI.accounts.clearError(props.account.id) + appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) emit('reauthorized') handleClose() @@ -301,6 +307,9 @@ const handleCookieAuth = async (sessionKey: string) => { extra }) + // Clear error status after successful re-authorization + await adminAPI.accounts.clearError(props.account.id) + appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) emit('reauthorized') handleClose() diff --git a/frontend/src/components/common/GroupBadge.vue b/frontend/src/components/common/GroupBadge.vue index 3f957c6e..51a9d09b 100644 --- a/frontend/src/components/common/GroupBadge.vue +++ b/frontend/src/components/common/GroupBadge.vue @@ -2,40 +2,32 @@ - - - - - - - - + + + {{ name }} + - {{ rateMultiplier }}x + {{ labelText }} diff --git a/frontend/src/components/common/PlatformIcon.vue b/frontend/src/components/common/PlatformIcon.vue new file mode 100644 index 00000000..4d46f27d --- /dev/null +++ b/frontend/src/components/common/PlatformIcon.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 08be3340..5d470152 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -182,6 +182,11 @@ export default { addBalanceWithCode: 'Add balance with a code', }, + // Groups (shared) + groups: { + subscription: 'Sub', + }, + // API Keys keys: { title: 'API Keys', @@ -515,6 +520,7 @@ export default { accounts: 'Accounts', status: 'Status', actions: 'Actions', + billingType: 'Billing Type', }, accountsCount: '{count} accounts', form: { @@ -527,12 +533,16 @@ export default { enterGroupName: 'Enter group name', optionalDescription: 'Optional description', platformHint: 'Select the platform this group is associated with', + platformNotEditable: 'Platform cannot be changed after creation', rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)', exclusiveHint: 'Exclusive (requires explicit user access)', noGroupsYet: 'No groups yet', createFirstGroup: 'Create your first group to organize API keys.', creating: 'Creating...', updating: 'Updating...', + limitDay: 'd', + limitWeek: 'w', + limitMonth: 'mo', groupCreated: 'Group created successfully', groupUpdated: 'Group updated successfully', groupDeleted: 'Group deleted successfully', @@ -661,6 +671,9 @@ export default { tokenRefreshed: 'Token refreshed successfully', accountDeleted: 'Account deleted successfully', rateLimitCleared: 'Rate limit cleared successfully', + resetStatus: 'Reset Status', + statusReset: 'Account status reset successfully', + failedToResetStatus: 'Failed to reset account status', failedToLoad: 'Failed to load accounts', failedToRefresh: 'Failed to refresh token', failedToDelete: 'Failed to delete account', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index ce416e1f..63780c6c 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -182,6 +182,11 @@ export default { addBalanceWithCode: '使用兑换码充值', }, + // Groups (shared) + groups: { + subscription: '订阅', + }, + // API Keys keys: { title: 'API 密钥', @@ -530,13 +535,16 @@ export default { deleteConfirmSubscription: "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。", columns: { name: '名称', + platform: '平台', rateMultiplier: '费率倍数', exclusive: '独占', - platforms: '平台', + type: '类型', priority: '优先级', apiKeys: 'API 密钥数', + accounts: '账号数', status: '状态', actions: '操作', + billingType: '计费类型', }, form: { name: '名称', @@ -588,10 +596,14 @@ export default { enterGroupName: '请输入分组名称', optionalDescription: '可选描述', platformHint: '选择此分组关联的平台', + platformNotEditable: '创建后不可更改平台', noGroupsYet: '暂无分组', createFirstGroup: '创建您的第一个分组来组织 API 密钥。', creating: '创建中...', updating: '更新中...', + limitDay: '日', + limitWeek: '周', + limitMonth: '月', groupCreated: '分组创建成功', groupUpdated: '分组更新成功', groupDeleted: '分组删除成功', @@ -749,6 +761,9 @@ export default { accountCreatedSuccess: '账号添加成功', accountUpdatedSuccess: '账号更新成功', accountDeletedSuccess: '账号删除成功', + resetStatus: '重置状态', + statusReset: '账号状态已重置', + failedToResetStatus: '重置账号状态失败', cookieRefreshedSuccess: 'Cookie 刷新成功', testSuccess: '账号测试通过', testFailed: '账号测试失败', diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 913611ea..b3da51b3 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -223,13 +223,13 @@ Claude {{ t('home.providers.supported') }} - -
+ +
G
GPT - {{ t('home.providers.soon') }} + {{ t('home.providers.supported') }}
diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index a7af49ac..04c6a959 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -139,6 +139,17 @@ + + @@ -186,8 +218,8 @@ @@ -323,15 +355,17 @@ @@ -472,6 +506,7 @@ import Modal from '@/components/common/Modal.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import EmptyState from '@/components/common/EmptyState.vue' import Select from '@/components/common/Select.vue' +import PlatformIcon from '@/components/common/PlatformIcon.vue' const { t } = useI18n() const appStore = useAppStore() @@ -479,6 +514,7 @@ const appStore = useAppStore() const columns = computed(() => [ { key: 'name', label: t('admin.groups.columns.name'), sortable: true }, { key: 'platform', label: t('admin.groups.columns.platform'), sortable: true }, + { key: 'billing_type', label: t('admin.groups.columns.billingType'), sortable: true }, { key: 'rate_multiplier', label: t('admin.groups.columns.rateMultiplier'), sortable: true }, { key: 'is_exclusive', label: t('admin.groups.columns.type'), sortable: true }, { key: 'account_count', label: t('admin.groups.columns.accounts'), sortable: true }, diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue index 94a5683a..4644265d 100644 --- a/frontend/src/views/user/KeysView.vue +++ b/frontend/src/views/user/KeysView.vue @@ -71,6 +71,7 @@ @@ -231,6 +232,7 @@ @@ -239,6 +241,7 @@