refactor: API Key 过期时间改用日期选择器,rate_limit 支持无限制

- 前端:将过期时间设置从"天数输入"改为"日期选择器",更直观
- 后端:新增 expires_at 字段(ISO 日期格式),兼容旧版 expire_days
- rate_limit 字段现在支持 null 表示无限制,移除默认值 100
- 解析逻辑:过期时间设为当天 UTC 23:59:59.999999
This commit is contained in:
fawney19
2026-01-05 02:16:16 +08:00
parent 689339117a
commit e7db76e581
9 changed files with 140 additions and 84 deletions

View File

@@ -42,7 +42,7 @@ export interface UserApiKeyExport {
allowed_endpoints?: string[] | null allowed_endpoints?: string[] | null
allowed_api_formats?: string[] | null allowed_api_formats?: string[] | null
allowed_models?: string[] | null allowed_models?: string[] | null
rate_limit?: number rate_limit?: number | null // null = 无限制
concurrent_limit?: number | null concurrent_limit?: number | null
force_capabilities?: any force_capabilities?: any
is_active: boolean is_active: boolean
@@ -220,7 +220,7 @@ export interface AdminApiKey {
total_requests?: number total_requests?: number
total_tokens?: number total_tokens?: number
total_cost_usd?: number total_cost_usd?: number
rate_limit?: number rate_limit?: number | null // null = 无限制
allowed_providers?: string[] | null // 允许的提供商列表 allowed_providers?: string[] | null // 允许的提供商列表
allowed_api_formats?: string[] | null // 允许的 API 格式列表 allowed_api_formats?: string[] | null // 允许的 API 格式列表
allowed_models?: string[] | null // 允许的模型列表 allowed_models?: string[] | null // 允许的模型列表
@@ -236,8 +236,8 @@ export interface CreateStandaloneApiKeyRequest {
allowed_providers?: string[] | null allowed_providers?: string[] | null
allowed_api_formats?: string[] | null allowed_api_formats?: string[] | null
allowed_models?: string[] | null allowed_models?: string[] | null
rate_limit?: number rate_limit?: number | null // null = 无限制
expire_days?: number | null // null = 永不过期 expires_at?: string | null // ISO 日期字符串,如 "2025-12-31"null = 永不过期
initial_balance_usd: number // 初始余额,必须设置 initial_balance_usd: number // 初始余额,必须设置
auto_delete_on_expiry?: boolean // 过期后是否自动删除 auto_delete_on_expiry?: boolean // 过期后是否自动删除
} }

View File

@@ -79,45 +79,45 @@
<div class="space-y-2"> <div class="space-y-2">
<Label <Label
for="form-expire-days" for="form-expires-at"
class="text-sm font-medium" class="text-sm font-medium"
>有效期设置</Label> >有效期设置</Label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Input <div class="relative flex-1">
id="form-expire-days" <Input
:model-value="form.expire_days ?? ''" id="form-expires-at"
type="number" :model-value="form.expires_at || ''"
min="1" type="date"
max="3650" :min="minExpiryDate"
placeholder="天数" class="h-9 pr-8"
:class="form.never_expire ? 'flex-1 h-9 opacity-50' : 'flex-1 h-9'" :placeholder="form.expires_at ? '' : '永不过期'"
:disabled="form.never_expire" @update:model-value="(v) => form.expires_at = v || undefined"
@update:model-value="(v) => form.expire_days = parseNumberInput(v, { min: 1, max: 3650 })" />
/> <button
<label class="flex items-center gap-1.5 border rounded-md px-2 py-1.5 bg-muted/50 cursor-pointer text-xs whitespace-nowrap"> v-if="form.expires_at"
<input type="button"
v-model="form.never_expire" class="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
type="checkbox" title="清空永不过期"
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer" @click="clearExpiryDate"
@change="onNeverExpireChange"
> >
永不过期 <X class="h-4 w-4" />
</label> </button>
</div>
<label <label
class="flex items-center gap-1.5 border rounded-md px-2 py-1.5 bg-muted/50 cursor-pointer text-xs whitespace-nowrap" class="flex items-center gap-1.5 border rounded-md px-2 py-1.5 bg-muted/50 cursor-pointer text-xs whitespace-nowrap"
:class="form.never_expire ? 'opacity-50' : ''" :class="!form.expires_at ? 'opacity-50 cursor-not-allowed' : ''"
> >
<input <input
v-model="form.auto_delete_on_expiry" v-model="form.auto_delete_on_expiry"
type="checkbox" type="checkbox"
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer" class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer"
:disabled="form.never_expire" :disabled="!form.expires_at"
> >
到期删除 到期删除
</label> </label>
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
不勾选"到期删除"则仅禁用 {{ form.expires_at ? '到期后' + (form.auto_delete_on_expiry ? '自动删除' : '仅禁用') + '(当天 UTC 23:59 失效)' : '留空表示永不过期' }}
</p> </p>
</div> </div>
@@ -280,7 +280,7 @@ import {
Input, Input,
Label, Label,
} from '@/components/ui' } from '@/components/ui'
import { Plus, SquarePen, Key, Shield, ChevronDown } from 'lucide-vue-next' import { Plus, SquarePen, Key, Shield, ChevronDown, X } from 'lucide-vue-next'
import { useFormDialog } from '@/composables/useFormDialog' import { useFormDialog } from '@/composables/useFormDialog'
import { ModelMultiSelect } from '@/components/common' import { ModelMultiSelect } from '@/components/common'
import { getProvidersSummary } from '@/api/endpoints/providers' import { getProvidersSummary } from '@/api/endpoints/providers'
@@ -294,8 +294,7 @@ export interface StandaloneKeyFormData {
id?: string id?: string
name: string name: string
initial_balance_usd?: number initial_balance_usd?: number
expire_days?: number expires_at?: string // ISO 日期字符串,如 "2025-12-31"undefined = 永不过期
never_expire: boolean
rate_limit?: number rate_limit?: number
auto_delete_on_expiry: boolean auto_delete_on_expiry: boolean
allowed_providers: string[] allowed_providers: string[]
@@ -329,8 +328,7 @@ const allApiFormats = ref<string[]>([])
const form = ref<StandaloneKeyFormData>({ const form = ref<StandaloneKeyFormData>({
name: '', name: '',
initial_balance_usd: 10, initial_balance_usd: 10,
expire_days: undefined, expires_at: undefined,
never_expire: true,
rate_limit: undefined, rate_limit: undefined,
auto_delete_on_expiry: false, auto_delete_on_expiry: false,
allowed_providers: [], allowed_providers: [],
@@ -338,12 +336,18 @@ const form = ref<StandaloneKeyFormData>({
allowed_models: [] allowed_models: []
}) })
// 计算最小可选日期(明天)
const minExpiryDate = computed(() => {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
return tomorrow.toISOString().split('T')[0]
})
function resetForm() { function resetForm() {
form.value = { form.value = {
name: '', name: '',
initial_balance_usd: 10, initial_balance_usd: 10,
expire_days: undefined, expires_at: undefined,
never_expire: true,
rate_limit: undefined, rate_limit: undefined,
auto_delete_on_expiry: false, auto_delete_on_expiry: false,
allowed_providers: [], allowed_providers: [],
@@ -360,8 +364,7 @@ function loadKeyData() {
id: props.apiKey.id, id: props.apiKey.id,
name: props.apiKey.name || '', name: props.apiKey.name || '',
initial_balance_usd: props.apiKey.initial_balance_usd, initial_balance_usd: props.apiKey.initial_balance_usd,
expire_days: props.apiKey.expire_days, expires_at: props.apiKey.expires_at,
never_expire: props.apiKey.never_expire,
rate_limit: props.apiKey.rate_limit, rate_limit: props.apiKey.rate_limit,
auto_delete_on_expiry: props.apiKey.auto_delete_on_expiry, auto_delete_on_expiry: props.apiKey.auto_delete_on_expiry,
allowed_providers: props.apiKey.allowed_providers || [], allowed_providers: props.apiKey.allowed_providers || [],
@@ -406,12 +409,10 @@ function toggleSelection(field: 'allowed_providers' | 'allowed_api_formats' | 'a
} }
} }
// 永不过期切换 // 清空过期日期(同时清空到期删除选项)
function onNeverExpireChange() { function clearExpiryDate() {
if (form.value.never_expire) { form.value.expires_at = undefined
form.value.expire_days = undefined form.value.auto_delete_on_expiry = false
form.value.auto_delete_on_expiry = false
}
} }
// 提交表单 // 提交表单

View File

@@ -850,28 +850,20 @@ async function deleteApiKey(apiKey: AdminApiKey) {
} }
function editApiKey(apiKey: AdminApiKey) { function editApiKey(apiKey: AdminApiKey) {
// 计算过期天数 // 解析过期日期为 YYYY-MM-DD 格式
let expireDays: number | undefined = undefined // 保留原始日期,不做时间过滤(避免编辑当天过期的 Key 时意外清空)
let neverExpire = true let expiresAt: string | undefined = undefined
if (apiKey.expires_at) { if (apiKey.expires_at) {
const expiresDate = new Date(apiKey.expires_at) const expiresDate = new Date(apiKey.expires_at)
const now = new Date() expiresAt = expiresDate.toISOString().split('T')[0]
const diffMs = expiresDate.getTime() - now.getTime()
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24))
if (diffDays > 0) {
expireDays = diffDays
neverExpire = false
}
} }
editingKeyData.value = { editingKeyData.value = {
id: apiKey.id, id: apiKey.id,
name: apiKey.name || '', name: apiKey.name || '',
expire_days: expireDays, expires_at: expiresAt,
never_expire: neverExpire, rate_limit: apiKey.rate_limit ?? undefined,
rate_limit: apiKey.rate_limit || 100,
auto_delete_on_expiry: apiKey.auto_delete_on_expiry || false, auto_delete_on_expiry: apiKey.auto_delete_on_expiry || false,
allowed_providers: apiKey.allowed_providers || [], allowed_providers: apiKey.allowed_providers || [],
allowed_api_formats: apiKey.allowed_api_formats || [], allowed_api_formats: apiKey.allowed_api_formats || [],
@@ -1033,14 +1025,25 @@ function closeKeyFormDialog() {
// 统一处理表单提交 // 统一处理表单提交
async function handleKeyFormSubmit(data: StandaloneKeyFormData) { async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
// 验证过期日期(如果设置了,必须晚于今天)
if (data.expires_at) {
const selectedDate = new Date(data.expires_at)
const today = new Date()
today.setHours(0, 0, 0, 0)
if (selectedDate <= today) {
error('过期日期必须晚于今天')
return
}
}
keyFormDialogRef.value?.setSaving(true) keyFormDialogRef.value?.setSaving(true)
try { try {
if (data.id) { if (data.id) {
// 更新 // 更新
const updateData: Partial<CreateStandaloneApiKeyRequest> = { const updateData: Partial<CreateStandaloneApiKeyRequest> = {
name: data.name || undefined, name: data.name || undefined,
rate_limit: data.rate_limit, rate_limit: data.rate_limit ?? null, // undefined = 无限制,显式传 null
expire_days: data.never_expire ? null : (data.expire_days || null), expires_at: data.expires_at || null, // undefined/空 = 永不过期
auto_delete_on_expiry: data.auto_delete_on_expiry, auto_delete_on_expiry: data.auto_delete_on_expiry,
// 空数组表示清除限制(允许全部),后端会将空数组存为 NULL // 空数组表示清除限制(允许全部),后端会将空数组存为 NULL
allowed_providers: data.allowed_providers, allowed_providers: data.allowed_providers,
@@ -1058,8 +1061,8 @@ async function handleKeyFormSubmit(data: StandaloneKeyFormData) {
const createData: CreateStandaloneApiKeyRequest = { const createData: CreateStandaloneApiKeyRequest = {
name: data.name || undefined, name: data.name || undefined,
initial_balance_usd: data.initial_balance_usd, initial_balance_usd: data.initial_balance_usd,
rate_limit: data.rate_limit, rate_limit: data.rate_limit ?? null, // undefined = 无限制,显式传 null
expire_days: data.never_expire ? null : (data.expire_days || null), expires_at: data.expires_at || null, // undefined/空 = 永不过期
auto_delete_on_expiry: data.auto_delete_on_expiry, auto_delete_on_expiry: data.auto_delete_on_expiry,
// 空数组表示不设置限制(允许全部),后端会将空数组存为 NULL // 空数组表示不设置限制(允许全部),后端会将空数组存为 NULL
allowed_providers: data.allowed_providers, allowed_providers: data.allowed_providers,

View File

@@ -3,7 +3,7 @@
独立余额Key不关联用户配额有独立余额限制用于给非注册用户使用。 独立余额Key不关联用户配额有独立余额限制用于给非注册用户使用。
""" """
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
@@ -11,14 +11,50 @@ from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.pipeline import ApiRequestPipeline from src.api.base.pipeline import ApiRequestPipeline
from src.core.exceptions import NotFoundException from src.core.exceptions import InvalidRequestException, NotFoundException
from src.core.logger import logger from src.core.logger import logger
from src.database import get_db from src.database import get_db
from src.models.api import CreateApiKeyRequest from src.models.api import CreateApiKeyRequest
from src.models.database import ApiKey, User from src.models.database import ApiKey
from src.services.user.apikey import ApiKeyService from src.services.user.apikey import ApiKeyService
def parse_expiry_date(date_str: Optional[str]) -> Optional[datetime]:
"""解析过期日期字符串为 datetime 对象UTC 时区)。
Args:
date_str: 日期字符串,支持 "YYYY-MM-DD" 或 ISO 格式
Returns:
datetime 对象(当天 23:59:59.999999 UTC或 None 如果输入为空
Raises:
BadRequestException: 日期格式无效
"""
if not date_str or not date_str.strip():
return None
date_str = date_str.strip()
# 尝试 YYYY-MM-DD 格式
try:
parsed_date = datetime.strptime(date_str, "%Y-%m-%d")
# 设置为当天结束时间 (23:59:59.999999 UTC)
return parsed_date.replace(
hour=23, minute=59, second=59, microsecond=999999, tzinfo=timezone.utc
)
except ValueError:
pass
# 尝试完整 ISO 格式
try:
return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
except ValueError:
pass
raise InvalidRequestException(f"无效的日期格式: {date_str},请使用 YYYY-MM-DD 格式")
router = APIRouter(prefix="/api/admin/api-keys", tags=["Admin - API Keys (Standalone)"]) router = APIRouter(prefix="/api/admin/api-keys", tags=["Admin - API Keys (Standalone)"])
pipeline = ApiRequestPipeline() pipeline = ApiRequestPipeline()
@@ -215,6 +251,9 @@ class AdminCreateStandaloneKeyAdapter(AdminApiAdapter):
# 独立Key需要关联到管理员用户从context获取 # 独立Key需要关联到管理员用户从context获取
admin_user_id = context.user.id admin_user_id = context.user.id
# 解析过期时间(优先使用 expires_at其次使用 expire_days
expires_at_dt = parse_expiry_date(self.key_data.expires_at)
# 创建独立Key # 创建独立Key
api_key, plain_key = ApiKeyService.create_api_key( api_key, plain_key = ApiKeyService.create_api_key(
db=db, db=db,
@@ -224,7 +263,8 @@ class AdminCreateStandaloneKeyAdapter(AdminApiAdapter):
allowed_api_formats=self.key_data.allowed_api_formats, allowed_api_formats=self.key_data.allowed_api_formats,
allowed_models=self.key_data.allowed_models, allowed_models=self.key_data.allowed_models,
rate_limit=self.key_data.rate_limit, # None 表示不限制 rate_limit=self.key_data.rate_limit, # None 表示不限制
expire_days=self.key_data.expire_days, expire_days=self.key_data.expire_days, # 兼容旧版
expires_at=expires_at_dt, # 优先使用
initial_balance_usd=self.key_data.initial_balance_usd, initial_balance_usd=self.key_data.initial_balance_usd,
is_standalone=True, # 标记为独立Key is_standalone=True, # 标记为独立Key
auto_delete_on_expiry=self.key_data.auto_delete_on_expiry, auto_delete_on_expiry=self.key_data.auto_delete_on_expiry,
@@ -270,7 +310,8 @@ class AdminUpdateApiKeyAdapter(AdminApiAdapter):
update_data = {} update_data = {}
if self.key_data.name is not None: if self.key_data.name is not None:
update_data["name"] = self.key_data.name update_data["name"] = self.key_data.name
if self.key_data.rate_limit is not None: # rate_limit: 显式传递时更新(包括 null 表示无限制)
if "rate_limit" in self.key_data.model_fields_set:
update_data["rate_limit"] = self.key_data.rate_limit update_data["rate_limit"] = self.key_data.rate_limit
if ( if (
hasattr(self.key_data, "auto_delete_on_expiry") hasattr(self.key_data, "auto_delete_on_expiry")
@@ -287,19 +328,21 @@ class AdminUpdateApiKeyAdapter(AdminApiAdapter):
update_data["allowed_models"] = self.key_data.allowed_models update_data["allowed_models"] = self.key_data.allowed_models
# 处理过期时间 # 处理过期时间
if self.key_data.expire_days is not None: # 优先使用 expires_at如果显式传递且有值
if self.key_data.expire_days > 0: if self.key_data.expires_at and self.key_data.expires_at.strip():
from datetime import timedelta update_data["expires_at"] = parse_expiry_date(self.key_data.expires_at)
elif "expires_at" in self.key_data.model_fields_set:
# expires_at 明确传递为 null 或空字符串,设为永不过期
update_data["expires_at"] = None
# 兼容旧版 expire_days
elif "expire_days" in self.key_data.model_fields_set:
if self.key_data.expire_days is not None and self.key_data.expire_days > 0:
update_data["expires_at"] = datetime.now(timezone.utc) + timedelta( update_data["expires_at"] = datetime.now(timezone.utc) + timedelta(
days=self.key_data.expire_days days=self.key_data.expire_days
) )
else: else:
# expire_days = 0 或负数表示永不过期 # expire_days = None/0/负数 表示永不过期
update_data["expires_at"] = None update_data["expires_at"] = None
elif hasattr(self.key_data, "expire_days") and self.key_data.expire_days is None:
# 明确传递 None设为永不过期
update_data["expires_at"] = None
# 使用 ApiKeyService 更新 # 使用 ApiKeyService 更新
updated_key = ApiKeyService.update_api_key(db, self.key_id, **update_data) updated_key = ApiKeyService.update_api_key(db, self.key_id, **update_data)

View File

@@ -1133,7 +1133,7 @@ class AdminImportUsersAdapter(AdminApiAdapter):
allowed_endpoints=key_data.get("allowed_endpoints"), allowed_endpoints=key_data.get("allowed_endpoints"),
allowed_api_formats=key_data.get("allowed_api_formats"), allowed_api_formats=key_data.get("allowed_api_formats"),
allowed_models=key_data.get("allowed_models"), allowed_models=key_data.get("allowed_models"),
rate_limit=key_data.get("rate_limit", 100), rate_limit=key_data.get("rate_limit"), # None = 无限制
concurrent_limit=key_data.get("concurrent_limit", 5), concurrent_limit=key_data.get("concurrent_limit", 5),
force_capabilities=key_data.get("force_capabilities"), force_capabilities=key_data.get("force_capabilities"),
is_active=key_data.get("is_active", True), is_active=key_data.get("is_active", True),

View File

@@ -431,7 +431,7 @@ class AdminCreateUserKeyAdapter(AdminApiAdapter):
name=key_data.name, name=key_data.name,
allowed_providers=key_data.allowed_providers, allowed_providers=key_data.allowed_providers,
allowed_models=key_data.allowed_models, allowed_models=key_data.allowed_models,
rate_limit=key_data.rate_limit or 100, rate_limit=key_data.rate_limit, # None = 无限制
expire_days=key_data.expire_days, expire_days=key_data.expire_days,
initial_balance_usd=None, # 普通Key不设置余额限制 initial_balance_usd=None, # 普通Key不设置余额限制
is_standalone=False, # 不是独立Key is_standalone=False, # 不是独立Key

View File

@@ -309,8 +309,9 @@ class CreateApiKeyRequest(BaseModel):
allowed_endpoints: Optional[List[str]] = None # 允许使用的端点 ID 列表 allowed_endpoints: Optional[List[str]] = None # 允许使用的端点 ID 列表
allowed_api_formats: Optional[List[str]] = None # 允许使用的 API 格式列表 allowed_api_formats: Optional[List[str]] = None # 允许使用的 API 格式列表
allowed_models: Optional[List[str]] = None # 允许使用的模型名称列表 allowed_models: Optional[List[str]] = None # 允许使用的模型名称列表
rate_limit: Optional[int] = 100 rate_limit: Optional[int] = None # None = 无限制
expire_days: Optional[int] = None # None = 永不过期,数字 = 多少天后过期 expire_days: Optional[int] = None # None = 永不过期,数字 = 多少天后过期(兼容旧版)
expires_at: Optional[str] = None # ISO 日期字符串,如 "2025-12-31",优先于 expire_days
initial_balance_usd: Optional[float] = Field( initial_balance_usd: Optional[float] = Field(
None, description="初始余额USD仅用于独立KeyNone = 无限制" None, description="初始余额USD仅用于独立KeyNone = 无限制"
) )

View File

@@ -150,7 +150,7 @@ class ApiKey(Base):
allowed_endpoints = Column(JSON, nullable=True) # 允许使用的端点 ID 列表 allowed_endpoints = Column(JSON, nullable=True) # 允许使用的端点 ID 列表
allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表 allowed_api_formats = Column(JSON, nullable=True) # 允许使用的 API 格式列表
allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表 allowed_models = Column(JSON, nullable=True) # 允许使用的模型名称列表
rate_limit = Column(Integer, default=100) # 每分钟请求限制 rate_limit = Column(Integer, default=None, nullable=True) # 每分钟请求限制None = 无限制
concurrent_limit = Column(Integer, default=5, nullable=True) # 并发请求限制 concurrent_limit = Column(Integer, default=5, nullable=True) # 并发请求限制
# Key 能力配置 # Key 能力配置

View File

@@ -25,9 +25,10 @@ class ApiKeyService:
allowed_providers: Optional[List[str]] = None, allowed_providers: Optional[List[str]] = None,
allowed_api_formats: Optional[List[str]] = None, allowed_api_formats: Optional[List[str]] = None,
allowed_models: Optional[List[str]] = None, allowed_models: Optional[List[str]] = None,
rate_limit: int = 100, rate_limit: Optional[int] = None,
concurrent_limit: int = 5, concurrent_limit: int = 5,
expire_days: Optional[int] = None, expire_days: Optional[int] = None,
expires_at: Optional[datetime] = None, # 直接传入过期时间,优先于 expire_days
initial_balance_usd: Optional[float] = None, initial_balance_usd: Optional[float] = None,
is_standalone: bool = False, is_standalone: bool = False,
auto_delete_on_expiry: bool = False, auto_delete_on_expiry: bool = False,
@@ -44,6 +45,7 @@ class ApiKeyService:
rate_limit: 速率限制 rate_limit: 速率限制
concurrent_limit: 并发限制 concurrent_limit: 并发限制
expire_days: 过期天数None = 永不过期 expire_days: 过期天数None = 永不过期
expires_at: 直接指定过期时间,优先于 expire_days
initial_balance_usd: 初始余额USD仅用于独立KeyNone = 无限制 initial_balance_usd: 初始余额USD仅用于独立KeyNone = 无限制
is_standalone: 是否为独立余额Key仅管理员可创建 is_standalone: 是否为独立余额Key仅管理员可创建
auto_delete_on_expiry: 过期后是否自动删除True=物理删除False=仅禁用) auto_delete_on_expiry: 过期后是否自动删除True=物理删除False=仅禁用)
@@ -54,10 +56,10 @@ class ApiKeyService:
key_hash = ApiKey.hash_key(key) key_hash = ApiKey.hash_key(key)
key_encrypted = crypto_service.encrypt(key) # 加密存储密钥 key_encrypted = crypto_service.encrypt(key) # 加密存储密钥
# 计算过期时间 # 计算过期时间:优先使用 expires_at其次使用 expire_days
expires_at = None final_expires_at = expires_at
if expire_days: if final_expires_at is None and expire_days:
expires_at = datetime.now(timezone.utc) + timedelta(days=expire_days) final_expires_at = datetime.now(timezone.utc) + timedelta(days=expire_days)
# 空数组转为 None表示不限制 # 空数组转为 None表示不限制
api_key = ApiKey( api_key = ApiKey(
@@ -70,7 +72,7 @@ class ApiKeyService:
allowed_models=allowed_models or None, allowed_models=allowed_models or None,
rate_limit=rate_limit, rate_limit=rate_limit,
concurrent_limit=concurrent_limit, concurrent_limit=concurrent_limit,
expires_at=expires_at, expires_at=final_expires_at,
balance_used_usd=0.0, balance_used_usd=0.0,
current_balance_usd=initial_balance_usd, # 直接使用初始余额None = 无限制 current_balance_usd=initial_balance_usd, # 直接使用初始余额None = 无限制
is_standalone=is_standalone, is_standalone=is_standalone,
@@ -145,6 +147,9 @@ class ApiKeyService:
# 允许显式设置为空数组/None 的字段(空数组会转为 None表示"全部" # 允许显式设置为空数组/None 的字段(空数组会转为 None表示"全部"
nullable_list_fields = {"allowed_providers", "allowed_api_formats", "allowed_models"} nullable_list_fields = {"allowed_providers", "allowed_api_formats", "allowed_models"}
# 允许显式设置为 None 的字段(如 expires_at=None 表示永不过期rate_limit=None 表示无限制)
nullable_fields = {"expires_at", "rate_limit"}
for field, value in kwargs.items(): for field, value in kwargs.items():
if field not in updatable_fields: if field not in updatable_fields:
continue continue
@@ -153,6 +158,9 @@ class ApiKeyService:
if value is not None: if value is not None:
# 空数组转为 None表示允许全部 # 空数组转为 None表示允许全部
setattr(api_key, field, value if value else None) setattr(api_key, field, value if value else None)
elif field in nullable_fields:
# 这些字段允许显式设置为 None
setattr(api_key, field, value)
elif value is not None: elif value is not None:
setattr(api_key, field, value) setattr(api_key, field, value)