重构 API 为 8 领域 + 应用层架构

将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 14:48:41 +08:00
parent 14e49374ac
commit 4c92157299
47 changed files with 169 additions and 138 deletions

View File

@@ -0,0 +1,296 @@
import { Injectable, BadRequestException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { WalletService } from '../ledger/wallet.service';
import { AuthService } from '../identity/auth.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../shared/common/decorators';
@Injectable()
export class AgentsService {
constructor(
private prisma: PrismaService,
private wallet: WalletService,
private auth: AuthService,
) {}
async getProfile(agentId: bigint) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: agentId },
});
if (!profile) throw new BadRequestException('Agent profile not found');
const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
return { ...profile, availableCredit: available };
}
async recalculateUsedCredit(agentId: bigint) {
const directPlayers = await this.prisma.user.findMany({
where: { parentId: agentId, userType: 'PLAYER' },
include: { wallet: true },
});
let directLiability = new Decimal(0);
for (const p of directPlayers) {
if (p.wallet) {
directLiability = directLiability
.add(p.wallet.availableBalance)
.add(p.wallet.frozenBalance);
}
}
const childAgents = await this.prisma.agentProfile.findMany({
where: { parentAgentId: agentId },
});
let childExposure = new Decimal(0);
for (const child of childAgents) {
const exposure = Decimal.max(child.creditLimit, child.usedCredit);
childExposure = childExposure.add(exposure);
}
const usedCredit = directLiability.add(childExposure);
await this.prisma.agentProfile.update({
where: { userId: agentId },
data: {
usedCredit,
directPlayerLiability: directLiability,
childAgentExposure: childExposure,
},
});
return usedCredit;
}
async adjustCredit(
agentId: bigint,
amount: Decimal | number,
operatorId: bigint,
requestId: string,
remark?: string,
) {
const amt = new Decimal(amount);
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: agentId },
});
if (!profile) throw new BadRequestException('Agent not found');
const creditBefore = profile.creditLimit;
const creditAfter = creditBefore.add(amt);
if (creditAfter.lt(0)) throw new BadRequestException('Credit limit cannot be negative');
await this.prisma.$transaction(async (tx) => {
await tx.agentProfile.update({
where: { userId: agentId },
data: { creditLimit: creditAfter },
});
await tx.agentCreditTransaction.create({
data: {
agentId,
transactionType: amt.gte(0) ? 'CREDIT_INCREASE' : 'CREDIT_DECREASE',
amount: amt,
creditBefore,
creditAfter,
operatorId,
requestId,
remark,
},
});
});
if (profile.parentAgentId) {
await this.recalculateUsedCredit(profile.parentAgentId);
}
return { creditAfter };
}
async depositToPlayer(
agentId: bigint,
playerId: bigint,
amount: number,
requestId: string,
) {
const player = await this.prisma.user.findUnique({ where: { id: playerId } });
if (!player || player.parentId !== agentId) {
throw new ForbiddenException('Can only deposit to direct players');
}
const profile = await this.getProfile(agentId);
const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
const amt = new Decimal(amount);
if (available.lt(amt)) {
throw new BadRequestException('Insufficient agent credit');
}
await this.wallet.deposit(playerId, amt, agentId, 'Agent deposit', requestId);
await this.recalculateUsedCredit(agentId);
return { success: true };
}
async withdrawFromPlayer(
agentId: bigint,
playerId: bigint,
amount: number,
requestId: string,
) {
const player = await this.prisma.user.findUnique({ where: { id: playerId } });
if (!player || player.parentId !== agentId) {
throw new ForbiddenException('Can only withdraw from direct players');
}
await this.wallet.withdraw(playerId, amount, agentId, 'Agent withdraw', requestId);
await this.recalculateUsedCredit(agentId);
return { success: true };
}
async createAgent(
operatorId: bigint,
data: {
username: string;
password: string;
level: number;
parentAgentId?: bigint;
creditLimit?: number;
},
) {
if (data.level === 2 && !data.parentAgentId) {
throw new BadRequestException('Level 2 agent requires parent');
}
const hash = await this.auth.hashPassword(data.password);
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
username: data.username,
userType: 'AGENT',
parentId: data.parentAgentId,
agentLevel: data.level,
},
});
await tx.userAuth.create({
data: { userId: user.id, passwordHash: hash },
});
await tx.agentProfile.create({
data: {
userId: user.id,
level: data.level,
parentAgentId: data.parentAgentId,
creditLimit: data.creditLimit ?? 0,
},
});
// Build closure table
await tx.agentClosure.create({
data: { ancestorId: user.id, descendantId: user.id, depth: 0 },
});
if (data.parentAgentId) {
const ancestors = await tx.agentClosure.findMany({
where: { descendantId: data.parentAgentId },
});
for (const a of ancestors) {
await tx.agentClosure.create({
data: {
ancestorId: a.ancestorId,
descendantId: user.id,
depth: a.depth + 1,
},
});
}
if (data.parentAgentId) {
await this.recalculateUsedCredit(data.parentAgentId);
}
}
return user;
});
}
async createPlayer(
operatorId: bigint,
data: { username: string; password: string; parentId: bigint },
) {
const hash = await this.auth.hashPassword(data.password);
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
username: data.username,
userType: 'PLAYER',
parentId: data.parentId,
},
});
await tx.userAuth.create({
data: { userId: user.id, passwordHash: hash },
});
await tx.wallet.create({
data: { userId: user.id },
});
await tx.userPreference.create({
data: { userId: user.id },
});
const parent = await tx.user.findUnique({ where: { id: data.parentId } });
if (parent?.userType === 'AGENT') {
await this.recalculateUsedCredit(data.parentId);
}
return user;
});
}
async getDirectPlayers(agentId: bigint) {
return this.prisma.user.findMany({
where: { parentId: agentId, userType: 'PLAYER' },
include: { wallet: true },
});
}
async getChildAgents(agentId: bigint) {
return this.prisma.agentProfile.findMany({
where: { parentAgentId: agentId },
include: { user: true },
});
}
async getReportSummary(agentId: bigint) {
const profile = await this.getProfile(agentId);
const players = await this.getDirectPlayers(agentId);
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayBets = await this.prisma.bet.aggregate({
where: {
agentId,
placedAt: { gte: today },
},
_sum: { stake: true, actualReturn: true },
_count: true,
});
return {
profile,
directPlayerCount: players.length,
directPlayerTotalBalance: players.reduce(
(sum, p) =>
sum +
Number(p.wallet?.availableBalance ?? 0) +
Number(p.wallet?.frozenBalance ?? 0),
0,
),
todayBetCount: todayBets._count,
todayStake: todayBets._sum.stake,
todayReturn: todayBets._sum.actualReturn,
};
}
}