feat: multi-tier agent hierarchy, wallet ledger, and player UX polish
Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
MinLength,
|
||||
IsIn,
|
||||
Min,
|
||||
Max,
|
||||
Equals,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
@@ -257,6 +258,19 @@ class AgentSuspendSettingsDto {
|
||||
suspendBlockPlayerLogin?: boolean;
|
||||
}
|
||||
|
||||
class AgentHierarchySettingsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
maxAgentLevel?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
defaultSubAgentCreditRatio?: number;
|
||||
}
|
||||
|
||||
class ResetDatabaseDto {
|
||||
@IsString()
|
||||
@Equals('RESET')
|
||||
@@ -340,6 +354,16 @@ class UpdateAgentAdminDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
freezeDirectPlayers?: boolean;
|
||||
|
||||
/** 冻结时是否禁止直属玩家登录 */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
blockDirectPlayerLogin?: boolean;
|
||||
|
||||
/** 解冻时是否级联解冻直属玩家 */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
unfreezeDirectPlayers?: boolean;
|
||||
}
|
||||
|
||||
class DepositDto {
|
||||
@@ -945,6 +969,30 @@ export class AdminController {
|
||||
return jsonResponse(settings);
|
||||
}
|
||||
|
||||
@Get('agents/settings/hierarchy')
|
||||
@RequirePermissions(P.settings)
|
||||
async getAgentHierarchySettings() {
|
||||
const settings = await this.systemConfig.getAgentHierarchySettings();
|
||||
return jsonResponse(settings);
|
||||
}
|
||||
|
||||
@Put('agents/settings/hierarchy')
|
||||
@RequirePermissions(P.settings)
|
||||
async updateAgentHierarchySettings(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Body() dto: AgentHierarchySettingsDto,
|
||||
) {
|
||||
const settings = await this.systemConfig.updateAgentHierarchySettings(dto);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'UPDATE_AGENT_HIERARCHY_SETTINGS',
|
||||
module: 'AGENTS',
|
||||
afterData: JSON.stringify(settings),
|
||||
});
|
||||
return jsonResponse(settings);
|
||||
}
|
||||
|
||||
@Get('settings/betting-limits')
|
||||
@RequirePermissions(P.settings)
|
||||
async getBettingLimits() {
|
||||
@@ -1122,6 +1170,13 @@ export class AdminController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get('agents/level-counts')
|
||||
@RequirePermissions(P.agentsView)
|
||||
async getAgentLevelCounts() {
|
||||
const counts = await this.agents.countAgentsByLevel();
|
||||
return jsonResponse(counts);
|
||||
}
|
||||
|
||||
@Get('agents')
|
||||
@RequirePermissions(P.agentsView)
|
||||
async listAgents(
|
||||
@@ -1130,15 +1185,21 @@ export class AdminController {
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('level') level?: string,
|
||||
@Query('minLevel') minLevel?: string,
|
||||
@Query('maxLevel') maxLevel?: string,
|
||||
@Query('parentAgentId') parentAgentId?: string,
|
||||
) {
|
||||
const parsedLevel = level === '2' ? 2 : level === '1' ? 1 : undefined;
|
||||
const parsedLevel = level != null && level !== '' ? parseInt(level, 10) : undefined;
|
||||
const parsedMinLevel = minLevel != null && minLevel !== '' ? parseInt(minLevel, 10) : undefined;
|
||||
const parsedMaxLevel = maxLevel != null && maxLevel !== '' ? parseInt(maxLevel, 10) : undefined;
|
||||
const result = await this.agents.listAgentsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 10,
|
||||
keyword,
|
||||
status,
|
||||
level: parsedLevel,
|
||||
level: Number.isFinite(parsedLevel) ? parsedLevel : undefined,
|
||||
minLevel: Number.isFinite(parsedMinLevel) ? parsedMinLevel : undefined,
|
||||
maxLevel: Number.isFinite(parsedMaxLevel) ? parsedMaxLevel : undefined,
|
||||
parentAgentId: parentAgentId ? BigInt(parentAgentId) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
@@ -1270,9 +1331,33 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Get('wallet/transactions')
|
||||
@RequirePermissions(P.walletDeposit, P.walletWithdraw)
|
||||
async walletTransactions(@Query('userId') userId: string, @Query('page') page?: string) {
|
||||
const result = await this.wallet.getTransactions(BigInt(userId), page ? parseInt(page) : 1);
|
||||
@RequirePermissions(P.walletDeposit, P.walletWithdraw, P.reports)
|
||||
async walletTransactions(
|
||||
@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('typeCategory') typeCategory?: string,
|
||||
@Query('dateFrom') dateFrom?: string,
|
||||
@Query('dateTo') dateTo?: string,
|
||||
) {
|
||||
const result = await this.wallet.listWalletTransactionsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
||||
playerId: playerId ? BigInt(playerId) : undefined,
|
||||
parentAgentId: parentAgentId ? BigInt(parentAgentId) : undefined,
|
||||
parentAgentKeyword,
|
||||
keyword,
|
||||
operatorKeyword,
|
||||
transactionType,
|
||||
typeCategory,
|
||||
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
||||
dateTo: dateTo ? new Date(dateTo) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { appForbidden } from '../../shared/common/app-error';
|
||||
import { AgentsService } from '../../domains/agent/agents.service';
|
||||
import { WalletService } from '../../domains/ledger/wallet.service';
|
||||
import { BetsService } from '../../domains/betting/bets.service';
|
||||
@@ -131,6 +132,14 @@ class UpdateSubAgentDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
freezeDirectPlayers?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
blockDirectPlayerLogin?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
unfreezeDirectPlayers?: boolean;
|
||||
}
|
||||
|
||||
@ApiTags('Agent Portal')
|
||||
@@ -145,10 +154,20 @@ export class AgentPortalController {
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
private async canManageSubAgents(level: number) {
|
||||
const maxLevel = await this.agents.getMaxAgentLevel();
|
||||
return this.agents.canCreateSubAgent(level, maxLevel);
|
||||
}
|
||||
|
||||
@Get('profile')
|
||||
async profile(@CurrentUser('id') agentId: bigint) {
|
||||
async profile(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) {
|
||||
const profile = await this.agents.getProfile(agentId);
|
||||
return jsonResponse(profile);
|
||||
const maxLevel = await this.agents.getMaxAgentLevel();
|
||||
return jsonResponse({
|
||||
...profile,
|
||||
maxAgentLevel: maxLevel,
|
||||
canManageSubAgents: this.agents.canCreateSubAgent(level, maxLevel),
|
||||
});
|
||||
}
|
||||
|
||||
@Get('players')
|
||||
@@ -218,7 +237,8 @@ export class AgentPortalController {
|
||||
|
||||
@Get('agents')
|
||||
async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) {
|
||||
if (level !== 1) {
|
||||
const maxLevel = await this.agents.getMaxAgentLevel();
|
||||
if (!this.agents.canCreateSubAgent(level, maxLevel)) {
|
||||
return jsonResponse([]);
|
||||
}
|
||||
const agents = await this.agents.listChildAgentsSummary(agentId);
|
||||
@@ -227,13 +247,14 @@ export class AgentPortalController {
|
||||
|
||||
@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 maxLevel = await this.agents.getMaxAgentLevel();
|
||||
if (!this.agents.canCreateSubAgent(level, maxLevel)) {
|
||||
throw appForbidden('AGENT_MAX_LEVEL_REACHED');
|
||||
}
|
||||
const user = await this.agents.createAgent(agentId, {
|
||||
username: dto.username,
|
||||
password: dto.password,
|
||||
level: 2,
|
||||
level: level + 1,
|
||||
parentAgentId: agentId,
|
||||
creditLimit: dto.creditLimit,
|
||||
cashbackRate: dto.cashbackRate,
|
||||
@@ -281,8 +302,8 @@ export class AgentPortalController {
|
||||
@CurrentUser('agentLevel') level: number,
|
||||
@Param('id') subAgentId: string,
|
||||
) {
|
||||
if (level !== 1) {
|
||||
return jsonResponse(null, 'Only level 1 agents can manage sub-agents');
|
||||
if (!(await this.canManageSubAgents(level))) {
|
||||
throw appForbidden('AGENT_MAX_LEVEL_REACHED');
|
||||
}
|
||||
const detail = await this.agents.getSubAgentForParent(agentId, BigInt(subAgentId));
|
||||
return jsonResponse(detail);
|
||||
@@ -295,8 +316,8 @@ export class AgentPortalController {
|
||||
@Param('id') subAgentId: string,
|
||||
@Body() dto: UpdateSubAgentDto,
|
||||
) {
|
||||
if (level !== 1) {
|
||||
return jsonResponse(null, 'Only level 1 agents can manage sub-agents');
|
||||
if (!(await this.canManageSubAgents(level))) {
|
||||
throw appForbidden('AGENT_MAX_LEVEL_REACHED');
|
||||
}
|
||||
const detail = await this.agents.updateSubAgentForParent(agentId, BigInt(subAgentId), dto);
|
||||
return jsonResponse(detail);
|
||||
@@ -308,7 +329,7 @@ export class AgentPortalController {
|
||||
@CurrentUser('agentLevel') level: number,
|
||||
@Param('id') subAgentId: string,
|
||||
) {
|
||||
if (level !== 1) {
|
||||
if (!(await this.canManageSubAgents(level))) {
|
||||
return jsonResponse([]);
|
||||
}
|
||||
await this.agents.assertDirectChildAgent(agentId, BigInt(subAgentId));
|
||||
@@ -448,4 +469,56 @@ export class AgentPortalController {
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('wallet/ledger-transactions')
|
||||
async walletLedgerTransactions(
|
||||
@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('typeCategory') typeCategory?: 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.listWalletTransactionsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
||||
playerId: playerId ? BigInt(playerId) : undefined,
|
||||
parentAgentId: parsedParentAgentId,
|
||||
parentAgentKeyword,
|
||||
scopedParentAgentIds,
|
||||
keyword,
|
||||
operatorKeyword,
|
||||
transactionType,
|
||||
typeCategory,
|
||||
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
||||
dateTo: dateTo ? new Date(dateTo) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user