feat: 手动充值、邀请码注册与后台管理增强
新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分), 支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏, 并补充前台注册、充值页及多语言错误码。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,4 +17,6 @@ export const P = {
|
||||
content: 'content.manage',
|
||||
audit: 'audit.view',
|
||||
resetDatabase: 'settings.reset_database',
|
||||
depositManage: 'deposit.manage',
|
||||
depositReview: 'deposit.review',
|
||||
} as const;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { I18nModule } from '../../domains/operations/i18n/i18n.module';
|
||||
import { BetsModule } from '../../domains/betting/bets.module';
|
||||
import { DatabaseModule } from '../../infrastructure/database/database.module';
|
||||
import { SmokeTestModule } from '../../domains/operations/smoke-tests/smoke-test.module';
|
||||
import { DepositModule } from '../../domains/deposit/deposit.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -29,6 +30,7 @@ import { SmokeTestModule } from '../../domains/operations/smoke-tests/smoke-test
|
||||
BetsModule,
|
||||
DatabaseModule,
|
||||
SmokeTestModule,
|
||||
DepositModule,
|
||||
],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminDashboardService, PermissionsGuard],
|
||||
|
||||
@@ -8,11 +8,19 @@ import {
|
||||
Query,
|
||||
Headers,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { extname, join } from 'path';
|
||||
import { JwtAuthGuard, PlayerGuard } from '../../domains/identity/guards';
|
||||
import { CurrentUser, Public } from '../../shared/common/decorators';
|
||||
import { jsonResponse } from '../../shared/common/filters';
|
||||
import { appBadRequest } from '../../shared/common/app-error';
|
||||
import { getUploadRoot } from '../../shared/uploads/upload-paths';
|
||||
import { UsersService } from '../../domains/identity/users.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { WalletService } from '../../domains/ledger/wallet.service';
|
||||
@@ -21,6 +29,7 @@ import { OutrightService } from '../../domains/catalog/outright.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 { DepositService } from '../../domains/deposit/deposit.service';
|
||||
import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@@ -98,6 +107,7 @@ export class PlayerController {
|
||||
private content: ContentService,
|
||||
private cashback: CashbackService,
|
||||
private systemConfig: SystemConfigService,
|
||||
private deposit: DepositService,
|
||||
) {}
|
||||
|
||||
private async formatPlayerProfile(user: NonNullable<Awaited<ReturnType<UsersService['findById']>>>) {
|
||||
@@ -290,4 +300,68 @@ export class PlayerController {
|
||||
const items = await this.cashback.getUserCashbacks(userId);
|
||||
return jsonResponse(items);
|
||||
}
|
||||
|
||||
// ============ Deposit / Recharge ============
|
||||
|
||||
@Get('payment-methods')
|
||||
async paymentMethods(
|
||||
@Query('methodType') methodType?: string,
|
||||
@CurrentUser('locale') userLocale?: string,
|
||||
@Headers('x-locale') headerLocale?: string,
|
||||
) {
|
||||
const locale = userLocale || headerLocale || 'zh-CN';
|
||||
const items = await this.deposit.listPlayerPaymentMethods(methodType || undefined, locale);
|
||||
return jsonResponse(items);
|
||||
}
|
||||
|
||||
@Post('deposit-orders')
|
||||
@UseInterceptors(FileInterceptor('screenshot', { limits: { fileSize: 5 * 1024 * 1024 } }))
|
||||
async createDepositOrder(
|
||||
@CurrentUser('id') userId: bigint,
|
||||
@UploadedFile() file: { originalname: string; mimetype: string; buffer: Buffer; size: number } | undefined,
|
||||
@Body() body: { paymentMethodId: string; amount: string },
|
||||
) {
|
||||
if (!file) throw appBadRequest('SCREENSHOT_REQUIRED');
|
||||
if (!file.mimetype.startsWith('image/')) throw appBadRequest('FILE_MUST_BE_IMAGE');
|
||||
|
||||
const amount = parseFloat(body.amount);
|
||||
if (!amount || amount <= 0) throw appBadRequest('INVALID_AMOUNT');
|
||||
if (!body.paymentMethodId) throw appBadRequest('PAYMENT_METHOD_REQUIRED');
|
||||
|
||||
// Save screenshot
|
||||
const ext = extname(file.originalname || '.jpg').toLowerCase() || '.jpg';
|
||||
const filename = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
||||
const root = getUploadRoot();
|
||||
const targetDir = join(root, 'deposits');
|
||||
await mkdir(targetDir, { recursive: true });
|
||||
await writeFile(join(targetDir, filename), file.buffer);
|
||||
const screenshotUrl = `/uploads/deposits/${filename}`;
|
||||
|
||||
const order = await this.deposit.createDepositOrder(
|
||||
userId,
|
||||
BigInt(body.paymentMethodId),
|
||||
amount,
|
||||
screenshotUrl,
|
||||
);
|
||||
|
||||
return jsonResponse({
|
||||
id: order.id.toString(),
|
||||
orderNo: order.orderNo,
|
||||
amount: order.amount.toString(),
|
||||
status: order.status,
|
||||
createdAt: order.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('deposit-orders')
|
||||
async myDepositOrders(
|
||||
@CurrentUser('id') userId: bigint,
|
||||
@Query('page') page?: string,
|
||||
) {
|
||||
const result = await this.deposit.getPlayerDepositOrders(
|
||||
userId,
|
||||
page ? parseInt(page, 10) : 1,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { MatchesModule } from '../../domains/catalog/matches.module';
|
||||
import { BetsModule } from '../../domains/betting/bets.module';
|
||||
import { ContentModule } from '../../domains/operations/content/content.module';
|
||||
import { CashbackModule } from '../../domains/operations/cashback/cashback.module';
|
||||
import { DepositModule } from '../../domains/deposit/deposit.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule, WalletModule, MatchesModule, BetsModule, ContentModule, CashbackModule],
|
||||
imports: [UsersModule, WalletModule, MatchesModule, BetsModule, ContentModule, CashbackModule, DepositModule],
|
||||
controllers: [PlayerController],
|
||||
})
|
||||
export class PlayerModule {}
|
||||
|
||||
Reference in New Issue
Block a user