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:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "agent_profiles" ADD COLUMN "block_direct_player_login" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -131,6 +131,7 @@ model AgentProfile {
|
||||
maxSingleDeposit Decimal? @map("max_single_deposit") @db.Decimal(18, 4)
|
||||
maxDailyDeposit Decimal? @map("max_daily_deposit") @db.Decimal(18, 4)
|
||||
cashbackRate Decimal @default(0) @map("cashback_rate") @db.Decimal(8, 4)
|
||||
blockDirectPlayerLogin Boolean @default(false) @map("block_direct_player_login")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,96 @@ export class AgentsService {
|
||||
private systemConfig: SystemConfigService,
|
||||
) {}
|
||||
|
||||
async getMaxAgentLevel(): Promise<number> {
|
||||
const settings = await this.systemConfig.getAgentHierarchySettings();
|
||||
return settings.maxAgentLevel;
|
||||
}
|
||||
|
||||
canCreateSubAgent(agentLevel: number, maxLevel: number): boolean {
|
||||
if (maxLevel === 0) return true;
|
||||
return agentLevel < maxLevel;
|
||||
}
|
||||
|
||||
private async buildAgentAncestorChainMap(parentAgentIds: (bigint | null | undefined)[]) {
|
||||
const cache = new Map<string, { username: string; parentAgentId: bigint | null }>();
|
||||
const pending = new Set<bigint>();
|
||||
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) pending.add(id);
|
||||
}
|
||||
|
||||
while (pending.size > 0) {
|
||||
const batch = [...pending];
|
||||
pending.clear();
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: batch } },
|
||||
select: {
|
||||
userId: true,
|
||||
parentAgentId: true,
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
});
|
||||
for (const profile of profiles) {
|
||||
cache.set(profile.userId.toString(), {
|
||||
username: profile.user.username,
|
||||
parentAgentId: profile.parentAgentId,
|
||||
});
|
||||
if (profile.parentAgentId && !cache.has(profile.parentAgentId.toString())) {
|
||||
pending.add(profile.parentAgentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const build = (startId: bigint | null | undefined): string[] => {
|
||||
const chain: string[] = [];
|
||||
let cur = startId ?? null;
|
||||
while (cur) {
|
||||
const hit = cache.get(cur.toString());
|
||||
if (!hit) break;
|
||||
chain.unshift(hit.username);
|
||||
cur = hit.parentAgentId;
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
|
||||
const map = new Map<string, string[]>();
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) map.set(id.toString(), build(id));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private async validateAgentLevel(level: number, parentAgentId?: bigint) {
|
||||
if (!Number.isInteger(level) || level < 1) {
|
||||
throw appBadRequest('AGENT_LEVEL_INVALID');
|
||||
}
|
||||
|
||||
const maxLevel = await this.getMaxAgentLevel();
|
||||
if (maxLevel > 0 && level > maxLevel) {
|
||||
throw appBadRequest('AGENT_MAX_LEVEL_REACHED');
|
||||
}
|
||||
|
||||
if (level === 1) {
|
||||
if (parentAgentId) throw appBadRequest('AGENT_LEVEL_ROOT_INVALID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parentAgentId) {
|
||||
throw appBadRequest('LEVEL2_REQUIRES_PARENT');
|
||||
}
|
||||
|
||||
const parent = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: parentAgentId },
|
||||
});
|
||||
if (!parent) throw appBadRequest('PARENT_AGENT_NOT_FOUND');
|
||||
if (parent.level !== level - 1) {
|
||||
throw appBadRequest('AGENT_PARENT_LEVEL_MISMATCH');
|
||||
}
|
||||
if (maxLevel > 0 && !this.canCreateSubAgent(parent.level, maxLevel)) {
|
||||
throw appBadRequest('AGENT_MAX_LEVEL_REACHED');
|
||||
}
|
||||
}
|
||||
|
||||
async getProfile(agentId: bigint) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: agentId },
|
||||
@@ -560,7 +650,9 @@ export class AgentsService {
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
level?: 1 | 2;
|
||||
level?: number;
|
||||
minLevel?: number;
|
||||
maxLevel?: number;
|
||||
parentAgentId?: bigint;
|
||||
}) {
|
||||
const page = Math.max(1, params?.page ?? 1);
|
||||
@@ -568,10 +660,13 @@ export class AgentsService {
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: Prisma.AgentProfileWhereInput = {};
|
||||
if (params?.level === 2) {
|
||||
where.level = 2;
|
||||
} else if (params?.level === 1) {
|
||||
where.level = 1;
|
||||
if (params?.level != null) {
|
||||
where.level = params.level;
|
||||
} else if (params?.minLevel != null || params?.maxLevel != null) {
|
||||
const levelFilter: { gte?: number; lte?: number } = {};
|
||||
if (params.minLevel != null) levelFilter.gte = params.minLevel;
|
||||
if (params.maxLevel != null) levelFilter.lte = params.maxLevel;
|
||||
where.level = levelFilter;
|
||||
} else if (params?.parentAgentId !== undefined) {
|
||||
where.parentAgentId = params.parentAgentId;
|
||||
} else {
|
||||
@@ -644,9 +739,15 @@ export class AgentsService {
|
||||
})
|
||||
: [];
|
||||
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
|
||||
const parentChainMap = await this.buildAgentAncestorChainMap(
|
||||
profiles.map((p) => p.parentAgentId),
|
||||
);
|
||||
|
||||
const items = profiles.map((p) => {
|
||||
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
|
||||
const parentChain = p.parentAgentId
|
||||
? (parentChainMap.get(p.parentAgentId.toString()) ?? [])
|
||||
: [];
|
||||
return {
|
||||
id: p.id.toString(),
|
||||
userId: p.userId.toString(),
|
||||
@@ -658,6 +759,8 @@ export class AgentsService {
|
||||
parentUsername: p.parentAgentId
|
||||
? parentUsernameMap.get(p.parentAgentId.toString()) ?? null
|
||||
: null,
|
||||
parentChain,
|
||||
parentChainLabel: parentChain.length ? parentChain.join(' / ') : null,
|
||||
creditLimit: p.creditLimit.toString(),
|
||||
usedCredit: p.usedCredit.toString(),
|
||||
availableCredit: available.toString(),
|
||||
@@ -679,6 +782,19 @@ export class AgentsService {
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async countAgentsByLevel(): Promise<Record<number, number>> {
|
||||
const groups = await this.prisma.agentProfile.groupBy({
|
||||
by: ['level'],
|
||||
where: { user: { deletedAt: null } },
|
||||
_count: { _all: true },
|
||||
});
|
||||
const out: Record<number, number> = {};
|
||||
for (const g of groups) {
|
||||
out[g.level] = g._count._all;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async getAgentAdminDetail(agentId: bigint) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: agentId },
|
||||
@@ -902,6 +1018,8 @@ export class AgentsService {
|
||||
username?: string;
|
||||
password?: string;
|
||||
freezeDirectPlayers?: boolean;
|
||||
blockDirectPlayerLogin?: boolean;
|
||||
unfreezeDirectPlayers?: boolean;
|
||||
},
|
||||
) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
@@ -945,8 +1063,15 @@ export class AgentsService {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle status change (with optional cascade freeze)
|
||||
// Handle status change (per-action cascade freeze / login block)
|
||||
if (data.status) {
|
||||
const profilePatch: Prisma.AgentProfileUpdateInput = { status: data.status };
|
||||
if (data.status === 'SUSPENDED') {
|
||||
profilePatch.blockDirectPlayerLogin = data.blockDirectPlayerLogin === true;
|
||||
} else if (data.status === 'ACTIVE') {
|
||||
profilePatch.blockDirectPlayerLogin = false;
|
||||
}
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.user.update({
|
||||
where: { id: agentId },
|
||||
@@ -954,22 +1079,23 @@ export class AgentsService {
|
||||
}),
|
||||
this.prisma.agentProfile.update({
|
||||
where: { userId: agentId },
|
||||
data: { status: data.status },
|
||||
data: profilePatch,
|
||||
}),
|
||||
]);
|
||||
|
||||
// 级联冻结:需后台开启且管理员/操作方显式勾选(MVP 默认不冻结玩家)
|
||||
const suspendSettings = await this.systemConfig.getAgentSuspendSettings();
|
||||
if (
|
||||
data.status === 'SUSPENDED' &&
|
||||
data.freezeDirectPlayers &&
|
||||
suspendSettings.suspendFreezeDirectPlayers
|
||||
) {
|
||||
if (data.status === 'SUSPENDED' && data.freezeDirectPlayers) {
|
||||
await this.prisma.user.updateMany({
|
||||
where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
|
||||
data: { status: 'SUSPENDED' },
|
||||
});
|
||||
}
|
||||
|
||||
if (data.status === 'ACTIVE' && data.unfreezeDirectPlayers) {
|
||||
await this.prisma.user.updateMany({
|
||||
where: { parentId: agentId, userType: 'PLAYER', deletedAt: null, status: 'SUSPENDED' },
|
||||
data: { status: 'ACTIVE' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.locale) {
|
||||
@@ -1167,12 +1293,7 @@ export class AgentsService {
|
||||
maxDailyDeposit?: number | null;
|
||||
},
|
||||
) {
|
||||
if (data.level !== 1 && data.level !== 2) {
|
||||
throw appBadRequest('AGENT_LEVEL_INVALID');
|
||||
}
|
||||
if (data.level === 2 && !data.parentAgentId) {
|
||||
throw appBadRequest('LEVEL2_REQUIRES_PARENT');
|
||||
}
|
||||
await this.validateAgentLevel(data.level, data.parentAgentId);
|
||||
|
||||
if (data.parentAgentId) {
|
||||
await this.assertChildAgentWithinParent(data.parentAgentId, {
|
||||
@@ -1300,10 +1421,17 @@ export class AgentsService {
|
||||
throw appBadRequest('PROMOTE_USE_CREDIT_NOT_BALANCE');
|
||||
}
|
||||
const parentAgentId = data.parentAgentId ?? data.parentId;
|
||||
if (parentAgentId == null) {
|
||||
throw appBadRequest('TIER2_REQUIRES_PARENT_AGENT');
|
||||
}
|
||||
const parentProfile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: parentAgentId },
|
||||
});
|
||||
if (!parentProfile) throw appBadRequest('PARENT_AGENT_NOT_FOUND');
|
||||
return this.createAgent(operatorId, {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
level: 2,
|
||||
level: parentProfile.level + 1,
|
||||
parentAgentId,
|
||||
creditLimit: data.creditLimit ?? 0,
|
||||
cashbackRate: data.cashbackRate ?? 0,
|
||||
@@ -1470,11 +1598,12 @@ export class AgentsService {
|
||||
email?: string;
|
||||
status?: string;
|
||||
freezeDirectPlayers?: boolean;
|
||||
blockDirectPlayerLogin?: boolean;
|
||||
unfreezeDirectPlayers?: boolean;
|
||||
},
|
||||
) {
|
||||
await this.assertDirectChildAgent(parentAgentId, subAgentId);
|
||||
const { freezeDirectPlayers: _ignored, ...safeData } = data;
|
||||
return this.updateAgentAdmin(subAgentId, safeData);
|
||||
return this.updateAgentAdmin(subAgentId, data);
|
||||
}
|
||||
|
||||
async getSubtreeAgentIds(agentId: bigint) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { LoginDto, ChangePasswordDto } from './auth.dto';
|
||||
import { Public, CurrentUser } from '../../shared/common/decorators';
|
||||
import { JwtAuthGuard } from './guards';
|
||||
@@ -9,7 +10,10 @@ import { jsonResponse } from '../../shared/common/filters';
|
||||
@ApiTags('Auth')
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private auth: AuthService) {}
|
||||
constructor(
|
||||
private auth: AuthService,
|
||||
private systemConfig: SystemConfigService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('player/auth/login')
|
||||
@@ -50,13 +54,25 @@ export class AuthController {
|
||||
@CurrentUser('role') role: string | undefined,
|
||||
@CurrentUser('agentLevel') agentLevel: number | null | undefined,
|
||||
) {
|
||||
const level = userType === 'AGENT' ? agentLevel ?? null : null;
|
||||
let maxAgentLevel: number | null = null;
|
||||
let canManageSubAgents = false;
|
||||
if (userType === 'AGENT' && level != null && level > 0) {
|
||||
const hierarchy = await this.systemConfig.getAgentHierarchySettings();
|
||||
maxAgentLevel = hierarchy.maxAgentLevel;
|
||||
canManageSubAgents =
|
||||
maxAgentLevel === 0 || level < maxAgentLevel;
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
id: userId.toString(),
|
||||
username,
|
||||
userType,
|
||||
locale,
|
||||
role,
|
||||
agentLevel: userType === 'AGENT' ? agentLevel ?? null : null,
|
||||
agentLevel: level,
|
||||
maxAgentLevel,
|
||||
canManageSubAgents,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { SystemConfigModule } from '../../shared/config/system-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SystemConfigModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
|
||||
@@ -62,15 +62,20 @@ export class AuthService {
|
||||
}
|
||||
|
||||
if (portal === 'player' && user.parentId) {
|
||||
const agentSettings = await this.systemConfig.getAgentSuspendSettings();
|
||||
if (agentSettings.suspendBlockPlayerLogin) {
|
||||
const parentAgent = await this.prisma.user.findUnique({
|
||||
where: { id: user.parentId },
|
||||
select: { userType: true, status: true },
|
||||
});
|
||||
if (parentAgent?.userType === 'AGENT' && parentAgent.status !== 'ACTIVE') {
|
||||
throw appForbidden('PARENT_AGENT_SUSPENDED');
|
||||
}
|
||||
const parentAgent = await this.prisma.user.findUnique({
|
||||
where: { id: user.parentId },
|
||||
select: {
|
||||
userType: true,
|
||||
status: true,
|
||||
agentProfile: { select: { blockDirectPlayerLogin: true } },
|
||||
},
|
||||
});
|
||||
if (
|
||||
parentAgent?.userType === 'AGENT' &&
|
||||
parentAgent.status !== 'ACTIVE' &&
|
||||
parentAgent.agentProfile?.blockDirectPlayerLogin
|
||||
) {
|
||||
throw appForbidden('PARENT_AGENT_SUSPENDED');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,61 @@ export class UsersService {
|
||||
parent?: {
|
||||
username: string;
|
||||
agentLevel: number | null;
|
||||
parent?: { username: string; agentLevel: number | null } | null;
|
||||
parent?: { username: string; agentLevel: number | null; parent?: unknown } | null;
|
||||
} | null,
|
||||
chain?: string[],
|
||||
): string[] {
|
||||
if (chain?.length) return chain;
|
||||
if (!parent) return [];
|
||||
if (parent.agentLevel === 2 && parent.parent?.username) {
|
||||
return [parent.parent.username, parent.username];
|
||||
const ancestors: string[] = [];
|
||||
let cur: typeof parent | null | undefined = parent;
|
||||
while (cur) {
|
||||
ancestors.unshift(cur.username);
|
||||
cur = cur.parent as typeof parent | null | undefined;
|
||||
}
|
||||
return [parent.username];
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
private async buildAffiliationChainMap(parentIds: (bigint | null | undefined)[]) {
|
||||
const map = new Map<string, string[]>();
|
||||
const pending = new Set<bigint>();
|
||||
const cache = new Map<string, { username: string; parentId: bigint | null }>();
|
||||
|
||||
for (const pid of parentIds) {
|
||||
if (pid) pending.add(pid);
|
||||
}
|
||||
|
||||
while (pending.size > 0) {
|
||||
const batch = [...pending];
|
||||
pending.clear();
|
||||
const agents = await this.prisma.user.findMany({
|
||||
where: { id: { in: batch }, userType: 'AGENT', deletedAt: null },
|
||||
select: { id: true, username: true, parentId: true },
|
||||
});
|
||||
for (const agent of agents) {
|
||||
cache.set(agent.id.toString(), { username: agent.username, parentId: agent.parentId });
|
||||
if (agent.parentId && !cache.has(agent.parentId.toString())) {
|
||||
pending.add(agent.parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const build = (startId: bigint | null | undefined): string[] => {
|
||||
const chain: string[] = [];
|
||||
let cur = startId ?? null;
|
||||
while (cur) {
|
||||
const hit = cache.get(cur.toString());
|
||||
if (!hit) break;
|
||||
chain.unshift(hit.username);
|
||||
cur = hit.parentId;
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
|
||||
for (const pid of parentIds) {
|
||||
if (pid) map.set(pid.toString(), build(pid));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private formatPlayerRow(
|
||||
@@ -58,8 +105,9 @@ export class UsersService {
|
||||
auth?: { lastLoginAt: Date | null } | null;
|
||||
},
|
||||
bet?: { count: number; totalStake: string; totalReturn: string },
|
||||
affiliationChain?: string[],
|
||||
) {
|
||||
const affiliationAgents = this.buildAffiliationAgents(u.parent);
|
||||
const affiliationAgents = this.buildAffiliationAgents(u.parent, affiliationChain);
|
||||
return {
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
@@ -242,8 +290,15 @@ export class UsersService {
|
||||
]);
|
||||
|
||||
const betMap = await this.loadBetStatsMap(rows.map((r) => r.id));
|
||||
const affiliationMap = await this.buildAffiliationChainMap(rows.map((r) => r.parentId));
|
||||
return {
|
||||
items: rows.map((u) => this.formatPlayerRow(u, betMap.get(u.id.toString()))),
|
||||
items: rows.map((u) =>
|
||||
this.formatPlayerRow(
|
||||
u,
|
||||
betMap.get(u.id.toString()),
|
||||
u.parentId ? affiliationMap.get(u.parentId.toString()) : undefined,
|
||||
),
|
||||
),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -269,6 +324,7 @@ export class UsersService {
|
||||
});
|
||||
if (!user) throw appNotFound('PLAYER_NOT_FOUND');
|
||||
|
||||
const affiliationMap = await this.buildAffiliationChainMap([user.parentId]);
|
||||
const [betCount, betStake] = await Promise.all([
|
||||
this.prisma.bet.count({ where: { userId: playerId } }),
|
||||
this.prisma.bet.aggregate({
|
||||
@@ -278,7 +334,11 @@ export class UsersService {
|
||||
]);
|
||||
|
||||
return {
|
||||
...this.formatPlayerRow(user),
|
||||
...this.formatPlayerRow(
|
||||
user,
|
||||
undefined,
|
||||
user.parentId ? affiliationMap.get(user.parentId.toString()) : undefined,
|
||||
),
|
||||
lastLoginAt: user.auth?.lastLoginAt ?? null,
|
||||
loginFailCount: user.auth?.loginFailCount ?? 0,
|
||||
lockedUntil: user.auth?.lockedUntil ?? null,
|
||||
|
||||
@@ -309,6 +309,227 @@ export class WalletService {
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
private walletTypeCategoryWhere(category?: string): Prisma.WalletTransactionWhereInput {
|
||||
const cat = category?.trim();
|
||||
if (cat === 'deposit') {
|
||||
return { transactionType: { in: ['MANUAL_DEPOSIT', 'DEPOSIT', 'MANUAL_ADJUST'] } };
|
||||
}
|
||||
if (cat === 'withdraw') {
|
||||
return { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
}
|
||||
if (cat === 'bet') {
|
||||
return {
|
||||
transactionType: {
|
||||
in: [
|
||||
'BET_FREEZE',
|
||||
'BET_DEDUCT',
|
||||
'BET_SETTLE_WIN',
|
||||
'BET_SETTLE_LOSE',
|
||||
'BET_SETTLE_PUSH',
|
||||
'BET_WIN',
|
||||
'BET_REFUND',
|
||||
'BET_VOID',
|
||||
'BET_VOID_REFUND',
|
||||
'RESETTLE_REVERSE',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (cat === 'cashback') {
|
||||
return { transactionType: { in: ['CASHBACK', 'CASHBACK_DEPOSIT'] } };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async listWalletTransactionsAdmin(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
playerId?: bigint;
|
||||
parentAgentId?: bigint;
|
||||
parentAgentKeyword?: string;
|
||||
scopedParentAgentIds?: bigint[];
|
||||
keyword?: string;
|
||||
operatorKeyword?: string;
|
||||
transactionType?: string;
|
||||
typeCategory?: string;
|
||||
dateFrom?: Date;
|
||||
dateTo?: Date;
|
||||
}) {
|
||||
const page = Math.max(1, params.page ?? 1);
|
||||
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: Prisma.WalletTransactionWhereInput = {};
|
||||
|
||||
const explicitType = params.transactionType?.trim();
|
||||
if (explicitType) {
|
||||
where.transactionType = explicitType;
|
||||
} else if (params.typeCategory?.trim()) {
|
||||
Object.assign(where, this.walletTypeCategoryWhere(params.typeCategory));
|
||||
}
|
||||
|
||||
if (params.dateFrom || params.dateTo) {
|
||||
where.createdAt = {};
|
||||
if (params.dateFrom) where.createdAt.gte = params.dateFrom;
|
||||
if (params.dateTo) where.createdAt.lte = params.dateTo;
|
||||
}
|
||||
|
||||
const operatorKeyword = params.operatorKeyword?.trim();
|
||||
if (operatorKeyword) {
|
||||
const matchedOps = await this.prisma.user.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
username: { contains: operatorKeyword, mode: 'insensitive' },
|
||||
},
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const operatorIds = matchedOps.map((u) => u.id);
|
||||
if (!operatorIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
where.operatorId = { in: operatorIds };
|
||||
}
|
||||
|
||||
let playerIds: bigint[] | undefined;
|
||||
|
||||
if (params.playerId) {
|
||||
playerIds = [params.playerId];
|
||||
} else {
|
||||
const playerWhere: Prisma.UserWhereInput = {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (params.parentAgentId) {
|
||||
playerWhere.parentId = params.parentAgentId;
|
||||
} else if (params.parentAgentKeyword?.trim()) {
|
||||
const matchedAgents = await this.prisma.user.findMany({
|
||||
where: {
|
||||
userType: 'AGENT',
|
||||
deletedAt: null,
|
||||
username: { contains: params.parentAgentKeyword.trim(), mode: 'insensitive' },
|
||||
...(params.scopedParentAgentIds?.length
|
||||
? { id: { in: params.scopedParentAgentIds } }
|
||||
: {}),
|
||||
},
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const agentIds = matchedAgents.map((a) => a.id);
|
||||
if (!agentIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
playerWhere.parentId = { in: agentIds };
|
||||
} else if (params.scopedParentAgentIds?.length) {
|
||||
playerWhere.parentId = { in: params.scopedParentAgentIds };
|
||||
}
|
||||
|
||||
const keyword = params.keyword?.trim();
|
||||
if (keyword) {
|
||||
playerWhere.username = { contains: keyword, mode: 'insensitive' };
|
||||
}
|
||||
|
||||
if (
|
||||
params.parentAgentId ||
|
||||
params.parentAgentKeyword?.trim() ||
|
||||
params.scopedParentAgentIds?.length ||
|
||||
keyword
|
||||
) {
|
||||
const players = await this.prisma.user.findMany({
|
||||
where: playerWhere,
|
||||
select: { id: true },
|
||||
take: 500,
|
||||
});
|
||||
playerIds = players.map((p) => p.id);
|
||||
if (!playerIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playerIds) {
|
||||
where.userId = { in: playerIds };
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.walletTransaction.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where }),
|
||||
]);
|
||||
|
||||
const userIds = [...new Set(rows.map((r) => r.userId))];
|
||||
const operatorIds = [
|
||||
...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
|
||||
const [players, operators] = await Promise.all([
|
||||
userIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true, parentId: true },
|
||||
})
|
||||
: [],
|
||||
operatorIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [],
|
||||
]);
|
||||
|
||||
const parentIds = [
|
||||
...new Set(players.map((p) => p.parentId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
const parentAgents = parentIds.length
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const playerById = new Map(players.map((p) => [p.id.toString(), p]));
|
||||
const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));
|
||||
const parentById = new Map(parentAgents.map((a) => [a.id.toString(), a.username]));
|
||||
|
||||
return {
|
||||
items: rows.map((row) => {
|
||||
const player = playerById.get(row.userId.toString());
|
||||
const parentId = player?.parentId;
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
transactionId: row.transactionId,
|
||||
playerId: row.userId.toString(),
|
||||
playerUsername: player?.username ?? null,
|
||||
parentAgentId: parentId?.toString() ?? null,
|
||||
parentAgentUsername: parentId ? (parentById.get(parentId.toString()) ?? null) : null,
|
||||
transactionType: row.transactionType,
|
||||
amount: row.amount.toString(),
|
||||
balanceBefore: row.balanceBefore.toString(),
|
||||
balanceAfter: row.balanceAfter.toString(),
|
||||
frozenBefore: row.frozenBefore.toString(),
|
||||
frozenAfter: row.frozenAfter.toString(),
|
||||
referenceType: row.referenceType,
|
||||
referenceId: row.referenceId,
|
||||
betNo: row.referenceType === 'BET' ? row.referenceId : null,
|
||||
operatorId: row.operatorId?.toString() ?? null,
|
||||
operatorUsername: row.operatorId
|
||||
? (operatorById.get(row.operatorId.toString()) ?? null)
|
||||
: null,
|
||||
remark: row.remark,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async listTransferTransactions(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -5,6 +5,15 @@ export const PLAYER_ALLOW_PASSWORD_CHANGE = 'player.allow_password_change';
|
||||
export const PLAYER_ALLOW_USERNAME_CHANGE = 'player.allow_username_change';
|
||||
export const AGENT_SUSPEND_FREEZE_DIRECT_PLAYERS = 'agent.suspend_freeze_direct_players';
|
||||
export const AGENT_SUSPEND_BLOCK_PLAYER_LOGIN = 'agent.suspend_block_player_login';
|
||||
export const AGENT_MAX_LEVEL = 'agent.max_level';
|
||||
export const AGENT_DEFAULT_SUB_CREDIT_RATIO = 'agent.default_sub_credit_ratio';
|
||||
|
||||
export type AgentHierarchySettings = {
|
||||
/** 最大代理层级;0 = 不限制 */
|
||||
maxAgentLevel: number;
|
||||
/** 创建下级代理时默认授信占上级可用授信的比例(1–100) */
|
||||
defaultSubAgentCreditRatio: number;
|
||||
};
|
||||
|
||||
export type PlayerAccountSettings = {
|
||||
allowPasswordChange: boolean;
|
||||
@@ -40,6 +49,25 @@ export class SystemConfigService {
|
||||
});
|
||||
}
|
||||
|
||||
async getInt(key: string, defaultValue: number): Promise<number> {
|
||||
const row = await this.prisma.systemConfig.findUnique({ where: { configKey: key } });
|
||||
if (!row) return defaultValue;
|
||||
const parsed = parseInt(row.configValue, 10);
|
||||
return Number.isFinite(parsed) ? parsed : defaultValue;
|
||||
}
|
||||
|
||||
async setInt(key: string, value: number, description?: string) {
|
||||
await this.prisma.systemConfig.upsert({
|
||||
where: { configKey: key },
|
||||
create: {
|
||||
configKey: key,
|
||||
configValue: String(value),
|
||||
description,
|
||||
},
|
||||
update: { configValue: String(value) },
|
||||
});
|
||||
}
|
||||
|
||||
async getPlayerAccountSettings(): Promise<PlayerAccountSettings> {
|
||||
const [allowPasswordChange, allowUsernameChange] = await Promise.all([
|
||||
this.getBoolean(PLAYER_ALLOW_PASSWORD_CHANGE, true),
|
||||
@@ -91,4 +119,40 @@ export class SystemConfigService {
|
||||
}
|
||||
return this.getAgentSuspendSettings();
|
||||
}
|
||||
|
||||
async getAgentHierarchySettings(): Promise<AgentHierarchySettings> {
|
||||
const [maxAgentLevel, defaultSubAgentCreditRatio] = await Promise.all([
|
||||
this.getInt(AGENT_MAX_LEVEL, 0),
|
||||
this.getInt(AGENT_DEFAULT_SUB_CREDIT_RATIO, 50),
|
||||
]);
|
||||
return { maxAgentLevel, defaultSubAgentCreditRatio };
|
||||
}
|
||||
|
||||
async updateAgentHierarchySettings(data: Partial<AgentHierarchySettings>) {
|
||||
if (data.maxAgentLevel !== undefined) {
|
||||
if (!Number.isInteger(data.maxAgentLevel) || data.maxAgentLevel < 0) {
|
||||
throw new Error('maxAgentLevel must be a non-negative integer');
|
||||
}
|
||||
await this.setInt(
|
||||
AGENT_MAX_LEVEL,
|
||||
data.maxAgentLevel,
|
||||
'最大代理层级;0 表示不限制',
|
||||
);
|
||||
}
|
||||
if (data.defaultSubAgentCreditRatio !== undefined) {
|
||||
if (
|
||||
!Number.isInteger(data.defaultSubAgentCreditRatio) ||
|
||||
data.defaultSubAgentCreditRatio < 1 ||
|
||||
data.defaultSubAgentCreditRatio > 100
|
||||
) {
|
||||
throw new Error('defaultSubAgentCreditRatio must be an integer between 1 and 100');
|
||||
}
|
||||
await this.setInt(
|
||||
AGENT_DEFAULT_SUB_CREDIT_RATIO,
|
||||
data.defaultSubAgentCreditRatio,
|
||||
'创建下级代理时默认授信占上级可用授信的比例(%)',
|
||||
);
|
||||
}
|
||||
return this.getAgentHierarchySettings();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user