video.controller.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import {
  2. Controller,
  3. Get,
  4. Param,
  5. Query,
  6. Post,
  7. Body,
  8. UseGuards,
  9. Req,
  10. UnauthorizedException,
  11. } from '@nestjs/common';
  12. import {
  13. ApiOperation,
  14. ApiQuery,
  15. ApiResponse,
  16. ApiTags,
  17. ApiBearerAuth,
  18. } from '@nestjs/swagger';
  19. import { Request } from 'express';
  20. import { VideoService } from './video.service';
  21. import {
  22. VideoDetailDto,
  23. VideoCategoryWithTagsResponseDto,
  24. VideoListRequestDto,
  25. VideoListResponseDto,
  26. VideoSearchByTagRequestDto,
  27. VideoClickDto,
  28. RecommendedVideosDto,
  29. } from './dto';
  30. import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
  31. interface JwtUser {
  32. uid: string;
  33. sub?: string;
  34. jti?: string;
  35. }
  36. interface RequestWithUser extends Request {
  37. user?: JwtUser;
  38. }
  39. @ApiTags('视频')
  40. @Controller('video')
  41. export class VideoController {
  42. constructor(private readonly videoService: VideoService) {}
  43. /**
  44. * GET /api/v1/video/recommended
  45. *
  46. * Get recommended videos from Redis.
  47. */
  48. @Get('recommended')
  49. @ApiOperation({
  50. summary: '获取推荐视频列表',
  51. description: '从 Redis 获取推荐视频列表。',
  52. })
  53. @ApiResponse({
  54. status: 200,
  55. description: '推荐视频列表',
  56. type: RecommendedVideosDto,
  57. })
  58. async getRecommendedVideos(
  59. @Req() req: RequestWithUser,
  60. ): Promise<RecommendedVideosDto> {
  61. const uid = req.user?.uid;
  62. if (!uid) {
  63. throw new UnauthorizedException('Missing uid in JWT payload');
  64. }
  65. const ip = this.getClientIp(req);
  66. const userAgent = this.getUserAgent(req);
  67. return this.videoService.getRecommendedVideos();
  68. }
  69. /**
  70. * GET /api/v1/video/category/:channelId/:categoryId/latest
  71. *
  72. * Get latest videos for a category from Redis.
  73. */
  74. @Get('category/:channelId/:categoryId/latest')
  75. @ApiOperation({
  76. summary: '获取分类最新视频',
  77. description: '从 Redis 获取指定频道和分类的最新视频列表。',
  78. })
  79. @ApiResponse({
  80. status: 200,
  81. description: '最新视频列表',
  82. type: VideoDetailDto,
  83. isArray: true,
  84. })
  85. async getLatestVideosByCategory(
  86. @Param('channelId') channelId: string,
  87. @Param('categoryId') categoryId: string,
  88. ): Promise<VideoDetailDto[]> {
  89. return this.videoService.getLatestVideosByCategory(categoryId);
  90. }
  91. /**
  92. * GET /api/v1/video/categories-with-tags
  93. *
  94. * Get all video categories with their associated tags.
  95. * Returns categories fetched from Redis cache (app:category:all),
  96. * with tags for each category fetched from (box:app:tag:list:{categoryId}).
  97. */
  98. @Get('categories-with-tags')
  99. @ApiOperation({
  100. summary: '获取所有分类及其标签',
  101. description:
  102. '返回所有视频分类及其关联的标签。数据来源:Redis 缓存(由 box-mgnt-api 构建)。',
  103. })
  104. @ApiResponse({
  105. status: 200,
  106. description: '分类及其标签列表',
  107. type: VideoCategoryWithTagsResponseDto,
  108. })
  109. async getCategoriesWithTags(): Promise<VideoCategoryWithTagsResponseDto> {
  110. return this.videoService.getCategoriesWithTags();
  111. }
  112. /**
  113. * POST /api/v1/video/list
  114. *
  115. * Get paginated list of videos for a category with optional tag filtering.
  116. * Request body contains page, size, categoryId, and optional tagName.
  117. * Returns paginated video list with metadata.
  118. */
  119. @Post('list')
  120. @ApiOperation({
  121. summary: '分页获取视频列表',
  122. description:
  123. '按分类(和可选的标签)分页获取视频列表。支持按页码和每页数量分页。',
  124. })
  125. @ApiResponse({
  126. status: 200,
  127. description: '成功返回分页视频列表',
  128. type: VideoListResponseDto,
  129. })
  130. async getVideoList(
  131. @Body() req: VideoListRequestDto,
  132. ): Promise<VideoListResponseDto> {
  133. return this.videoService.getVideoList(req);
  134. }
  135. /**
  136. * POST /api/v1/video/search-by-tag
  137. *
  138. * Search videos by tag name across all categories.
  139. * Collects all videos tagged with the specified tag name from all categories.
  140. */
  141. @Post('search-by-tag')
  142. @ApiOperation({
  143. summary: '按标签名称全局搜索视频',
  144. description: '跨所有分类搜索具有指定标签名称的视频,返回分页结果。',
  145. })
  146. @ApiResponse({
  147. status: 200,
  148. description: '成功返回搜索结果',
  149. type: VideoListResponseDto,
  150. })
  151. async searchByTag(
  152. @Body() req: VideoSearchByTagRequestDto,
  153. ): Promise<VideoListResponseDto> {
  154. return this.videoService.searchVideosByTagName(req);
  155. }
  156. /**
  157. * POST /video/click
  158. *
  159. * Record video click event for analytics.
  160. * Protected endpoint that requires JWT authentication.
  161. */
  162. @Post('click')
  163. @UseGuards(JwtAuthGuard)
  164. @ApiBearerAuth()
  165. @ApiOperation({
  166. summary: '视频点击事件上报',
  167. description:
  168. '记录视频点击事件(uid/IP/UA 由服务端填充,不接受客户端时间戳)。',
  169. })
  170. @ApiResponse({
  171. status: 200,
  172. description: '成功',
  173. schema: { example: { status: 1, code: 'OK' } },
  174. })
  175. @ApiResponse({ status: 401, description: '未授权 - 需要JWT token' })
  176. async recordVideoClick(
  177. @Body() body: VideoClickDto,
  178. @Req() req: RequestWithUser,
  179. ): Promise<{ status: number; code: string }> {
  180. const uid = req.user?.uid;
  181. if (!uid) {
  182. throw new UnauthorizedException('Missing uid in JWT payload');
  183. }
  184. const ip = this.getClientIp(req);
  185. const userAgent = this.getUserAgent(req);
  186. await this.videoService.recordVideoClick(uid, body, ip, userAgent);
  187. return { status: 1, code: 'OK' };
  188. }
  189. private getClientIp(req: Request): string {
  190. return (
  191. (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
  192. (req.headers['x-real-ip'] as string) ||
  193. req.ip ||
  194. req.socket?.remoteAddress ||
  195. 'unknown'
  196. );
  197. }
  198. private getUserAgent(req: Request): string {
  199. return (req.headers['user-agent'] as string) || 'unknown';
  200. }
  201. }