|
|
@@ -1,17 +1,52 @@
|
|
|
// apps/box-app-api/src/feature/ads/ad.controller.ts
|
|
|
-import { Controller, Get, Logger, Query, Post, Body } from '@nestjs/common';
|
|
|
-import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|
|
+import {
|
|
|
+ Controller,
|
|
|
+ Get,
|
|
|
+ Logger,
|
|
|
+ Query,
|
|
|
+ Post,
|
|
|
+ Body,
|
|
|
+ Param,
|
|
|
+ UseGuards,
|
|
|
+ Req,
|
|
|
+ NotFoundException,
|
|
|
+} from '@nestjs/common';
|
|
|
+import {
|
|
|
+ ApiOperation,
|
|
|
+ ApiResponse,
|
|
|
+ ApiTags,
|
|
|
+ ApiBearerAuth,
|
|
|
+} from '@nestjs/swagger';
|
|
|
+import { Request } from 'express';
|
|
|
import { AdService } from './ad.service';
|
|
|
import { GetAdPlacementQueryDto } from './dto/get-ad-placement.dto';
|
|
|
import { AdDto } from './dto/ad.dto';
|
|
|
import { AdListRequestDto, AdListResponseDto } from './dto';
|
|
|
+import { AdUrlResponseDto } from './dto/ad-url-response.dto';
|
|
|
+import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
|
+import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service';
|
|
|
+import { AdsClickEventPayload } from '@box/common/events/ads-click-event.dto';
|
|
|
+
|
|
|
+// Extend Express Request to include user from JWT
|
|
|
+interface JwtUser {
|
|
|
+ uid: string;
|
|
|
+ sub: string;
|
|
|
+ jti: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface RequestWithUser extends Request {
|
|
|
+ user: JwtUser;
|
|
|
+}
|
|
|
|
|
|
@ApiTags('广告')
|
|
|
@Controller('ads')
|
|
|
export class AdController {
|
|
|
private readonly logger = new Logger(AdController.name);
|
|
|
|
|
|
- constructor(private readonly adService: AdService) {}
|
|
|
+ constructor(
|
|
|
+ private readonly adService: AdService,
|
|
|
+ private readonly rabbitmqPublisher: RabbitmqPublisherService,
|
|
|
+ ) {}
|
|
|
|
|
|
/**
|
|
|
* GET /ads/placement
|
|
|
@@ -89,4 +124,94 @@ export class AdController {
|
|
|
|
|
|
return response;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * GET /ads/:id/url
|
|
|
+ *
|
|
|
+ * Protected endpoint that requires JWT authentication.
|
|
|
+ * Returns the ad URL for the given ad ID and publishes an ADS_CLICK event.
|
|
|
+ *
|
|
|
+ * Example: GET /ads/507f1f77bcf86cd799439011/url
|
|
|
+ */
|
|
|
+ @Get(':id/url')
|
|
|
+ @UseGuards(JwtAuthGuard)
|
|
|
+ @ApiBearerAuth()
|
|
|
+ @ApiOperation({
|
|
|
+ summary: '获取广告URL',
|
|
|
+ description:
|
|
|
+ '通过广告ID获取广告链接。需要JWT认证。返回广告URL并记录点击事件。',
|
|
|
+ })
|
|
|
+ @ApiResponse({
|
|
|
+ status: 200,
|
|
|
+ description: '成功返回广告URL',
|
|
|
+ type: AdUrlResponseDto,
|
|
|
+ })
|
|
|
+ @ApiResponse({ status: 401, description: '未授权 - 需要JWT token' })
|
|
|
+ @ApiResponse({ status: 404, description: '广告不存在或已过期' })
|
|
|
+ async getAdUrl(
|
|
|
+ @Param('id') adsId: string,
|
|
|
+ @Req() req: RequestWithUser,
|
|
|
+ ): Promise<AdUrlResponseDto> {
|
|
|
+ const uid = req.user?.uid;
|
|
|
+
|
|
|
+ if (!uid) {
|
|
|
+ this.logger.error('JWT payload missing uid');
|
|
|
+ throw new NotFoundException('User not authenticated');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load and validate the ad
|
|
|
+ const ad = await this.adService.getAdByIdValidated(adsId);
|
|
|
+
|
|
|
+ if (!ad) {
|
|
|
+ throw new NotFoundException(
|
|
|
+ 'Ad not found, disabled, or outside valid date range',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract client IP
|
|
|
+ const ip =
|
|
|
+ (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
|
|
|
+ (req.headers['x-real-ip'] as string) ||
|
|
|
+ req.ip ||
|
|
|
+ 'unknown';
|
|
|
+
|
|
|
+ // Extract optional headers (you can adjust these based on your client setup)
|
|
|
+ const userAgent = req.headers['user-agent'];
|
|
|
+ const appVersion = req.headers['app-version'] as string | undefined;
|
|
|
+ const os = req.headers['os'] as string | undefined;
|
|
|
+
|
|
|
+ // Publish ADS_CLICK event
|
|
|
+ const clickEvent: AdsClickEventPayload = {
|
|
|
+ adsId: ad.id,
|
|
|
+ adType: ad.adType,
|
|
|
+ channelId: ad.channelId,
|
|
|
+ adsModuleId: ad.adsModuleId,
|
|
|
+ uid,
|
|
|
+ ip,
|
|
|
+ appVersion,
|
|
|
+ os,
|
|
|
+ clickAt: Date.now(), // BigInt epoch milliseconds
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.logger.log(
|
|
|
+ `Publishing ads.click event for adsId=${adsId}, uid=${uid}`,
|
|
|
+ );
|
|
|
+ await this.rabbitmqPublisher.publishAdsClick(clickEvent);
|
|
|
+ } catch (error) {
|
|
|
+ // Log error but don't fail the request
|
|
|
+ const errorMessage =
|
|
|
+ error instanceof Error ? error.message : String(error);
|
|
|
+ const errorStack = error instanceof Error ? error.stack : undefined;
|
|
|
+ this.logger.error(
|
|
|
+ `Failed to publish ads.click event for adsId=${adsId}: ${errorMessage}`,
|
|
|
+ errorStack,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ adsId: ad.id,
|
|
|
+ adsUrl: ad.adsUrl,
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|