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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
@@ -24,7 +26,18 @@ import { I18nService } from '../../domains/operations/i18n/i18n.service';
|
||||
import { AuditService } from '../../domains/operations/audit/audit.service';
|
||||
import { BetsService } from '../../domains/betting/bets.service';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { IsString, IsNumber, IsOptional, IsArray, IsBoolean, MinLength } from 'class-validator';
|
||||
import { AdminDashboardService } from './admin-dashboard.service';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
MinLength,
|
||||
IsIn,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import type { ZhiboMatchExport, ZhiboMatchesBundleExport } from '../../domains/catalog/zhibo-match.types';
|
||||
|
||||
class CreateUserDto {
|
||||
@IsString()
|
||||
@@ -43,6 +56,116 @@ class CreateUserDto {
|
||||
creditLimit?: number;
|
||||
}
|
||||
|
||||
class CreatePlayerAdminDto {
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locale?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
initialDeposit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
class UpdatePlayerAdminDto {
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'SUSPENDED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locale?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
/** 传空字符串表示改为平台直属(无代理) */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
class CreateAgentAdminDto {
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
creditLimit!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locale?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
cashbackRate?: number;
|
||||
}
|
||||
|
||||
class UpdateAgentAdminDto {
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'SUSPENDED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locale?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
cashbackRate?: number;
|
||||
}
|
||||
|
||||
class DepositDto {
|
||||
@IsNumber()
|
||||
amount!: number;
|
||||
@@ -55,15 +178,24 @@ class DepositDto {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
class CreateMatchDto {
|
||||
class CreatePlatformMatchDto {
|
||||
@IsString()
|
||||
leagueId!: string;
|
||||
leagueEn!: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamId!: string;
|
||||
leagueZh!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamId!: string;
|
||||
homeTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamZh!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamZh!: string;
|
||||
|
||||
@IsString()
|
||||
startTime!: string;
|
||||
@@ -71,6 +203,15 @@ class CreateMatchDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isHot?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
displayOrder?: number;
|
||||
}
|
||||
|
||||
function isZhiboBundlePayload(body: unknown): body is ZhiboMatchesBundleExport {
|
||||
if (!body || typeof body !== 'object') return false;
|
||||
return Array.isArray((body as ZhiboMatchesBundleExport).matches);
|
||||
}
|
||||
|
||||
class ScoreDto {
|
||||
@@ -123,44 +264,73 @@ export class AdminController {
|
||||
private audit: AuditService,
|
||||
private bets: BetsService,
|
||||
private prisma: PrismaService,
|
||||
private readonly dashboardService: AdminDashboardService,
|
||||
) {}
|
||||
|
||||
@Get('dashboard')
|
||||
async dashboard() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const [todayBets, pendingMatches, totalPlayers] = await Promise.all([
|
||||
this.prisma.bet.aggregate({
|
||||
where: { placedAt: { gte: today } },
|
||||
_sum: { stake: true, actualReturn: true },
|
||||
_count: true,
|
||||
}),
|
||||
this.prisma.match.count({ where: { status: 'PENDING_SETTLEMENT' } }),
|
||||
this.prisma.user.count({ where: { userType: 'PLAYER' } }),
|
||||
]);
|
||||
|
||||
return jsonResponse({
|
||||
todayBetCount: todayBets._count,
|
||||
todayStake: todayBets._sum.stake,
|
||||
todayPayout: todayBets._sum.actualReturn,
|
||||
pendingSettlement: pendingMatches,
|
||||
totalPlayers,
|
||||
});
|
||||
async getDashboard() {
|
||||
const overview = await this.dashboardService.getOverview();
|
||||
return jsonResponse(overview);
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
async listUsers(@Query('page') page?: string) {
|
||||
const result = await this.users.listPlayers(page ? parseInt(page) : 1);
|
||||
async listUsers(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('parentId') parentId?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
const result = await this.users.listPlayers(
|
||||
page ? parseInt(page, 10) : 1,
|
||||
pageSize ? parseInt(pageSize, 10) : 10,
|
||||
{
|
||||
keyword,
|
||||
parentId: parentId ? BigInt(parentId) : undefined,
|
||||
status,
|
||||
},
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('users/:id')
|
||||
async getUserDetail(@Param('id') id: string) {
|
||||
const detail = await this.users.getPlayerAdminDetail(BigInt(id));
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Put('users/:id')
|
||||
async updateUser(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePlayerAdminDto,
|
||||
) {
|
||||
const detail = await this.users.updatePlayerAdmin(BigInt(id), dto);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'UPDATE_PLAYER',
|
||||
module: 'USERS',
|
||||
targetId: id,
|
||||
});
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Post('users')
|
||||
async createPlayer(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) {
|
||||
async createPlayer(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Body() dto: CreatePlayerAdminDto,
|
||||
) {
|
||||
const user = await this.agents.createPlayer(operatorId, {
|
||||
username: dto.username,
|
||||
password: dto.password,
|
||||
parentId: dto.parentId ? BigInt(dto.parentId) : operatorId,
|
||||
parentId: dto.parentId ? BigInt(dto.parentId) : undefined,
|
||||
locale: dto.locale,
|
||||
phone: dto.phone,
|
||||
email: dto.email,
|
||||
initialDeposit: dto.initialDeposit,
|
||||
depositRemark: dto.remark,
|
||||
depositRequestId: `create-player-${dto.username}-${Date.now()}`,
|
||||
});
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
@@ -169,24 +339,73 @@ export class AdminController {
|
||||
module: 'USERS',
|
||||
targetId: user.id.toString(),
|
||||
});
|
||||
return jsonResponse(user);
|
||||
const detail = await this.users.getPlayerAdminDetail(user.id);
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Get('agents/options')
|
||||
async listAgentOptions() {
|
||||
const agents = await this.prisma.user.findMany({
|
||||
where: { userType: 'AGENT', deletedAt: null, agentLevel: 1 },
|
||||
select: { id: true, username: true },
|
||||
orderBy: { username: 'asc' },
|
||||
});
|
||||
return jsonResponse(
|
||||
agents.map((a) => ({ id: a.id.toString(), username: a.username })),
|
||||
);
|
||||
}
|
||||
|
||||
@Get('agents')
|
||||
async listAgents() {
|
||||
const agents = await this.prisma.agentProfile.findMany({
|
||||
include: { user: true },
|
||||
async listAgents(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
) {
|
||||
const result = await this.agents.listAgentsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 10,
|
||||
keyword,
|
||||
});
|
||||
return jsonResponse(agents);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('agents/:id')
|
||||
async getAgentDetail(@Param('id') id: string) {
|
||||
const detail = await this.agents.getAgentAdminDetail(BigInt(id));
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Put('agents/:id')
|
||||
async updateAgent(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateAgentAdminDto,
|
||||
) {
|
||||
const detail = await this.agents.updateAgentAdmin(BigInt(id), dto);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'UPDATE_AGENT',
|
||||
module: 'AGENTS',
|
||||
targetId: id,
|
||||
});
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Post('agents')
|
||||
async createAgent(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) {
|
||||
async createAgent(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Body() dto: CreateAgentAdminDto,
|
||||
) {
|
||||
const user = await this.agents.createAgent(operatorId, {
|
||||
username: dto.username,
|
||||
password: dto.password,
|
||||
level: 1,
|
||||
creditLimit: dto.creditLimit,
|
||||
locale: dto.locale,
|
||||
phone: dto.phone,
|
||||
email: dto.email,
|
||||
cashbackRate: dto.cashbackRate,
|
||||
});
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
@@ -195,7 +414,8 @@ export class AdminController {
|
||||
module: 'AGENTS',
|
||||
targetId: user.id.toString(),
|
||||
});
|
||||
return jsonResponse(user);
|
||||
const detail = await this.agents.getAgentAdminDetail(user.id);
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Post('agents/:id/credit')
|
||||
@@ -257,27 +477,100 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Get('matches')
|
||||
async listMatches() {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
include: { markets: { include: { selections: true } } },
|
||||
orderBy: { startTime: 'desc' },
|
||||
async listMatches(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
) {
|
||||
const p = Math.max(1, page ? parseInt(page, 10) : 1);
|
||||
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100);
|
||||
const skip = (p - 1) * size;
|
||||
const where: { deletedAt: null; status?: string; OR?: object[] } = { deletedAt: null };
|
||||
if (status) where.status = status;
|
||||
const kw = keyword?.trim();
|
||||
if (kw) {
|
||||
where.OR = [
|
||||
{ matchName: { contains: kw, mode: 'insensitive' } },
|
||||
{ homeTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
{ awayTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
];
|
||||
}
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.match.findMany({
|
||||
where,
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
},
|
||||
orderBy: [{ displayOrder: 'asc' }, { startTime: 'desc' }],
|
||||
skip,
|
||||
take: size,
|
||||
}),
|
||||
this.prisma.match.count({ where }),
|
||||
]);
|
||||
return jsonResponse({ items, total, page: p, pageSize: size });
|
||||
}
|
||||
|
||||
@Get('matches/:id')
|
||||
async getMatch(@Param('id') id: string) {
|
||||
const match = await this.matches.getAdminMatchDetail(BigInt(id));
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Put('matches/:id')
|
||||
async updateMatch(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreatePlatformMatchDto,
|
||||
) {
|
||||
const match = await this.matches.updatePlatformMatch(BigInt(id), {
|
||||
leagueEn: dto.leagueEn,
|
||||
leagueZh: dto.leagueZh,
|
||||
homeTeamEn: dto.homeTeamEn,
|
||||
homeTeamZh: dto.homeTeamZh,
|
||||
awayTeamEn: dto.awayTeamEn,
|
||||
awayTeamZh: dto.awayTeamZh,
|
||||
startTime: new Date(dto.startTime),
|
||||
isHot: dto.isHot,
|
||||
displayOrder: dto.displayOrder,
|
||||
updatedBy: operatorId,
|
||||
});
|
||||
return jsonResponse(matches);
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Delete('matches/:id')
|
||||
async deleteMatch(@Param('id') id: string) {
|
||||
await this.matches.deleteMatch(BigInt(id));
|
||||
return jsonResponse({ deleted: true });
|
||||
}
|
||||
|
||||
@Post('matches')
|
||||
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateMatchDto) {
|
||||
const match = await this.matches.createMatch({
|
||||
leagueId: BigInt(dto.leagueId),
|
||||
homeTeamId: BigInt(dto.homeTeamId),
|
||||
awayTeamId: BigInt(dto.awayTeamId),
|
||||
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreatePlatformMatchDto) {
|
||||
const match = await this.matches.createPlatformMatch({
|
||||
leagueEn: dto.leagueEn,
|
||||
leagueZh: dto.leagueZh,
|
||||
homeTeamEn: dto.homeTeamEn,
|
||||
homeTeamZh: dto.homeTeamZh,
|
||||
awayTeamEn: dto.awayTeamEn,
|
||||
awayTeamZh: dto.awayTeamZh,
|
||||
startTime: new Date(dto.startTime),
|
||||
isHot: dto.isHot,
|
||||
displayOrder: dto.displayOrder,
|
||||
createdBy: operatorId,
|
||||
});
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/import')
|
||||
async importMatches(@CurrentUser('id') operatorId: bigint, @Body() dto: ZhiboMatchesBundleExport) {
|
||||
if (!isZhiboBundlePayload(dto)) {
|
||||
throw new BadRequestException('Invalid import payload: matches[] required');
|
||||
}
|
||||
const result = await this.matches.importZhiboMatchesBundle(dto, operatorId);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('matches/:id/publish')
|
||||
async publishMatch(@Param('id') id: string) {
|
||||
const match = await this.matches.publishMatch(BigInt(id));
|
||||
@@ -343,20 +636,31 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Get('bets')
|
||||
async listBets(@Query('status') status?: string, @Query('page') page?: string) {
|
||||
const skip = ((page ? parseInt(page) : 1) - 1) * 20;
|
||||
const where = status ? { status } : {};
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.bet.findMany({
|
||||
where,
|
||||
include: { selections: true, user: true },
|
||||
orderBy: { placedAt: 'desc' },
|
||||
skip,
|
||||
take: 20,
|
||||
}),
|
||||
this.prisma.bet.count({ where }),
|
||||
]);
|
||||
return jsonResponse({ items, total });
|
||||
async listBets(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('betType') betType?: string,
|
||||
@Query('placedFrom') placedFrom?: string,
|
||||
@Query('placedTo') placedTo?: string,
|
||||
) {
|
||||
const result = await this.bets.listBetsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 10,
|
||||
keyword,
|
||||
status: status || undefined,
|
||||
betType: betType || undefined,
|
||||
placedFrom,
|
||||
placedTo,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('bets/:id')
|
||||
async getBet(@Param('id') id: string) {
|
||||
const detail = await this.bets.getBetAdminDetail(BigInt(id));
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Post('cashbacks/preview')
|
||||
@@ -393,8 +697,16 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Get('audit-logs')
|
||||
async auditLogs(@Query('page') page?: string, @Query('module') module?: string) {
|
||||
const result = await this.audit.list(page ? parseInt(page) : 1, 50, module);
|
||||
async auditLogs(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('module') module?: string,
|
||||
) {
|
||||
const result = await this.audit.list(
|
||||
page ? parseInt(page, 10) : 1,
|
||||
pageSize ? parseInt(pageSize, 10) : 10,
|
||||
module || undefined,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminDashboardService } from './admin-dashboard.service';
|
||||
import { UsersModule } from '../../domains/identity/users.module';
|
||||
import { AgentsModule } from '../../domains/agent/agents.module';
|
||||
import { WalletModule } from '../../domains/ledger/wallet.module';
|
||||
@@ -25,5 +26,6 @@ import { BetsModule } from '../../domains/betting/bets.module';
|
||||
BetsModule,
|
||||
],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminDashboardService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -147,8 +147,14 @@ export class AgentPortalController {
|
||||
}
|
||||
|
||||
@Get('bets')
|
||||
async listBets(@CurrentUser('id') agentId: bigint, @Query('page') page?: string) {
|
||||
const skip = ((page ? parseInt(page) : 1) - 1) * 20;
|
||||
async listBets(
|
||||
@CurrentUser('id') agentId: bigint,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const p = Math.max(1, page ? parseInt(page, 10) : 1);
|
||||
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100);
|
||||
const skip = (p - 1) * size;
|
||||
const descendants = await this.prisma.agentClosure.findMany({
|
||||
where: { ancestorId: agentId },
|
||||
});
|
||||
@@ -160,11 +166,11 @@ export class AgentPortalController {
|
||||
include: { selections: true, user: true },
|
||||
orderBy: { placedAt: 'desc' },
|
||||
skip,
|
||||
take: 20,
|
||||
take: size,
|
||||
}),
|
||||
this.prisma.bet.count({ where: { agentId: { in: agentIds } } }),
|
||||
]);
|
||||
return jsonResponse({ items, total });
|
||||
return jsonResponse({ items, total, page: p, pageSize: size });
|
||||
}
|
||||
|
||||
@Get('reports/summary')
|
||||
|
||||
Reference in New Issue
Block a user