Files
thebet365/apps/api/src/applications/admin/admin-dashboard.service.ts
Mars 80adc0e928 feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局

- ECharts 控制台概览、注单管理中文化与列宽优化

- zhibo 赛事字段迁移与导入,玩家编辑可改所属代理

- 管理端 API 分页与 dashboard 统计接口

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 13:49:31 +08:00

210 lines
6.4 KiB
TypeScript

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,
})),
};
}
}