重构 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,189 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard, AgentGuard } from '../../domains/identity/guards';
import { CurrentUser } from '../../shared/common/decorators';
import { jsonResponse } from '../../shared/common/filters';
import { AgentsService } from '../../domains/agent/agents.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { BetsService } from '../../domains/betting/bets.service';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { IsString, IsNumber, MinLength, IsOptional } from 'class-validator';
class CreatePlayerDto {
@IsString()
username!: string;
@IsString()
@MinLength(8)
password!: string;
}
class CreateSubAgentDto extends CreatePlayerDto {
@IsOptional()
@IsNumber()
creditLimit?: number;
}
class TransferDto {
@IsNumber()
amount!: number;
@IsString()
requestId!: string;
}
class CreditDto extends TransferDto {
@IsOptional()
@IsString()
remark?: string;
}
@ApiTags('Agent Portal')
@Controller('agent')
@UseGuards(JwtAuthGuard, AgentGuard)
@ApiBearerAuth()
export class AgentPortalController {
constructor(
private agents: AgentsService,
private wallet: WalletService,
private bets: BetsService,
private prisma: PrismaService,
) {}
@Get('profile')
async profile(@CurrentUser('id') agentId: bigint) {
const profile = await this.agents.getProfile(agentId);
return jsonResponse(profile);
}
@Get('players')
async listPlayers(@CurrentUser('id') agentId: bigint) {
const players = await this.agents.getDirectPlayers(agentId);
return jsonResponse(players);
}
@Post('players')
async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) {
const user = await this.agents.createPlayer(agentId, {
username: dto.username,
password: dto.password,
parentId: agentId,
});
return jsonResponse(user);
}
@Get('agents')
async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) {
if (level !== 1) {
return jsonResponse([]);
}
const agents = await this.agents.getChildAgents(agentId);
return jsonResponse(agents);
}
@Post('agents')
async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) {
if (level !== 1) {
return jsonResponse(null, 'Only level 1 agents can create sub-agents');
}
const user = await this.agents.createAgent(agentId, {
username: dto.username,
password: dto.password,
level: 2,
parentAgentId: agentId,
creditLimit: dto.creditLimit,
});
return jsonResponse(user);
}
@Post('players/:id/deposit')
async depositToPlayer(
@CurrentUser('id') agentId: bigint,
@Param('id') playerId: string,
@Body() dto: TransferDto,
) {
const result = await this.agents.depositToPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId);
return jsonResponse(result);
}
@Post('players/:id/withdraw')
async withdrawFromPlayer(
@CurrentUser('id') agentId: bigint,
@Param('id') playerId: string,
@Body() dto: TransferDto,
) {
const result = await this.agents.withdrawFromPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId);
return jsonResponse(result);
}
@Post('agents/:id/credit')
async allocateCredit(
@CurrentUser('id') agentId: bigint,
@Param('id') subAgentId: string,
@Body() dto: CreditDto,
) {
const subAgent = await this.prisma.agentProfile.findUnique({
where: { userId: BigInt(subAgentId) },
});
if (!subAgent || subAgent.parentAgentId !== agentId) {
return jsonResponse(null, 'Not your sub-agent');
}
const result = await this.agents.adjustCredit(
BigInt(subAgentId),
dto.amount,
agentId,
dto.requestId,
dto.remark,
);
return jsonResponse(result);
}
@Get('bets')
async listBets(@CurrentUser('id') agentId: bigint, @Query('page') page?: string) {
const skip = ((page ? parseInt(page) : 1) - 1) * 20;
const descendants = await this.prisma.agentClosure.findMany({
where: { ancestorId: agentId },
});
const agentIds = descendants.map((d) => d.descendantId);
const [items, total] = await Promise.all([
this.prisma.bet.findMany({
where: { agentId: { in: agentIds } },
include: { selections: true, user: true },
orderBy: { placedAt: 'desc' },
skip,
take: 20,
}),
this.prisma.bet.count({ where: { agentId: { in: agentIds } } }),
]);
return jsonResponse({ items, total });
}
@Get('reports/summary')
async reportSummary(@CurrentUser('id') agentId: bigint) {
const summary = await this.agents.getReportSummary(agentId);
return jsonResponse(summary);
}
@Get('wallet-transactions')
async walletTransactions(@CurrentUser('id') agentId: bigint, @Query('playerId') playerId?: string) {
const players = playerId
? [BigInt(playerId)]
: (await this.agents.getDirectPlayers(agentId)).map((p) => p.id);
const transactions = await this.prisma.walletTransaction.findMany({
where: { userId: { in: players } },
orderBy: { createdAt: 'desc' },
take: 50,
});
return jsonResponse(transactions);
}
}