Эх сурвалжийг харах

feat(channel): add ChannelModule and ChannelService for channel info retrieval
refactor(auth): integrate ChannelService into AuthService for channel data
delete: remove unused DTOs and Stats module related files
refactor(video): optimize VideoService methods and improve cache handling

Dave 2 сар өмнө
parent
commit
8613f6be6a

+ 2 - 0
apps/box-app-api/src/app.module.ts

@@ -18,6 +18,7 @@ import { RabbitmqModule } from './rabbitmq/rabbitmq.module';
 import { AuthModule } from './feature/auth/auth.module';
 import { RecommendationModule } from './feature/recommendation/recommendation.module';
 import path from 'path';
+import { ChannelModule } from './feature/channel/channel.module';
 
 @Module({
   imports: [
@@ -62,6 +63,7 @@ import path from 'path';
     HomepageModule,
     SysParamsModule,
     MediaConfigModule,
+    ChannelModule,
   ],
   providers: [
     {

+ 2 - 0
apps/box-app-api/src/feature/auth/auth.module.ts

@@ -11,6 +11,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
 import { JwtAuthGuard } from './guards/jwt-auth.guard';
 import { AdModule } from '../ads/ad.module';
 import { SysParamsModule } from '../sys-params/sys-params.module';
+import { ChannelModule } from '../channel/channel.module';
 
 @Module({
   imports: [
@@ -19,6 +20,7 @@ import { SysParamsModule } from '../sys-params/sys-params.module';
     CoreModule,
     AdModule,
     SysParamsModule,
+    ChannelModule,
     PassportModule.register({ defaultStrategy: 'jwt' }),
     JwtModule.registerAsync({
       imports: [ConfigModule],

+ 4 - 19
apps/box-app-api/src/feature/auth/auth.service.ts

@@ -3,10 +3,10 @@ import { Injectable, Logger } from '@nestjs/common';
 import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto';
 import { nowSecBigInt } from '@box/common/time/time.util';
 import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service';
-import { PrismaMongoStatsService } from '../../prisma/prisma-mongo-stats.service';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
 import { AdService } from '../ads/ad.service';
 import { SysParamsService } from '../sys-params/sys-params.service';
+import { ChannelService } from '../channel/channel.service';
 
 type LoginParams = {
   uid: string;
@@ -32,10 +32,10 @@ export class AuthService {
 
   constructor(
     private readonly rabbitmqPublisher: RabbitmqPublisherService,
-    private readonly prismaMongoStatsService: PrismaMongoStatsService,
     private readonly prismaMongoService: PrismaMongoService, // box-admin
     private readonly adService: AdService,
     private readonly sysParamsService: SysParamsService,
+    private readonly channelService: ChannelService,
   ) {}
 
   async login(params: LoginParams): Promise<LoginResult> {
@@ -108,11 +108,12 @@ export class AuthService {
     // For now return null to keep behaviour deterministic.
     const startupAds = await this.adService.listAdsByType(1, 20);
     const conf = await this.sysParamsService.getSysCnf();
+    const channel = await this.channelService.getChannelById(finalChannelId);
 
     return {
       uid,
       channelId: finalChannelId,
-      channel: await this.getChannleInfo(finalChannelId),
+      channel: channel,
       startupAds,
       conf,
     };
@@ -182,22 +183,6 @@ export class AuthService {
   // Channel resolution (box-admin)
   // ---------------------------
 
-  // get channel info and return channelId, landingUrl, videoCdn, coverCdn, clientName, clientNotice
-  private async getChannleInfo(channelId: string): Promise<any> {
-    const channel = await this.prismaMongoService.channel.findUnique({
-      where: { channelId },
-      select: {
-        channelId: true,
-        landingUrl: true,
-        videoCdn: true,
-        coverCdn: true,
-        clientName: true,
-        clientNotice: true,
-      },
-    });
-    return channel;
-  }
-
   private async resolveFirstLoginChannel(
     channelIdInput?: string,
   ): Promise<any> {

+ 10 - 0
apps/box-app-api/src/feature/channel/channel.module.ts

@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+import { PrismaMongoModule } from '../../prisma/prisma-mongo.module';
+import { ChannelService } from './channel.service';
+
+@Module({
+  imports: [PrismaMongoModule],
+  providers: [ChannelService],
+  exports: [ChannelService],
+})
+export class ChannelModule {}

+ 56 - 0
apps/box-app-api/src/feature/channel/channel.service.ts

@@ -0,0 +1,56 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { RedisService } from '@box/db/redis/redis.service';
+import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
+
+@Injectable()
+export class ChannelService {
+  private readonly logger = new Logger(ChannelService.name);
+
+  constructor(
+    private readonly redis: RedisService,
+    private readonly prisma: PrismaMongoService,
+  ) {}
+
+  async getChannelById(channelId: string): Promise<any | null> {
+    try {
+      this.logger.log(`Cache miss for channel of channelId=${channelId}`);
+
+      // Try to get from Redis cache first
+      const cacheKey = `channel:${channelId}`;
+      const cachedChannel = await this.redis.get(cacheKey);
+      if (cachedChannel) {
+        this.logger.log(`Cache hit for channelId: ${channelId}`);
+        return JSON.parse(cachedChannel);
+      }
+
+      this.logger.log(
+        `Cache miss for channelId: ${channelId}. Fetching from DB.`,
+      );
+
+      // Fetch from MongoDB
+      const channel = await this.prisma.channel.findUnique({
+        where: { channelId },
+        select: {
+          channelId: true,
+          landingUrl: true,
+          videoCdn: true,
+          coverCdn: true,
+          clientName: true,
+          clientNotice: true,
+        },
+      });
+
+      if (channel) {
+        // Store in Redis cache for future requests
+        await this.redis.setJson(cacheKey, channel, 24 * 3600); // Cache for 24 hours
+      }
+
+      return channel;
+    } catch (error) {
+      this.logger.error(
+        `Error fetching channel by ID ${channelId}: ${error instanceof Error ? error.message : String(error)}`,
+      );
+      return null;
+    }
+  }
+}

+ 0 - 39
apps/box-app-api/src/feature/stats/dto/ad-click.dto.ts

@@ -1,39 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
-
-export class AdClickDto {
-  @ApiProperty({ description: '广告 ID', example: '652e7bcf4f1a2b4f98ad1234' })
-  @IsNotEmpty()
-  @IsString()
-  adId: string;
-
-  @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
-  @IsNotEmpty()
-  @IsString()
-  channelId: string;
-
-  @ApiProperty({ description: '业务场景(如 home, detail)', example: 'home' })
-  @IsNotEmpty()
-  @IsString()
-  scene: string;
-
-  @ApiProperty({ description: '广告位', example: 'banner_top' })
-  @IsNotEmpty()
-  @IsString()
-  slot: string;
-
-  @ApiProperty({ description: '广告类型', example: 'BANNER' })
-  @IsNotEmpty()
-  @IsString()
-  adType: string;
-
-  @ApiProperty({ description: '客户端版本', example: '1.0.0', required: false })
-  @IsOptional()
-  @IsString()
-  appVersion?: string;
-
-  @ApiProperty({ description: '操作系统', example: 'Android', required: false })
-  @IsOptional()
-  @IsString()
-  os?: string;
-}

+ 0 - 51
apps/box-app-api/src/feature/stats/dto/ad-impression.dto.ts

@@ -1,51 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsOptional, IsString, IsInt, Min } from 'class-validator';
-import { Type } from 'class-transformer';
-
-export class AdImpressionDto {
-  @ApiProperty({ description: '广告 ID', example: '652e7bcf4f1a2b4f98ad1234' })
-  @IsNotEmpty()
-  @IsString()
-  adId: string;
-
-  @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
-  @IsNotEmpty()
-  @IsString()
-  channelId: string;
-
-  @ApiProperty({ description: '业务场景(如 home, detail)', example: 'home' })
-  @IsNotEmpty()
-  @IsString()
-  scene: string;
-
-  @ApiProperty({ description: '广告位', example: 'banner_top' })
-  @IsNotEmpty()
-  @IsString()
-  slot: string;
-
-  @ApiProperty({ description: '广告类型', example: 'BANNER' })
-  @IsNotEmpty()
-  @IsString()
-  adType: string;
-
-  @ApiProperty({
-    description: '可见时长(毫秒)',
-    example: 1200,
-    required: false,
-  })
-  @IsOptional()
-  @Type(() => Number)
-  @IsInt()
-  @Min(0)
-  visibleDurationMs?: number;
-
-  @ApiProperty({ description: '客户端版本', example: '1.0.0', required: false })
-  @IsOptional()
-  @IsString()
-  appVersion?: string;
-
-  @ApiProperty({ description: '操作系统', example: 'Android', required: false })
-  @IsOptional()
-  @IsString()
-  os?: string;
-}

+ 0 - 41
apps/box-app-api/src/feature/stats/dto/video-click.dto.ts

@@ -1,41 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
-
-export class VideoClickDto {
-  @ApiProperty({ description: '视频 ID', example: '652e7bcf4f1a2b4f98ad5678' })
-  @IsNotEmpty()
-  @IsString()
-  videoId: string;
-
-  // @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
-  // @IsNotEmpty()
-  // @IsString()
-  // channelId: string;
-
-  @ApiProperty({
-    description: '分类 ID',
-    example: '652e7bcf4f1a2b4f98ad3333',
-    required: false,
-  })
-  @IsOptional()
-  @IsString()
-  categoryId?: string;
-
-  @ApiProperty({
-    description: '业务场景(如 home, detail, feed)',
-    example: 'detail',
-  })
-  @IsNotEmpty()
-  @IsString()
-  scene: string;
-
-  @ApiProperty({ description: '客户端版本', example: '1.0.0', required: false })
-  @IsOptional()
-  @IsString()
-  appVersion?: string;
-
-  @ApiProperty({ description: '操作系统', example: 'iOS', required: false })
-  @IsOptional()
-  @IsString()
-  os?: string;
-}

+ 0 - 109
apps/box-app-api/src/feature/stats/stats-events.service.ts

@@ -1,109 +0,0 @@
-import { Injectable, Logger } from '@nestjs/common';
-import { randomUUID } from 'crypto';
-import {
-  RabbitmqPublisherService,
-  StatsAdClickEventPayload,
-  StatsAdImpressionEventPayload,
-  StatsVideoClickEventPayload,
-} from '../../rabbitmq/rabbitmq-publisher.service';
-
-export interface AdClickEvent {
-  uid: string;
-  adId: string;
-  channelId: string;
-  scene: string;
-  slot: string;
-  adType: string;
-  clickedAt: bigint;
-  ip: string;
-  userAgent: string;
-  appVersion?: string;
-  os?: string;
-  createAt: bigint;
-  updateAt: bigint;
-}
-
-export interface VideoClickEvent {
-  uid: string;
-  videoId: string;
-  // channelId: string;
-  categoryId?: string;
-  scene: string;
-  clickedAt: bigint;
-  ip: string;
-  userAgent: string;
-  appVersion?: string;
-  os?: string;
-  createAt: bigint;
-  updateAt: bigint;
-}
-
-export interface AdImpressionEvent {
-  uid: string;
-  adId: string;
-  // channelId: string;
-  scene: string;
-  slot: string;
-  adType: string;
-  impressionAt: bigint;
-  visibleDurationMs?: number;
-  ip: string;
-  userAgent: string;
-  appVersion?: string;
-  os?: string;
-  createAt: bigint;
-  updateAt: bigint;
-}
-
-@Injectable()
-export class StatsEventsService {
-  private readonly logger = new Logger(StatsEventsService.name);
-
-  constructor(private readonly rabbitmqPublisher: RabbitmqPublisherService) {}
-
-  async publishAdClick(event: AdClickEvent): Promise<void> {
-    const payload: StatsAdClickEventPayload = {
-      messageId: randomUUID(),
-      ...event,
-    };
-
-    await this.safePublish('stats.ad.click', () =>
-      this.rabbitmqPublisher.publishStatsAdClick(payload),
-    );
-  }
-
-  async publishVideoClick(event: VideoClickEvent): Promise<void> {
-    const payload: StatsVideoClickEventPayload = {
-      messageId: randomUUID(),
-      ...event,
-    };
-
-    await this.safePublish('stats.video.click', () =>
-      this.rabbitmqPublisher.publishStatsVideoClick(payload),
-    );
-  }
-
-  async publishAdImpression(event: AdImpressionEvent): Promise<void> {
-    const payload: StatsAdImpressionEventPayload = {
-      messageId: randomUUID(),
-      ...event,
-    };
-
-    await this.safePublish('stats.ad.impression', () =>
-      this.rabbitmqPublisher.publishStatsAdImpression(payload),
-    );
-  }
-
-  private async safePublish(
-    label: string,
-    publishFn: () => Promise<void>,
-  ): Promise<void> {
-    try {
-      await publishFn();
-    } catch (error) {
-      const message = error instanceof Error ? error.message : String(error);
-      const stack = error instanceof Error ? error.stack : undefined;
-      this.logger.error(`Failed to publish ${label}: ${message}`, stack);
-    }
-  }
-}

+ 0 - 178
apps/box-app-api/src/feature/stats/stats.controller.ts

@@ -1,178 +0,0 @@
-import {
-  Body,
-  Controller,
-  Logger,
-  Post,
-  Req,
-  UnauthorizedException,
-  UseGuards,
-} from '@nestjs/common';
-import {
-  ApiBearerAuth,
-  ApiOperation,
-  ApiTags,
-  ApiResponse,
-} from '@nestjs/swagger';
-import { Request } from 'express';
-import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
-import { AdClickDto } from './dto/ad-click.dto';
-import { VideoClickDto } from './dto/video-click.dto';
-import { AdImpressionDto } from './dto/ad-impression.dto';
-import { StatsEventsService } from './stats-events.service';
-import { nowEpochMsBigInt } from '@box/common/time/time.util';
-
-interface JwtUser {
-  uid: string;
-  sub?: string;
-  jti?: string;
-}
-
-interface RequestWithUser extends Request {
-  user?: JwtUser;
-}
-
-@ApiTags('统计')
-@ApiBearerAuth()
-@Controller('stats')
-export class StatsController {
-  private readonly logger = new Logger(StatsController.name);
-
-  constructor(private readonly statsEventsService: StatsEventsService) {}
-
-  @Post('ad/click')
-  @UseGuards(JwtAuthGuard)
-  @ApiOperation({
-    summary: '广告点击事件上报',
-    description:
-      '记录广告点击事件(uid/IP/UA 由服务端填充,不接受客户端时间戳)。',
-  })
-  @ApiResponse({
-    status: 200,
-    description: '成功',
-    schema: { example: { status: 1, code: 'OK' } },
-  })
-  async recordAdClick(
-    @Body() body: AdClickDto,
-    @Req() req: RequestWithUser,
-  ): Promise<{ status: number; code: string }> {
-    const uid = req.user?.uid;
-
-    if (!uid) {
-      throw new UnauthorizedException('Missing uid in JWT payload');
-    }
-
-    const ip = this.getClientIp(req);
-    const userAgent = this.getUserAgent(req);
-    const clickedAt = nowEpochMsBigInt();
-    const createAt = clickedAt;
-    const updateAt = clickedAt;
-
-    await this.statsEventsService.publishAdClick({
-      uid,
-      ...body,
-      clickedAt,
-      ip,
-      userAgent,
-      createAt,
-      updateAt,
-    });
-
-    return { status: 1, code: 'OK' };
-  }
-
-  @Post('video/click')
-  @UseGuards(JwtAuthGuard)
-  @ApiOperation({
-    summary: '视频点击事件上报',
-    description:
-      '记录视频点击事件(uid/IP/UA 由服务端填充,不接受客户端时间戳)。',
-  })
-  @ApiResponse({
-    status: 200,
-    description: '成功',
-    schema: { example: { status: 1, code: 'OK' } },
-  })
-  async recordVideoClick(
-    @Body() body: VideoClickDto,
-    @Req() req: RequestWithUser,
-  ): Promise<{ status: number; code: string }> {
-    const uid = req.user?.uid;
-
-    if (!uid) {
-      throw new UnauthorizedException('Missing uid in JWT payload');
-    }
-
-    const ip = this.getClientIp(req);
-    const userAgent = this.getUserAgent(req);
-    const clickedAt = nowEpochMsBigInt();
-    const createAt = clickedAt;
-    const updateAt = clickedAt;
-
-    await this.statsEventsService.publishVideoClick({
-      uid,
-      ...body,
-      clickedAt,
-      ip,
-      userAgent,
-      createAt,
-      updateAt,
-    });
-
-    return { status: 1, code: 'OK' };
-  }
-
-  @Post('ad/impression')
-  @UseGuards(JwtAuthGuard)
-  @ApiOperation({
-    summary: '广告曝光事件上报',
-    description:
-      '记录广告曝光事件(uid/IP/UA 由服务端填充,不接受客户端时间戳)。',
-  })
-  @ApiResponse({
-    status: 200,
-    description: '成功',
-    schema: { example: { status: 1, code: 'OK' } },
-  })
-  async recordAdImpression(
-    @Body() body: AdImpressionDto,
-    @Req() req: RequestWithUser,
-  ): Promise<{ status: number; code: string }> {
-    const uid = req.user?.uid;
-
-    if (!uid) {
-      throw new UnauthorizedException('Missing uid in JWT payload');
-    }
-
-    const ip = this.getClientIp(req);
-    const userAgent = this.getUserAgent(req);
-    const impressionAt = nowEpochMsBigInt();
-    const createAt = impressionAt;
-    const updateAt = impressionAt;
-
-    await this.statsEventsService.publishAdImpression({
-      uid,
-      ...body,
-      impressionAt,
-      ip,
-      userAgent,
-      createAt,
-      updateAt,
-    });
-
-    return { status: 1, code: 'OK' };
-  }
-
-  private getClientIp(req: Request): string {
-    return (
-      (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
-      (req.headers['x-real-ip'] as string) ||
-      req.ip ||
-      req.socket?.remoteAddress ||
-      'unknown'
-    );
-  }
-
-  private getUserAgent(req: Request): string {
-    return (req.headers['user-agent'] as string) || 'unknown';
-  }
-}

+ 0 - 13
apps/box-app-api/src/feature/stats/stats.module.ts

@@ -1,13 +0,0 @@
-import { Module } from '@nestjs/common';
-import { AuthModule } from '../auth/auth.module';
-import { StatsController } from './stats.controller';
-import { StatsEventsService } from './stats-events.service';
-import { RabbitmqModule } from '../../rabbitmq/rabbitmq.module';
-
-@Module({
-  imports: [AuthModule, RabbitmqModule],
-  controllers: [StatsController],
-  providers: [StatsEventsService],
-  exports: [StatsEventsService],
-})
-export class StatsModule {}

+ 39 - 67
apps/box-app-api/src/feature/video/video.service.ts

@@ -3,23 +3,9 @@ import { Injectable, Logger } from '@nestjs/common';
 import { RedisService } from '@box/db/redis/redis.service';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
-import type { VideoHomeSectionKey } from '@box/common/cache/ts-cache-key.provider';
+import { VideoCacheHelper } from '@box/common/cache/video-cache.helper';
 import {
-  RawVideoPayloadRow,
-  toVideoPayload,
-  VideoPayload,
-  parseVideoPayload,
-  VideoCacheHelper,
-} from '@box/common/cache/video-cache.helper';
-import {
-  VideoCategoryDto,
-  VideoTagDto,
-  VideoDetailDto,
-  VideoPageDto,
-  VideoCategoryWithTagsResponseDto,
   VideoListRequestDto,
-  VideoListResponseDto,
-  VideoSearchByTagRequestDto,
   VideoClickDto,
   RecommendedVideosDto,
   VideoItemDto,
@@ -38,14 +24,6 @@ import {
 import { CategoryDto } from '../homepage/dto/homepage.dto';
 import { VideoListItemDto } from './dto/video-list-response.dto';
 
-/**
- * VideoService provides read-only access to video data from Redis cache.
- * All data is prebuilt and maintained by box-mgnt-api cache builders.
- * Follows the new Redis cache semantics where:
- * - Video list keys store video IDs only (not JSON objects)
- * - Tag metadata keys store tag JSON objects
- * - Video details are fetched separately using video IDs
- */
 @Injectable()
 export class VideoService {
   private readonly logger = new Logger(VideoService.name);
@@ -59,14 +37,7 @@ export class VideoService {
     this.cacheHelper = new VideoCacheHelper(redis);
   }
 
-  /**
-   * Get home section videos for a channel.
-   * Reads from appVideoHomeSectionKey (LIST of videoIds).
-   * Returns video details for each ID.
-   */
-  async getHomeSectionVideos(
-    channelId: string
-  ): Promise<any[]> {
+  async getHomeSectionVideos(channelId: string): Promise<any[]> {
     try {
       const channel = await this.mongoPrisma.channel.findUnique({
         where: { channelId },
@@ -75,11 +46,14 @@ export class VideoService {
       const result: { tag: string; records: VideoListItemDto[] }[] = [];
 
       for (const tag of channel.tagNames) {
-        const records = await this.getVideoList({
-          random: true,
-          tag,
-          size: 7,
-        }, 3600 * 24);
+        const records = await this.getVideoList(
+          {
+            random: true,
+            tag,
+            size: 7,
+          },
+          3600 * 24,
+        );
 
         result.push({
           tag,
@@ -97,20 +71,18 @@ export class VideoService {
     }
   }
 
-  /**
-   * Get paginated list of videos for a category with optional tag filtering.
-   * Reads video IDs from Redis cache, fetches full details from MongoDB,
-   * and returns paginated results.
-   */
-  async getVideoList(dto: VideoListRequestDto, ttl?: number): Promise<VideoListItemDto[]> {
+  async getVideoList(
+    dto: VideoListRequestDto,
+    ttl?: number,
+  ): Promise<VideoListItemDto[]> {
     const { page, size, tag, keyword, random } = dto;
     const start = (page - 1) * size;
 
-    const cacheKey = `video:list:${Buffer.from(
-      JSON.stringify(dto),
-    ).toString('base64')}`;
+    const cacheKey = `video:list:${Buffer.from(JSON.stringify(dto)).toString(
+      'base64',
+    )}`;
 
-    if(!ttl){
+    if (!ttl) {
       ttl = random ? 15 : 300;
     }
 
@@ -121,11 +93,11 @@ export class VideoService {
         return cache;
       }
 
-      let where: any = {
-        status: 'Completed'
+      const where: any = {
+        status: 'Completed',
       };
 
-      if(random){
+      if (random) {
         if (tag) {
           where.secondTags = tag;
         }
@@ -134,7 +106,7 @@ export class VideoService {
           where.title = {
             $regex: keyword,
             $options: 'i',
-          }
+          };
         }
 
         fallbackRecords = (await this.mongoPrisma.videoMedia.aggregateRaw({
@@ -154,7 +126,7 @@ export class VideoService {
             },
           ],
         })) as unknown as VideoListItemDto[];
-      }else{
+      } else {
         if (tag) {
           where.secondTags = {
             has: tag,
@@ -163,8 +135,9 @@ export class VideoService {
 
         if (keyword) {
           where.title = {
-            contains: keyword, mode: 'insensitive'
-          }
+            contains: keyword,
+            mode: 'insensitive',
+          };
         }
 
         fallbackRecords = (await this.mongoPrisma.videoMedia.findMany({
@@ -198,16 +171,6 @@ export class VideoService {
     }
   }
 
-  /**
-   * Record video click event.
-   * Publishes a stats.video.click event to RabbitMQ for analytics processing.
-   * Uses fire-and-forget pattern for non-blocking operation.
-   *
-   * @param uid - User ID from JWT
-   * @param body - Video click data from client
-   * @param ip - Client IP address
-   * @param userAgent - User agent string (unused but kept for compatibility)
-   */
   async recordVideoClick(
     uid: string,
     body: VideoClickDto,
@@ -471,7 +434,10 @@ export class VideoService {
   async getGuessLikeVideos(tag: string): Promise<VideoItemDto[]> {
     try {
       // Try to fetch from Redis cache first
-      const cached = await this.readCachedVideoList(tsCacheKeys.video.guess() + encodeURIComponent(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;
@@ -493,9 +459,15 @@ export class VideoService {
         this.mapVideoToDto(v),
       );
 
-      this.redis.setJson(tsCacheKeys.video.guess() + encodeURIComponent(tag), items, 3600).catch(err => {
-        this.logger.warn("Redis setJson video.guess failed", err);
-      });
+      this.redis
+        .setJson(
+          tsCacheKeys.video.guess() + encodeURIComponent(tag),
+          items,
+          3600,
+        )
+        .catch((err) => {
+          this.logger.warn('Redis setJson video.guess failed', err);
+        });
 
       return items;
     } catch (error) {