重构 API 为 8 领域 + 应用层架构
将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
296
apps/api/src/domains/agent/agents.service.ts
Normal file
296
apps/api/src/domains/agent/agents.service.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user