feat: 开户备注、账单展示优化与后台代理管理增强

- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示

- 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分

- 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端

- 后台代理/玩家表格操作栏重构,充值订单增加备注列

- 前台个人中心、充值、账单与验证码组件体验优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 17:23:58 +08:00
parent 10485ecfaf
commit 03e72ca9b2
46 changed files with 3721 additions and 1059 deletions

View File

@@ -556,6 +556,12 @@ class UpdatePlatformMatchDto {
awayTeamLogoUrl?: string;
}
class ReopenMatchDto {
@IsOptional()
@IsString()
startTime?: string;
}
class BatchMatchOddsDto {
@IsArray()
updates!: OutrightOddsUpdateItemDto[];
@@ -1643,6 +1649,14 @@ export class AdminController {
return jsonResponse(match);
}
@Post('matches/:id/reopen')
@RequirePermissions(P.matches)
async reopenMatch(@Param('id') id: string, @Body() dto: ReopenMatchDto) {
const startTime = dto.startTime ? new Date(dto.startTime) : undefined;
const match = await this.matches.reopenMatch(BigInt(id), startTime);
return jsonResponse(match);
}
@Post('matches/:id/cancel')
@RequirePermissions(P.matches)
async cancelMatch(@Param('id') id: string) {

View File

@@ -12,7 +12,8 @@ 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 { appForbidden } from '../../shared/common/app-error';
import { appBadRequest, appForbidden } from '../../shared/common/app-error';
import { validateInitialDepositRemark } from '@thebet365/shared';
import { AgentsService } from '../../domains/agent/agents.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { BetsService } from '../../domains/betting/bets.service';
@@ -164,7 +165,11 @@ export class AgentPortalController {
const profile = await this.agents.getProfile(agentId);
const maxLevel = await this.agents.getMaxAgentLevel();
return jsonResponse({
...profile,
level: profile.level,
creditLimit: profile.creditLimit.toString(),
usedCredit: profile.usedCredit.toString(),
availableCredit: profile.availableCredit.toString(),
cashbackRate: profile.cashbackRate.toString(),
maxAgentLevel: maxLevel,
canManageSubAgents: this.agents.canCreateSubAgent(level, maxLevel),
});
@@ -176,6 +181,25 @@ export class AgentPortalController {
return jsonResponse(players);
}
@Get('players/scoped')
async listScopedPlayers(
@CurrentUser('id') agentId: bigint,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('keyword') keyword?: string,
@Query('status') status?: string,
@Query('parentAgentId') parentAgentId?: string,
) {
const result = await this.agents.listSubtreePlayersForPortal(agentId, {
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
keyword,
status,
parentAgentId,
});
return jsonResponse(result);
}
@Post('players')
async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) {
const user = await this.agents.createPlayer(agentId, {
@@ -188,12 +212,14 @@ export class AgentPortalController {
});
if (dto.initialDeposit != null && dto.initialDeposit > 0) {
const remarkResult = validateInitialDepositRemark(dto.initialDeposit, dto.remark, 'agent');
if (!remarkResult.ok) throw appBadRequest(remarkResult.code);
await this.agents.depositToPlayer(
agentId,
user.id,
dto.initialDeposit,
`agent-create-${user.id}-${Date.now()}`,
dto.remark ?? '开户初始余额',
remarkResult.remark,
);
}
@@ -245,6 +271,40 @@ export class AgentPortalController {
return jsonResponse(agents);
}
@Get('agents/level-counts')
async subtreeAgentLevelCounts(@CurrentUser('id') agentId: bigint) {
const counts = await this.agents.countSubtreeAgentsByLevel(agentId);
return jsonResponse(counts);
}
@Get('agents/options')
async subtreeAgentOptions(@CurrentUser('id') agentId: bigint) {
const options = await this.agents.listSubtreeAgentOptions(agentId);
return jsonResponse(options);
}
@Get('agents/by-level')
async subtreeAgentsByLevel(
@CurrentUser('id') agentId: bigint,
@Query('level') level: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('keyword') keyword?: string,
@Query('status') status?: string,
) {
const lvl = parseInt(level, 10);
if (!Number.isFinite(lvl) || lvl < 1) {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
const result = await this.agents.listSubtreeAgentsAtLevel(agentId, lvl, {
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
keyword,
status,
});
return jsonResponse(result);
}
@Post('agents')
async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) {
const maxLevel = await this.agents.getMaxAgentLevel();
@@ -323,6 +383,19 @@ export class AgentPortalController {
return jsonResponse(detail);
}
@Get('agents/:id/downline')
async getSubAgentDownline(
@CurrentUser('id') agentId: bigint,
@CurrentUser('agentLevel') level: number,
@Param('id') subAgentId: string,
) {
if (!(await this.canManageSubAgents(level))) {
return jsonResponse({ agents: [], players: [] });
}
const downline = await this.agents.getDirectChildDownlineView(agentId, BigInt(subAgentId));
return jsonResponse(downline);
}
@Get('agents/:id/players')
async listSubAgentPlayers(
@CurrentUser('id') agentId: bigint,
@@ -332,8 +405,8 @@ export class AgentPortalController {
if (!(await this.canManageSubAgents(level))) {
return jsonResponse([]);
}
await this.agents.assertDirectChildAgent(agentId, BigInt(subAgentId));
const players = await this.agents.getDirectPlayers(BigInt(subAgentId));
await this.agents.assertDescendantAgent(agentId, BigInt(subAgentId));
const players = await this.agents.getPortalAgentDirectPlayers(agentId, BigInt(subAgentId));
return jsonResponse(players);
}
@@ -494,16 +567,7 @@ export class AgentPortalController {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
if (playerId) {
const player = await this.prisma.user.findFirst({
where: { id: BigInt(playerId), userType: 'PLAYER', deletedAt: null },
select: { id: true, parentId: true },
});
if (
!player?.parentId ||
!scopedParentAgentIds.some((id) => id === player.parentId)
) {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
await this.agents.requirePlayerInPortalSubtree(agentId, BigInt(playerId));
}
const result = await this.wallet.listWalletTransactionsAdmin({
page: page ? parseInt(page, 10) : 1,