| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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<VideoCacheRebuildResponse>} 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<VideoCacheRebuildResponse> {
- 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<VideoCacheDeletionStats> {
- 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;
- }
|