sync(theme-4): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-4 样式。
This commit is contained in:
@@ -15,10 +15,12 @@ import {
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { mkdir, writeFile, unlink } from 'fs/promises';
|
||||
import { mkdir, writeFile, unlink, readdir, stat } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { extname, join } from 'path';
|
||||
import { JwtAuthGuard, AdminGuard, PermissionsGuard } from '../../domains/identity/guards';
|
||||
import { ContentService } from '../../domains/operations/content/content.service';
|
||||
import { DepositScreenshotCleanupService } from '../../domains/deposit/deposit-screenshot-cleanup.service';
|
||||
import { CurrentUser, RequirePermissions } from '../../shared/common/decorators';
|
||||
import { jsonResponse } from '../../shared/common/filters';
|
||||
import { appBadRequest, appForbidden } from '../../shared/common/app-error';
|
||||
@@ -62,6 +64,7 @@ import {
|
||||
IsIn,
|
||||
Min,
|
||||
Max,
|
||||
MaxLength,
|
||||
Equals,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
@@ -1109,6 +1112,44 @@ class InboxNotifySettingsDto {
|
||||
deposit?: boolean;
|
||||
}
|
||||
|
||||
class BroadcastTranslationDto {
|
||||
@IsString()
|
||||
locale!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
body?: string;
|
||||
}
|
||||
|
||||
class CreatePlayerMessageBroadcastDto {
|
||||
@IsArray()
|
||||
translations!: BroadcastTranslationDto[];
|
||||
|
||||
@IsIn(['ALL', 'USER'])
|
||||
targetType!: 'ALL' | 'USER';
|
||||
|
||||
@ValidateIf((dto: CreatePlayerMessageBroadcastDto) => dto.targetType === 'USER')
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
targetUsername?: string;
|
||||
}
|
||||
|
||||
class UpdateDepositCleanupConfigDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
keepDays?: number;
|
||||
}
|
||||
|
||||
class CashbackPreviewDto {
|
||||
@IsString()
|
||||
periodStart!: string;
|
||||
@@ -1236,6 +1277,7 @@ export class AdminController {
|
||||
private playerMessages: PlayerMessagesService,
|
||||
private staff: AdminStaffService,
|
||||
private presence: PresenceService,
|
||||
private depositCleanup: DepositScreenshotCleanupService,
|
||||
) {}
|
||||
|
||||
@Get('presence/online-count')
|
||||
@@ -1997,6 +2039,8 @@ export class AdminController {
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('hasBets') hasBets?: string,
|
||||
@Query('orderBy') orderBy?: string,
|
||||
@Query('startFrom') startFrom?: string,
|
||||
@Query('startTo') startTo?: string,
|
||||
) {
|
||||
const result = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
|
||||
status: status || undefined,
|
||||
@@ -2006,6 +2050,8 @@ export class AdminController {
|
||||
pageSize: pageSize ? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20)) : 20,
|
||||
hasBets: hasBets || undefined,
|
||||
orderBy: orderBy || undefined,
|
||||
startFrom: startFrom ? new Date(startFrom) : undefined,
|
||||
startTo: startTo ? new Date(startTo) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
@@ -2843,6 +2889,29 @@ export class AdminController {
|
||||
return jsonResponse(preview);
|
||||
}
|
||||
|
||||
@Get('matches/:id/settlement/preview')
|
||||
@RequirePermissions(P.settlement, P.reports)
|
||||
async getActiveSettlementPreview(
|
||||
@Param('id') id: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const matchId = BigInt(id);
|
||||
const preview = await this.settlement.getActivePreview(matchId, {
|
||||
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
|
||||
pageSize: pageSize ? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 10)) : 10,
|
||||
});
|
||||
return jsonResponse(preview);
|
||||
}
|
||||
|
||||
@Get('matches/:id/settlement/history')
|
||||
@RequirePermissions(P.settlement, P.reports)
|
||||
async getMatchSettlementHistory(@Param('id') id: string) {
|
||||
const matchId = BigInt(id);
|
||||
const history = await this.settlement.getMatchSettlementHistory(matchId);
|
||||
return jsonResponse(history);
|
||||
}
|
||||
|
||||
@Get('settlement/:batchId/preview-items')
|
||||
@RequirePermissions(P.settlement)
|
||||
async getSettlementPreviewItems(
|
||||
@@ -3061,8 +3130,24 @@ export class AdminController {
|
||||
@Query('category') category?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('imagesOnly') imagesOnly?: string,
|
||||
) {
|
||||
const where = category && UPLOAD_CATEGORIES.includes(category as any) ? { category } : {};
|
||||
const where: {
|
||||
category?: string | { in: string[] };
|
||||
mimeType?: { startsWith: string };
|
||||
} = {};
|
||||
|
||||
if (category && UPLOAD_CATEGORIES.includes(category as UploadCategory)) {
|
||||
where.category = category;
|
||||
} else if (imagesOnly === '1' || imagesOnly === 'true') {
|
||||
// 媒体库选择器:默认可选 banners/teams/contents/payments,不含 deposits
|
||||
where.category = { in: ['banners', 'teams', 'contents', 'payments'] };
|
||||
}
|
||||
|
||||
if (imagesOnly === '1' || imagesOnly === 'true') {
|
||||
where.mimeType = { startsWith: 'image/' };
|
||||
}
|
||||
|
||||
const take = Math.min(parseInt(pageSize ?? '50', 10) || 50, 200);
|
||||
const skip = (Math.max(parseInt(page ?? '1', 10) || 1, 1) - 1) * take;
|
||||
|
||||
@@ -3158,6 +3243,103 @@ export class AdminController {
|
||||
return urls;
|
||||
}
|
||||
|
||||
@Get('files/storage-stats')
|
||||
@RequirePermissions(P.content)
|
||||
async getStorageStats() {
|
||||
const statsGroup = await this.prisma.uploadedFile.groupBy({
|
||||
by: ['category'],
|
||||
_count: { _all: true },
|
||||
_sum: { size: true }
|
||||
});
|
||||
|
||||
const categories = statsGroup.map((g) => ({
|
||||
category: g.category,
|
||||
count: g._count._all,
|
||||
sizeBytes: g._sum.size ?? 0,
|
||||
}));
|
||||
|
||||
// Calculate deposits on disk
|
||||
let depositCount = 0;
|
||||
let depositSizeBytes = 0;
|
||||
const depositsDir = join(getUploadRoot(), 'deposits');
|
||||
try {
|
||||
const files = await readdir(depositsDir);
|
||||
for (const file of files) {
|
||||
const filePath = join(depositsDir, file);
|
||||
try {
|
||||
const fileStats = await stat(filePath);
|
||||
if (fileStats.isFile()) {
|
||||
depositCount++;
|
||||
depositSizeBytes += fileStats.size;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
categories.push({
|
||||
category: 'deposits',
|
||||
count: depositCount,
|
||||
sizeBytes: depositSizeBytes,
|
||||
});
|
||||
|
||||
const totalCount = categories.reduce((sum, c) => sum + c.count, 0);
|
||||
const totalSizeBytes = categories.reduce((sum, c) => sum + c.sizeBytes, 0);
|
||||
|
||||
return jsonResponse({
|
||||
categories,
|
||||
total: {
|
||||
count: totalCount,
|
||||
sizeBytes: totalSizeBytes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Get('deposits/screenshot-cleanup-config')
|
||||
@RequirePermissions(P.content)
|
||||
async getScreenshotCleanupConfig() {
|
||||
const config = await this.systemConfig.getDepositScreenshotCleanupConfig();
|
||||
return jsonResponse(config);
|
||||
}
|
||||
|
||||
@Put('deposits/screenshot-cleanup-config')
|
||||
@RequirePermissions(P.content)
|
||||
async updateScreenshotCleanupConfig(@Body() body: UpdateDepositCleanupConfigDto) {
|
||||
const config = await this.systemConfig.updateDepositScreenshotCleanupConfig(body);
|
||||
return jsonResponse(config);
|
||||
}
|
||||
|
||||
@Delete('deposits/screenshots')
|
||||
@RequirePermissions(P.content)
|
||||
async cleanOldScreenshots(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Query('before') beforeStr?: string,
|
||||
) {
|
||||
if (!beforeStr) throw appBadRequest('BEFORE_DATE_REQUIRED');
|
||||
const beforeDate = new Date(beforeStr);
|
||||
if (Number.isNaN(beforeDate.getTime())) {
|
||||
throw appBadRequest('INVALID_BEFORE_DATE');
|
||||
}
|
||||
if (beforeDate.getTime() > Date.now()) {
|
||||
throw appBadRequest('BEFORE_DATE_CANNOT_BE_FUTURE');
|
||||
}
|
||||
|
||||
const result = await this.depositCleanup.cleanOldDepositScreenshots(beforeDate);
|
||||
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'PURGE_DEPOSIT_SCREENSHOTS',
|
||||
module: 'MEDIA',
|
||||
afterData: JSON.stringify({
|
||||
before: beforeDate.toISOString(),
|
||||
cleanedCount: result.cleaned,
|
||||
freedBytes: result.freedBytes,
|
||||
}),
|
||||
});
|
||||
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('contents/inbox-notify-settings')
|
||||
@RequirePermissions(P.content, P.reports)
|
||||
async getInboxNotifySettings() {
|
||||
@@ -3172,6 +3354,68 @@ export class AdminController {
|
||||
return jsonResponse(settings);
|
||||
}
|
||||
|
||||
@Get('player-message-broadcasts')
|
||||
@RequirePermissions(P.content, P.reports)
|
||||
async listPlayerMessageBroadcasts(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const p = Math.max(1, page ? parseInt(page, 10) || 1 : 1);
|
||||
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 20), 50);
|
||||
const result = await this.playerMessages.listBroadcasts(p, size);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('player-message-broadcasts')
|
||||
@RequirePermissions(P.content)
|
||||
async createPlayerMessageBroadcast(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Body() dto: CreatePlayerMessageBroadcastDto,
|
||||
) {
|
||||
const operator = await this.prisma.user.findUnique({
|
||||
where: { id: operatorId },
|
||||
select: { username: true },
|
||||
});
|
||||
const item = await this.playerMessages.createCustomBroadcast({
|
||||
translations: dto.translations,
|
||||
targetType: dto.targetType,
|
||||
targetUsername: dto.targetUsername,
|
||||
createdById: operatorId,
|
||||
createdByUsername: operator?.username ?? null,
|
||||
});
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'SEND_PLAYER_MESSAGE_BROADCAST',
|
||||
module: 'CONTENT',
|
||||
afterData: JSON.stringify({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
targetType: item.targetType,
|
||||
recipientCount: item.recipientCount,
|
||||
}),
|
||||
});
|
||||
return jsonResponse(item);
|
||||
}
|
||||
|
||||
@Delete('player-message-broadcasts/:id')
|
||||
@RequirePermissions(P.content)
|
||||
async deletePlayerMessageBroadcast(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const result = await this.playerMessages.deleteBroadcast(BigInt(id));
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'DELETE_PLAYER_MESSAGE_BROADCAST',
|
||||
module: 'CONTENT',
|
||||
targetId: id,
|
||||
afterData: JSON.stringify(result),
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('contents')
|
||||
@RequirePermissions(P.content, P.reports)
|
||||
async listContents(
|
||||
|
||||
Reference in New Issue
Block a user