import { BadRequestException, Body, Controller, Headers, HttpCode, Logger, Post, Req, } from '@nestjs/common'; import { ApiBody, ApiHeaders, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse, ApiProperty, } from '@nestjs/swagger'; import { Request } from 'express'; import { StatsAdClickPublisherService } from './stats-ad-click.publisher.service'; import { extractClientIp } from '../../utils/client-ip.util'; import { IsString } 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 } @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'); } this.publisher.publishAdClick({ uid: body.uid, channelId: body.channelId, adsId: body.adsId, headers: req.headers, clientIp: extractClientIp(req), }); return { ok: true }; } private extractForwardedFor(headers: Request['headers']): string | undefined { const header = headers?.['x-forwarded-for']; if (!header) return undefined; const value = Array.isArray(header) ? header[0] : header; const first = value.split(',')[0]?.trim(); return first || undefined; } }