video-cache-admin.controller.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { Controller, Post, Logger, BadRequestException } from '@nestjs/common';
  2. import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
  3. import { RedisService } from '@box/db/redis/redis.service';
  4. import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
  5. import { VideoCategoryCacheBuilder } from '@box/core/cache/video/category/video-category-cache.builder';
  6. import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
  7. /**
  8. * VideoCacheAdminController
  9. *
  10. * PRODUCTION-SAFE ADMIN ENDPOINT
  11. * ═══════════════════════════════════════════════════════════════════════════
  12. *
  13. * This controller provides AUTHORIZED ADMIN-ONLY endpoints for video cache management.
  14. * Unlike dev endpoints (/dev/cache/...), this is designed for production use.
  15. *
  16. * Routes:
  17. * - POST /api/v1/mgnt/admin/cache/video/rebuild
  18. *
  19. * Access Control:
  20. * - Protected by RbacGuard + JwtAuthGuard
  21. * - Requires proper API permissions (configured via role-based menu API permissions)
  22. * - Suitable only for super-admin or trusted operators
  23. *
  24. * Response:
  25. * - Minimal, no internal Redis details or raw key information
  26. * - Success indicator + deletion counts + rebuild timestamp
  27. * - Detailed logging via Logger for audit trail
  28. *
  29. * ═══════════════════════════════════════════════════════════════════════════
  30. * IMPORTANT: This is production-safe and will NOT be disabled in production.
  31. * Access is controlled via role-based permissions (RbacGuard).
  32. * ═══════════════════════════════════════════════════════════════════════════
  33. */
  34. @ApiTags('缓存管理 - Admin (生产)', 'Cache Management - Admin (Production)')
  35. @Controller('api/v1/mgnt/admin/cache/video')
  36. export class VideoCacheAdminController {
  37. private readonly logger = new Logger(VideoCacheAdminController.name);
  38. constructor(
  39. private readonly redisService: RedisService,
  40. private readonly mongoPrisma: MongoPrismaService,
  41. private readonly videoCategoryCacheBuilder: VideoCategoryCacheBuilder,
  42. ) {}
  43. /**
  44. * POST /api/v1/mgnt/admin/cache/video/rebuild
  45. *
  46. * Rebuild video cache: clear old keys and rebuild all categories/tags.
  47. *
  48. * This endpoint:
  49. * 1. Scans for and deletes video cache keys matching patterns
  50. * 2. Calls VideoCategoryCacheBuilder.buildAll() to repopulate cache
  51. * 3. Returns deletion counts and rebuild timestamp
  52. *
  53. * Patterns deleted:
  54. * - box:app:video:category:list:* (category video lists)
  55. * - box:app:video:tag:list:* (tag-filtered video lists)
  56. * - box:app:tag:list:* (tag metadata lists)
  57. *
  58. * @returns {Promise<VideoCacheRebuildResponse>} Success indicator, deletion stats, rebuild timestamp
  59. * @throws {BadRequestException} If Redis scan or build operation fails
  60. */
  61. @Post('rebuild')
  62. @ApiOperation({
  63. summary: 'Rebuild video cache (admin only)',
  64. description:
  65. 'Clear video cache keys and rebuild from database. Requires admin role permissions.',
  66. })
  67. @ApiResponse({
  68. status: 200,
  69. description: 'Cache rebuild completed successfully',
  70. schema: {
  71. type: 'object',
  72. example: {
  73. success: true,
  74. deleted: {
  75. categoryLists: 15,
  76. tagVideoLists: 42,
  77. tagMetadataLists: 8,
  78. total: 65,
  79. },
  80. rebuiltAt: '2025-12-06T10:30:45.123Z',
  81. },
  82. },
  83. })
  84. @ApiResponse({
  85. status: 400,
  86. description: 'Cache operation failed (e.g., Redis error, build error)',
  87. })
  88. @ApiResponse({
  89. status: 403,
  90. description: 'Forbidden - User lacks admin permissions',
  91. })
  92. async rebuildVideoCache(): Promise<VideoCacheRebuildResponse> {
  93. const startTime = Date.now();
  94. this.logger.log('[AdminCache] Starting video cache rebuild...');
  95. try {
  96. // Step 1: Delete video cache keys by pattern
  97. const deletionStats = await this.deleteVideoCache();
  98. // Step 2: Rebuild video cache from database
  99. this.logger.log('[AdminCache] Triggering cache builder rebuild...');
  100. try {
  101. await this.videoCategoryCacheBuilder.buildAll();
  102. this.logger.log('[AdminCache] Cache rebuild completed successfully');
  103. } catch (buildError) {
  104. this.logger.error(
  105. `[AdminCache] Cache rebuild failed: ${buildError instanceof Error ? buildError.message : 'Unknown error'}`,
  106. buildError instanceof Error ? buildError.stack : undefined,
  107. );
  108. throw new BadRequestException('Cache rebuild operation failed');
  109. }
  110. // Step 3: Return success response with stats
  111. const duration = Date.now() - startTime;
  112. this.logger.log(
  113. `[AdminCache] Video cache rebuild completed successfully (${duration}ms)`,
  114. );
  115. return {
  116. success: true,
  117. deleted: deletionStats,
  118. rebuiltAt: new Date().toISOString(),
  119. };
  120. } catch (error) {
  121. const duration = Date.now() - startTime;
  122. this.logger.error(
  123. `[AdminCache] Video cache rebuild failed after ${duration}ms: ${error instanceof Error ? error.message : 'Unknown error'}`,
  124. error instanceof Error ? error.stack : undefined,
  125. );
  126. if (error instanceof BadRequestException) {
  127. throw error;
  128. }
  129. throw new BadRequestException('Video cache rebuild operation failed');
  130. }
  131. }
  132. /**
  133. * Delete video cache keys matching specific patterns
  134. *
  135. * Patterns deleted:
  136. * - box:app:video:category:list:*
  137. * - box:app:video:tag:list:*
  138. * - box:app:tag:list:*
  139. */
  140. private async deleteVideoCache(): Promise<VideoCacheDeletionStats> {
  141. this.logger.log('[AdminCache] Deleting video cache keys...');
  142. const stats: VideoCacheDeletionStats = {
  143. categoryLists: 0,
  144. tagVideoLists: 0,
  145. tagMetadataLists: 0,
  146. total: 0,
  147. };
  148. try {
  149. // Delete category video lists: box:app:video:category:list:*
  150. const categoryListKeys = await this.redisService.keys(
  151. tsCacheKeys.video.categoryList('*'),
  152. );
  153. if (categoryListKeys.length > 0) {
  154. const deleted = await this.redisService.del(...categoryListKeys);
  155. stats.categoryLists = deleted;
  156. stats.total += deleted;
  157. this.logger.log(`[AdminCache] Deleted ${deleted} category video lists`);
  158. }
  159. // Delete tag video lists: box:app:video:tag:list:*:*
  160. const tagListKeys = await this.redisService.keys(
  161. tsCacheKeys.video.tagList('*', '*'),
  162. );
  163. if (tagListKeys.length > 0) {
  164. const deleted = await this.redisService.del(...tagListKeys);
  165. stats.tagVideoLists = deleted;
  166. stats.total += deleted;
  167. this.logger.log(`[AdminCache] Deleted ${deleted} tag video lists`);
  168. }
  169. // Delete tag metadata lists: box:app:tag:list:*
  170. const tagMetadataKeys = await this.redisService.keys(
  171. tsCacheKeys.tag.metadataByCategory('*'),
  172. );
  173. if (tagMetadataKeys.length > 0) {
  174. const deleted = await this.redisService.del(...tagMetadataKeys);
  175. stats.tagMetadataLists = deleted;
  176. stats.total += deleted;
  177. this.logger.log(`[AdminCache] Deleted ${deleted} tag metadata lists`);
  178. }
  179. this.logger.log(`[AdminCache] Total keys deleted: ${stats.total}`);
  180. return stats;
  181. } catch (error) {
  182. this.logger.error(
  183. `[AdminCache] Failed to delete cache keys: ${error instanceof Error ? error.message : 'Unknown error'}`,
  184. error instanceof Error ? error.stack : undefined,
  185. );
  186. throw new BadRequestException('Failed to delete video cache keys');
  187. }
  188. }
  189. }
  190. /**
  191. * Response shape for video cache rebuild endpoint
  192. */
  193. export interface VideoCacheDeletionStats {
  194. categoryLists: number;
  195. tagVideoLists: number;
  196. tagMetadataLists: number;
  197. total: number;
  198. }
  199. export interface VideoCacheRebuildResponse {
  200. success: boolean;
  201. deleted: VideoCacheDeletionStats;
  202. rebuiltAt: string;
  203. }