重构 API 为 8 领域 + 应用层架构
将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
400
apps/api/src/applications/admin/admin.controller.ts
Normal file
400
apps/api/src/applications/admin/admin.controller.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard, AdminGuard } from '../../domains/identity/guards';
|
||||
import { ContentService } from '../../domains/operations/content/content.service';
|
||||
import { CurrentUser } from '../../shared/common/decorators';
|
||||
import { jsonResponse } from '../../shared/common/filters';
|
||||
import { UsersService } from '../../domains/identity/users.service';
|
||||
import { AgentsService } from '../../domains/agent/agents.service';
|
||||
import { WalletService } from '../../domains/ledger/wallet.service';
|
||||
import { MatchesService } from '../../domains/catalog/matches.service';
|
||||
import { MarketsService } from '../../domains/odds/markets.service';
|
||||
import { SettlementService } from '../../domains/settlement/settlement.service';
|
||||
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
|
||||
import { I18nService } from '../../domains/operations/i18n/i18n.service';
|
||||
import { AuditService } from '../../domains/operations/audit/audit.service';
|
||||
import { BetsService } from '../../domains/betting/bets.service';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { IsString, IsNumber, IsOptional, IsArray, IsBoolean, MinLength } from 'class-validator';
|
||||
|
||||
class CreateUserDto {
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
creditLimit?: number;
|
||||
}
|
||||
|
||||
class DepositDto {
|
||||
@IsNumber()
|
||||
amount!: number;
|
||||
|
||||
@IsString()
|
||||
requestId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
class CreateMatchDto {
|
||||
@IsString()
|
||||
leagueId!: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamId!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamId!: string;
|
||||
|
||||
@IsString()
|
||||
startTime!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isHot?: boolean;
|
||||
}
|
||||
|
||||
class ScoreDto {
|
||||
@IsNumber()
|
||||
htHome!: number;
|
||||
|
||||
@IsNumber()
|
||||
htAway!: number;
|
||||
|
||||
@IsNumber()
|
||||
ftHome!: number;
|
||||
|
||||
@IsNumber()
|
||||
ftAway!: number;
|
||||
}
|
||||
|
||||
class MarketTemplatesDto {
|
||||
@IsArray()
|
||||
marketTypes!: string[];
|
||||
}
|
||||
|
||||
class UpdateOddsDto {
|
||||
@IsNumber()
|
||||
odds!: number;
|
||||
}
|
||||
|
||||
class CashbackPreviewDto {
|
||||
@IsString()
|
||||
periodStart!: string;
|
||||
|
||||
@IsString()
|
||||
periodEnd!: string;
|
||||
}
|
||||
|
||||
@ApiTags('Admin')
|
||||
@Controller('admin')
|
||||
@UseGuards(JwtAuthGuard, AdminGuard)
|
||||
@ApiBearerAuth()
|
||||
export class AdminController {
|
||||
constructor(
|
||||
private users: UsersService,
|
||||
private agents: AgentsService,
|
||||
private wallet: WalletService,
|
||||
private matches: MatchesService,
|
||||
private markets: MarketsService,
|
||||
private settlement: SettlementService,
|
||||
private cashback: CashbackService,
|
||||
private content: ContentService,
|
||||
private i18n: I18nService,
|
||||
private audit: AuditService,
|
||||
private bets: BetsService,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
@Get('dashboard')
|
||||
async dashboard() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const [todayBets, pendingMatches, totalPlayers] = await Promise.all([
|
||||
this.prisma.bet.aggregate({
|
||||
where: { placedAt: { gte: today } },
|
||||
_sum: { stake: true, actualReturn: true },
|
||||
_count: true,
|
||||
}),
|
||||
this.prisma.match.count({ where: { status: 'PENDING_SETTLEMENT' } }),
|
||||
this.prisma.user.count({ where: { userType: 'PLAYER' } }),
|
||||
]);
|
||||
|
||||
return jsonResponse({
|
||||
todayBetCount: todayBets._count,
|
||||
todayStake: todayBets._sum.stake,
|
||||
todayPayout: todayBets._sum.actualReturn,
|
||||
pendingSettlement: pendingMatches,
|
||||
totalPlayers,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
async listUsers(@Query('page') page?: string) {
|
||||
const result = await this.users.listPlayers(page ? parseInt(page) : 1);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('users')
|
||||
async createPlayer(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) {
|
||||
const user = await this.agents.createPlayer(operatorId, {
|
||||
username: dto.username,
|
||||
password: dto.password,
|
||||
parentId: dto.parentId ? BigInt(dto.parentId) : operatorId,
|
||||
});
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'CREATE_PLAYER',
|
||||
module: 'USERS',
|
||||
targetId: user.id.toString(),
|
||||
});
|
||||
return jsonResponse(user);
|
||||
}
|
||||
|
||||
@Get('agents')
|
||||
async listAgents() {
|
||||
const agents = await this.prisma.agentProfile.findMany({
|
||||
include: { user: true },
|
||||
});
|
||||
return jsonResponse(agents);
|
||||
}
|
||||
|
||||
@Post('agents')
|
||||
async createAgent(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) {
|
||||
const user = await this.agents.createAgent(operatorId, {
|
||||
username: dto.username,
|
||||
password: dto.password,
|
||||
level: 1,
|
||||
creditLimit: dto.creditLimit,
|
||||
});
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'CREATE_AGENT',
|
||||
module: 'AGENTS',
|
||||
targetId: user.id.toString(),
|
||||
});
|
||||
return jsonResponse(user);
|
||||
}
|
||||
|
||||
@Post('agents/:id/credit')
|
||||
async adjustCredit(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: DepositDto,
|
||||
) {
|
||||
const result = await this.agents.adjustCredit(
|
||||
BigInt(id),
|
||||
dto.amount,
|
||||
operatorId,
|
||||
dto.requestId,
|
||||
dto.remark,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('wallet/deposit')
|
||||
async deposit(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) {
|
||||
const result = await this.wallet.deposit(
|
||||
BigInt(dto.userId),
|
||||
dto.amount,
|
||||
operatorId,
|
||||
dto.remark,
|
||||
dto.requestId,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('wallet/withdraw')
|
||||
async withdraw(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) {
|
||||
const result = await this.wallet.withdraw(
|
||||
BigInt(dto.userId),
|
||||
dto.amount,
|
||||
operatorId,
|
||||
dto.remark,
|
||||
dto.requestId,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('wallet/transactions')
|
||||
async walletTransactions(@Query('userId') userId: string, @Query('page') page?: string) {
|
||||
const result = await this.wallet.getTransactions(BigInt(userId), page ? parseInt(page) : 1);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('leagues')
|
||||
async createLeague(@Body() dto: { code: string; translations: Record<string, string> }) {
|
||||
const league = await this.matches.createLeague(dto.code, dto.translations);
|
||||
return jsonResponse(league);
|
||||
}
|
||||
|
||||
@Post('teams')
|
||||
async createTeam(@Body() dto: { code: string; translations: Record<string, string> }) {
|
||||
const team = await this.matches.createTeam(dto.code, dto.translations);
|
||||
return jsonResponse(team);
|
||||
}
|
||||
|
||||
@Get('matches')
|
||||
async listMatches() {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
include: { markets: { include: { selections: true } } },
|
||||
orderBy: { startTime: 'desc' },
|
||||
});
|
||||
return jsonResponse(matches);
|
||||
}
|
||||
|
||||
@Post('matches')
|
||||
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateMatchDto) {
|
||||
const match = await this.matches.createMatch({
|
||||
leagueId: BigInt(dto.leagueId),
|
||||
homeTeamId: BigInt(dto.homeTeamId),
|
||||
awayTeamId: BigInt(dto.awayTeamId),
|
||||
startTime: new Date(dto.startTime),
|
||||
isHot: dto.isHot,
|
||||
createdBy: operatorId,
|
||||
});
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/:id/publish')
|
||||
async publishMatch(@Param('id') id: string) {
|
||||
const match = await this.matches.publishMatch(BigInt(id));
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/:id/close')
|
||||
async closeMatch(@Param('id') id: string) {
|
||||
const match = await this.matches.closeMatch(BigInt(id));
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/:id/cancel')
|
||||
async cancelMatch(@Param('id') id: string) {
|
||||
await this.matches.cancelMatch(BigInt(id));
|
||||
const voided = await this.settlement.voidMatchBets(BigInt(id));
|
||||
return jsonResponse(voided);
|
||||
}
|
||||
|
||||
@Post('matches/:id/markets/templates')
|
||||
async generateTemplates(@Param('id') id: string, @Body() dto: MarketTemplatesDto) {
|
||||
const markets = await this.markets.generateTemplates(BigInt(id), dto.marketTypes);
|
||||
return jsonResponse(markets);
|
||||
}
|
||||
|
||||
@Put('selections/:id/odds')
|
||||
async updateOdds(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateOddsDto,
|
||||
) {
|
||||
const selection = await this.markets.updateOdds(BigInt(id), dto.odds, operatorId);
|
||||
return jsonResponse(selection);
|
||||
}
|
||||
|
||||
@Post('matches/:id/settlement/score')
|
||||
async recordScore(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ScoreDto,
|
||||
) {
|
||||
const result = await this.settlement.recordScore(
|
||||
BigInt(id),
|
||||
dto.htHome,
|
||||
dto.htAway,
|
||||
dto.ftHome,
|
||||
dto.ftAway,
|
||||
operatorId,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('matches/:id/settlement/preview')
|
||||
async settlementPreview(@CurrentUser('id') operatorId: bigint, @Param('id') id: string) {
|
||||
const preview = await this.settlement.previewSettlement(BigInt(id), operatorId);
|
||||
return jsonResponse(preview);
|
||||
}
|
||||
|
||||
@Post('settlement/:batchId/confirm')
|
||||
async confirmSettlement(@CurrentUser('id') operatorId: bigint, @Param('batchId') batchId: string) {
|
||||
const result = await this.settlement.confirmSettlement(BigInt(batchId), operatorId);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('bets')
|
||||
async listBets(@Query('status') status?: string, @Query('page') page?: string) {
|
||||
const skip = ((page ? parseInt(page) : 1) - 1) * 20;
|
||||
const where = status ? { status } : {};
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.bet.findMany({
|
||||
where,
|
||||
include: { selections: true, user: true },
|
||||
orderBy: { placedAt: 'desc' },
|
||||
skip,
|
||||
take: 20,
|
||||
}),
|
||||
this.prisma.bet.count({ where }),
|
||||
]);
|
||||
return jsonResponse({ items, total });
|
||||
}
|
||||
|
||||
@Post('cashbacks/preview')
|
||||
async cashbackPreview(@Body() dto: CashbackPreviewDto) {
|
||||
const preview = await this.cashback.previewBatch(
|
||||
new Date(dto.periodStart),
|
||||
new Date(dto.periodEnd),
|
||||
);
|
||||
return jsonResponse(preview);
|
||||
}
|
||||
|
||||
@Post('cashbacks/:batchId/confirm')
|
||||
async cashbackConfirm(@CurrentUser('id') operatorId: bigint, @Param('batchId') batchId: string) {
|
||||
const result = await this.cashback.confirmBatch(BigInt(batchId), operatorId);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('contents')
|
||||
async listContents(@Query('type') type?: string) {
|
||||
const items = await this.content.listAll(type);
|
||||
return jsonResponse(items);
|
||||
}
|
||||
|
||||
@Post('contents')
|
||||
async createContent(@Body() dto: Parameters<ContentService['create']>[0]) {
|
||||
const item = await this.content.create(dto);
|
||||
return jsonResponse(item);
|
||||
}
|
||||
|
||||
@Get('i18n/messages')
|
||||
async getMessages(@Query('locale') locale = 'en-US') {
|
||||
const messages = await this.i18n.getMessages(locale);
|
||||
return jsonResponse(messages);
|
||||
}
|
||||
|
||||
@Get('audit-logs')
|
||||
async auditLogs(@Query('page') page?: string, @Query('module') module?: string) {
|
||||
const result = await this.audit.list(page ? parseInt(page) : 1, 50, module);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user