ads-stats.controller.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import {
  2. BadRequestException,
  3. Body,
  4. Controller,
  5. Headers,
  6. HttpCode,
  7. Logger,
  8. Post,
  9. Req,
  10. } from '@nestjs/common';
  11. import {
  12. ApiBody,
  13. ApiHeaders,
  14. ApiOperation,
  15. ApiResponse,
  16. ApiTags,
  17. ApiBadRequestResponse,
  18. ApiProperty,
  19. } from '@nestjs/swagger';
  20. import { Request } from 'express';
  21. import { StatsAdClickPublisherService } from './stats-ad-click.publisher.service';
  22. import { extractClientIp } from '../../utils/client-ip.util';
  23. import { IsString } from 'class-validator';
  24. export class AdClickRequestDto {
  25. @ApiProperty({ description: '用户唯一设备ID', example: 'xxxxxx' })
  26. @IsString()
  27. uid!: string;
  28. @ApiProperty({ description: '渠道ID', example: 'AAA' })
  29. @IsString()
  30. channelId!: string;
  31. @ApiProperty({
  32. description: '广告 Mongo ObjectId',
  33. example: '64b7c2f967ce4d6799c047d1',
  34. })
  35. @IsString()
  36. adsId!: string; // ✅ required
  37. }
  38. @ApiTags('Ads Stats')
  39. @Controller('stats')
  40. export class AdsStatsController {
  41. private readonly logger = new Logger(AdsStatsController.name);
  42. constructor(private readonly publisher: StatsAdClickPublisherService) {}
  43. @Post('ad-click')
  44. @HttpCode(202)
  45. @ApiOperation({
  46. summary: '广告点击上报',
  47. description: '用于广告点击统计。uid、channelId、adsId 必填。',
  48. })
  49. @ApiBody({
  50. type: AdClickRequestDto,
  51. description: 'uid、channelId、adsId 必填(adsId 为 Mongo ObjectId)',
  52. examples: {
  53. example: {
  54. summary: '示例',
  55. value: {
  56. uid: 'xxxxxx',
  57. channelId: 'AAA',
  58. adsId: '64b7c2f967ce4d6799c047d1',
  59. },
  60. },
  61. },
  62. })
  63. @ApiResponse({
  64. status: 202,
  65. description: '已接收(异步处理)',
  66. schema: { example: { ok: true } },
  67. })
  68. @ApiBadRequestResponse({
  69. description: '参数错误(缺少 uid/channelId/adsId)',
  70. })
  71. publishAdClick(
  72. @Req() req: Request,
  73. @Body() body: AdClickRequestDto,
  74. ): { ok: true } {
  75. if (!body?.uid || !body?.channelId || !body?.adsId) {
  76. throw new BadRequestException('uid, channelId and adsId are required');
  77. }
  78. this.publisher.publishAdClick({
  79. uid: body.uid,
  80. channelId: body.channelId,
  81. adsId: body.adsId,
  82. headers: req.headers,
  83. clientIp: extractClientIp(req),
  84. });
  85. return { ok: true };
  86. }
  87. private extractForwardedFor(headers: Request['headers']): string | undefined {
  88. const header = headers?.['x-forwarded-for'];
  89. if (!header) return undefined;
  90. const value = Array.isArray(header) ? header[0] : header;
  91. const first = value.split(',')[0]?.trim();
  92. return first || undefined;
  93. }
  94. }