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 @@
+
+
@@ -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 @@
@@ -358,6 +361,7 @@
>
@@ -394,7 +398,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import UseKeyModal from '@/components/keys/UseKeyModal.vue'
import GroupBadge from '@/components/common/GroupBadge.vue'
-import type { ApiKey, Group, PublicSettings, SubscriptionType } from '@/types'
+import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform } from '@/types'
import type { Column } from '@/components/common/types'
import type { BatchApiKeyUsageStats } from '@/api/usage'
@@ -403,6 +407,7 @@ interface GroupOption {
label: string
rate: number
subscriptionType: SubscriptionType
+ platform: GroupPlatform
}
const appStore = useAppStore()
@@ -491,7 +496,8 @@ const groupOptions = computed(() =>
value: group.id,
label: group.name,
rate: group.rate_multiplier,
- subscriptionType: group.subscription_type
+ subscriptionType: group.subscription_type,
+ platform: group.platform
}))
)