重构
This commit is contained in:
337
apps/api/src/domains/agent/agent-network.service.ts
Normal file
337
apps/api/src/domains/agent/agent-network.service.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user