Admin: add match/player overview sub-nav; refine settlement flow and league match management UI; improve action button enabled/disabled styles; enhance logo upload and outright odds sync. API: expose matchPhase/bettingOpen for closed matches; league publish guards; settlement preview with auto score save; outright team auto-sync. Player: watermark for closed/settled states; keep match and bet details visible; remove default login credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
452 lines
12 KiB
TypeScript
452 lines
12 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Put,
|
|
Body,
|
|
Param,
|
|
Query,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { JwtAuthGuard, AgentGuard } from '../../domains/identity/guards';
|
|
import { CurrentUser } from '../../shared/common/decorators';
|
|
import { jsonResponse } from '../../shared/common/filters';
|
|
import { AgentsService } from '../../domains/agent/agents.service';
|
|
import { WalletService } from '../../domains/ledger/wallet.service';
|
|
import { BetsService } from '../../domains/betting/bets.service';
|
|
import { PrismaService } from '../../shared/prisma/prisma.service';
|
|
import { IsString, IsNumber, MinLength, IsOptional, Min, IsBoolean } from 'class-validator';
|
|
|
|
class CreatePlayerDto {
|
|
@IsString()
|
|
username!: string;
|
|
|
|
@IsString()
|
|
@MinLength(8)
|
|
password!: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
phone?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
email?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
locale?: string;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
initialDeposit?: number;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
remark?: string;
|
|
}
|
|
|
|
class CreateSubAgentDto extends CreatePlayerDto {
|
|
@IsOptional()
|
|
@IsNumber()
|
|
creditLimit?: number;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
cashbackRate?: number;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
maxSingleDeposit?: number;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
maxDailyDeposit?: number;
|
|
}
|
|
|
|
class UpdatePlayerDto {
|
|
@IsOptional()
|
|
@IsString()
|
|
username?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(8)
|
|
password?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
phone?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
email?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
status?: string;
|
|
}
|
|
|
|
class TransferDto {
|
|
@IsNumber()
|
|
amount!: number;
|
|
|
|
@IsString()
|
|
requestId!: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
remark?: string;
|
|
}
|
|
|
|
class CreditDto extends TransferDto {}
|
|
|
|
class UpdateSubAgentDto {
|
|
@IsOptional()
|
|
@IsString()
|
|
username?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(8)
|
|
password?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
phone?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
email?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
status?: string;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
freezeDirectPlayers?: boolean;
|
|
}
|
|
|
|
@ApiTags('Agent Portal')
|
|
@Controller('agent')
|
|
@UseGuards(JwtAuthGuard, AgentGuard)
|
|
@ApiBearerAuth()
|
|
export class AgentPortalController {
|
|
constructor(
|
|
private agents: AgentsService,
|
|
private wallet: WalletService,
|
|
private bets: BetsService,
|
|
private prisma: PrismaService,
|
|
) {}
|
|
|
|
@Get('profile')
|
|
async profile(@CurrentUser('id') agentId: bigint) {
|
|
const profile = await this.agents.getProfile(agentId);
|
|
return jsonResponse(profile);
|
|
}
|
|
|
|
@Get('players')
|
|
async listPlayers(@CurrentUser('id') agentId: bigint) {
|
|
const players = await this.agents.getDirectPlayers(agentId);
|
|
return jsonResponse(players);
|
|
}
|
|
|
|
@Post('players')
|
|
async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) {
|
|
const user = await this.agents.createPlayer(agentId, {
|
|
username: dto.username,
|
|
password: dto.password,
|
|
parentId: agentId,
|
|
locale: dto.locale,
|
|
phone: dto.phone,
|
|
email: dto.email,
|
|
});
|
|
|
|
if (dto.initialDeposit != null && dto.initialDeposit > 0) {
|
|
await this.agents.depositToPlayer(
|
|
agentId,
|
|
user.id,
|
|
dto.initialDeposit,
|
|
`agent-create-${user.id}-${Date.now()}`,
|
|
dto.remark ?? '开户初始余额',
|
|
);
|
|
}
|
|
|
|
const wallet = await this.prisma.wallet.findUnique({ where: { userId: user.id } });
|
|
|
|
return jsonResponse({
|
|
id: user.id,
|
|
username: user.username,
|
|
status: user.status,
|
|
createdAt: user.createdAt,
|
|
availableBalance: wallet?.availableBalance ?? 0,
|
|
});
|
|
}
|
|
|
|
@Get('players/:id')
|
|
async getPlayer(@CurrentUser('id') agentId: bigint, @Param('id') playerId: string) {
|
|
const detail = await this.agents.getDirectPlayerDetail(agentId, BigInt(playerId));
|
|
return jsonResponse(detail);
|
|
}
|
|
|
|
@Get('players/:id/transfer-context')
|
|
async getPlayerTransferContext(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Param('id') playerId: string,
|
|
) {
|
|
const ctx = await this.agents.getPlayerTransferContext(BigInt(playerId), {
|
|
actingAgentId: agentId,
|
|
});
|
|
return jsonResponse(ctx);
|
|
}
|
|
|
|
@Put('players/:id')
|
|
async updatePlayer(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Param('id') playerId: string,
|
|
@Body() dto: UpdatePlayerDto,
|
|
) {
|
|
const detail = await this.agents.updateDirectPlayer(agentId, BigInt(playerId), dto);
|
|
return jsonResponse(detail);
|
|
}
|
|
|
|
@Get('agents')
|
|
async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) {
|
|
if (level !== 1) {
|
|
return jsonResponse([]);
|
|
}
|
|
const agents = await this.agents.listChildAgentsSummary(agentId);
|
|
return jsonResponse(agents);
|
|
}
|
|
|
|
@Post('agents')
|
|
async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) {
|
|
if (level !== 1) {
|
|
return jsonResponse(null, 'Only level 1 agents can create sub-agents');
|
|
}
|
|
const user = await this.agents.createAgent(agentId, {
|
|
username: dto.username,
|
|
password: dto.password,
|
|
level: 2,
|
|
parentAgentId: agentId,
|
|
creditLimit: dto.creditLimit,
|
|
cashbackRate: dto.cashbackRate,
|
|
maxSingleDeposit: dto.maxSingleDeposit,
|
|
maxDailyDeposit: dto.maxDailyDeposit,
|
|
});
|
|
return jsonResponse(user);
|
|
}
|
|
|
|
@Post('players/:id/deposit')
|
|
async depositToPlayer(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Param('id') playerId: string,
|
|
@Body() dto: TransferDto,
|
|
) {
|
|
const result = await this.agents.depositToPlayer(
|
|
agentId,
|
|
BigInt(playerId),
|
|
dto.amount,
|
|
dto.requestId,
|
|
dto.remark,
|
|
);
|
|
return jsonResponse(result);
|
|
}
|
|
|
|
@Post('players/:id/withdraw')
|
|
async withdrawFromPlayer(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Param('id') playerId: string,
|
|
@Body() dto: TransferDto,
|
|
) {
|
|
const result = await this.agents.withdrawFromPlayer(
|
|
agentId,
|
|
BigInt(playerId),
|
|
dto.amount,
|
|
dto.requestId,
|
|
dto.remark,
|
|
);
|
|
return jsonResponse(result);
|
|
}
|
|
|
|
@Get('agents/:id')
|
|
async getSubAgent(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@CurrentUser('agentLevel') level: number,
|
|
@Param('id') subAgentId: string,
|
|
) {
|
|
if (level !== 1) {
|
|
return jsonResponse(null, 'Only level 1 agents can manage sub-agents');
|
|
}
|
|
const detail = await this.agents.getSubAgentForParent(agentId, BigInt(subAgentId));
|
|
return jsonResponse(detail);
|
|
}
|
|
|
|
@Put('agents/:id')
|
|
async updateSubAgent(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@CurrentUser('agentLevel') level: number,
|
|
@Param('id') subAgentId: string,
|
|
@Body() dto: UpdateSubAgentDto,
|
|
) {
|
|
if (level !== 1) {
|
|
return jsonResponse(null, 'Only level 1 agents can manage sub-agents');
|
|
}
|
|
const detail = await this.agents.updateSubAgentForParent(agentId, BigInt(subAgentId), dto);
|
|
return jsonResponse(detail);
|
|
}
|
|
|
|
@Get('agents/:id/players')
|
|
async listSubAgentPlayers(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@CurrentUser('agentLevel') level: number,
|
|
@Param('id') subAgentId: string,
|
|
) {
|
|
if (level !== 1) {
|
|
return jsonResponse([]);
|
|
}
|
|
await this.agents.assertDirectChildAgent(agentId, BigInt(subAgentId));
|
|
const players = await this.agents.getDirectPlayers(BigInt(subAgentId));
|
|
return jsonResponse(players);
|
|
}
|
|
|
|
@Post('agents/:id/credit')
|
|
async allocateCredit(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Param('id') subAgentId: string,
|
|
@Body() dto: CreditDto,
|
|
) {
|
|
const subAgent = await this.prisma.agentProfile.findUnique({
|
|
where: { userId: BigInt(subAgentId) },
|
|
});
|
|
if (!subAgent || subAgent.parentAgentId !== agentId) {
|
|
return jsonResponse(null, 'Not your sub-agent');
|
|
}
|
|
const result = await this.agents.adjustCredit(
|
|
BigInt(subAgentId),
|
|
dto.amount,
|
|
agentId,
|
|
dto.requestId,
|
|
dto.remark,
|
|
);
|
|
return jsonResponse(result);
|
|
}
|
|
|
|
@Get('bets')
|
|
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 agentIds = await this.agents.getSubtreeAgentIds(agentId);
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.bet.findMany({
|
|
where: { agentId: { in: agentIds } },
|
|
include: { selections: true, user: true },
|
|
orderBy: { placedAt: 'desc' },
|
|
skip,
|
|
take: size,
|
|
}),
|
|
this.prisma.bet.count({ where: { agentId: { in: agentIds } } }),
|
|
]);
|
|
return jsonResponse({ items, total, page: p, pageSize: size });
|
|
}
|
|
|
|
@Get('reports/summary')
|
|
async reportSummary(@CurrentUser('id') agentId: bigint) {
|
|
const summary = await this.agents.getReportSummary(agentId);
|
|
return jsonResponse(summary);
|
|
}
|
|
|
|
@Get('credit-transactions')
|
|
async listCreditTransactions(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Query('page') page?: string,
|
|
@Query('pageSize') pageSize?: string,
|
|
@Query('agentId') filterAgentId?: string,
|
|
@Query('keyword') keyword?: string,
|
|
@Query('operatorKeyword') operatorKeyword?: string,
|
|
@Query('transactionType') transactionType?: string,
|
|
@Query('dateFrom') dateFrom?: string,
|
|
@Query('dateTo') dateTo?: string,
|
|
) {
|
|
const scopedAgentIds = await this.agents.getSubtreeAgentIds(agentId);
|
|
const parsedAgentId = filterAgentId ? BigInt(filterAgentId) : undefined;
|
|
if (parsedAgentId && !scopedAgentIds.some((id) => id === parsedAgentId)) {
|
|
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
}
|
|
const result = await this.agents.listCreditTransactions({
|
|
page: page ? parseInt(page, 10) : 1,
|
|
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
|
agentId: parsedAgentId,
|
|
keyword,
|
|
operatorKeyword,
|
|
transactionType,
|
|
scopedAgentIds,
|
|
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
|
dateTo: dateTo ? new Date(dateTo) : undefined,
|
|
});
|
|
return jsonResponse(result);
|
|
}
|
|
|
|
@Get('wallet-transactions')
|
|
async walletTransactions(
|
|
@CurrentUser('id') agentId: bigint,
|
|
@Query('page') page?: string,
|
|
@Query('pageSize') pageSize?: string,
|
|
@Query('playerId') playerId?: string,
|
|
@Query('parentAgentId') parentAgentId?: string,
|
|
@Query('parentAgentKeyword') parentAgentKeyword?: string,
|
|
@Query('keyword') keyword?: string,
|
|
@Query('operatorKeyword') operatorKeyword?: string,
|
|
@Query('transactionType') transactionType?: string,
|
|
@Query('dateFrom') dateFrom?: string,
|
|
@Query('dateTo') dateTo?: string,
|
|
) {
|
|
const scopedParentAgentIds = await this.agents.getSubtreeAgentIds(agentId);
|
|
const parsedParentAgentId = parentAgentId ? BigInt(parentAgentId) : undefined;
|
|
if (
|
|
parsedParentAgentId &&
|
|
!scopedParentAgentIds.some((id) => id === parsedParentAgentId)
|
|
) {
|
|
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
}
|
|
if (playerId) {
|
|
const player = await this.prisma.user.findFirst({
|
|
where: { id: BigInt(playerId), userType: 'PLAYER', deletedAt: null },
|
|
select: { id: true, parentId: true },
|
|
});
|
|
if (
|
|
!player?.parentId ||
|
|
!scopedParentAgentIds.some((id) => id === player.parentId)
|
|
) {
|
|
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
}
|
|
}
|
|
const result = await this.wallet.listTransferTransactions({
|
|
page: page ? parseInt(page, 10) : 1,
|
|
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
|
playerId: playerId ? BigInt(playerId) : undefined,
|
|
parentAgentId: parsedParentAgentId,
|
|
parentAgentKeyword,
|
|
scopedParentAgentIds,
|
|
keyword,
|
|
operatorKeyword,
|
|
transactionType,
|
|
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
|
dateTo: dateTo ? new Date(dateTo) : undefined,
|
|
});
|
|
return jsonResponse(result);
|
|
}
|
|
}
|