Files
thebet365/apps/api/src/domains/agent/agent-network.service.ts
2026-06-13 17:38:25 +08:00

338 lines
11 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
export interface AgentScope {
rootAgentId: bigint;
rootLevel: number;
subtreeIds: bigint[];
descendantIds: bigint[];
directChildIds: bigint[];
subtreeIdSet: Set<string>;
}
type ClosureRow = {
ancestorId?: bigint;
descendantId: bigint;
depth: number;
};
function uniqueBigints(values: bigint[]) {
const seen = new Set<string>();
const out: bigint[] = [];
for (const value of values) {
const key = value.toString();
if (seen.has(key)) continue;
seen.add(key);
out.push(value);
}
return out;
}
@Injectable()
export class AgentNetworkService {
constructor(private prisma: PrismaService) {}
async buildAgentAncestorChainMap(parentAgentIds: (bigint | null | undefined)[]) {
return this.buildAncestorChainMap(parentAgentIds, undefined);
}
async buildScopedAncestorChainMap(
parentAgentIds: (bigint | null | undefined)[],
rootAgentId: bigint,
) {
return this.buildAncestorChainMap(parentAgentIds, rootAgentId);
}
async resolveScope(rootAgentId: bigint): Promise<AgentScope> {
const [profile, closureRows] = await Promise.all([
this.prisma.agentProfile.findUnique({
where: { userId: rootAgentId },
}),
this.findSubtreeClosureRows(rootAgentId),
]);
if (!profile) throw appBadRequest('AGENT_PROFILE_NOT_FOUND');
const hasClosureRows = closureRows.length > 0;
const subtreeIds = hasClosureRows
? this.idsFromSubtreeClosure(rootAgentId, closureRows)
: await this.getSubtreeAgentIdsFromProfiles(rootAgentId);
const descendantIds = subtreeIds.filter((id) => id !== rootAgentId);
const directChildIds = hasClosureRows
? uniqueBigints(
closureRows
.filter((row) => row.depth === 1)
.map((row) => row.descendantId),
)
: await this.getDirectChildAgentIds(rootAgentId);
return {
rootAgentId,
rootLevel: profile.level,
subtreeIds,
descendantIds,
directChildIds,
subtreeIdSet: new Set(subtreeIds.map((id) => id.toString())),
};
}
assertAgentInScope(scope: AgentScope, agentId: bigint) {
if (!scope.subtreeIdSet.has(agentId.toString())) {
throw appForbidden('NOT_SUB_AGENT');
}
}
async requirePlayerInPortalSubtree(rootAgentId: bigint, playerId: bigint) {
const scope = await this.resolveScope(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() };
}
async getChildAgents(agentId: bigint) {
return this.prisma.agentProfile.findMany({
where: { parentAgentId: agentId },
include: { user: true },
});
}
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 assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: subAgentId },
});
if (!profile || profile.parentAgentId !== parentAgentId) {
throw appForbidden('NOT_SUB_AGENT');
}
return profile;
}
async getSubtreeAgentIds(agentId: bigint) {
const closureRows = await this.findSubtreeClosureRows(agentId);
if (closureRows.length > 0) {
return this.idsFromSubtreeClosure(agentId, closureRows);
}
return this.getSubtreeAgentIdsFromProfiles(agentId);
}
private async buildAncestorChainMap(
parentAgentIds: (bigint | null | undefined)[],
scopedRootAgentId: bigint | undefined,
) {
const agentIds = uniqueBigints(parentAgentIds.filter((id): id is bigint => id != null));
const map = new Map<string, string[]>();
if (agentIds.length === 0) return map;
const closureRows = await this.prisma.agentClosure.findMany({
where: { descendantId: { in: agentIds } },
select: { ancestorId: true, descendantId: true, depth: true },
orderBy: [{ descendantId: 'asc' }, { depth: 'desc' }],
});
const rowsByDescendant = new Map<string, ClosureRow[]>();
for (const row of closureRows) {
const key = row.descendantId.toString();
rowsByDescendant.set(key, [...(rowsByDescendant.get(key) ?? []), row]);
}
const idsNeedingProfileFallback: bigint[] = [];
const ancestorIds = new Set<bigint>();
for (const id of agentIds) {
const rows = rowsByDescendant.get(id.toString());
if (!rows || rows.length === 0) {
idsNeedingProfileFallback.push(id);
continue;
}
const scopedRows = scopedRootAgentId
? this.sliceRowsToScopedRoot(rows, scopedRootAgentId)
: rows;
for (const row of scopedRows) {
if (row.ancestorId) ancestorIds.add(row.ancestorId);
}
}
const users =
ancestorIds.size > 0
? await this.prisma.user.findMany({
where: { id: { in: [...ancestorIds] } },
select: { id: true, username: true },
})
: [];
const usernameMap = new Map(users.map((u) => [u.id.toString(), u.username]));
for (const id of agentIds) {
const rows = rowsByDescendant.get(id.toString());
if (!rows || rows.length === 0) continue;
const scopedRows = scopedRootAgentId
? this.sliceRowsToScopedRoot(rows, scopedRootAgentId)
: rows;
const chain = scopedRows
.map((row) => (row.ancestorId ? usernameMap.get(row.ancestorId.toString()) : undefined))
.filter((username): username is string => Boolean(username));
map.set(id.toString(), chain.length === scopedRows.length ? chain : []);
}
if (idsNeedingProfileFallback.length > 0) {
const fallbackMap = scopedRootAgentId
? await this.buildScopedAncestorChainMapFromProfiles(idsNeedingProfileFallback, scopedRootAgentId)
: await this.buildAncestorChainMapFromProfiles(idsNeedingProfileFallback);
for (const [key, value] of fallbackMap) {
map.set(key, value);
}
}
for (const id of parentAgentIds) {
if (id && !map.has(id.toString())) {
map.set(id.toString(), []);
}
}
return map;
}
private sliceRowsToScopedRoot(rows: ClosureRow[], rootAgentId: bigint) {
const rootRow = rows.find((row) => row.ancestorId === rootAgentId);
if (!rootRow) return [];
return rows.filter((row) => row.depth <= rootRow.depth);
}
private async buildAncestorChainMapFromProfiles(agentIds: bigint[]) {
const cache = await this.loadAncestorProfileCache(agentIds, undefined);
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;
};
return new Map(agentIds.map((id) => [id.toString(), build(id)]));
}
private async buildScopedAncestorChainMapFromProfiles(agentIds: bigint[], rootAgentId: bigint) {
const rootKey = rootAgentId.toString();
const cache = await this.loadAncestorProfileCache(agentIds, rootAgentId);
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 : [];
};
return new Map(agentIds.map((id) => [id.toString(), build(id)]));
}
private async loadAncestorProfileCache(agentIds: bigint[], stopBeforeParentOf?: bigint) {
const cache = new Map<string, { username: string; parentAgentId: bigint | null }>();
const pending = new Set(agentIds);
if (stopBeforeParentOf) pending.add(stopBeforeParentOf);
const stopKey = stopBeforeParentOf?.toString();
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() !== stopKey &&
!cache.has(profile.parentAgentId.toString())
) {
pending.add(profile.parentAgentId);
}
}
}
return cache;
}
private async findSubtreeClosureRows(agentId: bigint) {
return this.prisma.agentClosure.findMany({
where: { ancestorId: agentId },
select: { descendantId: true, depth: true },
orderBy: [{ depth: 'asc' }, { descendantId: 'asc' }],
});
}
private idsFromSubtreeClosure(agentId: bigint, rows: ClosureRow[]) {
return uniqueBigints([agentId, ...rows.map((row) => row.descendantId)]);
}
private async getDirectChildAgentIds(agentId: bigint) {
const children = await this.prisma.agentProfile.findMany({
where: { parentAgentId: agentId },
select: { userId: true },
orderBy: { createdAt: 'desc' },
});
return children.map((child) => child.userId);
}
private async getSubtreeAgentIdsFromProfiles(agentId: bigint) {
const ids: bigint[] = [];
const queue: bigint[] = [agentId];
const seen = new Set<string>();
while (queue.length > 0) {
const current = queue.shift()!;
const key = current.toString();
if (seen.has(key)) continue;
seen.add(key);
ids.push(current);
const children = await this.prisma.agentProfile.findMany({
where: { parentAgentId: current },
select: { userId: true },
});
for (const child of children) {
queue.push(child.userId);
}
}
return ids;
}
}