feat(admin+api): 列表页子路由重构、结算历史与充值截图清理
赛事/代理管理从表格展开改为子路由详情页;结算页增加预览弹窗与历史记录;媒体库支持截图存储统计与自动/手动清理;Player 端充值截图压缩为 WebP 300KB。
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';
|
||||
@@ -1109,6 +1111,17 @@ class InboxNotifySettingsDto {
|
||||
deposit?: boolean;
|
||||
}
|
||||
|
||||
class UpdateDepositCleanupConfigDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
keepDays?: number;
|
||||
}
|
||||
|
||||
class CashbackPreviewDto {
|
||||
@IsString()
|
||||
periodStart!: string;
|
||||
@@ -1236,6 +1249,7 @@ export class AdminController {
|
||||
private playerMessages: PlayerMessagesService,
|
||||
private staff: AdminStaffService,
|
||||
private presence: PresenceService,
|
||||
private depositCleanup: DepositScreenshotCleanupService,
|
||||
) {}
|
||||
|
||||
@Get('presence/online-count')
|
||||
@@ -1997,6 +2011,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 +2022,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 +2861,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(
|
||||
@@ -3158,6 +3199,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() {
|
||||
|
||||
@@ -558,6 +558,8 @@ export class MatchesService {
|
||||
pageSize?: number;
|
||||
hasBets?: string;
|
||||
orderBy?: string;
|
||||
startFrom?: Date;
|
||||
startTo?: Date;
|
||||
},
|
||||
) {
|
||||
const where: Prisma.MatchWhereInput = {
|
||||
@@ -566,6 +568,11 @@ export class MatchesService {
|
||||
isOutright: false,
|
||||
};
|
||||
if (opts.status) where.status = opts.status;
|
||||
if (opts.startFrom || opts.startTo) {
|
||||
where.startTime = {};
|
||||
if (opts.startFrom) where.startTime.gte = opts.startFrom;
|
||||
if (opts.startTo) where.startTime.lte = opts.startTo;
|
||||
}
|
||||
const kw = opts.keyword?.trim();
|
||||
if (kw) {
|
||||
where.OR = [
|
||||
@@ -626,6 +633,10 @@ export class MatchesService {
|
||||
const stakeB = parseFloat(b.totalStake);
|
||||
return stakeB - stakeA;
|
||||
});
|
||||
} else if (opts.orderBy === 'kickoffAsc') {
|
||||
filteredItems.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
|
||||
} else if (opts.orderBy === 'kickoffDesc') {
|
||||
filteredItems.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
||||
} else {
|
||||
filteredItems.sort((a, b) => {
|
||||
if (a.displayOrder !== b.displayOrder) {
|
||||
@@ -640,12 +651,19 @@ export class MatchesService {
|
||||
return { items: paginatedItems, total, page, pageSize };
|
||||
}
|
||||
|
||||
const orderBy =
|
||||
opts.orderBy === 'kickoffAsc'
|
||||
? [{ startTime: 'asc' as const }, { displayOrder: 'asc' as const }]
|
||||
: opts.orderBy === 'kickoffDesc'
|
||||
? [{ startTime: 'desc' as const }, { displayOrder: 'asc' as const }]
|
||||
: [{ displayOrder: 'asc' as const }, { startTime: 'desc' as const }];
|
||||
|
||||
const [total, rows] = await Promise.all([
|
||||
this.prisma.match.count({ where }),
|
||||
this.prisma.match.findMany({
|
||||
where,
|
||||
include: { homeTeam: true, awayTeam: true },
|
||||
orderBy: [{ displayOrder: 'asc' }, { startTime: 'desc' }],
|
||||
orderBy,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
@@ -865,9 +883,32 @@ export class MatchesService {
|
||||
|
||||
async getAdminMatchDetail(matchId: bigint) {
|
||||
const match = await this.requireAdminMatch(matchId);
|
||||
const scoreRow = await this.prisma.matchScore.findUnique({
|
||||
let scoreRow = await this.prisma.matchScore.findUnique({
|
||||
where: { matchId },
|
||||
});
|
||||
if (!scoreRow) {
|
||||
const previewBatch = await this.prisma.settlementBatch.findFirst({
|
||||
where: { matchId, status: 'PREVIEW' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
if (previewBatch) {
|
||||
scoreRow = {
|
||||
htHomeScore: previewBatch.htHomeScore,
|
||||
htAwayScore: previewBatch.htAwayScore,
|
||||
ftHomeScore: previewBatch.ftHomeScore,
|
||||
ftAwayScore: previewBatch.ftAwayScore,
|
||||
homeCorners: previewBatch.homeCorners,
|
||||
awayCorners: previewBatch.awayCorners,
|
||||
homeYellowCards: previewBatch.homeYellowCards,
|
||||
awayYellowCards: previewBatch.awayYellowCards,
|
||||
homeRedCards: previewBatch.homeRedCards,
|
||||
awayRedCards: previewBatch.awayRedCards,
|
||||
homeCards: previewBatch.homeCards,
|
||||
awayCards: previewBatch.awayCards,
|
||||
winnerTeamId: null,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
const markets = await this.prisma.market.findMany({
|
||||
where: { matchId },
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } } },
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { getUploadRoot } from '../../shared/uploads/upload-paths';
|
||||
import { join, dirname } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { mkdir, stat, unlink, writeFile } from 'fs/promises';
|
||||
|
||||
@Injectable()
|
||||
export class DepositScreenshotCleanupService implements OnModuleInit {
|
||||
private readonly logger = new Logger(DepositScreenshotCleanupService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private systemConfigService: SystemConfigService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.ensureExpiredPlaceholderExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保已过期/已清理的截图占位图存在
|
||||
*/
|
||||
async ensureExpiredPlaceholderExists() {
|
||||
// 1x1 像素透明 PNG
|
||||
const defaultExpiredPngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
const root = getUploadRoot();
|
||||
const expiredPath = join(root, 'defaults', 'expired.png');
|
||||
try {
|
||||
await mkdir(dirname(expiredPath), { recursive: true });
|
||||
// 强制写入以确保生成最新的透明 1x1 占位图
|
||||
await writeFile(expiredPath, Buffer.from(defaultExpiredPngBase64, 'base64'));
|
||||
this.logger.log('Ensured expired screenshot default placeholder (transparent 1x1).');
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to create default expired screenshot placeholder', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日凌晨3点定时执行清理
|
||||
*/
|
||||
@Cron('0 0 3 * * *')
|
||||
async handleScheduledCleanup() {
|
||||
this.logger.log('Scheduled deposit screenshot cleanup job started');
|
||||
try {
|
||||
const config = await this.systemConfigService.getDepositScreenshotCleanupConfig();
|
||||
if (!config.enabled) {
|
||||
this.logger.log('Scheduled cleanup is disabled, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const beforeDate = new Date();
|
||||
beforeDate.setDate(beforeDate.getDate() - config.keepDays);
|
||||
this.logger.log(`Cleaning deposit screenshots older than ${config.keepDays} days (before ${beforeDate.toISOString()})`);
|
||||
|
||||
const result = await this.cleanOldDepositScreenshots(beforeDate);
|
||||
this.logger.log(`Scheduled cleanup completed. Cleaned: ${result.cleaned} screenshots, Freed: ${(result.freedBytes / 1024 / 1024).toFixed(2)} MB`);
|
||||
} catch (err) {
|
||||
this.logger.error('Scheduled cleanup failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定日期之前的已处理充值订单截图
|
||||
*/
|
||||
async cleanOldDepositScreenshots(before: Date): Promise<{ cleaned: number; freedBytes: number }> {
|
||||
const orders = await this.prisma.depositOrder.findMany({
|
||||
where: {
|
||||
createdAt: { lt: before },
|
||||
status: { in: ['APPROVED', 'REJECTED'] },
|
||||
screenshotUrl: {
|
||||
startsWith: '/uploads/',
|
||||
not: { startsWith: '/uploads/defaults/' },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
screenshotUrl: true,
|
||||
},
|
||||
});
|
||||
|
||||
let cleaned = 0;
|
||||
let freedBytes = 0;
|
||||
const batchSize = 100;
|
||||
const root = getUploadRoot();
|
||||
|
||||
for (let i = 0; i < orders.length; i += batchSize) {
|
||||
const chunk = orders.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
chunk.map(async (order) => {
|
||||
const url = order.screenshotUrl;
|
||||
if (!url.startsWith('/uploads/')) return;
|
||||
const relative = url.slice('/uploads/'.length);
|
||||
// 安全路径校验,防止目录穿越
|
||||
if (!relative || relative.includes('..') || relative.includes('\\')) return;
|
||||
const filePath = join(root, relative);
|
||||
|
||||
let size = 0;
|
||||
try {
|
||||
const fileStats = await stat(filePath);
|
||||
size = fileStats.size;
|
||||
await unlink(filePath);
|
||||
freedBytes += size;
|
||||
} catch {
|
||||
// 文件不存在或已被删除,静默跳过,但依然更新数据库
|
||||
}
|
||||
|
||||
await this.prisma.depositOrder.update({
|
||||
where: { id: order.id },
|
||||
data: { screenshotUrl: '/uploads/defaults/expired.png' },
|
||||
});
|
||||
cleaned++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { cleaned, freedBytes };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DepositService } from './deposit.service';
|
||||
import { DepositScreenshotCleanupService } from './deposit-screenshot-cleanup.service';
|
||||
import { WalletModule } from '../ledger/wallet.module';
|
||||
import { AgentsModule } from '../agent/agents.module';
|
||||
import { PlayerMessagesModule } from '../player-messages/player-messages.module';
|
||||
@@ -7,7 +8,7 @@ import { SystemConfigModule } from '../../shared/config/system-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [WalletModule, AgentsModule, PlayerMessagesModule, SystemConfigModule],
|
||||
providers: [DepositService],
|
||||
exports: [DepositService],
|
||||
providers: [DepositService, DepositScreenshotCleanupService],
|
||||
exports: [DepositService, DepositScreenshotCleanupService],
|
||||
})
|
||||
export class DepositModule {}
|
||||
|
||||
@@ -388,6 +388,83 @@ export class SettlementService {
|
||||
};
|
||||
}
|
||||
|
||||
async getActivePreview(
|
||||
matchId: bigint,
|
||||
opts?: { page?: number; pageSize?: number },
|
||||
) {
|
||||
const batch = await this.prisma.settlementBatch.findFirst({
|
||||
where: { matchId, status: 'PREVIEW' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
if (!batch) return null;
|
||||
|
||||
const existingScore = await this.prisma.matchScore.findUnique({
|
||||
where: { matchId },
|
||||
});
|
||||
const computation = await this.computePreviewComputation(matchId, {
|
||||
htHome: batch.htHomeScore ?? 0,
|
||||
htAway: batch.htAwayScore ?? 0,
|
||||
ftHome: batch.ftHomeScore ?? 0,
|
||||
ftAway: batch.ftAwayScore ?? 0,
|
||||
homeCorners: batch.homeCorners ?? null,
|
||||
awayCorners: batch.awayCorners ?? null,
|
||||
homeYellowCards: batch.homeYellowCards ?? null,
|
||||
awayYellowCards: batch.awayYellowCards ?? null,
|
||||
homeRedCards: batch.homeRedCards ?? null,
|
||||
awayRedCards: batch.awayRedCards ?? null,
|
||||
homeCards: batch.homeCards ?? null,
|
||||
awayCards: batch.awayCards ?? null,
|
||||
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||
});
|
||||
|
||||
return this.buildPreviewResponse(computation, batch, opts);
|
||||
}
|
||||
|
||||
async getMatchSettlementHistory(matchId: bigint) {
|
||||
const batches = await this.prisma.settlementBatch.findMany({
|
||||
where: { matchId, status: 'CONFIRMED' },
|
||||
orderBy: { confirmedAt: 'desc' },
|
||||
});
|
||||
|
||||
const operatorIds = batches
|
||||
.map((b) => b.operatorId)
|
||||
.filter((id): id is bigint => id !== null);
|
||||
|
||||
const operators =
|
||||
operatorIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const operatorMap = new Map(operators.map((o) => [o.id.toString(), o.username]));
|
||||
|
||||
return batches.map((b) => ({
|
||||
id: b.id.toString(),
|
||||
batchNo: b.batchNo,
|
||||
htHomeScore: b.htHomeScore,
|
||||
htAwayScore: b.htAwayScore,
|
||||
ftHomeScore: b.ftHomeScore,
|
||||
ftAwayScore: b.ftAwayScore,
|
||||
homeCorners: b.homeCorners,
|
||||
awayCorners: b.awayCorners,
|
||||
homeYellowCards: b.homeYellowCards,
|
||||
awayYellowCards: b.awayYellowCards,
|
||||
homeRedCards: b.homeRedCards,
|
||||
awayRedCards: b.awayRedCards,
|
||||
homeCards: b.homeCards,
|
||||
awayCards: b.awayCards,
|
||||
totalBets: b.totalBets,
|
||||
totalPayout: b.totalPayout.toString(),
|
||||
totalRefund: b.totalRefund.toString(),
|
||||
confirmedAt: b.confirmedAt?.toISOString() ?? null,
|
||||
isResettle: b.isResettle,
|
||||
reason: b.reason,
|
||||
operatorUsername: b.operatorId ? operatorMap.get(b.operatorId.toString()) ?? '—' : '—',
|
||||
}));
|
||||
}
|
||||
|
||||
private buildPreviewResponse(
|
||||
computation: {
|
||||
scoreInput: ScoreInput;
|
||||
|
||||
@@ -263,4 +263,31 @@ export class SystemConfigService {
|
||||
}
|
||||
return this.getInboxNotifySettings();
|
||||
}
|
||||
|
||||
async getDepositScreenshotCleanupConfig(): Promise<{ enabled: boolean; keepDays: number }> {
|
||||
const enabled = await this.getBoolean('deposit.cleanup.enabled', false);
|
||||
const keepDays = await this.getInt('deposit.cleanup.keep_days', 180);
|
||||
return { enabled, keepDays };
|
||||
}
|
||||
|
||||
async updateDepositScreenshotCleanupConfig(data: { enabled?: boolean; keepDays?: number }) {
|
||||
if (data.enabled !== undefined) {
|
||||
await this.setBoolean(
|
||||
'deposit.cleanup.enabled',
|
||||
data.enabled,
|
||||
'是否开启定时清理充值截图',
|
||||
);
|
||||
}
|
||||
if (data.keepDays !== undefined) {
|
||||
if (!Number.isInteger(data.keepDays) || data.keepDays <= 0) {
|
||||
throw new Error('keepDays must be a positive integer');
|
||||
}
|
||||
await this.setInt(
|
||||
'deposit.cleanup.keep_days',
|
||||
data.keepDays,
|
||||
'充值截图保留天数',
|
||||
);
|
||||
}
|
||||
return this.getDepositScreenshotCleanupConfig();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user