FC_DAN\c9837 1 місяць тому
батько
коміт
8ddd3f1365

+ 1 - 1
apps/box-app-api/src/feature/video/dto/video-list-request.dto.ts

@@ -10,7 +10,7 @@ export class VideoListRequestDto {
   })
   @IsNumber()
   @Min(1)
-  page: number;
+  page?: number;
 
   @ApiProperty({
     description: '每页数量,最多100条',

+ 42 - 2
apps/box-app-api/src/feature/video/video.controller.ts

@@ -50,7 +50,7 @@ export class VideoController {
    *
    * Search cached recommended videos by secondTags (supports comma-separated tags).
    */
-  @Get('search')
+  @Post('search')
   @ApiOperation({
     summary: '搜索视频',
     description: '',
@@ -61,12 +61,52 @@ export class VideoController {
     type: VideoItemDto,
     isArray: true,
   })
-  async searchBySecondTags(
+  async search(
     @Body() req: VideoListRequestDto,
   ): Promise<VideoItemDto[]> {
     return await this.videoService.getVideoList(req);
   }
 
+  @Get('homevideo')
+  @ApiOperation({
+    summary: '首页视频',
+  })
+  @ApiQuery({
+    name: 'channelId',
+    required: true,
+    description: '渠道ID',
+  })
+  @ApiResponse({
+    status: 200,
+    description: '标签+视频',
+    isArray: true,
+  })
+  async homeVideo(
+    @Query('channelId') channelId: string,
+  ): Promise<any[]> {
+    return await this.videoService.getHomeSectionVideos(channelId);
+  }  
+
+@Get('guess')
+  @ApiOperation({
+    summary: '猜你喜欢',
+  })
+  @ApiQuery({
+    name: 'tag',
+    required: true,
+    description: '标签',
+  })
+  @ApiResponse({
+    status: 200,
+    description: '视频',
+    isArray: true,
+  })
+  async guess(
+    @Query('tag') tag: string,
+  ): Promise<any[]> {
+    return await this.videoService.getGuessLikeVideos(tag);
+  }    
+
   /**
    * GET /api/v1/video/latest
    *

+ 44 - 147
apps/box-app-api/src/feature/video/video.service.ts

@@ -60,153 +60,37 @@ export class VideoService {
   }
 
   /**
-   * Get video detail by videoId.
-   * Reads from appVideoDetailKey (JSON).
-   */
-  async getVideoDetail(videoId: string): Promise<VideoDetailDto | null> {
-    try {
-      const key = tsCacheKeys.video.detail(videoId);
-      const cached = await this.redis.getJson<VideoDetailDto | null>(key);
-      return cached ?? null;
-    } catch (err) {
-      this.logger.error(
-        `Error fetching video detail for videoId=${videoId}`,
-        err instanceof Error ? err.stack : String(err),
-      );
-      return null;
-    }
-  }
-
-  /**
    * Get home section videos for a channel.
    * Reads from appVideoHomeSectionKey (LIST of videoIds).
    * Returns video details for each ID.
    */
   async getHomeSectionVideos(
-    channelId: string,
-    section: VideoHomeSectionKey,
-  ): Promise<VideoDetailDto[]> {
-    try {
-      const key = tsCacheKeys.video.homeSection(channelId, section);
-
-      // Use helper to read all video IDs from the LIST
-      const videoIds = await this.cacheHelper.getVideoIdList(key);
-
-      if (!videoIds || videoIds.length === 0) {
-        return [];
-      }
-
-      // Fetch details for all videoIds
-      const details = await this.getVideoDetailsBatch(videoIds);
-      return details.filter((d) => d !== null) as VideoDetailDto[];
-    } catch (err) {
-      this.logger.error(
-        `Error fetching home section videos for channelId=${channelId}, section=${section}`,
-        err instanceof Error ? err.stack : String(err),
-      );
-      return [];
-    }
-  }
-
-  private async getVideoDetailsBatch(
-    videoIds: string[],
-  ): Promise<(VideoDetailDto | null)[]> {
-    if (!videoIds || videoIds.length === 0) {
-      return [];
-    }
-
-    try {
-      const keys = videoIds.map((id) => tsCacheKeys.video.detail(id));
-      const results: (VideoDetailDto | null)[] = [];
-
-      // Fetch all in parallel
-      for (const key of keys) {
-        const cached = await this.redis.getJson<VideoDetailDto | null>(key);
-        results.push(cached ?? null);
-      }
-
-      return results;
-    } catch (err) {
-      this.logger.error(
-        `Error fetching video details batch`,
-        err instanceof Error ? err.stack : String(err),
-      );
-      return videoIds.map(() => null);
-    }
-  }
-
-  private async getVideoPayloadsByIds(
-    videoIds: string[],
-  ): Promise<VideoPayload[]> {
-    if (!videoIds || videoIds.length === 0) {
-      return [];
-    }
-
+    channelId: string
+  ): Promise<any[]> {
     try {
-      const keys = videoIds.map((id) => tsCacheKeys.video.payload(id));
-      const cached = await this.redis.mget(keys);
-
-      const payloadMap = new Map<string, VideoPayload>();
-      const missing = new Set<string>();
-
-      cached.forEach((raw, idx) => {
-        const id = videoIds[idx];
-        if (!raw) {
-          missing.add(id);
-          return;
-        }
-
-        const parsed = parseVideoPayload(raw);
-        if (!parsed) {
-          missing.add(id);
-          return;
-        }
-
-        payloadMap.set(id, parsed);
+      const channel = await this.mongoPrisma.channel.findUnique({
+        where: { channelId },
       });
 
-      if (missing.size > 0) {
-        const records = await this.mongoPrisma.videoMedia.findMany({
-          where: { id: { in: Array.from(missing) } },
-          select: {
-            id: true,
-            title: true,
-            coverImg: true,
-            coverImgNew: true,
-            videoTime: true,
-            country: true,
-            firstTag: true,
-            secondTags: true,
-            preFileName: true,
-            desc: true,
-            size: true,
-            updatedAt: true,
-            filename: true,
-            fieldNameFs: true,
-            ext: true,
-          },
-        });
+      const result: { tag: string; records: VideoListItemDto[] }[] = [];
 
-        if (records.length > 0) {
-          const pipelineEntries = records.map((row: RawVideoPayloadRow) => ({
-            key: tsCacheKeys.video.payload(row.id),
-            value: toVideoPayload(row),
-          }));
+      for (const tag of channel.tagNames) {
+        const records = await this.getVideoList({
+          random: true,
+          tag,
+          size: 7,
+        }, 3600 * 24);
 
-          await this.redis.pipelineSetJson(pipelineEntries);
-          for (const row of records) {
-            payloadMap.set(row.id, toVideoPayload(row));
-            missing.delete(row.id);
-          }
-        }
+        result.push({
+          tag,
+          records,
+        });
       }
 
-      return videoIds
-        .map((id) => payloadMap.get(id))
-        .filter((payload): payload is VideoPayload => Boolean(payload));
+      return result;
     } catch (err) {
       this.logger.error(
-        `Error fetching video payloads for ids=${videoIds.join(',')}`,
+        `Error fetching home section videos for channelId=${channelId}`,
         err instanceof Error ? err.stack : String(err),
       );
       return [];
@@ -218,7 +102,7 @@ export class VideoService {
    * Reads video IDs from Redis cache, fetches full details from MongoDB,
    * and returns paginated results.
    */
-  async getVideoList(dto: VideoListRequestDto): Promise<VideoListItemDto[]> {
+  async getVideoList(dto: VideoListRequestDto, ttl?: number): Promise<VideoListItemDto[]> {
     const { page, size, tag, keyword, random } = dto;
     const start = (page - 1) * size;
 
@@ -226,7 +110,9 @@ export class VideoService {
       JSON.stringify(dto),
     ).toString('base64')}`;
 
-    const ttl = random ? 15 : 300;
+    if(!ttl){
+      ttl = random ? 15 : 300;
+    }
 
     let fallbackRecords: VideoListItemDto[] = [];
     try {
@@ -239,19 +125,18 @@ export class VideoService {
         status: 'Completed'
       };
 
-      if (tag) {
-        where.secondTags = {
-          has: tag,
-        };
-      }
+      if(random){
+        if (tag) {
+          where.secondTags = tag;
+        }
 
-      if (keyword) {
-        where.title = {
-          contains: keyword, mode: 'insensitive'
+        if (keyword) {
+          where.title = {
+            $regex: keyword,
+            $options: 'i',
+          }
         }
-      }
 
-      if(random){
         fallbackRecords = (await this.mongoPrisma.videoMedia.aggregateRaw({
           pipeline: [
             { $match: where },
@@ -269,6 +154,18 @@ export class VideoService {
           ],
         })) as unknown as VideoListItemDto[];
       }else{
+        if (tag) {
+          where.secondTags = {
+            has: tag,
+          };
+        }
+
+        if (keyword) {
+          where.title = {
+            contains: keyword, mode: 'insensitive'
+          }
+        }
+
         fallbackRecords = (await this.mongoPrisma.videoMedia.findMany({
           where,
           orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
@@ -572,7 +469,7 @@ export class VideoService {
   async getGuessLikeVideos(tag: string): Promise<VideoItemDto[]> {
     try {
       // Try to fetch from Redis cache first
-      const cached = this.readCachedVideoList(tsCacheKeys.video.guess() + tag, 'guess like videos');
+      const cached = await this.readCachedVideoList(tsCacheKeys.video.guess() + encodeURIComponent(tag), 'guess like videos');
 
       if (cached && Array.isArray(cached) && cached.length > 0) {
         return cached;
@@ -594,7 +491,7 @@ export class VideoService {
         this.mapVideoToDto(v),
       );
 
-      this.redis.setJson(tsCacheKeys.video.guess() + tag, items, 3600).catch(err => {
+      this.redis.setJson(tsCacheKeys.video.guess() + encodeURIComponent(tag), items, 3600).catch(err => {
         this.logger.warn("Redis setJson video.guess failed", err);
       });
 

+ 1 - 1
libs/common/src/cache/cache-keys.ts

@@ -94,7 +94,7 @@ export const CacheKeys = {
 
   appVideoHomeSectionKey: (
     channelId: string,
-    section: VideoHomeSectionKey,
+    section: string,
   ): string => `box:app:video:list:home:${channelId}:${section}`,
 
   // ─────────────────────────────────────────────

+ 1 - 1
libs/common/src/cache/ts-cache-key.provider.ts

@@ -210,7 +210,7 @@ export interface TsCacheKeyBuilder {
      * Elements: Video IDs (strings)
      * Order: Most recent first (limited to N items)
      */
-    homeSection(channelId: string, section: VideoHomeSectionKey): string;
+    homeSection(channelId: string, section: string): string;
     recommended(): string;
     latest(): string;
     list(): string;