import { Controller, Get, Param, Query, Post, Body, UseGuards, Req, UnauthorizedException, } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags, ApiBearerAuth, } from '@nestjs/swagger'; import { Request } from 'express'; import { VideoService } from './video.service'; import { VideoDetailDto, VideoCategoryWithTagsResponseDto, VideoListRequestDto, VideoListResponseDto, VideoSearchByTagRequestDto, VideoClickDto, RecommendedVideosDto, } from './dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; interface JwtUser { uid: string; sub?: string; jti?: string; } interface RequestWithUser extends Request { user?: JwtUser; } @ApiTags('视频') @Controller('video') export class VideoController { constructor(private readonly videoService: VideoService) {} /** * GET /api/v1/video/recommended * * Get recommended videos from Redis. */ @Get('recommended') @ApiOperation({ summary: '获取推荐视频列表', description: '从 Redis 获取推荐视频列表。', }) @ApiResponse({ status: 200, description: '推荐视频列表', type: RecommendedVideosDto, }) async getRecommendedVideos( @Req() req: RequestWithUser, ): Promise { 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); return this.videoService.getRecommendedVideos(); } /** * GET /api/v1/video/category/:channelId/:categoryId/latest * * Get latest videos for a category from Redis. */ @Get('category/:channelId/:categoryId/latest') @ApiOperation({ summary: '获取分类最新视频', description: '从 Redis 获取指定频道和分类的最新视频列表。', }) @ApiResponse({ status: 200, description: '最新视频列表', type: VideoDetailDto, isArray: true, }) async getLatestVideosByCategory( @Param('channelId') channelId: string, @Param('categoryId') categoryId: string, ): Promise { return this.videoService.getLatestVideosByCategory(categoryId); } /** * GET /api/v1/video/categories-with-tags * * Get all video categories with their associated tags. * Returns categories fetched from Redis cache (app:category:all), * with tags for each category fetched from (box:app:tag:list:{categoryId}). */ @Get('categories-with-tags') @ApiOperation({ summary: '获取所有分类及其标签', description: '返回所有视频分类及其关联的标签。数据来源:Redis 缓存(由 box-mgnt-api 构建)。', }) @ApiResponse({ status: 200, description: '分类及其标签列表', type: VideoCategoryWithTagsResponseDto, }) async getCategoriesWithTags(): Promise { return this.videoService.getCategoriesWithTags(); } /** * POST /api/v1/video/list * * Get paginated list of videos for a category with optional tag filtering. * Request body contains page, size, categoryId, and optional tagName. * Returns paginated video list with metadata. */ @Post('list') @ApiOperation({ summary: '分页获取视频列表', description: '按分类(和可选的标签)分页获取视频列表。支持按页码和每页数量分页。', }) @ApiResponse({ status: 200, description: '成功返回分页视频列表', type: VideoListResponseDto, }) async getVideoList( @Body() req: VideoListRequestDto, ): Promise { return this.videoService.getVideoList(req); } /** * POST /api/v1/video/search-by-tag * * Search videos by tag name across all categories. * Collects all videos tagged with the specified tag name from all categories. */ @Post('search-by-tag') @ApiOperation({ summary: '按标签名称全局搜索视频', description: '跨所有分类搜索具有指定标签名称的视频,返回分页结果。', }) @ApiResponse({ status: 200, description: '成功返回搜索结果', type: VideoListResponseDto, }) async searchByTag( @Body() req: VideoSearchByTagRequestDto, ): Promise { return this.videoService.searchVideosByTagName(req); } /** * POST /video/click * * Record video click event for analytics. * Protected endpoint that requires JWT authentication. */ @Post('click') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: '视频点击事件上报', description: '记录视频点击事件(uid/IP/UA 由服务端填充,不接受客户端时间戳)。', }) @ApiResponse({ status: 200, description: '成功', schema: { example: { status: 1, code: 'OK' } }, }) @ApiResponse({ status: 401, description: '未授权 - 需要JWT token' }) 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); await this.videoService.recordVideoClick(uid, body, ip, userAgent); 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'; } }