feat: 开户备注、账单展示优化与后台代理管理增强
- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示 - 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分 - 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端 - 后台代理/玩家表格操作栏重构,充值订单增加备注列 - 前台个人中心、充值、账单与验证码组件体验优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { AuthService } from '../identity/auth.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { generateBatchNo } from '../../shared/common/decorators';
|
||||
import { assertPlayerUsername } from '@thebet365/shared';
|
||||
import { assertPlayerUsername, validateInitialDepositRemark } from '@thebet365/shared';
|
||||
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
|
||||
import { assignInviteCodeWithHistory } from '../../shared/common/invite-code.util';
|
||||
|
||||
@@ -87,6 +87,157 @@ export class AgentsService {
|
||||
return map;
|
||||
}
|
||||
|
||||
private async agentCashbackRateMap(agentUserIds: bigint[]): Promise<Map<string, string>> {
|
||||
if (agentUserIds.length === 0) return new Map();
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: agentUserIds } },
|
||||
select: { userId: true, cashbackRate: true },
|
||||
});
|
||||
return new Map(profiles.map((p) => [p.userId.toString(), p.cashbackRate.toString()]));
|
||||
}
|
||||
|
||||
private async playerEffectiveCashbackRateMap(
|
||||
players: Array<{ id: bigint; parentId: bigint | null }>,
|
||||
parentCashbackMap: Map<string, string>,
|
||||
): Promise<Map<string, string>> {
|
||||
if (players.length === 0) return new Map();
|
||||
const playerIds = players.map((p) => p.id);
|
||||
const customRules = await this.prisma.cashbackRule.findMany({
|
||||
where: {
|
||||
targetType: 'USER',
|
||||
targetId: { in: playerIds },
|
||||
isActive: true,
|
||||
marketType: null,
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
const customMap = new Map<string, string>();
|
||||
for (const rule of customRules) {
|
||||
if (!rule.targetId) continue;
|
||||
const id = rule.targetId.toString();
|
||||
if (!customMap.has(id) && new Decimal(rule.rate).gt(0)) {
|
||||
customMap.set(id, rule.rate.toString());
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Map<string, string>();
|
||||
for (const p of players) {
|
||||
const key = p.id.toString();
|
||||
const custom = customMap.get(key);
|
||||
if (custom) {
|
||||
result.set(key, custom);
|
||||
continue;
|
||||
}
|
||||
if (p.parentId) {
|
||||
result.set(key, parentCashbackMap.get(p.parentId.toString()) ?? '0');
|
||||
} else {
|
||||
result.set(key, '0');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 代理端:从当前登录代理向下构建上级链,不包含更上层代理 */
|
||||
private async buildScopedAncestorChainMap(
|
||||
parentAgentIds: (bigint | null | undefined)[],
|
||||
rootAgentId: bigint,
|
||||
) {
|
||||
const cache = new Map<string, { username: string; parentAgentId: bigint | null }>();
|
||||
const pending = new Set<bigint>();
|
||||
const rootKey = rootAgentId.toString();
|
||||
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) pending.add(id);
|
||||
}
|
||||
pending.add(rootAgentId);
|
||||
|
||||
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 &&
|
||||
profile.parentAgentId.toString() !== rootKey &&
|
||||
!cache.has(profile.parentAgentId.toString())
|
||||
) {
|
||||
pending.add(profile.parentAgentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const build = (startId: bigint | null | undefined): string[] => {
|
||||
if (!startId) return [];
|
||||
const chain: string[] = [];
|
||||
let cur: bigint | null = startId;
|
||||
let reachedRoot = false;
|
||||
while (cur) {
|
||||
const hit = cache.get(cur.toString());
|
||||
if (!hit) return [];
|
||||
chain.unshift(hit.username);
|
||||
if (cur.toString() === rootKey) {
|
||||
reachedRoot = true;
|
||||
break;
|
||||
}
|
||||
cur = hit.parentAgentId;
|
||||
}
|
||||
return reachedRoot ? chain : [];
|
||||
};
|
||||
|
||||
const map = new Map<string, string[]>();
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) map.set(id.toString(), build(id));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private async getAgentPortalScope(rootAgentId: bigint) {
|
||||
const profile = await this.getProfile(rootAgentId);
|
||||
const subtreeIds = await this.getSubtreeAgentIds(rootAgentId);
|
||||
const descendantIds = subtreeIds.filter((id) => id !== rootAgentId);
|
||||
const subtreeIdSet = new Set(subtreeIds.map((id) => id.toString()));
|
||||
return {
|
||||
rootAgentId,
|
||||
rootLevel: profile.level,
|
||||
subtreeIds,
|
||||
descendantIds,
|
||||
subtreeIdSet,
|
||||
};
|
||||
}
|
||||
|
||||
private assertAgentInPortalSubtree(
|
||||
scope: { subtreeIdSet: Set<string> },
|
||||
agentId: bigint,
|
||||
) {
|
||||
if (!scope.subtreeIdSet.has(agentId.toString())) {
|
||||
throw appForbidden('NOT_SUB_AGENT');
|
||||
}
|
||||
}
|
||||
|
||||
/** 代理端:玩家必须挂在当前代理子树内某代理下(自己或下级) */
|
||||
async requirePlayerInPortalSubtree(rootAgentId: bigint, playerId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const player = await this.prisma.user.findFirst({
|
||||
where: { id: playerId, userType: 'PLAYER', deletedAt: null },
|
||||
include: { wallet: true, preferences: true, auth: true },
|
||||
});
|
||||
if (!player?.parentId || !scope.subtreeIdSet.has(player.parentId.toString())) {
|
||||
throw appForbidden('MANAGE_DIRECT_PLAYERS_ONLY');
|
||||
}
|
||||
return { player, scope, isDirect: player.parentId.toString() === rootAgentId.toString() };
|
||||
}
|
||||
|
||||
private async validateAgentLevel(level: number, parentAgentId?: bigint) {
|
||||
if (!Number.isInteger(level) || level < 1) {
|
||||
throw appBadRequest('AGENT_LEVEL_INVALID');
|
||||
@@ -341,6 +492,7 @@ export class AgentsService {
|
||||
operatorId,
|
||||
remark ?? '管理员上分',
|
||||
requestId,
|
||||
'ADMIN_DEPOSIT',
|
||||
);
|
||||
const player = await this.prisma.user.findUnique({
|
||||
where: { id: playerId },
|
||||
@@ -366,6 +518,7 @@ export class AgentsService {
|
||||
operatorId,
|
||||
remark ?? '管理员下分',
|
||||
requestId,
|
||||
'ADMIN_WITHDRAW',
|
||||
);
|
||||
const player = await this.prisma.user.findUnique({
|
||||
where: { id: playerId },
|
||||
@@ -519,7 +672,7 @@ export class AgentsService {
|
||||
|
||||
await this.assertAgentDepositLimits(agentId, amt);
|
||||
|
||||
await this.wallet.deposit(playerId, amt, agentId, remark ?? '代理上分', requestId);
|
||||
await this.wallet.deposit(playerId, amt, agentId, remark ?? '代理上分', requestId, 'AGENT_DEPOSIT');
|
||||
await this.recalculateUsedCredit(agentId);
|
||||
|
||||
return { success: true };
|
||||
@@ -534,7 +687,14 @@ export class AgentsService {
|
||||
) {
|
||||
await this.requireDirectPlayer(agentId, playerId);
|
||||
|
||||
await this.wallet.withdraw(playerId, amount, agentId, remark ?? '代理下分', requestId);
|
||||
await this.wallet.withdraw(
|
||||
playerId,
|
||||
amount,
|
||||
agentId,
|
||||
remark ?? '代理下分',
|
||||
requestId,
|
||||
'AGENT_WITHDRAW',
|
||||
);
|
||||
await this.recalculateUsedCredit(agentId);
|
||||
|
||||
return { success: true };
|
||||
@@ -1515,6 +1675,8 @@ export class AgentsService {
|
||||
|
||||
const initial = data.initialDeposit ?? 0;
|
||||
if (initial > 0) {
|
||||
const remarkResult = validateInitialDepositRemark(initial, data.depositRemark, 'admin');
|
||||
if (!remarkResult.ok) throw appBadRequest(remarkResult.code);
|
||||
const requestId =
|
||||
data.depositRequestId ?? `admin-create-${user.id}-${Date.now()}`;
|
||||
await this.assertPlayerParentCreditForDeposit(user.id, initial);
|
||||
@@ -1522,8 +1684,9 @@ export class AgentsService {
|
||||
user.id,
|
||||
initial,
|
||||
operatorId,
|
||||
data.depositRemark ?? '开户初始余额',
|
||||
remarkResult.remark,
|
||||
requestId,
|
||||
'ADMIN_DEPOSIT',
|
||||
);
|
||||
if (parentId) {
|
||||
await this.recalculateUsedCredit(parentId);
|
||||
@@ -1533,6 +1696,34 @@ export class AgentsService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async getPortalAgentDirectPlayers(rootAgentId: bigint, targetAgentId: bigint) {
|
||||
await this.assertDescendantAgent(rootAgentId, targetAgentId);
|
||||
const players = await this.getDirectPlayers(targetAgentId);
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: targetAgentId },
|
||||
select: {
|
||||
cashbackRate: true,
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
});
|
||||
const rootKey = rootAgentId.toString();
|
||||
const targetKey = targetAgentId.toString();
|
||||
const parentAgentUsername = profile?.user.username ?? '—';
|
||||
const parentCashbackMap = await this.agentCashbackRateMap([targetAgentId]);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
players.map((p) => ({ id: BigInt(p.id), parentId: targetAgentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
return players.map((p) => ({
|
||||
...p,
|
||||
parentAgentId: targetKey,
|
||||
parentAgentUsername,
|
||||
cashbackRate: playerCashbackMap.get(p.id) ?? '0',
|
||||
inChain: true,
|
||||
isDirect: targetKey === rootKey,
|
||||
}));
|
||||
}
|
||||
|
||||
async getDirectPlayers(agentId: bigint) {
|
||||
const rows = await this.prisma.user.findMany({
|
||||
where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
|
||||
@@ -1592,6 +1783,8 @@ export class AgentsService {
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentAgentId: p.parentAgentId?.toString() ?? null,
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
@@ -1601,6 +1794,403 @@ export class AgentsService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Read-only downline under a direct sub-agent (all descendant agents + subtree players). */
|
||||
async getDirectChildDownlineView(parentAgentId: bigint, subAgentId: bigint) {
|
||||
await this.assertDirectChildAgent(parentAgentId, subAgentId);
|
||||
|
||||
const subtreeIds = await this.getSubtreeAgentIds(subAgentId);
|
||||
const descendantAgentIds = subtreeIds.filter((id) => id !== subAgentId);
|
||||
|
||||
let agents: Array<{
|
||||
userId: string;
|
||||
username: string;
|
||||
userStatus: string;
|
||||
status: string;
|
||||
level: number;
|
||||
parentUsername: string;
|
||||
creditLimit: string;
|
||||
usedCredit: string;
|
||||
availableCredit: string;
|
||||
directPlayerCount: number;
|
||||
createdAt: Date;
|
||||
}> = [];
|
||||
|
||||
if (descendantAgentIds.length > 0) {
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: descendantAgentIds } },
|
||||
include: { user: true },
|
||||
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
const parentAgentIds = [
|
||||
...new Set(
|
||||
profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentUsers =
|
||||
parentAgentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentAgentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentNameMap = new Map(
|
||||
parentUsers.map((u) => [u.id.toString(), u.username]),
|
||||
);
|
||||
|
||||
const playerCounts = await this.prisma.user.groupBy({
|
||||
by: ['parentId'],
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
parentId: { in: descendantAgentIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const countMap = new Map(
|
||||
playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
|
||||
);
|
||||
|
||||
agents = profiles.map((p) => {
|
||||
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
|
||||
return {
|
||||
userId: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentUsername: p.parentAgentId
|
||||
? parentNameMap.get(p.parentAgentId.toString()) ?? '—'
|
||||
: '—',
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
|
||||
createdAt: p.createdAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const playerRows =
|
||||
subtreeIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
parentId: { in: subtreeIds },
|
||||
},
|
||||
include: {
|
||||
wallet: true,
|
||||
usedInvite: { select: { code: true } },
|
||||
parent: { select: { username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
: [];
|
||||
|
||||
const parentAgentIdsForPlayers = [
|
||||
...new Set(
|
||||
playerRows
|
||||
.map((u) => u.parentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentCashbackMap = await this.agentCashbackRateMap(parentAgentIdsForPlayers);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
playerRows.map((u) => ({ id: u.id, parentId: u.parentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
|
||||
const players = playerRows.map((u) => ({
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
status: u.status,
|
||||
createdAt: u.createdAt,
|
||||
inviteCode: u.usedInvite?.code ?? null,
|
||||
parentAgentUsername: u.parent?.username ?? '—',
|
||||
cashbackRate: playerCashbackMap.get(u.id.toString()) ?? '0',
|
||||
wallet: u.wallet
|
||||
? {
|
||||
availableBalance: u.wallet.availableBalance.toString(),
|
||||
frozenBalance: u.wallet.frozenBalance.toString(),
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
agents: agents.map((a) => ({
|
||||
...a,
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
})),
|
||||
players,
|
||||
};
|
||||
}
|
||||
|
||||
async assertDescendantAgent(rootAgentId: bigint, targetAgentId: bigint) {
|
||||
const subtreeIds = await this.getSubtreeAgentIds(rootAgentId);
|
||||
if (!subtreeIds.some((id) => id === targetAgentId)) {
|
||||
throw appForbidden('NOT_SUB_AGENT');
|
||||
}
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: targetAgentId },
|
||||
});
|
||||
if (!profile) throw appNotFound('AGENT_NOT_FOUND');
|
||||
return profile;
|
||||
}
|
||||
|
||||
async countSubtreeAgentsByLevel(rootAgentId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
if (scope.descendantIds.length === 0) return {} as Record<number, number>;
|
||||
|
||||
const groups = await this.prisma.agentProfile.groupBy({
|
||||
by: ['level'],
|
||||
where: {
|
||||
userId: { in: scope.descendantIds },
|
||||
level: { gt: scope.rootLevel },
|
||||
user: { deletedAt: null },
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const out: Record<number, number> = {};
|
||||
for (const g of groups) {
|
||||
if (g.level > scope.rootLevel) {
|
||||
out[g.level] = g._count._all;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async listSubtreeAgentsAtLevel(
|
||||
rootAgentId: bigint,
|
||||
level: number,
|
||||
params?: { page?: number; pageSize?: number; keyword?: string; status?: string },
|
||||
) {
|
||||
const page = Math.max(1, params?.page ?? 1);
|
||||
const pageSize = Math.min(Math.max(1, params?.pageSize ?? 20), 100);
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
if (level <= scope.rootLevel || scope.descendantIds.length === 0) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
|
||||
const where: Prisma.AgentProfileWhereInput = {
|
||||
userId: { in: scope.descendantIds },
|
||||
level,
|
||||
};
|
||||
const kw = params?.keyword?.trim();
|
||||
const status = params?.status?.trim();
|
||||
const userWhere: Prisma.UserWhereInput = { deletedAt: null };
|
||||
if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
|
||||
userWhere.status = status;
|
||||
}
|
||||
if (kw) {
|
||||
userWhere.username = { contains: kw, mode: 'insensitive' };
|
||||
}
|
||||
where.user = userWhere;
|
||||
|
||||
const [profiles, total] = await Promise.all([
|
||||
this.prisma.agentProfile.findMany({
|
||||
where,
|
||||
include: { user: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.agentProfile.count({ where }),
|
||||
]);
|
||||
|
||||
const agentIds = profiles.map((p) => p.userId);
|
||||
const playerCounts =
|
||||
agentIds.length > 0
|
||||
? await this.prisma.user.groupBy({
|
||||
by: ['parentId'],
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
parentId: { in: agentIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
_count: { _all: true },
|
||||
})
|
||||
: [];
|
||||
const countMap = new Map(
|
||||
playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
|
||||
);
|
||||
|
||||
const parentAgentIds = [
|
||||
...new Set(
|
||||
profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null && scope.subtreeIdSet.has(id.toString())),
|
||||
),
|
||||
];
|
||||
const parentUsers =
|
||||
parentAgentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentAgentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
|
||||
const parentChainMap = await this.buildScopedAncestorChainMap(
|
||||
profiles.map((p) => p.parentAgentId),
|
||||
rootAgentId,
|
||||
);
|
||||
|
||||
const items = profiles.map((p) => {
|
||||
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
|
||||
const parentChain = p.parentAgentId
|
||||
? (parentChainMap.get(p.parentAgentId.toString()) ?? [])
|
||||
: [];
|
||||
const parentUsername =
|
||||
p.parentAgentId && scope.subtreeIdSet.has(p.parentAgentId.toString())
|
||||
? (parentUsernameMap.get(p.parentAgentId.toString()) ?? null)
|
||||
: null;
|
||||
return {
|
||||
userId: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentAgentId: p.parentAgentId?.toString() ?? null,
|
||||
parentUsername,
|
||||
parentChainLabel: parentChain.length ? parentChain.join(' / ') : null,
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
|
||||
createdAt: p.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async listSubtreePlayersForPortal(
|
||||
rootAgentId: bigint,
|
||||
params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
parentAgentId?: string;
|
||||
},
|
||||
) {
|
||||
const page = Math.max(1, params?.page ?? 1);
|
||||
const pageSize = Math.min(Math.max(1, params?.pageSize ?? 20), 100);
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const where: Prisma.UserWhereInput = {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
parentId: { in: scope.subtreeIds },
|
||||
};
|
||||
if (params?.parentAgentId) {
|
||||
this.assertAgentInPortalSubtree(scope, BigInt(params.parentAgentId));
|
||||
where.parentId = BigInt(params.parentAgentId);
|
||||
}
|
||||
const kw = params?.keyword?.trim();
|
||||
if (kw) {
|
||||
where.username = { contains: kw, mode: 'insensitive' };
|
||||
}
|
||||
const status = params?.status?.trim();
|
||||
if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.user.findMany({
|
||||
where,
|
||||
include: {
|
||||
wallet: true,
|
||||
usedInvite: { select: { code: true } },
|
||||
parent: { select: { id: true, username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
const parentIds = [
|
||||
...new Set(
|
||||
rows
|
||||
.map((u) => u.parentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentCashbackMap = await this.agentCashbackRateMap(parentIds);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
rows.map((u) => ({ id: u.id, parentId: u.parentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
|
||||
const rootKey = rootAgentId.toString();
|
||||
const items = rows.map((u) => {
|
||||
const parentId = u.parentId!.toString();
|
||||
return {
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
status: u.status,
|
||||
createdAt: u.createdAt.toISOString(),
|
||||
inviteCode: u.usedInvite?.code ?? null,
|
||||
parentAgentId: parentId,
|
||||
parentAgentUsername: u.parent?.username ?? '—',
|
||||
cashbackRate: playerCashbackMap.get(u.id.toString()) ?? '0',
|
||||
inChain: true,
|
||||
isDirect: parentId === rootKey,
|
||||
wallet: u.wallet
|
||||
? {
|
||||
availableBalance: u.wallet.availableBalance.toString(),
|
||||
frozenBalance: u.wallet.frozenBalance.toString(),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async listSubtreeAgentOptions(rootAgentId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: {
|
||||
userId: { in: scope.subtreeIds },
|
||||
OR: [{ userId: rootAgentId }, { level: { gt: scope.rootLevel } }],
|
||||
},
|
||||
include: { user: { select: { username: true } } },
|
||||
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
const parentIds = profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null && scope.subtreeIdSet.has(id.toString()));
|
||||
const parentUsers =
|
||||
parentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: [...new Set(parentIds)] } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
|
||||
|
||||
return profiles.map((p) => ({
|
||||
id: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
level: p.level,
|
||||
parentUsername:
|
||||
p.parentAgentId && scope.subtreeIdSet.has(p.parentAgentId.toString())
|
||||
? (parentUsernameMap.get(p.parentAgentId.toString()) ?? null)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
async assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: subAgentId },
|
||||
|
||||
Reference in New Issue
Block a user