feat(admin,api,player): 代理层级管理、额度上下分与玩家钱包详情

新增代理管理器与二级代理体系,完善信用额度/上下分上下文与冻结策略;代理端玩家与子代理管理增强;玩家端新增钱包详情页与交易筛选优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 15:34:12 +08:00
parent b2216abd0c
commit 414998ce36
54 changed files with 6641 additions and 481 deletions

View File

@@ -101,6 +101,15 @@ class CreatePlayerAdminDto {
@IsOptional()
asTier1Agent?: boolean;
/** 创建为二级代理(需要 parentAgentId */
@IsOptional()
asSubAgent?: boolean;
/** 二级代理的上级代理 ID */
@IsOptional()
@IsString()
parentAgentId?: string;
@IsOptional()
@IsNumber()
@Min(0)
@@ -110,6 +119,16 @@ class CreatePlayerAdminDto {
@IsNumber()
@Min(0)
cashbackRate?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxSingleDeposit?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxDailyDeposit?: number;
}
class UpdatePlayerAdminDto {
@@ -154,6 +173,16 @@ class PlayerAccountSettingsDto {
allowUsernameChange?: boolean;
}
class AgentSuspendSettingsDto {
@IsOptional()
@IsBoolean()
suspendFreezeDirectPlayers?: boolean;
@IsOptional()
@IsBoolean()
suspendBlockPlayerLogin?: boolean;
}
class ResetDatabaseDto {
@IsString()
@Equals('RESET')
@@ -181,6 +210,16 @@ class CreateAgentAdminDto {
@IsNumber()
@Min(0)
cashbackRate?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxSingleDeposit?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxDailyDeposit?: number;
}
class UpdateAgentAdminDto {
@@ -204,6 +243,29 @@ class UpdateAgentAdminDto {
@IsNumber()
@Min(0)
cashbackRate?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxSingleDeposit?: number;
@IsOptional()
@IsNumber()
@Min(0)
maxDailyDeposit?: number;
@IsOptional()
@IsString()
username?: string;
@IsOptional()
@IsString()
password?: string;
/** 冻结时是否级联冻结直属玩家 */
@IsOptional()
@IsBoolean()
freezeDirectPlayers?: boolean;
}
class DepositDto {
@@ -717,6 +779,30 @@ export class AdminController {
return jsonResponse(settings);
}
@Get('agents/settings/suspend')
@RequirePermissions(P.settings)
async getAgentSuspendSettings() {
const settings = await this.systemConfig.getAgentSuspendSettings();
return jsonResponse(settings);
}
@Put('agents/settings/suspend')
@RequirePermissions(P.settings)
async updateAgentSuspendSettings(
@CurrentUser('id') operatorId: bigint,
@Body() dto: AgentSuspendSettingsDto,
) {
const settings = await this.systemConfig.updateAgentSuspendSettings(dto);
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'UPDATE_AGENT_SUSPEND_SETTINGS',
module: 'AGENTS',
afterData: JSON.stringify(settings),
});
return jsonResponse(settings);
}
@Get('settings/betting-limits')
@RequirePermissions(P.settings)
async getBettingLimits() {
@@ -830,17 +916,21 @@ export class AdminController {
depositRemark: dto.remark,
depositRequestId: `create-player-${dto.username}-${Date.now()}`,
asTier1Agent: dto.asTier1Agent,
asSubAgent: dto.asSubAgent,
parentAgentId: dto.parentAgentId ? BigInt(dto.parentAgentId) : undefined,
creditLimit: dto.creditLimit,
cashbackRate: dto.cashbackRate,
maxSingleDeposit: dto.maxSingleDeposit,
maxDailyDeposit: dto.maxDailyDeposit,
});
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: dto.asTier1Agent ? 'CREATE_AGENT' : 'CREATE_PLAYER',
module: dto.asTier1Agent ? 'AGENTS' : 'USERS',
action: dto.asTier1Agent || dto.asSubAgent ? 'CREATE_AGENT' : 'CREATE_PLAYER',
module: dto.asTier1Agent || dto.asSubAgent ? 'AGENTS' : 'USERS',
targetId: user.id.toString(),
});
if (dto.asTier1Agent) {
if (dto.asTier1Agent || dto.asSubAgent) {
const detail = await this.agents.getAgentAdminDetail(user.id);
return jsonResponse(detail);
}
@@ -884,11 +974,13 @@ export class AdminController {
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('keyword') keyword?: string,
@Query('parentAgentId') parentAgentId?: string,
) {
const result = await this.agents.listAgentsAdmin({
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 10,
keyword,
parentAgentId: parentAgentId ? BigInt(parentAgentId) : undefined,
});
return jsonResponse(result);
}
@@ -929,6 +1021,8 @@ export class AdminController {
phone: dto.phone,
email: dto.email,
cashbackRate: dto.cashbackRate,
maxSingleDeposit: dto.maxSingleDeposit,
maxDailyDeposit: dto.maxDailyDeposit,
});
await this.audit.log({
operatorId,
@@ -961,7 +1055,7 @@ export class AdminController {
@Post('wallet/deposit')
@RequirePermissions(P.walletDeposit)
async deposit(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) {
const result = await this.wallet.deposit(
const result = await this.agents.adminDepositToPlayer(
BigInt(dto.userId),
dto.amount,
operatorId,
@@ -971,6 +1065,13 @@ export class AdminController {
return jsonResponse(result);
}
@Get('wallet/transfer-context/:userId')
@RequirePermissions(P.walletDeposit, P.walletWithdraw)
async walletTransferContext(@Param('userId') userId: string) {
const ctx = await this.agents.getPlayerTransferContext(BigInt(userId), { forAdmin: true });
return jsonResponse(ctx);
}
@Post('wallet/withdraw')
@RequirePermissions(P.walletWithdraw)
async withdraw(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) {

View File

@@ -2,6 +2,7 @@ import {
Controller,
Get,
Post,
Put,
Body,
Param,
Query,
@@ -15,7 +16,7 @@ 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 } from 'class-validator';
import { IsString, IsNumber, MinLength, IsOptional, Min, IsBoolean } from 'class-validator';
class CreatePlayerDto {
@IsString()
@@ -24,12 +25,71 @@ class CreatePlayerDto {
@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 {
@@ -46,6 +106,33 @@ class CreditDto extends TransferDto {
remark?: string;
}
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)
@@ -76,8 +163,57 @@ export class AgentPortalController {
username: dto.username,
password: dto.password,
parentId: agentId,
locale: dto.locale,
phone: dto.phone,
email: dto.email,
});
return jsonResponse(user);
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')
@@ -85,7 +221,7 @@ export class AgentPortalController {
if (level !== 1) {
return jsonResponse([]);
}
const agents = await this.agents.getChildAgents(agentId);
const agents = await this.agents.listChildAgentsSummary(agentId);
return jsonResponse(agents);
}
@@ -100,6 +236,9 @@ export class AgentPortalController {
level: 2,
parentAgentId: agentId,
creditLimit: dto.creditLimit,
cashbackRate: dto.cashbackRate,
maxSingleDeposit: dto.maxSingleDeposit,
maxDailyDeposit: dto.maxDailyDeposit,
});
return jsonResponse(user);
}
@@ -124,6 +263,47 @@ export class AgentPortalController {
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,

View File

@@ -250,8 +250,9 @@ export class PlayerController {
async transactions(
@CurrentUser('id') userId: bigint,
@Query('page') page?: string,
@Query('type') type?: string,
) {
const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1);
const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1, 20, type);
return jsonResponse(result);
}