This commit is contained in:
2026-08-06 17:47:59 +08:00
parent 1029555fdc
commit 61c3761745
7 changed files with 180 additions and 96 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-BxYDA-8S.js"></script> <script type="module" crossorigin src="/assets/index-D4AbZER1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css"> <link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
</head> </head>
<body> <body>
+6 -2
View File
@@ -438,8 +438,12 @@ export async function deleteApiKey(id: string): Promise<void> {
await api.delete(`/admin/api-keys/${id}`); await api.delete(`/admin/api-keys/${id}`);
} }
export async function getApiKeyUsage(id: string, days?: number): Promise<any> { export async function getApiKeyUsage(id: string, days?: number, page?: number, pageSize?: number): Promise<any> {
const qs = days ? `?days=${days}` : ''; const params = new URLSearchParams();
if (days) params.set('days', String(days));
if (page) params.set('page', String(page));
if (pageSize) params.set('page_size', String(pageSize));
const qs = params.toString() ? `?${params.toString()}` : '';
return api.get(`/admin/api-keys/${id}/usage${qs}`); return api.get(`/admin/api-keys/${id}/usage${qs}`);
} }
+27 -7
View File
@@ -264,9 +264,18 @@ const AdminApiKeys: React.FC = () => {
document.body.removeChild(textarea); document.body.removeChild(textarea);
}; };
const loadUsageDetail = async (keyId: string, page = 1, pageSize = 20) => {
try {
const usage = await getApiKeyUsage(keyId, 30, page, pageSize);
setUsageModal(prev => ({ ...prev, usage }));
} catch {
message.error('加载使用统计失败');
}
};
const viewUsage = async (key: ApiKey) => { const viewUsage = async (key: ApiKey) => {
try { try {
const usage = await getApiKeyUsage(key.id, 30); const usage = await getApiKeyUsage(key.id, 30, 1, 20);
setUsageModal({ open: true, key, usage }); setUsageModal({ open: true, key, usage });
} catch { } catch {
message.error('加载使用统计失败'); message.error('加载使用统计失败');
@@ -614,15 +623,26 @@ const AdminApiKeys: React.FC = () => {
</div> </div>
<Table <Table
columns={[ columns={[
{ title: '时间', dataIndex: 'createdAt', render: (v: string) => new Date(v).toLocaleString() }, { title: '时间', dataIndex: 'createdAt', width: 160, render: (v: string) => v ? new Date(v).toLocaleString() : '-' },
{ title: '类型', dataIndex: 'requestType' }, {
{ title: '型', dataIndex: 'modelName' }, title: '型', dataIndex: 'genType', width: 70,
{ title: '消耗(元)', dataIndex: 'creditsCost' }, render: (v: string) => <Tag color={v === 'video' ? 'blue' : 'green'}>{v === 'video' ? '视频' : '图片'}</Tag>,
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v}</Tag> }, },
{ title: '模型', dataIndex: 'modelName', width: 140, ellipsis: true },
{ title: '消耗(元)', dataIndex: 'creditsCost', width: 90, render: (v: number) => v?.toFixed(2) || '0.00' },
{ title: '状态', dataIndex: 'status', width: 70, render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v === 'success' ? '成功' : '失败'}</Tag> },
]} ]}
dataSource={usageModal.usage.items || []} dataSource={usageModal.usage.items || []}
rowKey="id" rowKey="id"
pagination={false} pagination={{
current: usageModal.usage.page || 1,
pageSize: usageModal.usage.pageSize || 20,
total: usageModal.usage.total || 0,
onChange: (p, ps) => loadUsageDetail(usageModal.key?.id || '', p, ps || 20),
showSizeChanger: true,
showTotal: (t) => `${t}`,
size: 'small',
}}
size="small" size="small"
/> />
</div> </div>
+51 -7
View File
@@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { import {
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography, Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
} from 'antd'; } from 'antd';
import { import {
TableOutlined, ReloadOutlined, TableOutlined, ReloadOutlined, SearchOutlined, ExportOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { getApiUsageAll } from '../api'; import { getApiUsageAll } from '../api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -32,12 +32,12 @@ const AdminApiUsage: React.FC = () => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50); const [pageSize, setPageSize] = useState(50);
const [searchCompany, setSearchCompany] = useState(''); const [searchText, setSearchText] = useState('');
const [filterGenType, setFilterGenType] = useState<string | undefined>(undefined); const [filterGenType, setFilterGenType] = useState<string | undefined>(undefined);
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined); const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]); const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
const load = async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const params: any = { const params: any = {
@@ -48,6 +48,7 @@ const AdminApiUsage: React.FC = () => {
if (filterStatus) params.status = filterStatus; if (filterStatus) params.status = filterStatus;
if (dateRange[0]) params.startDate = dateRange[0].startOf('day').toISOString(); if (dateRange[0]) params.startDate = dateRange[0].startOf('day').toISOString();
if (dateRange[1]) params.endDate = dateRange[1].endOf('day').toISOString(); if (dateRange[1]) params.endDate = dateRange[1].endOf('day').toISOString();
if (searchText.trim()) params.search = searchText.trim();
const data = await getApiUsageAll(params); const data = await getApiUsageAll(params);
setItems(data?.items || []); setItems(data?.items || []);
@@ -57,15 +58,48 @@ const AdminApiUsage: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [page, pageSize, filterGenType, filterStatus, dateRange, searchText]);
useEffect(() => { load(); }, [page, pageSize, filterGenType, filterStatus, dateRange]); useEffect(() => { load(); }, [load]);
const handleSearch = () => { const handleSearch = () => {
setPage(1); setPage(1);
load(); load();
}; };
// 导出 CSV
const handleExport = () => {
const headers = ['时间', '公司', 'api-key', '类型', '模型', '消耗(元)', 'Token', '耗时(ms)', '状态', '错误信息'];
const rows = items.map(item => [
item.createdAt ? new Date(item.createdAt).toLocaleString() : '',
item.companyName || '',
item.apiKeyPrefix || '',
item.genType === 'video' ? '视频' : '图片',
item.modelName || '',
(item.creditsCost || 0).toFixed(2),
item.tokensUsed || '',
item.requestDurationMs || '',
item.status === 'success' ? '成功' : '失败',
item.errorMessage || '',
]);
const csvContent = [headers, ...rows]
.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
.join('\n');
const BOM = '';
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `api_usage_${dayjs().format('YYYYMMDD_HHmmss')}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
message.success('导出成功');
};
const columns = [ const columns = [
{ {
title: '时间', title: '时间',
@@ -80,7 +114,7 @@ const AdminApiUsage: React.FC = () => {
render: (v: string) => v || '-', render: (v: string) => v || '-',
}, },
{ {
title: 'Key 前缀', title: 'api-key',
dataIndex: 'apiKeyPrefix', dataIndex: 'apiKeyPrefix',
width: 110, width: 110,
render: (v: string) => v ? <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>{v}</code> : '-', render: (v: string) => v ? <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>{v}</code> : '-',
@@ -150,6 +184,7 @@ const AdminApiUsage: React.FC = () => {
<Tag color="blue">: {totalCost.toFixed(2)} </Tag> <Tag color="blue">: {totalCost.toFixed(2)} </Tag>
<Tag color="green">: {successCount}</Tag> <Tag color="green">: {successCount}</Tag>
<Tag color="red">: {failedCount}</Tag> <Tag color="red">: {failedCount}</Tag>
<Button icon={<ExportOutlined />} onClick={handleExport}></Button>
<Button icon={<ReloadOutlined />} onClick={handleSearch}></Button> <Button icon={<ReloadOutlined />} onClick={handleSearch}></Button>
</Space> </Space>
</div> </div>
@@ -158,6 +193,15 @@ const AdminApiUsage: React.FC = () => {
{/* 筛选栏 */} {/* 筛选栏 */}
<Card variant="outlined" style={{ borderRadius: 12 }}> <Card variant="outlined" style={{ borderRadius: 12 }}>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}> <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Input
placeholder="搜索公司名或 Key 前缀"
prefix={<SearchOutlined />}
value={searchText}
onChange={e => setSearchText(e.target.value)}
onPressEnter={handleSearch}
allowClear
style={{ width: 220 }}
/>
<Select <Select
placeholder="类型" placeholder="类型"
value={filterGenType} value={filterGenType}
+13 -1
View File
@@ -261,6 +261,8 @@ async def save_upscale_config(
async def get_usage( async def get_usage(
key_id: str = Path(...), key_id: str = Path(...),
days: int = Query(30, ge=1, le=365), days: int = Query(30, ge=1, le=365),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin: User = Depends(get_admin_user), admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
) -> ApiUsageSummaryResponse: ) -> ApiUsageSummaryResponse:
@@ -271,7 +273,7 @@ async def get_usage(
raise HTTPException(status_code=404, detail="API Key 不存在") raise HTTPException(status_code=404, detail="API Key 不存在")
summary = await usage_log_service.get_usage_summary(db, api_key_id=key_id, days=days) summary = await usage_log_service.get_usage_summary(db, api_key_id=key_id, days=days)
total, logs = await usage_log_service.list_usage_logs(db, api_key_id=key_id, limit=100) total, logs = await usage_log_service.list_usage_logs(db, api_key_id=key_id, limit=page_size, skip=(page - 1) * page_size)
return ApiUsageSummaryResponse( return ApiUsageSummaryResponse(
total_requests=summary["total_requests"], total_requests=summary["total_requests"],
@@ -280,6 +282,9 @@ async def get_usage(
success_count=summary["success_count"], success_count=summary["success_count"],
failed_count=summary["failed_count"], failed_count=summary["failed_count"],
avg_duration_ms=summary["avg_duration_ms"], avg_duration_ms=summary["avg_duration_ms"],
total=total,
page=page,
page_size=page_size,
items=[ items=[
ApiUsageLogResponse( ApiUsageLogResponse(
id=log.id, id=log.id,
@@ -312,6 +317,7 @@ async def list_all_usage(
status_filter: str | None = Query(None, alias="status"), status_filter: str | None = Query(None, alias="status"),
start_date: datetime | None = None, start_date: datetime | None = None,
end_date: datetime | None = None, end_date: datetime | None = None,
search: str | None = None,
admin: User = Depends(get_admin_user), admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
) -> dict: ) -> dict:
@@ -336,6 +342,12 @@ async def list_all_usage(
filters.append(ApiUsageLog.created_at >= start_date) filters.append(ApiUsageLog.created_at >= start_date)
if end_date: if end_date:
filters.append(ApiUsageLog.created_at <= end_date) filters.append(ApiUsageLog.created_at <= end_date)
if search:
search_pattern = f"%{search}%"
filters.append(
(ApiKey.company_name.ilike(search_pattern))
| (ApiKey.api_key_prefix.ilike(search_pattern))
)
for f in filters: for f in filters:
query = query.where(f) query = query.where(f)
@@ -33,4 +33,7 @@ class ApiUsageSummaryResponse(BaseModel):
success_count: int success_count: int
failed_count: int failed_count: int
avg_duration_ms: int avg_duration_ms: int
total: int = 0
page: int = 1
page_size: int = 20
items: list[ApiUsageLogResponse] items: list[ApiUsageLogResponse]