Files
thebet365/apps/api/src/applications/player/player.controller.ts
Mars b5dca1bfb1 feat(player): 完善 H5 投注端与 API 演示数据
- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑
- 固定顶栏、公告与底栏,仅内容区滚动
- 底部导航与站点 favicon 使用 logo,登录页精简
- API 种子、冠军盘与历史注单增强

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 17:18:11 +08:00

205 lines
5.5 KiB
TypeScript

import {
Controller,
Get,
Post,
Patch,
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;
}
class UpdateProfileDto {
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
email?: 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);
}
@Patch('profile')
async updateProfile(@CurrentUser('id') userId: bigint, @Body() dto: UpdateProfileDto) {
const user = await this.users.updateProfile(userId, dto);
return jsonResponse(user);
}
@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('outrights')
async listOutrights(@CurrentUser('locale') locale: string) {
const items = await this.matches.listOutrights(locale);
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,
@CurrentUser('locale') locale: string,
@Query('status') status?: string,
@Query('page') page?: string,
) {
const result = await this.bets.getUserBets(userId, status, page ? parseInt(page) : 1);
const items = await this.matches.enrichBetsForHistory(result.items, locale);
return jsonResponse({ ...result, items });
}
@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);
}
}