| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- import {
- BadRequestException,
- Body,
- Controller,
- Headers,
- HttpCode,
- Logger,
- Post,
- Req,
- } from '@nestjs/common';
- import {
- ApiBody,
- ApiOperation,
- ApiResponse,
- ApiTags,
- ApiBadRequestResponse,
- ApiProperty,
- ApiPropertyOptional,
- } from '@nestjs/swagger';
- import { Request } from 'express';
- import { StatsAdClickPublisherService } from './stats-ad-click.publisher.service';
- import { extractClientIp } from '../../utils/client-ip.util';
- import { Transform } from 'class-transformer';
- import { IsOptional, IsString, Matches } from 'class-validator';
- export class AdClickRequestDto {
- @ApiProperty({ description: '用户唯一设备ID', example: 'xxxxxx' })
- @IsString()
- uid!: string;
- @ApiProperty({ description: '渠道ID', example: 'AAA' })
- @IsString()
- channelId!: string;
- @ApiProperty({
- description: '广告 Mongo ObjectId',
- example: '64b7c2f967ce4d6799c047d1',
- })
- @IsString()
- adsId!: string; // ✅ required
- @ApiPropertyOptional({
- description: 'Optional IPv4 hint (empty string/null accepted).',
- example: '1.2.3.4',
- })
- @Transform(({ value }) => {
- if (value === '' || value === null || value === undefined) {
- return undefined;
- }
- return typeof value === 'string' ? value.trim() : value;
- })
- @IsOptional()
- @Matches(
- /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/,
- { message: 'ipv4 must be a valid IPv4 address' },
- )
- ipv4?: string;
- }
- @ApiTags('Ads Stats')
- @Controller('stats')
- export class AdsStatsController {
- private readonly logger = new Logger(AdsStatsController.name);
- constructor(private readonly publisher: StatsAdClickPublisherService) {}
- @Post('ad-click')
- @HttpCode(202)
- @ApiOperation({
- summary: '广告点击上报',
- description: '用于广告点击统计。uid、channelId、adsId 必填。',
- })
- @ApiBody({
- type: AdClickRequestDto,
- description: 'uid、channelId、adsId 必填(adsId 为 Mongo ObjectId)',
- examples: {
- example: {
- summary: '示例',
- value: {
- uid: 'xxxxxx',
- channelId: 'AAA',
- adsId: '64b7c2f967ce4d6799c047d1',
- },
- },
- },
- })
- @ApiResponse({
- status: 202,
- description: '已接收(异步处理)',
- schema: { example: { ok: true } },
- })
- @ApiBadRequestResponse({
- description: '参数错误(缺少 uid/channelId/adsId)',
- })
- publishAdClick(
- @Req() req: Request,
- @Body() body: AdClickRequestDto,
- ): { ok: true } {
- if (!body?.uid || !body?.channelId || !body?.adsId) {
- throw new BadRequestException('uid, channelId and adsId are required');
- }
- const clientIp =
- // `ipv4` is a frontend-provided hint, used only when present & validated;
- // backend extraction remains the fallback.
- body.ipv4 || extractClientIp(req);
- this.publisher.publishAdClick({
- uid: body.uid,
- channelId: body.channelId,
- adsId: body.adsId,
- headers: req.headers,
- clientIp,
- });
- return { ok: true };
- }
- }
|