import { Controller, Post, Logger, BadRequestException } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { RedisService } from '@box/db/redis/redis.service'; import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service'; import { VideoCategoryCacheBuilder } from '@box/core/cache/video/category/video-category-cache.builder'; import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider'; /** * VideoCacheAdminController * * PRODUCTION-SAFE ADMIN ENDPOINT * ═══════════════════════════════════════════════════════════════════════════ * * This controller provides AUTHORIZED ADMIN-ONLY endpoints for video cache management. * Unlike dev endpoints (/dev/cache/...), this is designed for production use. * * Routes: * - POST /api/v1/mgnt/admin/cache/video/rebuild * * Access Control: * - Protected by RbacGuard + JwtAuthGuard * - Requires proper API permissions (configured via role-based menu API permissions) * - Suitable only for super-admin or trusted operators * * Response: * - Minimal, no internal Redis details or raw key information * - Success indicator + deletion counts + rebuild timestamp * - Detailed logging via Logger for audit trail * * ═══════════════════════════════════════════════════════════════════════════ * IMPORTANT: This is production-safe and will NOT be disabled in production. * Access is controlled via role-based permissions (RbacGuard). * ═══════════════════════════════════════════════════════════════════════════ */ @ApiTags('缓存管理 - Admin (生产)', 'Cache Management - Admin (Production)') @Controller('api/v1/mgnt/admin/cache/video') export class VideoCacheAdminController { private readonly logger = new Logger(VideoCacheAdminController.name); constructor( private readonly redisService: RedisService, private readonly mongoPrisma: MongoPrismaService, private readonly videoCategoryCacheBuilder: VideoCategoryCacheBuilder, ) {} /** * POST /api/v1/mgnt/admin/cache/video/rebuild * * Rebuild video cache: clear old keys and rebuild all categories/tags. * * This endpoint: * 1. Scans for and deletes video cache keys matching patterns * 2. Calls VideoCategoryCacheBuilder.buildAll() to repopulate cache * 3. Returns deletion counts and rebuild timestamp * * Patterns deleted: * - box:app:video:category:list:* (category video lists) * - box:app:video:tag:list:* (tag-filtered video lists) * - box:app:tag:list:* (tag metadata lists) * * @returns {Promise} Success indicator, deletion stats, rebuild timestamp * @throws {BadRequestException} If Redis scan or build operation fails */ @Post('rebuild') @ApiOperation({ summary: 'Rebuild video cache (admin only)', description: 'Clear video cache keys and rebuild from database. Requires admin role permissions.', }) @ApiResponse({ status: 200, description: 'Cache rebuild completed successfully', schema: { type: 'object', example: { success: true, deleted: { categoryLists: 15, tagVideoLists: 42, tagMetadataLists: 8, total: 65, }, rebuiltAt: '2025-12-06T10:30:45.123Z', }, }, }) @ApiResponse({ status: 400, description: 'Cache operation failed (e.g., Redis error, build error)', }) @ApiResponse({ status: 403, description: 'Forbidden - User lacks admin permissions', }) async rebuildVideoCache(): Promise { const startTime = Date.now(); this.logger.log('[AdminCache] Starting video cache rebuild...'); try { // Step 1: Delete video cache keys by pattern const deletionStats = await this.deleteVideoCache(); // Step 2: Rebuild video cache from database this.logger.log('[AdminCache] Triggering cache builder rebuild...'); try { await this.videoCategoryCacheBuilder.buildAll(); this.logger.log('[AdminCache] Cache rebuild completed successfully'); } catch (buildError) { this.logger.error( `[AdminCache] Cache rebuild failed: ${buildError instanceof Error ? buildError.message : 'Unknown error'}`, buildError instanceof Error ? buildError.stack : undefined, ); throw new BadRequestException('Cache rebuild operation failed'); } // Step 3: Return success response with stats const duration = Date.now() - startTime; this.logger.log( `[AdminCache] Video cache rebuild completed successfully (${duration}ms)`, ); return { success: true, deleted: deletionStats, rebuiltAt: new Date().toISOString(), }; } catch (error) { const duration = Date.now() - startTime; this.logger.error( `[AdminCache] Video cache rebuild failed after ${duration}ms: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error.stack : undefined, ); if (error instanceof BadRequestException) { throw error; } throw new BadRequestException('Video cache rebuild operation failed'); } } /** * Delete video cache keys matching specific patterns * * Patterns deleted: * - box:app:video:category:list:* * - box:app:video:tag:list:* * - box:app:tag:list:* */ private async deleteVideoCache(): Promise { this.logger.log('[AdminCache] Deleting video cache keys...'); const stats: VideoCacheDeletionStats = { categoryLists: 0, tagVideoLists: 0, tagMetadataLists: 0, total: 0, }; try { // Delete category video lists: box:app:video:category:list:* const categoryListKeys = await this.redisService.keys( tsCacheKeys.video.categoryList('*'), ); if (categoryListKeys.length > 0) { const deleted = await this.redisService.del(...categoryListKeys); stats.categoryLists = deleted; stats.total += deleted; this.logger.log(`[AdminCache] Deleted ${deleted} category video lists`); } // Delete tag video lists: box:app:video:tag:list:*:* const tagListKeys = await this.redisService.keys( tsCacheKeys.video.tagList('*', '*'), ); if (tagListKeys.length > 0) { const deleted = await this.redisService.del(...tagListKeys); stats.tagVideoLists = deleted; stats.total += deleted; this.logger.log(`[AdminCache] Deleted ${deleted} tag video lists`); } // Delete tag metadata lists: box:app:tag:list:* const tagMetadataKeys = await this.redisService.keys( tsCacheKeys.tag.metadataByCategory('*'), ); if (tagMetadataKeys.length > 0) { const deleted = await this.redisService.del(...tagMetadataKeys); stats.tagMetadataLists = deleted; stats.total += deleted; this.logger.log(`[AdminCache] Deleted ${deleted} tag metadata lists`); } this.logger.log(`[AdminCache] Total keys deleted: ${stats.total}`); return stats; } catch (error) { this.logger.error( `[AdminCache] Failed to delete cache keys: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error.stack : undefined, ); throw new BadRequestException('Failed to delete video cache keys'); } } } /** * Response shape for video cache rebuild endpoint */ export interface VideoCacheDeletionStats { categoryLists: number; tagVideoLists: number; tagMetadataLists: number; total: number; } export interface VideoCacheRebuildResponse { success: boolean; deleted: VideoCacheDeletionStats; rebuiltAt: string; }