1
This commit is contained in:
@@ -94,9 +94,12 @@ export async function getAdminStats(startDate?: string, endDate?: string): Promi
|
|||||||
return api.get(`/admin/stats${query ? `?${query}` : ''}`);
|
return api.get(`/admin/stats${query ? `?${query}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminUsers(search?: string): Promise<AdminUser[]> {
|
export async function getAdminUsers(page = 1, pageSize = 20, search?: string): Promise<{ items: AdminUser[]; total: number }> {
|
||||||
const q = search ? `?search=${encodeURIComponent(search)}` : '';
|
const params = new URLSearchParams();
|
||||||
return api.get(`/admin/users${q}`);
|
params.set('page', String(page));
|
||||||
|
params.set('page_size', String(pageSize));
|
||||||
|
if (search) params.set('search', search);
|
||||||
|
return api.get(`/admin/users?${params.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function adjustCredits(userId: string, amount: number, description: string): Promise<void> {
|
export async function adjustCredits(userId: string, amount: number, description: string): Promise<void> {
|
||||||
@@ -245,20 +248,26 @@ export async function getPaymentStats(params?: {
|
|||||||
return api.get(url);
|
return api.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminPaymentOrders(params?: { method?: string; status?: string }): Promise<{ items: any[] }> {
|
export async function getAdminPaymentOrders(params?: { method?: string; status?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (params?.method) qs.set('method', params.method);
|
if (params?.method) qs.set('payment_method', params.method);
|
||||||
if (params?.status) qs.set('status', params.status);
|
if (params?.status) qs.set('status', params.status);
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
if (params?.startDate) qs.set('start_date', params.startDate);
|
||||||
return api.get(`/admin/payment-orders${suffix}`);
|
if (params?.endDate) qs.set('end_date', params.endDate);
|
||||||
|
if (params?.page) qs.set('page', String(params.page));
|
||||||
|
if (params?.pageSize) qs.set('page_size', String(params.pageSize));
|
||||||
|
return api.get(`/admin/payment-orders?${qs.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refundPaymentOrder(orderNo: string): Promise<void> {
|
export async function refundPaymentOrder(orderNo: string): Promise<void> {
|
||||||
await api.post(`/admin/payment-orders/${orderNo}/refund`);
|
await api.post(`/admin/payment-orders/${orderNo}/refund`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminNotifications(): Promise<{ total: number; items: any[] }> {
|
export async function getAdminNotifications(page = 1, pageSize = 20): Promise<{ total: number; items: any[] }> {
|
||||||
return api.get('/admin/notifications');
|
const params = new URLSearchParams();
|
||||||
|
params.set('page', String(page));
|
||||||
|
params.set('page_size', String(pageSize));
|
||||||
|
return api.get(`/admin/notifications?${params.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAdminNotification(data: { title: string; content: string; type: string; target_user_id?: string }): Promise<void> {
|
export async function createAdminNotification(data: { title: string; content: string; type: string; target_user_id?: string }): Promise<void> {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
const [records, setRecords] = useState<CreditRecord[]>([]);
|
const [records, setRecords] = useState<CreditRecord[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [typeFilter, setTypeFilter] = useState<string>('');
|
const [typeFilter, setTypeFilter] = useState<string>('');
|
||||||
const [userNameFilter, setUserNameFilter] = useState<string>('');
|
const [userNameFilter, setUserNameFilter] = useState<string>('');
|
||||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
|
||||||
@@ -36,11 +38,13 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const filters: { type?: string; user_name?: string; start_date?: string; end_date?: string } = {};
|
const filters: { type?: string; user_name?: string; start_date?: string; end_date?: string; page?: number; page_size?: number } = {};
|
||||||
if (typeFilter) filters.type = typeFilter;
|
if (typeFilter) filters.type = typeFilter;
|
||||||
if (userNameFilter) filters.user_name = userNameFilter;
|
if (userNameFilter) filters.user_name = userNameFilter;
|
||||||
if (dateRange[0]) filters.start_date = dateRange[0].format('YYYY-MM-DD');
|
if (dateRange[0]) filters.start_date = dateRange[0].format('YYYY-MM-DD');
|
||||||
if (dateRange[1]) filters.end_date = dateRange[1].format('YYYY-MM-DD');
|
if (dateRange[1]) filters.end_date = dateRange[1].format('YYYY-MM-DD');
|
||||||
|
filters.page = page;
|
||||||
|
filters.page_size = pageSize;
|
||||||
const res = await getCreditRecords(Object.keys(filters).length > 0 ? filters : undefined);
|
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);
|
||||||
@@ -51,6 +55,13 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [page, pageSize, typeFilter, userNameFilter, dateRange]);
|
||||||
|
|
||||||
|
const handlePageChange = (p: number, ps: number) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@@ -191,7 +202,14 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
dataSource={records}
|
dataSource={records}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 条记录` }}
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
onChange: handlePageChange,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条记录`,
|
||||||
|
}}
|
||||||
scroll={{ x: 800 }}
|
scroll={{ x: 800 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -38,12 +38,16 @@ const AdminNotificationManager: React.FC = () => {
|
|||||||
const [readModal, setReadModal] = useState<{ open: boolean; notifId: string; title: string }>({ open: false, notifId: '', title: '' });
|
const [readModal, setReadModal] = useState<{ open: boolean; notifId: string; title: string }>({ open: false, notifId: '', title: '' });
|
||||||
const [readUsers, setReadUsers] = useState<ReadUser[]>([]);
|
const [readUsers, setReadUsers] = useState<ReadUser[]>([]);
|
||||||
const [readLoading, setReadLoading] = useState(false);
|
const [readLoading, setReadLoading] = useState(false);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getAdminNotifications();
|
const res = await getAdminNotifications(page, pageSize);
|
||||||
const userList = await getAdminUsers();
|
const userRes = await getAdminUsers(1, 1000);
|
||||||
|
const userList = userRes.items || [];
|
||||||
const userMap: Record<string, string> = {};
|
const userMap: Record<string, string> = {};
|
||||||
userList.forEach((u: any) => { userMap[u.id] = u.username; });
|
userList.forEach((u: any) => { userMap[u.id] = u.username; });
|
||||||
const items = (res.items || []).map((n: any) => ({
|
const items = (res.items || []).map((n: any) => ({
|
||||||
@@ -56,6 +60,7 @@ const AdminNotificationManager: React.FC = () => {
|
|||||||
createdAt: n.createdAt,
|
createdAt: n.createdAt,
|
||||||
}));
|
}));
|
||||||
setNotifications(items);
|
setNotifications(items);
|
||||||
|
setTotal(res.total || 0);
|
||||||
setUsers(userList.map((u: any) => ({ id: u.id, username: u.username })));
|
setUsers(userList.map((u: any) => ({ id: u.id, username: u.username })));
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载通知列表失败');
|
message.error('加载通知列表失败');
|
||||||
@@ -64,7 +69,12 @@ const AdminNotificationManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, [page, pageSize]);
|
||||||
|
|
||||||
|
const handlePageChange = (p: number, ps: number) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -161,7 +171,7 @@ const AdminNotificationManager: React.FC = () => {
|
|||||||
<Space>
|
<Space>
|
||||||
<BellOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
<BellOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||||
<Typography.Text strong style={{ fontSize: 16 }}>消息推送管理</Typography.Text>
|
<Typography.Text strong style={{ fontSize: 16 }}>消息推送管理</Typography.Text>
|
||||||
<Tag color="purple">{notifications.length} 条消息</Tag>
|
<Tag color="purple">共 {total} 条消息</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)}
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)}
|
||||||
style={{ borderRadius: 8 }}>
|
style={{ borderRadius: 8 }}>
|
||||||
@@ -174,7 +184,14 @@ const AdminNotificationManager: React.FC = () => {
|
|||||||
dataSource={notifications}
|
dataSource={notifications}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 条消息` }}
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
onChange: handlePageChange,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条消息`,
|
||||||
|
}}
|
||||||
scroll={{ x: 900 }}
|
scroll={{ x: 900 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import zhCN from 'antd/locale/zh_CN';
|
|||||||
import {
|
import {
|
||||||
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, ReloadOutlined, UndoOutlined
|
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, ReloadOutlined, UndoOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getPaymentStats, refundPaymentOrder } from '../api';
|
import { getPaymentStats, getAdminPaymentOrders, refundPaymentOrder } from '../api';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ const { RangePicker } = DatePicker;
|
|||||||
const AdminPaymentStats: React.FC = () => {
|
const AdminPaymentStats: React.FC = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [stats, setStats] = useState<any>(null);
|
const [stats, setStats] = useState<any>(null);
|
||||||
|
const [orderPage, setOrderPage] = useState(1);
|
||||||
|
const [orderPageSize, setOrderPageSize] = useState(10);
|
||||||
|
const [orderTotal, setOrderTotal] = useState(0);
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
paymentMethod?: string;
|
paymentMethod?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -29,8 +32,16 @@ const AdminPaymentStats: React.FC = () => {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getPaymentStats(filters);
|
const [statsData, ordersData] = await Promise.all([
|
||||||
setStats(data);
|
getPaymentStats(filters),
|
||||||
|
getAdminPaymentOrders({
|
||||||
|
...filters,
|
||||||
|
page: orderPage,
|
||||||
|
pageSize: orderPageSize,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
setStats({ ...statsData, recent: ordersData.items || [] });
|
||||||
|
setOrderTotal(ordersData.total);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载支付统计失败');
|
message.error('加载支付统计失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -38,7 +49,12 @@ const AdminPaymentStats: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { load(); }, [filters]);
|
useEffect(() => { load(); }, [filters, orderPage, orderPageSize]);
|
||||||
|
|
||||||
|
const handleOrderPageChange = (p: number, ps: number) => {
|
||||||
|
setOrderPage(p);
|
||||||
|
setOrderPageSize(ps);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setFilters({
|
setFilters({
|
||||||
@@ -269,10 +285,17 @@ const AdminPaymentStats: React.FC = () => {
|
|||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={stats.recent}
|
dataSource={stats.recent || []}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10, size: 'small' }}
|
pagination={{
|
||||||
|
current: orderPage,
|
||||||
|
pageSize: orderPageSize,
|
||||||
|
total: orderTotal,
|
||||||
|
onChange: handleOrderPageChange,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
}}
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
size="middle"
|
size="middle"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,16 +25,26 @@ const AdminUsers: React.FC = () => {
|
|||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [resetPwdForm] = Form.useForm();
|
const [resetPwdForm] = Form.useForm();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(20);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getAdminUsers(search || undefined);
|
const data = await getAdminUsers(page, pageSize, search || undefined);
|
||||||
setUsers(data);
|
setUsers(data.items || []);
|
||||||
|
setTotal(data.total || 0);
|
||||||
} catch { /* auth error handled by client */ }
|
} catch { /* auth error handled by client */ }
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, [page, pageSize, search]);
|
||||||
|
|
||||||
|
const handlePageChange = (p: number, ps: number) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearch = () => load();
|
const handleSearch = () => load();
|
||||||
|
|
||||||
@@ -250,7 +260,14 @@ const AdminUsers: React.FC = () => {
|
|||||||
dataSource={filteredUsers}
|
dataSource={filteredUsers}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 个用户` }}
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
onChange: handlePageChange,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 个用户`,
|
||||||
|
}}
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ router = APIRouter(prefix="/admin", tags=["admin"])
|
|||||||
|
|
||||||
# ── User Management ──────────────────────────────────────
|
# ── User Management ──────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/users", response_model=list[AdminUserOut])
|
@router.get("/users")
|
||||||
async def list_users(
|
async def list_users(
|
||||||
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),
|
||||||
@@ -86,13 +86,15 @@ async def list_users(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
query = select(User).order_by(User.created_at.desc())
|
query = select(User).order_by(User.created_at.desc())
|
||||||
|
count_query = select(func.count(User.id))
|
||||||
if search:
|
if search:
|
||||||
query = query.where(
|
search_filter = (User.username.ilike(f"%{search}%")) | (User.email.ilike(f"%{search}%"))
|
||||||
(User.username.ilike(f"%{search}%"))
|
query = query.where(search_filter)
|
||||||
| (User.email.ilike(f"%{search}%"))
|
count_query = count_query.where(search_filter)
|
||||||
)
|
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))
|
||||||
return result.scalars().all()
|
items = result.scalars().all()
|
||||||
|
return {"items": [AdminUserOut.model_validate(u) for u in items], "total": total}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/users", response_model=AdminUserOut)
|
@router.post("/users", response_model=AdminUserOut)
|
||||||
@@ -555,24 +557,6 @@ async def get_payment_stats(
|
|||||||
)
|
)
|
||||||
month_row = month_result.one()
|
month_row = month_result.one()
|
||||||
|
|
||||||
# Recent orders with filters
|
|
||||||
recent_filters = []
|
|
||||||
if payment_method:
|
|
||||||
recent_filters.append(PaymentOrder.payment_method == payment_method)
|
|
||||||
if status:
|
|
||||||
recent_filters.append(PaymentOrder.status == status)
|
|
||||||
recent_filters.append(PaymentOrder.created_at >= query_start)
|
|
||||||
recent_filters.append(PaymentOrder.created_at < query_end)
|
|
||||||
|
|
||||||
recent_result = await db.execute(
|
|
||||||
select(PaymentOrder, User)
|
|
||||||
.join(User, PaymentOrder.user_id == User.id)
|
|
||||||
.where(*recent_filters)
|
|
||||||
.order_by(PaymentOrder.created_at.desc())
|
|
||||||
.limit(50)
|
|
||||||
)
|
|
||||||
recent_data = recent_result.all()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"by_status": by_status,
|
"by_status": by_status,
|
||||||
"today": {
|
"today": {
|
||||||
@@ -583,76 +567,68 @@ async def get_payment_stats(
|
|||||||
"paid_count": month_row.paid_count,
|
"paid_count": month_row.paid_count,
|
||||||
"paid_amount": round(float(month_row.paid_amount), 2),
|
"paid_amount": round(float(month_row.paid_amount), 2),
|
||||||
},
|
},
|
||||||
"recent": [
|
|
||||||
{
|
|
||||||
"id": o.id,
|
|
||||||
"order_no": o.order_no,
|
|
||||||
"user_id": o.user_id,
|
|
||||||
"username": u.username,
|
|
||||||
"amount": round(o.amount, 2),
|
|
||||||
"credits": round(o.credits, 2),
|
|
||||||
"payment_method": o.payment_method,
|
|
||||||
"status": o.status if o.status in ("pending", "paid", "cancelled", "refunded") else "cancelled",
|
|
||||||
"trade_no": o.trade_no,
|
|
||||||
"paid_at": _iso(o.paid_at),
|
|
||||||
"created_at": _iso(o.created_at),
|
|
||||||
}
|
|
||||||
for o, u in recent_data
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/payment-orders")
|
@router.get("/payment-orders")
|
||||||
async def get_admin_payment_orders(
|
async def list_payment_orders(
|
||||||
method: str | None = None,
|
page: int = Query(1, ge=1),
|
||||||
status: str | None = None,
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
page: int = 1,
|
payment_method: str | None = Query(None),
|
||||||
page_size: int = 20,
|
status: str | None = Query(None),
|
||||||
|
start_date: str | None = Query(None),
|
||||||
|
end_date: str | None = Query(None),
|
||||||
admin: User = Depends(get_admin_user),
|
admin: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Return paginated payment orders for admin."""
|
"""Return paginated payment orders for admin dashboard."""
|
||||||
query = select(PaymentOrder)
|
query = select(PaymentOrder, User.username).join(User, PaymentOrder.user_id == User.id)
|
||||||
if method:
|
count_query = select(func.count(PaymentOrder.id))
|
||||||
query = query.where(PaymentOrder.payment_method == method)
|
|
||||||
|
filters = []
|
||||||
|
if payment_method:
|
||||||
|
filters.append(PaymentOrder.payment_method == payment_method)
|
||||||
if status:
|
if status:
|
||||||
query = query.where(PaymentOrder.status == status)
|
filters.append(PaymentOrder.status == status)
|
||||||
|
if start_date:
|
||||||
|
filters.append(PaymentOrder.created_at >= datetime.fromisoformat(start_date).replace(tzinfo=CST))
|
||||||
|
if end_date:
|
||||||
|
filters.append(PaymentOrder.created_at < (datetime.fromisoformat(end_date) + timedelta(days=1)).replace(tzinfo=CST))
|
||||||
|
|
||||||
# Count total
|
for f in filters:
|
||||||
count_result = await db.execute(
|
query = query.where(f)
|
||||||
select(func.count()).select_from(query.subquery())
|
count_query = count_query.where(f)
|
||||||
)
|
|
||||||
total = count_result.scalar() or 0
|
|
||||||
|
|
||||||
# Paginated results
|
total = (await db.execute(count_query)).scalar() or 0
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
query.order_by(PaymentOrder.created_at.desc())
|
query.order_by(PaymentOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||||
.offset((page - 1) * page_size)
|
|
||||||
.limit(page_size)
|
|
||||||
)
|
)
|
||||||
orders = result.scalars().all()
|
rows = result.all()
|
||||||
|
|
||||||
return {
|
items = [
|
||||||
"total": total,
|
{
|
||||||
"page": page,
|
"id": o.id,
|
||||||
"page_size": page_size,
|
"orderNo": o.order_no,
|
||||||
"items": [
|
"order_no": o.order_no,
|
||||||
{
|
"userId": o.user_id,
|
||||||
"id": o.id,
|
"user_id": o.user_id,
|
||||||
"order_no": o.order_no,
|
"username": username,
|
||||||
"user_id": o.user_id,
|
"amount": round(float(o.amount), 2),
|
||||||
"amount": round(o.amount, 2),
|
"credits": round(float(o.credits), 2),
|
||||||
"credits": round(o.credits, 2),
|
"paymentMethod": o.payment_method,
|
||||||
"payment_method": o.payment_method,
|
"payment_method": o.payment_method,
|
||||||
"status": o.status if o.status in ("pending", "paid", "cancelled", "refunded") else "cancelled",
|
"status": o.status,
|
||||||
"trade_no": o.trade_no,
|
"tradeNo": o.trade_no,
|
||||||
"paid_at": _iso(o.paid_at),
|
"trade_no": o.trade_no,
|
||||||
"created_at": _iso(o.created_at),
|
"paidAt": o.paid_at.isoformat() if o.paid_at else None,
|
||||||
}
|
"paid_at": o.paid_at.isoformat() if o.paid_at else None,
|
||||||
for o in orders
|
"createdAt": o.created_at.isoformat() if o.created_at else None,
|
||||||
],
|
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||||
}
|
}
|
||||||
|
for o, username in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||||
|
|
||||||
@router.put("/payment-configs/{config_id}")
|
@router.put("/payment-configs/{config_id}")
|
||||||
async def update_payment_config(
|
async def update_payment_config(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.services.credits import get_records
|
|||||||
router = APIRouter(prefix="/credits", tags=["credits"])
|
router = APIRouter(prefix="/credits", tags=["credits"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=CreditBalanceOut)
|
@router.get("")
|
||||||
async def get_credits(
|
async def get_credits(
|
||||||
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),
|
||||||
@@ -23,12 +23,11 @@ async def get_credits(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
records, total = await get_records(db, current_user.id, page, page_size)
|
records, total = await get_records(db, current_user.id, page, page_size)
|
||||||
from fastapi.responses import JSONResponse
|
return {
|
||||||
return JSONResponse(content={
|
|
||||||
"credits": round(current_user.credits, 2),
|
"credits": round(current_user.credits, 2),
|
||||||
"records": [CreditRecordOut.model_validate(r) for r in records],
|
"records": [CreditRecordOut.model_validate(r) for r in records],
|
||||||
"total": total,
|
"total": total,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
|||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/orders", response_model=list[PaymentOrderOut])
|
@router.get("/orders")
|
||||||
async def list_orders(
|
async def list_orders(
|
||||||
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),
|
||||||
@@ -296,10 +296,10 @@ async def list_orders(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
from app.services.payment import _check_and_expire_order
|
from app.services.payment import _check_and_expire_order
|
||||||
|
|
||||||
count_query = select(func.count(PaymentOrder.id)).where(PaymentOrder.user_id == current_user.id)
|
count_query = select(func.count(PaymentOrder.id)).where(PaymentOrder.user_id == current_user.id)
|
||||||
total = (await db.execute(count_query)).scalar() or 0
|
total = (await db.execute(count_query)).scalar() or 0
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(PaymentOrder)
|
select(PaymentOrder)
|
||||||
.where(PaymentOrder.user_id == current_user.id)
|
.where(PaymentOrder.user_id == current_user.id)
|
||||||
@@ -310,9 +310,8 @@ async def list_orders(
|
|||||||
orders = result.scalars().all()
|
orders = result.scalars().all()
|
||||||
for o in orders:
|
for o in orders:
|
||||||
await _check_and_expire_order(db, o)
|
await _check_and_expire_order(db, o)
|
||||||
|
|
||||||
from fastapi.responses import JSONResponse
|
return {"items": [PaymentOrderOut.model_validate(o) for o in orders], "total": total}
|
||||||
return JSONResponse(content={"items": orders, "total": total})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/orders/{order_no}", response_model=PaymentOrderOut)
|
@router.get("/orders/{order_no}", response_model=PaymentOrderOut)
|
||||||
|
|||||||
@@ -36,16 +36,25 @@ const CreditsPage: React.FC = () => {
|
|||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const [records, setRecords] = useState<CreditRecord[]>([]);
|
const [records, setRecords] = useState<CreditRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getCredits();
|
const data = await getCredits(page, pageSize);
|
||||||
setRecords(data.records);
|
setRecords(data.records);
|
||||||
|
setTotal(data.total);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
fetch();
|
fetch();
|
||||||
}, []);
|
}, [page, pageSize]);
|
||||||
|
|
||||||
|
const handlePageChange = (p: number, ps: number) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
};
|
||||||
|
|
||||||
const totalRecharge = records.filter((r) => r.type === 'recharge').reduce((sum, r) => sum + r.amount, 0);
|
const totalRecharge = records.filter((r) => r.type === 'recharge').reduce((sum, r) => sum + r.amount, 0);
|
||||||
const totalConsume = records.filter((r) => r.type === 'consume').reduce((sum, r) => sum + Math.abs(r.amount), 0);
|
const totalConsume = records.filter((r) => r.type === 'consume').reduce((sum, r) => sum + Math.abs(r.amount), 0);
|
||||||
@@ -129,7 +138,14 @@ const CreditsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
|
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
|
||||||
<Table columns={columns} dataSource={records} rowKey="id" loading={loading}
|
<Table columns={columns} dataSource={records} rowKey="id" loading={loading}
|
||||||
pagination={{ pageSize: 10, size: 'small' }}
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
onChange: handlePageChange,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
}}
|
||||||
scroll={{ x: 500 }} />
|
scroll={{ x: 500 }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user