重构 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,179 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard, PlayerGuard } from '../../domains/identity/guards';
import { CurrentUser } from '../../shared/common/decorators';
import { jsonResponse } from '../../shared/common/filters';
import { UsersService } from '../../domains/identity/users.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { MatchesService } from '../../domains/catalog/matches.service';
import { BetsService } from '../../domains/betting/bets.service';
import { ContentService } from '../../domains/operations/content/content.service';
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
class SingleBetDto {
@IsString()
selectionId!: string;
@IsString()
oddsVersion!: string;
@IsNumber()
@Min(0.01)
stake!: number;
@IsString()
requestId!: string;
}
class ParlayLegDto {
@IsString()
selectionId!: string;
@IsString()
oddsVersion!: string;
}
class ParlayBetDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => ParlayLegDto)
legs!: ParlayLegDto[];
@IsNumber()
@Min(0.01)
stake!: number;
@IsString()
requestId!: string;
}
class LocaleDto {
@IsString()
locale!: string;
}
@ApiTags('Player')
@Controller('player')
@UseGuards(JwtAuthGuard, PlayerGuard)
@ApiBearerAuth()
export class PlayerController {
constructor(
private users: UsersService,
private wallet: WalletService,
private matches: MatchesService,
private bets: BetsService,
private content: ContentService,
private cashback: CashbackService,
) {}
@Get('profile')
async profile(@CurrentUser('id') userId: bigint) {
const user = await this.users.findById(userId);
return jsonResponse(user);
}
@Post('language')
async setLanguage(@CurrentUser('id') userId: bigint, @Body() dto: LocaleDto) {
const result = await this.users.updateLocale(userId, dto.locale);
return jsonResponse(result);
}
@Get('home')
async home(@CurrentUser('locale') locale: string) {
const [banners, notices, ticker, hotMatches, todayMatches] = await Promise.all([
this.content.listActive('BANNER', locale),
this.content.listActive('NOTICE', locale),
this.content.listActive('TICKER', locale),
this.matches.listPublished(locale),
this.matches.listPublished(locale),
]);
return jsonResponse({
banners,
notices,
ticker,
hotMatches: (hotMatches as Array<{ isHot?: boolean }>).filter((m) => m.isHot),
todayMatches,
});
}
@Get('matches')
async listMatches(
@CurrentUser('locale') locale: string,
@Query('leagueId') leagueId?: string,
) {
const items = await this.matches.listPublished(locale, leagueId ? BigInt(leagueId) : undefined);
return jsonResponse(items);
}
@Get('matches/:id')
async matchDetail(@Param('id') id: string, @CurrentUser('locale') locale: string) {
const match = await this.matches.getMatchDetail(BigInt(id), locale);
return jsonResponse(match);
}
@Post('bets/single')
async singleBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: SingleBetDto) {
const bet = await this.bets.placeSingleBet(
userId,
parentId,
BigInt(dto.selectionId),
BigInt(dto.oddsVersion),
dto.stake,
dto.requestId,
);
return jsonResponse(bet);
}
@Post('bets/parlay')
async parlayBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: ParlayBetDto) {
const bet = await this.bets.placeParlayBet(
userId,
parentId,
dto.legs.map((l) => ({ selectionId: BigInt(l.selectionId), oddsVersion: BigInt(l.oddsVersion) })),
dto.stake,
dto.requestId,
);
return jsonResponse(bet);
}
@Get('bets')
async myBets(
@CurrentUser('id') userId: bigint,
@Query('status') status?: string,
@Query('page') page?: string,
) {
const result = await this.bets.getUserBets(userId, status, page ? parseInt(page) : 1);
return jsonResponse(result);
}
@Get('bets/:betNo')
async betDetail(@CurrentUser('id') userId: bigint, @Param('betNo') betNo: string) {
const bet = await this.bets.getBetByNo(betNo, userId);
return jsonResponse(bet);
}
@Get('wallet/transactions')
async transactions(
@CurrentUser('id') userId: bigint,
@Query('page') page?: string,
) {
const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1);
return jsonResponse(result);
}
@Get('cashbacks')
async cashbacks(@CurrentUser('id') userId: bigint) {
const items = await this.cashback.getUserCashbacks(userId);
return jsonResponse(items);
}
}