mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 简化优先级拖拽交换逻辑并优化用户配额处理
- 优先级拖拽改为直接交换优先级值,而非重新编号 - 添加 Provider 优先级的点击编辑功能 - 创建用户时由后端统一处理默认配额逻辑 - 移除前端 getDefaultQuota API 调用和后端对应端点 Close #97
This commit is contained in:
@@ -22,6 +22,7 @@ export interface CreateUserRequest {
|
|||||||
email: string
|
email: string
|
||||||
role?: 'admin' | 'user'
|
role?: 'admin' | 'user'
|
||||||
quota_usd?: number | null
|
quota_usd?: number | null
|
||||||
|
unlimited?: boolean
|
||||||
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
|
||||||
@@ -98,11 +99,6 @@ export const usersApi = {
|
|||||||
await apiClient.patch(`/api/admin/users/${userId}/quota`)
|
await apiClient.patch(`/api/admin/users/${userId}/quota`)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDefaultQuota(): Promise<{ default_quota_usd: number }> {
|
|
||||||
const response = await apiClient.get<{ default_quota_usd: number }>('/api/admin/users/defaults/quota')
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
|
|
||||||
// 管理员统计
|
// 管理员统计
|
||||||
async getUsageStats(): Promise<any> {
|
async getUsageStats(): Promise<any> {
|
||||||
const response = await apiClient.get('/api/admin/usage/stats')
|
const response = await apiClient.get('/api/admin/usage/stats')
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
||||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
<Info class="w-3.5 h-3.5 shrink-0" />
|
||||||
<span>拖拽调整顺序,位置越靠前优先级越高</span>
|
<span>拖拽调整顺序,点击序号可编辑(相同数字为同级,负载均衡)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
@@ -88,9 +88,27 @@
|
|||||||
<GripVertical class="w-4 h-4" />
|
<GripVertical class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 序号 -->
|
<!-- 可编辑序号 -->
|
||||||
<div class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground shrink-0">
|
<div class="shrink-0">
|
||||||
{{ index + 1 }}
|
<input
|
||||||
|
v-if="editingProviderPriority === provider.id"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:value="provider.provider_priority"
|
||||||
|
class="w-8 h-6 rounded-md bg-background border border-primary text-xs font-medium text-center focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
autofocus
|
||||||
|
@blur="finishEditProviderPriority(provider, $event)"
|
||||||
|
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
||||||
|
@keydown.escape="cancelEditProviderPriority()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
||||||
|
title="点击编辑优先级,相同数字为同级(负载均衡)"
|
||||||
|
@click.stop="startEditProviderPriority(provider)"
|
||||||
|
>
|
||||||
|
{{ provider.provider_priority }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提供商信息 -->
|
<!-- 提供商信息 -->
|
||||||
@@ -457,6 +475,9 @@ const saving = ref(false)
|
|||||||
// Key 优先级编辑状态
|
// Key 优先级编辑状态
|
||||||
const editingKeyPriority = ref<Record<string, string | null>>({}) // format -> keyId
|
const editingKeyPriority = ref<Record<string, string | null>>({}) // format -> keyId
|
||||||
|
|
||||||
|
// Provider 优先级编辑状态
|
||||||
|
const editingProviderPriority = ref<string | null>(null) // providerId
|
||||||
|
|
||||||
// 调度模式状态
|
// 调度模式状态
|
||||||
const schedulingMode = ref<'fixed_order' | 'load_balance' | 'cache_affinity'>('cache_affinity')
|
const schedulingMode = ref<'fixed_order' | 'load_balance' | 'cache_affinity'>('cache_affinity')
|
||||||
|
|
||||||
@@ -552,6 +573,35 @@ function finishEditKeyPriority(format: string, key: KeyWithMeta, event: FocusEve
|
|||||||
editingKeyPriority.value[format] = null
|
editingKeyPriority.value[format] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider 优先级编辑
|
||||||
|
function startEditProviderPriority(provider: ProviderWithEndpointsSummary) {
|
||||||
|
editingProviderPriority.value = provider.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditProviderPriority() {
|
||||||
|
editingProviderPriority.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishEditProviderPriority(provider: ProviderWithEndpointsSummary, event: FocusEvent) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const newPriority = parseInt(input.value, 10)
|
||||||
|
|
||||||
|
if (!isNaN(newPriority) && newPriority >= 1) {
|
||||||
|
// 更新该 provider 的优先级
|
||||||
|
const idx = sortedProviders.value.findIndex(p => p.id === provider.id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
sortedProviders.value[idx] = {
|
||||||
|
...sortedProviders.value[idx],
|
||||||
|
provider_priority: newPriority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按 provider_priority 重新排序
|
||||||
|
sortedProviders.value = [...sortedProviders.value].sort((a, b) => a.provider_priority - b.provider_priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
editingProviderPriority.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// Provider 拖拽处理
|
// Provider 拖拽处理
|
||||||
function handleProviderDragStart(index: number, event: DragEvent) {
|
function handleProviderDragStart(index: number, event: DragEvent) {
|
||||||
draggedProvider.value = index
|
draggedProvider.value = index
|
||||||
@@ -576,21 +626,32 @@ function handleProviderDragLeave() {
|
|||||||
|
|
||||||
function handleProviderDrop(dropIndex: number) {
|
function handleProviderDrop(dropIndex: number) {
|
||||||
if (draggedProvider.value === null || draggedProvider.value === dropIndex) {
|
if (draggedProvider.value === null || draggedProvider.value === dropIndex) {
|
||||||
|
draggedProvider.value = null
|
||||||
|
dragOverProvider.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = [...sortedProviders.value]
|
const providers = sortedProviders.value
|
||||||
const draggedItem = providers[draggedProvider.value]
|
const draggedItem = providers[draggedProvider.value]
|
||||||
|
const targetItem = providers[dropIndex]
|
||||||
|
const draggedPriority = draggedItem.provider_priority
|
||||||
|
const targetPriority = targetItem.provider_priority
|
||||||
|
|
||||||
providers.splice(draggedProvider.value, 1)
|
// 如果是同组内拖拽(同优先级),忽略操作
|
||||||
providers.splice(dropIndex, 0, draggedItem)
|
if (draggedPriority === targetPriority) {
|
||||||
|
draggedProvider.value = null
|
||||||
|
dragOverProvider.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sortedProviders.value = providers.map((provider, index) => ({
|
// 直接交换优先级
|
||||||
...provider,
|
draggedItem.provider_priority = targetPriority
|
||||||
provider_priority: index + 1
|
targetItem.provider_priority = draggedPriority
|
||||||
}))
|
|
||||||
|
|
||||||
|
// 重新排序
|
||||||
|
sortedProviders.value = [...providers].sort((a, b) => a.provider_priority - b.provider_priority)
|
||||||
draggedProvider.value = null
|
draggedProvider.value = null
|
||||||
|
dragOverProvider.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key 拖拽处理
|
// Key 拖拽处理
|
||||||
@@ -619,50 +680,31 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
|||||||
const dragIndex = draggedKey.value[format]
|
const dragIndex = draggedKey.value[format]
|
||||||
if (dragIndex === null || dragIndex === dropIndex) {
|
if (dragIndex === null || dragIndex === dropIndex) {
|
||||||
draggedKey.value[format] = null
|
draggedKey.value[format] = null
|
||||||
|
dragOverKey.value[format] = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = [...keysByFormat.value[format]]
|
const keys = keysByFormat.value[format]
|
||||||
const draggedItem = keys[dragIndex]
|
const draggedItem = keys[dragIndex]
|
||||||
|
const targetItem = keys[dropIndex]
|
||||||
|
const draggedPriority = draggedItem.priority
|
||||||
|
const targetPriority = targetItem.priority
|
||||||
|
|
||||||
// 记录每个 key 的原始优先级(在修改前)
|
// 如果是同组内拖拽(同优先级),忽略操作
|
||||||
const originalPriorityMap = new Map<string, number>()
|
if (draggedPriority === targetPriority) {
|
||||||
for (const key of keys) {
|
draggedKey.value[format] = null
|
||||||
originalPriorityMap.set(key.id, key.priority)
|
dragOverKey.value[format] = null
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重排数组
|
// 直接交换优先级
|
||||||
keys.splice(dragIndex, 1)
|
draggedItem.priority = targetPriority
|
||||||
keys.splice(dropIndex, 0, draggedItem)
|
targetItem.priority = draggedPriority
|
||||||
|
|
||||||
// 按新顺序为每个组分配新的优先级
|
// 重新排序
|
||||||
// 同组的 Key 保持相同的优先级
|
keysByFormat.value[format] = [...keys].sort((a, b) => a.priority - b.priority)
|
||||||
const groupNewPriority = new Map<number, number>() // 原优先级 -> 新优先级
|
|
||||||
let currentPriority = 1
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
if (key.id === draggedItem.id) {
|
|
||||||
// 被拖动的 Key 是独立的新组,获得当前优先级
|
|
||||||
key.priority = currentPriority
|
|
||||||
currentPriority++
|
|
||||||
} else {
|
|
||||||
// 使用记录的原始优先级,而不是可能已被修改的值
|
|
||||||
const originalPriority = originalPriorityMap.get(key.id)!
|
|
||||||
|
|
||||||
if (groupNewPriority.has(originalPriority)) {
|
|
||||||
// 这个组已经分配过优先级,使用相同的值
|
|
||||||
key.priority = groupNewPriority.get(originalPriority)!
|
|
||||||
} else {
|
|
||||||
// 这个组第一次出现,分配新优先级
|
|
||||||
groupNewPriority.set(originalPriority, currentPriority)
|
|
||||||
key.priority = currentPriority
|
|
||||||
currentPriority++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keysByFormat.value[format] = keys
|
|
||||||
draggedKey.value[format] = null
|
draggedKey.value[format] = null
|
||||||
|
dragOverKey.value[format] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
@@ -686,8 +728,8 @@ async function save() {
|
|||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
const providerUpdates = sortedProviders.value.map((provider, index) =>
|
const providerUpdates = sortedProviders.value.map((provider) =>
|
||||||
updateProvider(provider.id, { provider_priority: index + 1 })
|
updateProvider(provider.id, { provider_priority: provider.provider_priority })
|
||||||
)
|
)
|
||||||
|
|
||||||
const keyUpdates: Promise<any>[] = []
|
const keyUpdates: Promise<any>[] = []
|
||||||
|
|||||||
@@ -1095,27 +1095,14 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查目标 key 是否属于一个"组"(除了被拖拽的 key,还有其他 key 与目标同优先级)
|
|
||||||
// 组的定义:2 个及以上同优先级的 key
|
|
||||||
const keysAtTargetPriority = keys.filter(k =>
|
|
||||||
k.id !== draggedKey.id && (k.internal_priority ?? 0) === targetPriority
|
|
||||||
)
|
|
||||||
// 如果有 2 个及以上 key 在目标优先级(不含被拖拽的),说明目标在组内
|
|
||||||
const targetIsInGroup = keysAtTargetPriority.length >= 2
|
|
||||||
|
|
||||||
handleKeyDragEnd()
|
handleKeyDragEnd()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetIsInGroup) {
|
// 直接交换优先级
|
||||||
// 目标在组内,被拖拽的 key 加入该组
|
await Promise.all([
|
||||||
await updateProviderKey(draggedKey.id, { internal_priority: targetPriority })
|
updateProviderKey(draggedKey.id, { internal_priority: targetPriority }),
|
||||||
} else {
|
updateProviderKey(targetKey.id, { internal_priority: draggedPriority })
|
||||||
// 目标是单独的(或只有目标自己),交换优先级
|
])
|
||||||
await Promise.all([
|
|
||||||
updateProviderKey(draggedKey.id, { internal_priority: targetPriority }),
|
|
||||||
updateProviderKey(targetKey.id, { internal_priority: draggedPriority })
|
|
||||||
])
|
|
||||||
}
|
|
||||||
showSuccess('优先级已更新')
|
showSuccess('优先级已更新')
|
||||||
await loadEndpoints()
|
await loadEndpoints()
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
|
|||||||
@@ -143,7 +143,8 @@
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
max="10000"
|
max="10000"
|
||||||
placeholder="10"
|
:placeholder="isEditMode ? '10' : '使用系统默认'"
|
||||||
|
:disabled="form.unlimited"
|
||||||
:class="form.unlimited ? 'flex-1 h-10 opacity-50' : 'flex-1 h-10'"
|
:class="form.unlimited ? 'flex-1 h-10 opacity-50' : 'flex-1 h-10'"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center gap-2 border rounded-lg px-3 py-2 bg-muted/50 w-24">
|
<div class="flex items-center justify-center gap-2 border rounded-lg px-3 py-2 bg-muted/50 w-24">
|
||||||
@@ -363,7 +364,6 @@ import { ModelMultiSelect } from '@/components/common'
|
|||||||
import { getProvidersSummary } from '@/api/endpoints/providers'
|
import { getProvidersSummary } from '@/api/endpoints/providers'
|
||||||
import { getGlobalModels } from '@/api/global-models'
|
import { getGlobalModels } from '@/api/global-models'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { usersApi } from '@/api/users'
|
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
import type { ProviderWithEndpointsSummary, GlobalModelResponse } from '@/api/endpoints/types'
|
import type { ProviderWithEndpointsSummary, GlobalModelResponse } from '@/api/endpoints/types'
|
||||||
|
|
||||||
@@ -403,7 +403,6 @@ const endpointDropdownOpen = ref(false)
|
|||||||
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
||||||
const globalModels = ref<GlobalModelResponse[]>([])
|
const globalModels = ref<GlobalModelResponse[]>([])
|
||||||
const apiFormats = ref<Array<{ value: string; label: string }>>([])
|
const apiFormats = ref<Array<{ value: string; label: string }>>([])
|
||||||
const defaultQuota = ref<number>(10)
|
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@@ -411,7 +410,7 @@ const form = ref({
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: 10,
|
quota: null as number | null,
|
||||||
role: 'user' as 'admin' | 'user',
|
role: 'user' as 'admin' | 'user',
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -432,7 +431,7 @@ function resetForm() {
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: defaultQuota.value,
|
quota: null,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -481,22 +480,18 @@ const isFormValid = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载访问控制选项
|
// 加载访问控制选项
|
||||||
async function loadAccessControlOptions(): Promise<boolean> {
|
async function loadAccessControlOptions(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [providersData, modelsData, formatsData, quotaData] = await Promise.all([
|
const [providersData, modelsData, formatsData] = await Promise.all([
|
||||||
getProvidersSummary(),
|
getProvidersSummary(),
|
||||||
getGlobalModels({ limit: 1000, is_active: true }),
|
getGlobalModels({ limit: 1000, is_active: true }),
|
||||||
adminApi.getApiFormats(),
|
adminApi.getApiFormats()
|
||||||
usersApi.getDefaultQuota()
|
|
||||||
])
|
])
|
||||||
providers.value = providersData
|
providers.value = providersData
|
||||||
globalModels.value = modelsData.models || []
|
globalModels.value = modelsData.models || []
|
||||||
apiFormats.value = formatsData.formats || []
|
apiFormats.value = formatsData.formats || []
|
||||||
defaultQuota.value = quotaData.default_quota_usd
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('加载访问限制选项失败:', err)
|
log.error('加载访问限制选项失败:', err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +515,7 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const data: UserFormData & { password?: string } = {
|
const data: UserFormData & { password?: string; unlimited?: boolean } = {
|
||||||
username: form.value.username,
|
username: form.value.username,
|
||||||
email: form.value.email.trim(),
|
email: form.value.email.trim(),
|
||||||
quota_usd: form.value.unlimited ? null : form.value.quota,
|
quota_usd: form.value.unlimited ? null : form.value.quota,
|
||||||
@@ -530,6 +525,11 @@ async function handleSubmit() {
|
|||||||
allowed_models: form.value.allowed_models.length > 0 ? form.value.allowed_models : null
|
allowed_models: form.value.allowed_models.length > 0 ? form.value.allowed_models : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建模式下传递 unlimited 字段
|
||||||
|
if (!isEditMode.value) {
|
||||||
|
data.unlimited = form.value.unlimited
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditMode.value && props.user?.id) {
|
if (isEditMode.value && props.user?.id) {
|
||||||
data.id = props.user.id
|
data.id = props.user.id
|
||||||
}
|
}
|
||||||
@@ -557,13 +557,9 @@ function setSaving(value: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听打开状态,加载选项数据
|
// 监听打开状态,加载选项数据
|
||||||
watch(isOpen, async (val) => {
|
watch(isOpen, (val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
const success = await loadAccessControlOptions()
|
loadAccessControlOptions()
|
||||||
// 创建模式下,仅在加载成功时更新表单配额
|
|
||||||
if (!isEditMode.value && success) {
|
|
||||||
form.value.quota = defaultQuota.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -939,6 +939,7 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string })
|
|||||||
password: data.password!,
|
password: data.password!,
|
||||||
email: data.email || undefined,
|
email: data.email || undefined,
|
||||||
quota_usd: data.quota_usd,
|
quota_usd: data.quota_usd,
|
||||||
|
unlimited: (data as any).unlimited,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
allowed_providers: data.allowed_providers,
|
allowed_providers: data.allowed_providers,
|
||||||
allowed_api_formats: data.allowed_api_formats,
|
allowed_api_formats: data.allowed_api_formats,
|
||||||
|
|||||||
@@ -198,20 +198,6 @@ async def delete_user_api_key(
|
|||||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/defaults/quota")
|
|
||||||
async def get_default_quota(request: Request, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
获取默认用户配额
|
|
||||||
|
|
||||||
获取系统配置的默认用户配额值,用于创建用户时的默认值。
|
|
||||||
|
|
||||||
**返回字段**:
|
|
||||||
- `default_quota_usd`: 默认配额(USD)
|
|
||||||
"""
|
|
||||||
adapter = AdminGetDefaultQuotaAdapter()
|
|
||||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
||||||
|
|
||||||
|
|
||||||
# ============== 管理员适配器实现 ==============
|
# ============== 管理员适配器实现 ==============
|
||||||
|
|
||||||
|
|
||||||
@@ -233,6 +219,14 @@ class AdminCreateUserAdapter(AdminApiAdapter):
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise InvalidRequestException("角色参数不合法")
|
raise InvalidRequestException("角色参数不合法")
|
||||||
|
|
||||||
|
# 确定配额:unlimited 优先,其次是指定值,最后是系统默认
|
||||||
|
if request.unlimited:
|
||||||
|
quota_usd = None # None 表示无限制
|
||||||
|
elif request.quota_usd is not None:
|
||||||
|
quota_usd = request.quota_usd
|
||||||
|
else:
|
||||||
|
quota_usd = SystemConfigService.get_config(db, "default_user_quota_usd", default=10.0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = UserService.create_user(
|
user = UserService.create_user(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -240,7 +234,7 @@ class AdminCreateUserAdapter(AdminApiAdapter):
|
|||||||
username=request.username,
|
username=request.username,
|
||||||
password=request.password,
|
password=request.password,
|
||||||
role=role,
|
role=role,
|
||||||
quota_usd=request.quota_usd,
|
quota_usd=quota_usd,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise InvalidRequestException(str(exc))
|
raise InvalidRequestException(str(exc))
|
||||||
@@ -601,15 +595,3 @@ class AdminDeleteUserKeyAdapter(AdminApiAdapter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "API Key已删除"}
|
return {"message": "API Key已删除"}
|
||||||
|
|
||||||
|
|
||||||
class AdminGetDefaultQuotaAdapter(AdminApiAdapter):
|
|
||||||
"""获取系统默认用户配额"""
|
|
||||||
|
|
||||||
async def handle(self, context): # type: ignore[override]
|
|
||||||
db = context.db
|
|
||||||
default_quota = SystemConfigService.get_config(db, "default_user_quota_usd", default=10.0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"default_quota_usd": float(default_quota),
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -233,12 +233,13 @@ class CreateUserRequest(BaseModel):
|
|||||||
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
||||||
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
email: str = Field(..., min_length=3, max_length=255, description="邮箱地址")
|
||||||
role: Optional[UserRole] = Field(UserRole.USER, description="用户角色")
|
role: Optional[UserRole] = Field(UserRole.USER, description="用户角色")
|
||||||
quota_usd: Optional[float] = Field(default=10.0, description="USD配额,null表示无限制")
|
quota_usd: Optional[float] = Field(default=None, description="USD配额,null表示使用系统默认配额")
|
||||||
|
unlimited: bool = Field(default=False, description="是否无限配额")
|
||||||
|
|
||||||
@field_validator("quota_usd", mode="before")
|
@field_validator("quota_usd", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_quota_usd(cls, v):
|
def validate_quota_usd(cls, v):
|
||||||
"""验证配额值,允许null表示无限制"""
|
"""验证配额值,null表示使用系统默认配额"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
|
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
|
||||||
|
|||||||
Reference in New Issue
Block a user