1、修改后台数据概览页面

2、增加对应是时间范围修改
This commit is contained in:
2026-06-15 16:17:00 +08:00
parent 1de11d93d0
commit 5ee682d06e
10 changed files with 648 additions and 218 deletions
File diff suppressed because one or more lines are too long
+13 -13
View File
@@ -1,13 +1,13 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoGen.AI 管理后台</title> <title>VideoGen.AI 管理后台</title>
<script type="module" crossorigin src="/assets/index-CIsX_bex.js"></script> <script type="module" crossorigin src="/assets/index-D8zG-fgv.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+16 -3
View File
@@ -86,8 +86,12 @@ export async function markNotificationRead(id: string): Promise<void> {
// ── Admin ───────────────────────────────────────────────── // ── Admin ─────────────────────────────────────────────────
export async function getAdminStats(): Promise<AdminStats> { export async function getAdminStats(startDate?: string, endDate?: string): Promise<AdminStats> {
return api.get('/admin/stats'); const params: Record<string, string> = {};
if (startDate) params.start_date = startDate;
if (endDate) params.end_date = endDate;
const query = new URLSearchParams(params).toString();
return api.get(`/admin/stats${query ? `?${query}` : ''}`);
} }
export async function getAdminUsers(search?: string): Promise<AdminUser[]> { export async function getAdminUsers(search?: string): Promise<AdminUser[]> {
@@ -139,10 +143,19 @@ export async function uploadPdf(file: File, configKey: string): Promise<{ url: s
return res.json(); return res.json();
} }
export async function getCreditRecords(filters?: { user_id?: string; type?: string }): Promise<any> { export async function getCreditRecords(filters?: {
user_id?: string;
user_name?: string;
type?: string;
start_date?: string;
end_date?: string;
}): Promise<any> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters?.user_id) params.set('user_id', filters.user_id); if (filters?.user_id) params.set('user_id', filters.user_id);
if (filters?.user_name) params.set('user_name', filters.user_name);
if (filters?.type) params.set('type', filters.type); if (filters?.type) params.set('type', filters.type);
if (filters?.start_date) params.set('start_date', filters.start_date);
if (filters?.end_date) params.set('end_date', filters.end_date);
const q = params.toString() ? `?${params}` : ''; const q = params.toString() ? `?${params}` : '';
return api.get(`/admin/credit-records${q}`); return api.get(`/admin/credit-records${q}`);
} }
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Button, Card, Select, Space, Table, Tag, Typography, message, Button, Card, Select, Space, Table, Tag, Typography, message, Input, DatePicker,
} from 'antd'; } from 'antd';
import { import {
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { getCreditRecords } from '../api'; import { getCreditRecords } from '../api';
import { formatDate } from '../utils/formatDate'; import { formatDate } from '../utils/formatDate';
import dayjs from 'dayjs';
interface CreditRecord { interface CreditRecord {
id: string; id: string;
@@ -29,11 +30,18 @@ const AdminCreditRecords: React.FC = () => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [typeFilter, setTypeFilter] = useState<string>(''); const [typeFilter, setTypeFilter] = useState<string>('');
const [userNameFilter, setUserNameFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
const load = async (type?: string) => { const load = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await getCreditRecords(type ? { type } : undefined); const filters: { type?: string; user_name?: string; start_date?: string; end_date?: string } = {};
if (typeFilter) filters.type = typeFilter;
if (userNameFilter) filters.user_name = userNameFilter;
if (dateRange[0]) filters.start_date = dateRange[0].format('YYYY-MM-DD');
if (dateRange[1]) filters.end_date = dateRange[1].format('YYYY-MM-DD');
const res = await getCreditRecords(Object.keys(filters).length > 0 ? filters : undefined);
setRecords(res.items || []); setRecords(res.items || []);
setTotal(res.total || 0); setTotal(res.total || 0);
} catch { } catch {
@@ -45,9 +53,15 @@ const AdminCreditRecords: React.FC = () => {
useEffect(() => { load(); }, []); useEffect(() => { load(); }, []);
const handleTypeFilter = (value: string) => { const handleSearch = () => {
setTypeFilter(value); load();
load(value || undefined); };
const handleReset = () => {
setTypeFilter('');
setUserNameFilter('');
setDateRange([null, null]);
load();
}; };
const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0); const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
@@ -139,11 +153,11 @@ const AdminCreditRecords: React.FC = () => {
</div> </div>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}> <Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<Space> <Space wrap>
<Select <Select
value={typeFilter} value={typeFilter}
onChange={handleTypeFilter} onChange={(value) => setTypeFilter(value)}
style={{ width: 120 }} style={{ width: 120 }}
options={[ options={[
{ value: '', label: '全部类型' }, { value: '', label: '全部类型' },
@@ -152,8 +166,25 @@ const AdminCreditRecords: React.FC = () => {
{ value: 'refund', label: '退回' }, { value: 'refund', label: '退回' },
]} ]}
/> />
<Input
placeholder="用户名搜索"
value={userNameFilter}
onChange={(e) => setUserNameFilter(e.target.value)}
style={{ width: 180 }}
allowClear
/>
<DatePicker.RangePicker
value={dateRange}
onChange={(dates) => { if (dates) setDateRange([dates[0], dates[1]]); }}
placeholder={['开始日期', '结束日期']}
style={{ width: 250 }}
/>
</Space>
<Space>
<Button type="primary" onClick={handleSearch}></Button>
<Button onClick={handleReset}></Button>
<Button icon={<ReloadOutlined />} onClick={load}></Button>
</Space> </Space>
<Button icon={<ReloadOutlined />} onClick={() => load(typeFilter || undefined)}></Button>
</div> </div>
<Table <Table
columns={columns} columns={columns}
+323 -65
View File
@@ -1,90 +1,348 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Col, Row, Statistic, Typography, Table, Tag } from 'antd'; import { Card, Col, Row, Typography, DatePicker, Button, Space } from 'antd';
import { import {
UserOutlined, UserOutlined,
ProjectOutlined, ProjectOutlined,
PlayCircleOutlined, PlayCircleOutlined,
FileTextOutlined,
DollarOutlined, DollarOutlined,
ThunderboltOutlined, WalletOutlined,
ArrowUpOutlined, ArrowUpOutlined,
CalendarOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { getAdminStats } from '../api'; import { getAdminStats } from '../api';
import type { AdminStats } from '../types'; import type { AdminStats } from '../types';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const AdminDashboard: React.FC = () => { const AdminDashboard: React.FC = () => {
const [stats, setStats] = useState<AdminStats | null>(null); const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [startDate, setStartDate] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([dayjs().startOf('day'), dayjs()]);
const load = async () => {
setLoading(true);
try {
const start = startDate[0]?.format('YYYY-MM-DD') || undefined;
const end = startDate[1]?.format('YYYY-MM-DD') || undefined;
const data = await getAdminStats(start, end);
setStats(data);
} catch { /* auth error handled by client */ }
setLoading(false);
};
useEffect(() => { useEffect(() => {
const load = async () => {
setLoading(true);
try {
const data = await getAdminStats();
setStats(data);
} catch { /* auth error handled by client */ }
setLoading(false);
};
load(); load();
}, []); }, []);
const statCards = stats ? [ const handleDateChange = () => {
{ title: '总用户数', value: stats.totalUsers, icon: <UserOutlined />, color: '#6366f1', bg: 'rgba(99,102,241,0.08)' }, load();
{ title: '总项目数', value: stats.totalProjects, icon: <ProjectOutlined />, color: '#06b6d4', bg: 'rgba(6,182,212,0.08)' }, };
{ title: '总生成次数', value: stats.totalGenerations, icon: <PlayCircleOutlined />, color: '#10b981', bg: 'rgba(16,185,129,0.08)' },
{ title: '总收入(元)', value: stats.totalRevenue, icon: <DollarOutlined />, color: '#f59e0b', bg: 'rgba(245,158,11,0.08)', prefix: '¥' }, const handleToday = () => {
{ title: '今日消耗积分', value: stats.creditsConsumedToday, icon: <ThunderboltOutlined />, color: '#ef4444', bg: 'rgba(239,68,68,0.08)' }, setStartDate([dayjs().startOf('day'), dayjs()]);
setTimeout(() => load(), 0);
};
const handleYesterday = () => {
const yesterday = dayjs().subtract(1, 'day');
setStartDate([yesterday.startOf('day'), yesterday.endOf('day')]);
setTimeout(() => load(), 0);
};
const handleWeek = () => {
setStartDate([dayjs().startOf('week'), dayjs()]);
setTimeout(() => load(), 0);
};
const handleMonth = () => {
setStartDate([dayjs().startOf('month'), dayjs()]);
setTimeout(() => load(), 0);
};
const baseStats = stats ? [
{
title: '用户数量',
value: stats.totalUsers,
icon: <UserOutlined />,
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
prefix: '',
suffix: '位用户',
description: '平台注册用户总数'
},
{
title: '总项目数',
value: stats.totalProjects,
icon: <ProjectOutlined />,
gradient: 'linear-gradient(135deg, #00d4ff 0%, #0099cc 100%)',
prefix: '',
suffix: '个项目',
description: '创建的项目总数'
},
{
title: '项目记录',
value: stats.totalRecords,
icon: <FileTextOutlined />,
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
prefix: '',
suffix: '条记录',
description: '项目记录总数'
},
{
title: '创作记录',
value: stats.totalGenerations,
icon: <PlayCircleOutlined />,
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
prefix: '',
suffix: '次创作',
description: 'AI创作记录总数'
},
] : []; ] : [];
return ( const financeStats = stats ? [
<div> {
{/* Stats Cards */} title: '支付宝收入',
<Row gutter={[16, 16]}> value: stats.todayAlipayRevenue,
{statCards.map((s, i) => ( icon: <WalletOutlined />,
<Col xs={12} sm={8} lg={i < 4 ? 6 : 24} key={s.title}> gradient: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
<Card bordered={false} loading={loading} prefix: '¥',
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}> suffix: '',
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> description: '支付宝收款',
<div style={{ tag: '支付宝'
width: 44, height: 44, borderRadius: 10, },
background: s.bg, display: 'flex', {
alignItems: 'center', justifyContent: 'center', title: '微信收入',
fontSize: 20, color: s.color, flexShrink: 0, value: stats.todayWechatRevenue,
}}> icon: <DollarOutlined />,
{s.icon} gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
</div> prefix: '¥',
<div> suffix: '',
<div style={{ color: '#94a3b8', fontSize: 12, marginBottom: 2 }}>{s.title}</div> description: '微信收款',
<div style={{ fontSize: 22, fontWeight: 800, color: '#1a1a2e' }}> tag: '微信支付'
{s.prefix}{typeof s.value === 'number' ? s.value.toLocaleString() : s.value} },
</div> {
</div> title: '总收入',
</div> value: stats.totalRevenue,
</Card> icon: <ArrowUpOutlined />,
</Col> gradient: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
))} prefix: '¥',
</Row> suffix: '',
description: '平台总收入',
tag: '总收入'
},
{
title: '消耗积分',
value: stats.creditsConsumedToday,
icon: <DollarOutlined />,
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
prefix: '',
suffix: '积分',
description: '用户消耗积分',
tag: '积分消耗'
},
] : [];
{/* Quick Info */} const StatCard: React.FC<{
<Row gutter={[16, 16]} style={{ marginTop: 16 }}> title: string;
<Col xs={24} lg={12}> value: number;
<Card title="系统信息" bordered={false} icon: React.ReactNode;
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}> gradient: string;
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> prefix?: string;
{[ suffix?: string;
{ label: '平台名称', value: 'VideoGen.AI' }, description?: string;
{ label: 'API版本', value: 'v1.0.0' }, tag?: string;
{ label: '数据库', value: 'PostgreSQL' }, }> = ({ title, value, icon, gradient, prefix = '', suffix = '', description, tag }) => (
{ label: '视频引擎', value: 'Seedance 2.0' }, <Card
].map(item => ( bordered={false}
<div key={item.label} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #f5f6fa' }}> loading={loading}
<Typography.Text type="secondary">{item.label}</Typography.Text> hoverable
<Typography.Text strong>{item.value}</Typography.Text> style={{
</div> borderRadius: 16,
))} border: '1px solid rgba(0,0,0,0.04)',
</div> background: '#ffffff',
</Card> boxShadow: '0 4px 20px rgba(0,0,0,0.05)',
</Col> transition: 'all 0.3s ease',
</Row> overflow: 'hidden'
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 16,
padding: '16px 0'
}}>
<div style={{
width: 52,
height: 52,
borderRadius: 14,
background: gradient,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 24,
color: '#fff',
flexShrink: 0,
boxShadow: '0 8px 24px rgba(0,0,0,0.1)',
}}>
{icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 4
}}>
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>{title}</Typography.Text>
{tag && (
<span style={{
fontSize: 10,
padding: '2px 8px',
borderRadius: 10,
background: 'rgba(99,102,241,0.1)',
color: '#6366f1',
fontWeight: 500
}}>
{tag}
</span>
)}
</div>
<div style={{
fontSize: 28,
fontWeight: 800,
color: '#1e293b',
letterSpacing: -0.5,
marginBottom: 2
}}>
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
</div>
{description && (
<Typography.Text style={{ color: '#94a3b8', fontSize: 11 }}>
{description}
</Typography.Text>
)}
</div>
</div>
</Card>
);
return (
<div style={{ padding: 0 }}>
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '24px 24px 32px',
borderRadius: 0,
marginBottom: -24,
position: 'relative',
overflow: 'hidden'
}}>
<div style={{
position: 'absolute',
top: -50,
right: -50,
width: 200,
height: 200,
background: 'rgba(255,255,255,0.1)',
borderRadius: '50%'
}} />
<div style={{
position: 'absolute',
bottom: -30,
left: -30,
width: 150,
height: 150,
background: 'rgba(255,255,255,0.08)',
borderRadius: '50%'
}} />
<div style={{ position: 'relative', zIndex: 1 }}>
<Typography.Title level={2} style={{ color: '#fff', marginBottom: 4, fontWeight: 700 }}>
</Typography.Title>
<Typography.Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14 }}>
</Typography.Text>
</div>
</div>
<div style={{ marginTop: 40 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 16 }}>
<Space>
<Button
type={!startDate[0] || !startDate[1] || startDate[0]?.isSame(dayjs().startOf('day')) && startDate[1]?.isSame(dayjs(), 'day') ? 'primary' : 'default'}
size="small"
onClick={handleToday}
>
</Button>
<Button
type="default"
size="small"
onClick={handleYesterday}
>
</Button>
<Button
type="default"
size="small"
onClick={handleWeek}
>
</Button>
<Button
type="default"
size="small"
onClick={handleMonth}
>
</Button>
</Space>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0', padding: '6px 12px', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
<CalendarOutlined style={{ color: '#64748b', fontSize: 14 }} />
<DatePicker.RangePicker
value={startDate}
onChange={(dates) => { if (dates) setStartDate([dates[0], dates[1]]); }}
onBlur={handleDateChange}
placeholder={['开始日期', '结束日期']}
size="small"
/>
<Button
type="primary"
size="small"
onClick={handleDateChange}
style={{ borderRadius: 6 }}
>
</Button>
</div>
</div>
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}></Typography.Text>
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}></Typography.Text>
</div>
<Row gutter={[16, 16]}>
{baseStats.map((s) => (
<Col xs={12} sm={8} lg={6} key={s.title}>
<StatCard {...s} />
</Col>
))}
</Row>
</div>
<div style={{ marginTop: 24 }}>
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}></Typography.Text>
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}></Typography.Text>
</div>
<Row gutter={[16, 16]}>
{financeStats.map((s) => (
<Col xs={12} sm={8} lg={6} key={s.title}>
<StatCard {...s} />
</Col>
))}
</Row>
</div>
</div> </div>
); );
}; };
@@ -763,81 +763,76 @@ const AdminGenerationAiRecords: React.FC = () => {
}; };
return ( return (
<div style={{ padding: 24 }}> <div>
<Card <Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
title={( <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<Space> <Space>
<FileImageOutlined /> <PlayCircleOutlined style={{ fontSize: 18, color: '#6366f1' }} />
<span></span> <Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</Space> <Tag color="purple">{total} </Tag>
)} </Space>
extra={( <Space>
<Space wrap> <Select
<Select allowClear
allowClear placeholder="状态筛选"
placeholder="状态筛选" value={filterStatus || undefined}
value={filterStatus || undefined} style={{ width: 140 }}
style={{ width: 140 }} onChange={(v) => { setFilterStatus(v || ''); setPage(1); }}
onChange={(v) => { options={[
setFilterStatus(v || ''); { value: 'generating', label: '生成中' },
setPage(1); { value: 'completed', label: '已完成' },
}} { value: 'failed', label: '失败' },
options={[ ]}
{ value: 'generating', label: '生成中' }, />
{ value: 'completed', label: '已完成' }, <Select
{ value: 'failed', label: '失败' }, allowClear
]} placeholder="类型筛选"
/> value={filterGenType || undefined}
<Select style={{ width: 120 }}
allowClear onChange={(v) => { setFilterGenType(v || ''); setPage(1); }}
placeholder="类型筛选" options={[
value={filterGenType || undefined} { value: 'image', label: '图片' },
style={{ width: 120 }} { value: 'video', label: '视频' },
onChange={(v) => { ]}
setFilterGenType(v || ''); />
setPage(1); <Input
}} placeholder="用户ID搜索"
options={[ prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
{ value: 'image', label: '图片' }, style={{ width: 180 }}
{ value: 'video', label: '视频' }, value={inputUserId}
]} onChange={(e) => setInputUserId(e.target.value)}
/> onPressEnter={handleSearch}
<Input allowClear
placeholder="用户ID" />
value={inputUserId} <Input
onChange={(e) => setInputUserId(e.target.value)} placeholder="用户名搜索"
onPressEnter={handleSearch} prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
style={{ width: 180 }} style={{ width: 160 }}
allowClear value={inputUserName}
/> onChange={(e) => setInputUserName(e.target.value)}
<Input onPressEnter={handleSearch}
placeholder="用户名" allowClear
value={inputUserName} />
onChange={(e) => setInputUserName(e.target.value)} <Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
onPressEnter={handleSearch}
style={{ width: 160 }} </Button>
allowClear </Space>
/> </div>
<Button icon={<SearchOutlined />} onClick={handleSearch}></Button>
</Space>
)}
bordered={false}
style={{ borderRadius: 16 }}
>
<Table <Table
rowKey="id" rowKey="id"
loading={loading} loading={loading}
columns={columns as any} columns={columns as any}
dataSource={records} dataSource={records}
scroll={{ x: 1180 }} scroll={{ x: 1180 }}
pagination={{ pagination={{
current: page, current: page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
total, total,
showSizeChanger: false, showSizeChanger: false,
showTotal: (t) => `${t}`, showTotal: (t) => `${t}`,
onChange: (p) => setPage(p), onChange: (p) => setPage(p),
}} }}
/> />
</Card> </Card>
+3
View File
@@ -113,8 +113,11 @@ export interface AdminStats {
totalUsers: number; totalUsers: number;
totalProjects: number; totalProjects: number;
totalGenerations: number; totalGenerations: number;
totalRecords: number;
totalRevenue: number; totalRevenue: number;
creditsConsumedToday: number; creditsConsumedToday: number;
todayAlipayRevenue: number;
todayWechatRevenue: number;
} }
export interface PaymentStats { export interface PaymentStats {
+96 -22
View File
@@ -247,7 +247,10 @@ async def list_credit_records(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
user_id: str | None = Query(None), user_id: str | None = Query(None),
user_name: str | None = Query(None),
type: str | None = Query(None), type: str | None = Query(None),
start_date: str = Query(None),
end_date: str = Query(None),
admin: User = Depends(get_admin_user), admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
@@ -261,9 +264,25 @@ async def list_credit_records(
if user_id: if user_id:
query = query.where(CreditRecord.user_id == user_id) query = query.where(CreditRecord.user_id == user_id)
count_query = count_query.where(CreditRecord.user_id == user_id) count_query = count_query.where(CreditRecord.user_id == user_id)
if user_name:
query = query.where(User.username.like(f'%{user_name}%'))
count_query = count_query.join(User, CreditRecord.user_id == User.id).where(User.username.like(f'%{user_name}%'))
if type: if type:
query = query.where(CreditRecord.type == type) query = query.where(CreditRecord.type == type)
count_query = count_query.where(CreditRecord.type == type) count_query = count_query.where(CreditRecord.type == type)
try:
if start_date:
date_start = datetime.strptime(start_date, "%Y-%m-%d")
query = query.where(CreditRecord.created_at >= date_start)
count_query = count_query.where(CreditRecord.created_at >= date_start)
if end_date:
date_end = datetime.strptime(end_date, "%Y-%m-%d")
date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
query = query.where(CreditRecord.created_at <= date_end)
count_query = count_query.where(CreditRecord.created_at <= date_end)
except:
pass
total = (await db.execute(count_query)).scalar() or 0 total = (await db.execute(count_query)).scalar() or 0
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
@@ -1124,40 +1143,95 @@ async def list_operation_logs(
async def get_stats( async def get_stats(
admin: User = Depends(get_admin_user), admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
start_date: str = Query(None),
end_date: str = Query(None),
): ):
total_users = (await db.execute( total_users = (await db.execute(
select(func.count(User.id)).where(User.user_type == "frontend") select(func.count(User.id)).where(User.user_type == "frontend")
)).scalar() or 0 )).scalar() or 0
total_projects = (await db.execute(select(func.count(Project.id)).where(Project.deleted_at.is_(None)))).scalar() or 0
total_generations = ( today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
await db.execute(select(func.count(GenerationRecord.id)).where(GenerationRecord.deleted_at.is_(None)))
).scalar() or 0 try:
total_revenue = ( if start_date:
await db.execute( date_start = datetime.strptime(start_date, "%Y-%m-%d")
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where( else:
PaymentOrder.status == "paid" date_start = today_start
) if end_date:
) date_end = datetime.strptime(end_date, "%Y-%m-%d")
).scalar() or 0 date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
else:
date_end = datetime.now()
except:
date_start = today_start
date_end = datetime.now()
today_start = datetime.now().replace( total_projects = (await db.execute(
hour=0, minute=0, second=0, microsecond=0 select(func.count(Project.id)).where(
) Project.deleted_at.is_(None),
credits_today = ( Project.created_at >= date_start,
await db.execute( Project.created_at <= date_end,
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
CreditRecord.type == "consume",
CreditRecord.created_at >= today_start,
)
) )
).scalar() or 0 )).scalar() or 0
total_generations = (await db.execute(
select(func.count(GenerationAITask.id)).where(
GenerationAITask.created_at >= date_start,
GenerationAITask.created_at <= date_end,
)
)).scalar() or 0
total_records = (await db.execute(
select(func.count(GenerationRecord.id)).where(
GenerationRecord.deleted_at.is_(None),
GenerationRecord.created_at >= date_start,
GenerationRecord.created_at <= date_end,
)
)).scalar() or 0
total_revenue = (await db.execute(
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
PaymentOrder.status == "paid",
PaymentOrder.created_at >= date_start,
PaymentOrder.created_at <= date_end,
)
)).scalar() or 0
credits_consumed = (await db.execute(
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
CreditRecord.type == "consume",
CreditRecord.created_at >= date_start,
CreditRecord.created_at <= date_end,
)
)).scalar() or 0
alipay_revenue = (await db.execute(
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
PaymentOrder.status == "paid",
PaymentOrder.payment_method == "alipay",
PaymentOrder.created_at >= date_start,
PaymentOrder.created_at <= date_end,
)
)).scalar() or 0
wechat_revenue = (await db.execute(
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
PaymentOrder.status == "paid",
PaymentOrder.payment_method == "wechat",
PaymentOrder.created_at >= date_start,
PaymentOrder.created_at <= date_end,
)
)).scalar() or 0
return AdminStatsOut( return AdminStatsOut(
total_users=total_users, total_users=total_users,
total_projects=total_projects, total_projects=total_projects,
total_generations=total_generations, total_generations=total_generations,
total_records=total_records,
total_revenue=float(total_revenue), total_revenue=float(total_revenue),
credits_consumed_today=float(credits_today), credits_consumed_today=float(credits_consumed),
today_alipay_revenue=float(alipay_revenue),
today_wechat_revenue=float(wechat_revenue),
) )
+70 -28
View File
@@ -322,13 +322,54 @@ async def _seed_data():
# Seed menu configs # Seed menu configs
from app.models.menu_config import MenuConfig from app.models.menu_config import MenuConfig
default_menus = [ frontend_groups = [
("/projects", "我的项目", "HomeOutlined", 0, "frontend"), ("AI项目行业生成", "HomeOutlined", 0),
("/conversation", "AI创作", "StarOutlined", 1, "frontend"), ("AI对话生成", "HomeOutlined", 1),
("/records", "项目记录", "PlayCircleOutlined", 2, "frontend"), ("AI视频创作", "HomeOutlined", 2),
("/generated", "素材云", "StarOutlined", 3, "frontend"), ("资产管理", "HomeOutlined", 3),
("媒体关联", "HomeOutlined", 4),
] ]
for path, label, icon, order, target in default_menus: frontend_group_ids: dict[str, str] = {}
for label, icon, order in frontend_groups:
existing = await db.execute(
select(MenuConfig).where(
MenuConfig.label == label,
MenuConfig.menu_type == "group",
MenuConfig.menu_target == "frontend",
).limit(1)
)
group = existing.scalar_one_or_none()
if group:
frontend_group_ids[label] = group.id
else:
gid = generate_id()
frontend_group_ids[label] = gid
db.add(
MenuConfig(
id=gid,
path="",
label=label,
icon=icon,
sort_order=order,
is_active=True,
parent_id=None,
menu_type="group",
menu_target="frontend",
is_default=True,
)
)
frontend_pages = [
("/projects", "我的项目", "HomeOutlined", 0, "AI项目行业生成", True),
("/conversation", "AI创作", "StarOutlined", 0, "AI对话生成", True),
("/initial", "爆款开头复刻", "CodeOutlined", 0, "AI视频创作", False),
("/removelens", "拆镜复刻", "CameraOutlined", 1, "AI视频创作", False),
("/records", "项目记录", "PlayCircleOutlined", 0, "资产管理", True),
("/generated", "素材云", "CloudOutlined", 1, "资产管理", True),
("/authorization", "授权管理", "UserOutlined", 0, "媒体关联", False),
("/consume", "消耗列表", "FileTextOutlined", 1, "媒体关联", False),
]
for path, label, icon, order, group_label, is_default in frontend_pages:
existing = await db.execute( existing = await db.execute(
select(MenuConfig).where(MenuConfig.path == path).limit(1) select(MenuConfig).where(MenuConfig.path == path).limit(1)
) )
@@ -341,30 +382,33 @@ async def _seed_data():
icon=icon, icon=icon,
sort_order=order, sort_order=order,
is_active=True, is_active=True,
parent_id=frontend_group_ids.get(group_label),
menu_type="page", menu_type="page",
menu_target=target, menu_target="frontend",
is_default=True, is_default=is_default,
) )
) )
# Seed admin group menus first, then page menus with parent_id
admin_groups = [ admin_groups = [
("模型设置", "RobotOutlined", 98),
("模型配置", "RobotOutlined", 6), ("模型配置", "RobotOutlined", 6),
("系统设置", "SettingOutlined", 99), ("系统设置", "SettingOutlined", 99),
] ]
group_ids = {} admin_group_ids: dict[str, str] = {}
for label, icon, order in admin_groups: for label, icon, order in admin_groups:
existing = await db.execute( existing = await db.execute(
select(MenuConfig).where( select(MenuConfig).where(
MenuConfig.label == label, MenuConfig.label == label,
MenuConfig.menu_type == "group", MenuConfig.menu_type == "group",
MenuConfig.menu_target == "admin", MenuConfig.menu_target == "admin",
) ).limit(1)
.limit(1)
) )
group = existing.scalar_one_or_none() group = existing.scalar_one_or_none()
if not group: if group:
admin_group_ids[label] = group.id
else:
gid = generate_id() gid = generate_id()
admin_group_ids[label] = gid
db.add( db.add(
MenuConfig( MenuConfig(
id=gid, id=gid,
@@ -377,35 +421,33 @@ async def _seed_data():
menu_target="admin", menu_target="admin",
) )
) )
group_ids[label] = gid
else:
group_ids[label] = group.id
# (path, label, icon, sort_order, parent_group_label or None) admin_pages = [
admin_menus = [
("/", "数据概览", "DashboardOutlined", 0, None), ("/", "数据概览", "DashboardOutlined", 0, None),
("/users", "用户管理", "UserOutlined", 1, None), ("/users", "用户管理", "UserOutlined", 1, None),
("/credit-records", "交易流水", "WalletOutlined", 2, None), ("/credit-records", "交易流水", "WalletOutlined", 2, None),
("/generation-records", "生成记录", "VideoCameraOutlined", 3, None), ("/generation-ai", "创作记录", "BulbOutlined", 3, None),
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None), ("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
("/notifications", "消息推送", "BellOutlined", 5, None), ("/notifications", "消息推送", "BellOutlined", 5, None),
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型配置"), ("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
("/models", "模型配置", "RobotOutlined", 1, "模型"), ("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型"),
("/image-engines", "图片模型", "PictureOutlined", 2, "模型"), ("/models", "模型配置", "RobotOutlined", 1, "模型"),
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型"), ("/image-engines", "图片模型", "PictureOutlined", 2, "模型"),
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"), ("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"), ("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"), ("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"), ("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
("/operation-logs", "操作日志", "HistoryOutlined", 5, "系统设置"), ("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
] ]
for path, label, icon, order, parent_group in admin_menus: for path, label, icon, order, parent_group in admin_pages:
existing = await db.execute( existing = await db.execute(
select(MenuConfig).where( select(MenuConfig).where(
MenuConfig.path == path, MenuConfig.path == path,
MenuConfig.menu_target == "admin", MenuConfig.menu_target == "admin",
) ).limit(1)
.limit(1)
) )
if not existing.scalar_one_or_none(): if not existing.scalar_one_or_none():
db.add( db.add(
@@ -418,7 +460,7 @@ async def _seed_data():
is_active=True, is_active=True,
menu_type="page", menu_type="page",
menu_target="admin", menu_target="admin",
parent_id=group_ids.get(parent_group), parent_id=admin_group_ids.get(parent_group),
) )
) )
+3
View File
@@ -93,5 +93,8 @@ class AdminStatsOut(BaseModel):
total_users: int total_users: int
total_projects: int total_projects: int
total_generations: int total_generations: int
total_records: int
total_revenue: float total_revenue: float
credits_consumed_today: float credits_consumed_today: float
today_alipay_revenue: float
today_wechat_revenue: float