Files
thebet365/apps/api/src/applications/agent/agent-portal.controller.ts
Mars 03f54ca689 feat: split admin dashboard, improve match ops, and player closed-match UX
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>
2026-06-10 13:00:14 +08:00

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