feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
209
apps/api/src/applications/admin/admin-dashboard.service.ts
Normal file
209
apps/api/src/applications/admin/admin-dashboard.service.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
function dec(v: Decimal | null | undefined) {
|
||||
return v?.toString() ?? '0';
|
||||
}
|
||||
|
||||
function sub(a: Decimal | null | undefined, b: Decimal | null | undefined) {
|
||||
return new Decimal(a ?? 0).sub(b ?? 0).toString();
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminDashboardService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getOverview() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const trend7d = await Promise.all(
|
||||
Array.from({ length: 7 }, (_, i) => {
|
||||
const dayStart = new Date(today);
|
||||
dayStart.setDate(dayStart.getDate() - (6 - i));
|
||||
const dayEnd = new Date(dayStart);
|
||||
dayEnd.setDate(dayEnd.getDate() + 1);
|
||||
return this.prisma.bet
|
||||
.aggregate({
|
||||
where: { placedAt: { gte: dayStart, lt: dayEnd } },
|
||||
_sum: { stake: true, actualReturn: true },
|
||||
_count: true,
|
||||
})
|
||||
.then((agg) => ({
|
||||
date: dayStart.toISOString().slice(0, 10),
|
||||
label: `${dayStart.getMonth() + 1}/${dayStart.getDate()}`,
|
||||
betCount: agg._count,
|
||||
stake: dec(agg._sum.stake),
|
||||
payout: dec(agg._sum.actualReturn),
|
||||
ggr: sub(agg._sum.stake, agg._sum.actualReturn),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
const playerWhere = { userType: 'PLAYER', deletedAt: null };
|
||||
|
||||
const [
|
||||
todayBets,
|
||||
yesterdayBets,
|
||||
pendingBets,
|
||||
betStatusToday,
|
||||
matchGroups,
|
||||
matchTotal,
|
||||
playerTotal,
|
||||
playerActive,
|
||||
playerSuspended,
|
||||
playerDirect,
|
||||
newPlayersToday,
|
||||
agentProfiles,
|
||||
agentsActive,
|
||||
walletAgg,
|
||||
recentBets,
|
||||
recentPlayers,
|
||||
] = await Promise.all([
|
||||
this.prisma.bet.aggregate({
|
||||
where: { placedAt: { gte: today } },
|
||||
_sum: { stake: true, actualReturn: true },
|
||||
_count: true,
|
||||
}),
|
||||
this.prisma.bet.aggregate({
|
||||
where: {
|
||||
placedAt: {
|
||||
gte: new Date(today.getTime() - 86400000),
|
||||
lt: today,
|
||||
},
|
||||
},
|
||||
_sum: { stake: true, actualReturn: true },
|
||||
_count: true,
|
||||
}),
|
||||
this.prisma.bet.count({ where: { status: 'PENDING' } }),
|
||||
this.prisma.bet.groupBy({
|
||||
by: ['status'],
|
||||
where: { placedAt: { gte: today } },
|
||||
_count: { _all: true },
|
||||
_sum: { stake: true },
|
||||
}),
|
||||
this.prisma.match.groupBy({
|
||||
by: ['status'],
|
||||
where: { deletedAt: null },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.match.count({ where: { deletedAt: null } }),
|
||||
this.prisma.user.count({ where: playerWhere }),
|
||||
this.prisma.user.count({ where: { ...playerWhere, status: 'ACTIVE' } }),
|
||||
this.prisma.user.count({ where: { ...playerWhere, status: 'SUSPENDED' } }),
|
||||
this.prisma.user.count({
|
||||
where: { ...playerWhere, parentId: null },
|
||||
}),
|
||||
this.prisma.user.count({
|
||||
where: { ...playerWhere, createdAt: { gte: today } },
|
||||
}),
|
||||
this.prisma.agentProfile.aggregate({
|
||||
_sum: { creditLimit: true, usedCredit: true },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.agentProfile.count({ where: { status: 'ACTIVE' } }),
|
||||
this.prisma.wallet.aggregate({
|
||||
where: { user: playerWhere },
|
||||
_sum: { availableBalance: true, frozenBalance: true },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.bet.findMany({
|
||||
take: 8,
|
||||
orderBy: { placedAt: 'desc' },
|
||||
include: { user: { select: { username: true } } },
|
||||
}),
|
||||
this.prisma.user.findMany({
|
||||
where: playerWhere,
|
||||
take: 6,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
parent: { select: { username: true } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const matchByStatus: Record<string, number> = {};
|
||||
for (const g of matchGroups) {
|
||||
matchByStatus[g.status] = g._count._all;
|
||||
}
|
||||
|
||||
const todayBetByStatus: Record<string, { count: number; stake: string }> = {};
|
||||
for (const g of betStatusToday) {
|
||||
todayBetByStatus[g.status] = {
|
||||
count: g._count._all,
|
||||
stake: dec(g._sum.stake),
|
||||
};
|
||||
}
|
||||
|
||||
const creditLimit = agentProfiles._sum.creditLimit ?? new Decimal(0);
|
||||
const usedCredit = agentProfiles._sum.usedCredit ?? new Decimal(0);
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
trend7d,
|
||||
today: {
|
||||
betCount: todayBets._count,
|
||||
stake: dec(todayBets._sum.stake),
|
||||
payout: dec(todayBets._sum.actualReturn),
|
||||
ggr: sub(todayBets._sum.stake, todayBets._sum.actualReturn),
|
||||
newPlayers: newPlayersToday,
|
||||
},
|
||||
yesterday: {
|
||||
betCount: yesterdayBets._count,
|
||||
stake: dec(yesterdayBets._sum.stake),
|
||||
payout: dec(yesterdayBets._sum.actualReturn),
|
||||
ggr: sub(yesterdayBets._sum.stake, yesterdayBets._sum.actualReturn),
|
||||
},
|
||||
users: {
|
||||
playersTotal: playerTotal,
|
||||
playersActive: playerActive,
|
||||
playersSuspended: playerSuspended,
|
||||
playersDirect: playerDirect,
|
||||
agentsTotal: agentProfiles._count._all,
|
||||
agentsActive,
|
||||
},
|
||||
wallets: {
|
||||
totalAvailable: dec(walletAgg._sum.availableBalance),
|
||||
totalFrozen: dec(walletAgg._sum.frozenBalance),
|
||||
playerWalletCount: walletAgg._count._all,
|
||||
},
|
||||
agents: {
|
||||
totalCreditLimit: dec(creditLimit),
|
||||
totalUsedCredit: dec(usedCredit),
|
||||
totalAvailableCredit: creditLimit.sub(usedCredit).toString(),
|
||||
},
|
||||
matches: {
|
||||
total: matchTotal,
|
||||
draft: matchByStatus.DRAFT ?? 0,
|
||||
published: matchByStatus.PUBLISHED ?? 0,
|
||||
closed: matchByStatus.CLOSED ?? 0,
|
||||
cancelled: matchByStatus.CANCELLED ?? 0,
|
||||
pendingSettlement: matchByStatus.PENDING_SETTLEMENT ?? 0,
|
||||
settled: matchByStatus.SETTLED ?? 0,
|
||||
},
|
||||
bets: {
|
||||
pendingTotal: pendingBets,
|
||||
todayByStatus: todayBetByStatus,
|
||||
},
|
||||
recentBets: recentBets.map((b) => ({
|
||||
betNo: b.betNo,
|
||||
username: b.user.username,
|
||||
stake: dec(b.stake),
|
||||
status: b.status,
|
||||
placedAt: b.placedAt,
|
||||
})),
|
||||
recentPlayers: recentPlayers.map((p) => ({
|
||||
id: p.id.toString(),
|
||||
username: p.username,
|
||||
status: p.status,
|
||||
parentUsername: p.parent?.username ?? null,
|
||||
createdAt: p.createdAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user