| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- 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<RecommendedVideosDto> {
- 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<VideoDetailDto[]> {
- 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<VideoCategoryWithTagsResponseDto> {
- 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<VideoListResponseDto> {
- 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<VideoListResponseDto> {
- 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';
- }
- }
|