feat: 手动充值、邀请码注册与后台管理增强

新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分),
支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏,
并补充前台注册、充值页及多语言错误码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:20:11 +08:00
parent 618fb49511
commit 10485ecfaf
98 changed files with 7908 additions and 856 deletions

View File

@@ -41,6 +41,7 @@ import { SystemConfigService } from '../../shared/config/system-config.service';
import { P } from './admin-permissions';
import { DatabaseResetService } from '../../infrastructure/database/database-reset.service';
import { SmokeTestService } from '../../domains/operations/smoke-tests/smoke-test.service';
import { DepositService } from '../../domains/deposit/deposit.service';
import {
IsString,
IsNumber,
@@ -56,7 +57,7 @@ import {
} from 'class-validator';
import type { ZhiboMatchExport, ZhiboMatchesBundleExport } from '../../domains/catalog/zhibo-match.types';
const UPLOAD_CATEGORIES = ['banners', 'teams', 'contents'] as const;
const UPLOAD_CATEGORIES = ['banners', 'teams', 'contents', 'payments', 'deposits'] as const;
type UploadCategory = (typeof UPLOAD_CATEGORIES)[number];
const IMAGE_MIME_EXT: Record<string, string> = {
@@ -236,6 +237,25 @@ class UpdatePlayerAdminDto {
@IsString()
@MinLength(8)
password?: string;
/** 玩家专属返水比例小数null 或 0 表示清除单独设置、使用默认 */
@IsOptional()
@ValidateIf((_, v) => v != null)
@IsNumber()
@Min(0)
cashbackRate?: number | null;
}
class PlatformDirectCashbackSettingsDto {
@IsOptional()
@IsNumber()
@Min(0)
platformDirectRate?: number;
@IsOptional()
@IsNumber()
@Min(0)
adminInviteRate?: number;
}
class PlayerAccountSettingsDto {
@@ -912,6 +932,7 @@ export class AdminController {
private bettingLimits: BettingLimitsService,
private databaseReset: DatabaseResetService,
private smokeTests: SmokeTestService,
private depositService: DepositService,
) {}
@Get('dashboard')
@@ -1080,7 +1101,15 @@ export class AdminController {
@Param('id') id: string,
@Body() dto: UpdatePlayerAdminDto,
) {
const detail = await this.users.updatePlayerAdmin(BigInt(id), dto);
const detail = await this.users.updatePlayerAdmin(BigInt(id), {
status: dto.status,
locale: dto.locale,
phone: dto.phone,
email: dto.email,
username: dto.username,
password: dto.password,
cashbackRate: dto.cashbackRate,
});
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
@@ -2003,6 +2032,20 @@ export class AdminController {
return jsonResponse(detail);
}
@Get('settings/cashback/platform-direct')
@RequirePermissions(P.cashback, P.reports)
async getPlatformDirectCashbackSettings() {
const settings = await this.systemConfig.getPlatformDirectCashbackSettings();
return jsonResponse(settings);
}
@Put('settings/cashback/platform-direct')
@RequirePermissions(P.cashback)
async updatePlatformDirectCashbackSettings(@Body() dto: PlatformDirectCashbackSettingsDto) {
const settings = await this.systemConfig.updatePlatformDirectCashbackSettings(dto);
return jsonResponse(settings);
}
@Post('cashbacks/preview')
@RequirePermissions(P.cashback, P.reports)
async cashbackPreview(@Body() dto: CashbackPreviewDto) {
@@ -2307,4 +2350,134 @@ export class AdminController {
);
return jsonResponse(result);
}
// ============ Payment Methods ============
@Post('payment-methods')
@RequirePermissions(P.depositManage)
async createPaymentMethod(
@CurrentUser('id') operatorId: bigint,
@Body() body: {
methodType: string;
bankName?: string;
accountHolder?: string;
accountNumber?: string;
usdtAddress?: string;
qrCodeUrl?: string;
displayName?: string;
sortOrder?: number;
isActive?: boolean;
showOnPlayer?: boolean;
translations?: {
displayName?: Record<string, string>;
bankName?: Record<string, string>;
};
},
) {
if (!body.methodType || !['BANK', 'USDT'].includes(body.methodType)) {
throw appBadRequest('INVALID_METHOD_TYPE');
}
const method = await this.depositService.createPaymentMethod({
...body,
createdBy: operatorId,
});
return jsonResponse(method);
}
@Get('payment-methods')
@RequirePermissions(P.depositManage)
async listPaymentMethods(@Query('methodType') methodType?: string) {
const items = await this.depositService.listPaymentMethods({
methodType: methodType || undefined,
});
return jsonResponse(items);
}
@Put('payment-methods/:id')
@RequirePermissions(P.depositManage)
async updatePaymentMethod(
@Param('id') id: string,
@Body() body: {
bankName?: string;
accountHolder?: string;
accountNumber?: string;
usdtAddress?: string;
qrCodeUrl?: string;
displayName?: string;
sortOrder?: number;
isActive?: boolean;
showOnPlayer?: boolean;
translations?: {
displayName?: Record<string, string>;
bankName?: Record<string, string>;
};
},
) {
const method = await this.depositService.updatePaymentMethod(BigInt(id), body);
return jsonResponse(method);
}
@Delete('payment-methods/:id')
@RequirePermissions(P.depositManage)
async deletePaymentMethod(@Param('id') id: string) {
await this.depositService.deletePaymentMethod(BigInt(id));
return jsonResponse({ success: true });
}
// ============ Deposit Orders (Admin Review) ============
@Get('deposit-orders')
@RequirePermissions(P.depositReview)
async listDepositOrders(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('status') status?: string,
@Query('keyword') keyword?: string,
@Query('methodType') methodType?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
) {
const result = await this.depositService.listDepositOrders({
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
status: status || undefined,
keyword: keyword || undefined,
methodType: methodType || undefined,
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
dateTo: dateTo ? new Date(dateTo) : undefined,
});
return jsonResponse(result);
}
@Post('deposit-orders/:id/approve')
@RequirePermissions(P.depositReview)
async approveDepositOrder(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() body: { approvedAmount?: number; remark?: string },
) {
const result = await this.depositService.approveDepositOrder(
BigInt(id),
operatorId,
body.approvedAmount,
body.remark,
);
return jsonResponse(result);
}
@Post('deposit-orders/:id/reject')
@RequirePermissions(P.depositReview)
async rejectDepositOrder(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() body: { reason: string },
) {
if (!body.reason?.trim()) throw appBadRequest('REASON_REQUIRED');
const result = await this.depositService.rejectDepositOrder(
BigInt(id),
operatorId,
body.reason.trim(),
);
return jsonResponse(result);
}
}