feat(admin,api,player): 代理层级管理、额度上下分与玩家钱包详情
新增代理管理器与二级代理体系,完善信用额度/上下分上下文与冻结策略;代理端玩家与子代理管理增强;玩家端新增钱包详情页与交易筛选优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user