mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(admin): 新增数据管理模块,支持分类清空系统数据
添加 purge API 支持按类别清空配置、用户、使用记录、审计日志、请求体和统计数据, 前端系统设置页新增数据管理区块。
This commit is contained in:
@@ -448,6 +448,11 @@ export interface ApiKeyLockResponse {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function purge<T>(target: string): Promise<T> {
|
||||||
|
const response = await apiClient.post<T>(`/api/admin/system/purge/${target}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
// 管理员API密钥管理相关API
|
// 管理员API密钥管理相关API
|
||||||
export const adminApi = {
|
export const adminApi = {
|
||||||
// 获取所有独立余额Keys列表
|
// 获取所有独立余额Keys列表
|
||||||
@@ -850,6 +855,14 @@ export const adminApi = {
|
|||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 数据清空
|
||||||
|
purgeConfig: () => purge<{ message: string; deleted: Record<string, number> }>('config'),
|
||||||
|
purgeUsers: () => purge<{ message: string; deleted: Record<string, number> }>('users'),
|
||||||
|
purgeUsage: () => purge<{ message: string; deleted: Record<string, number> }>('usage'),
|
||||||
|
purgeAuditLogs: () => purge<{ message: string; deleted: Record<string, number> }>('audit-logs'),
|
||||||
|
purgeRequestBodies: () => purge<{ message: string; cleaned: Record<string, number> }>('request-bodies'),
|
||||||
|
purgeStats: () => purge<{ message: string }>('stats'),
|
||||||
|
|
||||||
async getTimeSeries(params?: {
|
async getTimeSeries(params?: {
|
||||||
start_date?: string
|
start_date?: string
|
||||||
end_date?: string
|
end_date?: string
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
@file-select="handleUsersFileSelect"
|
@file-select="handleUsersFileSelect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 数据管理 -->
|
||||||
|
<DataManagementSection id="section-data-mgmt" />
|
||||||
|
|
||||||
<!-- 网络代理 -->
|
<!-- 网络代理 -->
|
||||||
<ProxyConfigSection
|
<ProxyConfigSection
|
||||||
id="section-proxy"
|
id="section-proxy"
|
||||||
@@ -201,6 +204,7 @@ import { useScheduledTasks } from './system-settings/composables/useScheduledTas
|
|||||||
import SiteInfoSection from './system-settings/SiteInfoSection.vue'
|
import SiteInfoSection from './system-settings/SiteInfoSection.vue'
|
||||||
import ConfigManagementSection from './system-settings/ConfigManagementSection.vue'
|
import ConfigManagementSection from './system-settings/ConfigManagementSection.vue'
|
||||||
import UserDataSection from './system-settings/UserDataSection.vue'
|
import UserDataSection from './system-settings/UserDataSection.vue'
|
||||||
|
import DataManagementSection from './system-settings/DataManagementSection.vue'
|
||||||
import ProxyConfigSection from './system-settings/ProxyConfigSection.vue'
|
import ProxyConfigSection from './system-settings/ProxyConfigSection.vue'
|
||||||
import BasicConfigSection from './system-settings/BasicConfigSection.vue'
|
import BasicConfigSection from './system-settings/BasicConfigSection.vue'
|
||||||
import RequestLogSection from './system-settings/RequestLogSection.vue'
|
import RequestLogSection from './system-settings/RequestLogSection.vue'
|
||||||
@@ -219,6 +223,7 @@ const tocItems = [
|
|||||||
{ id: 'section-site-info', label: '站点信息' },
|
{ id: 'section-site-info', label: '站点信息' },
|
||||||
{ id: 'section-config-mgmt', label: '配置管理' },
|
{ id: 'section-config-mgmt', label: '配置管理' },
|
||||||
{ id: 'section-user-data', label: '用户数据管理' },
|
{ id: 'section-user-data', label: '用户数据管理' },
|
||||||
|
{ id: 'section-data-mgmt', label: '数据管理' },
|
||||||
{ id: 'section-proxy', label: '网络代理' },
|
{ id: 'section-proxy', label: '网络代理' },
|
||||||
{ id: 'section-basic', label: '基础配置' },
|
{ id: 'section-basic', label: '基础配置' },
|
||||||
{ id: 'section-request-log', label: '请求记录' },
|
{ id: 'section-request-log', label: '请求记录' },
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<CardSection
|
||||||
|
title="数据管理"
|
||||||
|
description="清空系统数据,操作不可逆,请谨慎使用"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="item in purgeItems"
|
||||||
|
:key="item.key"
|
||||||
|
class="flex flex-col gap-2 p-4 rounded-lg border border-border"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium">{{ item.title }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground flex-1">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
class="w-full mt-1"
|
||||||
|
:disabled="loadingKey === item.key"
|
||||||
|
@click="handlePurge(item)"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{{ loadingKey === item.key ? '清空中...' : item.buttonText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, markRaw, type Component } from 'vue'
|
||||||
|
import { Trash2, Settings, Users, BarChart3, Shield, FileText, PieChart } from 'lucide-vue-next'
|
||||||
|
import Button from '@/components/ui/button.vue'
|
||||||
|
import { CardSection } from '@/components/layout'
|
||||||
|
import { adminApi } from '@/api/admin'
|
||||||
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
|
|
||||||
|
interface PurgeItem {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
buttonText: string
|
||||||
|
icon: Component
|
||||||
|
confirmMessage: string
|
||||||
|
action: () => Promise<{ message: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, error } = useToast()
|
||||||
|
const { confirmDanger } = useConfirm()
|
||||||
|
const loadingKey = ref<string | null>(null)
|
||||||
|
|
||||||
|
const purgeItems: PurgeItem[] = [
|
||||||
|
{
|
||||||
|
key: 'config',
|
||||||
|
title: '清空配置',
|
||||||
|
description: '删除所有提供商、端点、API Key 和模型配置',
|
||||||
|
buttonText: '清空配置',
|
||||||
|
icon: markRaw(Settings),
|
||||||
|
confirmMessage: '确定要清空所有提供商配置吗?这将删除所有提供商、端点、API Key 和模型配置,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeConfig(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'users',
|
||||||
|
title: '清空用户',
|
||||||
|
description: '删除所有非管理员用户及其 API Keys',
|
||||||
|
buttonText: '清空用户',
|
||||||
|
icon: markRaw(Users),
|
||||||
|
confirmMessage: '确定要清空所有非管理员用户吗?管理员账户将被保留,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeUsers(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'usage',
|
||||||
|
title: '清空使用记录',
|
||||||
|
description: '删除全部使用记录和请求候选记录',
|
||||||
|
buttonText: '清空记录',
|
||||||
|
icon: markRaw(BarChart3),
|
||||||
|
confirmMessage: '确定要清空全部使用记录吗?所有请求统计数据将被永久删除,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeUsage(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'audit-logs',
|
||||||
|
title: '清空审计日志',
|
||||||
|
description: '删除全部审计日志记录',
|
||||||
|
buttonText: '清空日志',
|
||||||
|
icon: markRaw(Shield),
|
||||||
|
confirmMessage: '确定要清空全部审计日志吗?所有安全事件记录将被永久删除,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeAuditLogs(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'request-bodies',
|
||||||
|
title: '清空请求体',
|
||||||
|
description: '清空所有请求/响应体数据,保留统计信息',
|
||||||
|
buttonText: '清空请求体',
|
||||||
|
icon: markRaw(FileText),
|
||||||
|
confirmMessage: '确定要清空全部请求体吗?请求/响应内容将被清除,但 token 和成本等统计信息会保留,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeRequestBodies(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stats',
|
||||||
|
title: '清空聚合数据',
|
||||||
|
description: '清空仪表盘统计和聚合数据,保留原始使用记录',
|
||||||
|
buttonText: '清空聚合数据',
|
||||||
|
icon: markRaw(PieChart),
|
||||||
|
confirmMessage: '确定要清空全部聚合统计数据吗?仪表盘数据将被清除,用户和 Key 的累计统计也会归零,操作不可逆。',
|
||||||
|
action: () => adminApi.purgeStats(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handlePurge(item: PurgeItem) {
|
||||||
|
const confirmed = await confirmDanger(item.confirmMessage, item.title)
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
loadingKey.value = item.key
|
||||||
|
try {
|
||||||
|
const result = await item.action()
|
||||||
|
success(result.message)
|
||||||
|
} catch (e) {
|
||||||
|
error(parseApiError(e))
|
||||||
|
} finally {
|
||||||
|
loadingKey.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -505,6 +505,51 @@ async def reset_email_template(
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
# -------- 数据清空 API --------
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/config")
|
||||||
|
async def purge_config(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空所有提供商配置(管理员)"""
|
||||||
|
adapter = AdminPurgeConfigAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/users")
|
||||||
|
async def purge_users(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空所有非管理员用户(管理员)"""
|
||||||
|
adapter = AdminPurgeUsersAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/usage")
|
||||||
|
async def purge_usage(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空全部使用记录(管理员)"""
|
||||||
|
adapter = AdminPurgeUsageAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/audit-logs")
|
||||||
|
async def purge_audit_logs(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空全部审计日志(管理员)"""
|
||||||
|
adapter = AdminPurgeAuditLogsAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/request-bodies")
|
||||||
|
async def purge_request_bodies(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空全部请求体(管理员)"""
|
||||||
|
adapter = AdminPurgeRequestBodiesAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purge/stats")
|
||||||
|
async def purge_stats(request: Request, db: Session = Depends(get_db)) -> Any:
|
||||||
|
"""清空全部聚合统计数据(管理员)"""
|
||||||
|
adapter = AdminPurgeStatsAdapter()
|
||||||
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||||
|
|
||||||
|
|
||||||
# -------- 系统设置适配器 --------
|
# -------- 系统设置适配器 --------
|
||||||
|
|
||||||
|
|
||||||
@@ -2320,3 +2365,288 @@ class AdminResetEmailTemplateAdapter(AdminApiAdapter):
|
|||||||
"html": default_template["html"],
|
"html": default_template["html"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------- 数据清空适配器 --------
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeConfigAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空所有提供商配置(Provider、Endpoint、API Key、Model、GlobalModel)"""
|
||||||
|
from src.models.database import (
|
||||||
|
GeminiFileMapping,
|
||||||
|
GlobalModel,
|
||||||
|
Model,
|
||||||
|
ProviderAPIKey,
|
||||||
|
ProviderEndpoint,
|
||||||
|
UserPreference,
|
||||||
|
VideoTask,
|
||||||
|
)
|
||||||
|
from src.models.database_extensions import ApiKeyProviderMapping, ProviderUsageTracking
|
||||||
|
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
# 统计
|
||||||
|
providers_count = db.query(Provider).count()
|
||||||
|
endpoints_count = db.query(ProviderEndpoint).count()
|
||||||
|
keys_count = db.query(ProviderAPIKey).count()
|
||||||
|
models_count = db.query(Model).count()
|
||||||
|
global_models_count = db.query(GlobalModel).count()
|
||||||
|
|
||||||
|
# VideoTask 的 provider_id/endpoint_id/key_id 无 ondelete,置 NULL 保留任务记录
|
||||||
|
db.query(VideoTask).filter(
|
||||||
|
(VideoTask.provider_id.isnot(None))
|
||||||
|
| (VideoTask.endpoint_id.isnot(None))
|
||||||
|
| (VideoTask.key_id.isnot(None))
|
||||||
|
).update(
|
||||||
|
{
|
||||||
|
VideoTask.provider_id: None,
|
||||||
|
VideoTask.endpoint_id: None,
|
||||||
|
VideoTask.key_id: None,
|
||||||
|
},
|
||||||
|
synchronize_session=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 先清理有外键引用的关联表
|
||||||
|
db.query(GeminiFileMapping).delete()
|
||||||
|
db.query(ApiKeyProviderMapping).delete()
|
||||||
|
db.query(ProviderUsageTracking).delete()
|
||||||
|
|
||||||
|
# 清空 UserPreference 中的 default_provider_id(无 ondelete 设置)
|
||||||
|
db.query(UserPreference).filter(UserPreference.default_provider_id.isnot(None)).update(
|
||||||
|
{UserPreference.default_provider_id: None}, synchronize_session=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 按依赖顺序删除配置
|
||||||
|
db.query(Model).delete()
|
||||||
|
db.query(ProviderAPIKey).delete()
|
||||||
|
db.query(ProviderEndpoint).delete()
|
||||||
|
db.query(Provider).delete()
|
||||||
|
db.query(GlobalModel).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "配置已清空",
|
||||||
|
"deleted": {
|
||||||
|
"providers": providers_count,
|
||||||
|
"endpoints": endpoints_count,
|
||||||
|
"api_keys": keys_count,
|
||||||
|
"models": models_count,
|
||||||
|
"global_models": global_models_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeUsersAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空所有非管理员用户及其关联数据"""
|
||||||
|
from src.core.enums import UserRole
|
||||||
|
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
user_ids = [
|
||||||
|
uid for (uid,) in db.query(User.id).filter(User.role != UserRole.ADMIN).all()
|
||||||
|
]
|
||||||
|
users_count = len(user_ids)
|
||||||
|
|
||||||
|
if user_ids:
|
||||||
|
from src.models.database import VideoTask
|
||||||
|
|
||||||
|
# 删除关联的 VideoTask(user_id 无 ondelete 设置)
|
||||||
|
db.query(VideoTask).filter(VideoTask.user_id.in_(user_ids)).delete(
|
||||||
|
synchronize_session=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 统计关联 API Keys 数量(DB 级别 CASCADE 会随 User 自动删除)
|
||||||
|
keys_count = db.query(ApiKey).filter(ApiKey.user_id.in_(user_ids)).count()
|
||||||
|
|
||||||
|
# 将使用记录的 user_id 置空(保留记录)
|
||||||
|
db.query(Usage).filter(Usage.user_id.in_(user_ids)).update(
|
||||||
|
{Usage.user_id: None}, synchronize_session=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 删除用户
|
||||||
|
db.query(User).filter(User.id.in_(user_ids)).delete(synchronize_session=False)
|
||||||
|
db.commit()
|
||||||
|
else:
|
||||||
|
keys_count = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "非管理员用户已清空",
|
||||||
|
"deleted": {
|
||||||
|
"users": users_count,
|
||||||
|
"api_keys": keys_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_stats_and_reset_counters(db: Session) -> None:
|
||||||
|
"""清空预聚合统计表、重置累计计数字段、清除缓存。"""
|
||||||
|
from src.models.database import (
|
||||||
|
ProviderAPIKey,
|
||||||
|
StatsDaily,
|
||||||
|
StatsDailyApiKey,
|
||||||
|
StatsDailyError,
|
||||||
|
StatsDailyModel,
|
||||||
|
StatsDailyProvider,
|
||||||
|
StatsHourly,
|
||||||
|
StatsHourlyModel,
|
||||||
|
StatsHourlyProvider,
|
||||||
|
StatsHourlyUser,
|
||||||
|
StatsSummary,
|
||||||
|
StatsUserDaily,
|
||||||
|
)
|
||||||
|
from src.services.cache.invalidation import get_cache_invalidation_service
|
||||||
|
|
||||||
|
# 清空预聚合统计表
|
||||||
|
db.query(StatsHourly).delete()
|
||||||
|
db.query(StatsHourlyUser).delete()
|
||||||
|
db.query(StatsHourlyModel).delete()
|
||||||
|
db.query(StatsHourlyProvider).delete()
|
||||||
|
db.query(StatsDaily).delete()
|
||||||
|
db.query(StatsDailyModel).delete()
|
||||||
|
db.query(StatsDailyProvider).delete()
|
||||||
|
db.query(StatsDailyApiKey).delete()
|
||||||
|
db.query(StatsDailyError).delete()
|
||||||
|
db.query(StatsSummary).delete()
|
||||||
|
db.query(StatsUserDaily).delete()
|
||||||
|
|
||||||
|
# 重置 User 上的累计统计字段
|
||||||
|
db.query(User).update({User.used_usd: 0.0, User.total_usd: 0.0}, synchronize_session=False)
|
||||||
|
|
||||||
|
# 重置 ApiKey 上的缓存统计字段
|
||||||
|
db.query(ApiKey).update(
|
||||||
|
{
|
||||||
|
ApiKey.total_requests: 0,
|
||||||
|
ApiKey.total_cost_usd: 0.0,
|
||||||
|
ApiKey.balance_used_usd: 0.0,
|
||||||
|
},
|
||||||
|
synchronize_session=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 重置 ProviderAPIKey 上的使用统计
|
||||||
|
db.query(ProviderAPIKey).update(
|
||||||
|
{
|
||||||
|
ProviderAPIKey.request_count: 0,
|
||||||
|
ProviderAPIKey.success_count: 0,
|
||||||
|
ProviderAPIKey.error_count: 0,
|
||||||
|
ProviderAPIKey.total_response_time_ms: 0,
|
||||||
|
ProviderAPIKey.last_used_at: None,
|
||||||
|
ProviderAPIKey.last_error_at: None,
|
||||||
|
},
|
||||||
|
synchronize_session=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 清除缓存
|
||||||
|
try:
|
||||||
|
cache_service = get_cache_invalidation_service()
|
||||||
|
cache_service.clear_all_caches()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeUsageAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空全部使用记录及相关统计数据"""
|
||||||
|
from src.models.database import RequestCandidate
|
||||||
|
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
usage_count = db.query(Usage).count()
|
||||||
|
candidates_count = db.query(RequestCandidate).count()
|
||||||
|
|
||||||
|
# 清空使用记录
|
||||||
|
db.query(RequestCandidate).delete()
|
||||||
|
db.query(Usage).delete()
|
||||||
|
|
||||||
|
_purge_stats_and_reset_counters(db)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "使用记录已清空",
|
||||||
|
"deleted": {
|
||||||
|
"usage_records": usage_count,
|
||||||
|
"request_candidates": candidates_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeAuditLogsAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空全部审计日志"""
|
||||||
|
from src.models.database import AuditLog
|
||||||
|
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
count = db.query(AuditLog).count()
|
||||||
|
db.query(AuditLog).delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "审计日志已清空",
|
||||||
|
"deleted": {
|
||||||
|
"audit_logs": count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeRequestBodiesAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空全部请求体/响应体(保留使用记录的统计信息)"""
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
# 统计有 body 的记录数
|
||||||
|
with_body = (
|
||||||
|
db.query(Usage)
|
||||||
|
.filter(
|
||||||
|
(Usage.request_body.isnot(None))
|
||||||
|
| (Usage.response_body.isnot(None))
|
||||||
|
| (Usage.provider_request_body.isnot(None))
|
||||||
|
| (Usage.client_response_body.isnot(None))
|
||||||
|
| (Usage.request_body_compressed.isnot(None))
|
||||||
|
| (Usage.response_body_compressed.isnot(None))
|
||||||
|
| (Usage.provider_request_body_compressed.isnot(None))
|
||||||
|
| (Usage.client_response_body_compressed.isnot(None))
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 批量清空所有 body 字段
|
||||||
|
db.query(Usage).update(
|
||||||
|
{
|
||||||
|
Usage.request_body: None,
|
||||||
|
Usage.response_body: None,
|
||||||
|
Usage.provider_request_body: None,
|
||||||
|
Usage.client_response_body: None,
|
||||||
|
Usage.request_body_compressed: None,
|
||||||
|
Usage.response_body_compressed: None,
|
||||||
|
Usage.provider_request_body_compressed: None,
|
||||||
|
Usage.client_response_body_compressed: None,
|
||||||
|
Usage.request_headers: None,
|
||||||
|
Usage.response_headers: None,
|
||||||
|
Usage.provider_request_headers: None,
|
||||||
|
Usage.client_response_headers: None,
|
||||||
|
},
|
||||||
|
synchronize_session=False,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "请求体已清空",
|
||||||
|
"cleaned": {
|
||||||
|
"records_with_body": with_body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPurgeStatsAdapter(AdminApiAdapter):
|
||||||
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||||
|
"""清空全部聚合统计数据(保留原始使用记录)"""
|
||||||
|
db = context.db
|
||||||
|
|
||||||
|
_purge_stats_and_reset_counters(db)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "聚合统计数据已清空",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user