|
|
@@ -1,7 +1,5 @@
|
|
|
// apps/box-app-api/src/feature/ads/ad.service.ts
|
|
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
|
-import { ConfigService } from '@nestjs/config';
|
|
|
-import { HttpService } from '@nestjs/axios';
|
|
|
import { RedisService } from '@box/db/redis/redis.service';
|
|
|
import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
|
|
|
import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
|
|
|
@@ -58,21 +56,13 @@ export interface GetAdForPlacementParams {
|
|
|
@Injectable()
|
|
|
export class AdService {
|
|
|
private readonly logger = new Logger(AdService.name);
|
|
|
- private readonly mgntApiBaseUrl: string;
|
|
|
|
|
|
constructor(
|
|
|
private readonly redis: RedisService,
|
|
|
private readonly mongoPrisma: MongoPrismaService,
|
|
|
private readonly rabbitmqPublisher: RabbitmqPublisherService,
|
|
|
- private readonly configService: ConfigService,
|
|
|
- private readonly httpService: HttpService,
|
|
|
private readonly sysParamsService: SysParamsService,
|
|
|
- ) {
|
|
|
- // Get mgnt-api base URL for cache rebuild notifications
|
|
|
- this.mgntApiBaseUrl =
|
|
|
- this.configService.get<string>('MGNT_API_BASE_URL') ||
|
|
|
- 'http://localhost:3300';
|
|
|
- }
|
|
|
+ ) {}
|
|
|
|
|
|
/**
|
|
|
* Core method for app-api:
|
|
|
@@ -109,14 +99,11 @@ export class AdService {
|
|
|
usedIndexes.add(idx);
|
|
|
|
|
|
const entry = pool[idx];
|
|
|
- const adKey = tsCacheKeys.ad.byId(entry.id);
|
|
|
-
|
|
|
- const cachedAd =
|
|
|
- (await this.redis.getJson<CachedAd | null>(adKey)) ?? null;
|
|
|
+ const cachedAd = await this.fetchActiveAd(entry.id);
|
|
|
|
|
|
if (!cachedAd) {
|
|
|
this.logger.debug(
|
|
|
- `getAdForPlacement: missing per-ad cache for adId=${entry.id}, key=${adKey}, poolKey=${poolKey}`,
|
|
|
+ `getAdForPlacement: missing active ad data for adId=${entry.id}, poolKey=${poolKey}`,
|
|
|
);
|
|
|
continue;
|
|
|
}
|
|
|
@@ -228,6 +215,63 @@ export class AdService {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ private async fetchActiveAd(adId: string): Promise<CachedAd | null> {
|
|
|
+ const now = nowSecBigInt();
|
|
|
+ const ad = await this.mongoPrisma.ads.findUnique({
|
|
|
+ where: { id: adId },
|
|
|
+ select: {
|
|
|
+ id: true,
|
|
|
+ adId: true,
|
|
|
+ adType: true,
|
|
|
+ advertiser: true,
|
|
|
+ title: true,
|
|
|
+ adsContent: true,
|
|
|
+ adsCoverImg: true,
|
|
|
+ adsUrl: true,
|
|
|
+ imgSource: true,
|
|
|
+ startDt: true,
|
|
|
+ expiryDt: true,
|
|
|
+ seq: true,
|
|
|
+ status: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!ad) {
|
|
|
+ this.logger.debug(`Ad not found in MongoDB: adId=${adId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ad.status !== 1) {
|
|
|
+ this.logger.debug(`Ad is disabled: adId=${adId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ad.startDt > now) {
|
|
|
+ this.logger.debug(`Ad not started yet: adId=${adId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ad.expiryDt !== BigInt(0) && ad.expiryDt < now) {
|
|
|
+ this.logger.debug(`Ad expired: adId=${adId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: ad.id,
|
|
|
+ adId: ad.adId,
|
|
|
+ adType: ad.adType,
|
|
|
+ advertiser: ad.advertiser,
|
|
|
+ title: ad.title,
|
|
|
+ adsContent: ad.adsContent,
|
|
|
+ adsCoverImg: ad.adsCoverImg,
|
|
|
+ adsUrl: ad.adsUrl,
|
|
|
+ imgSource: ad.imgSource,
|
|
|
+ startDt: ad.startDt,
|
|
|
+ expiryDt: ad.expiryDt,
|
|
|
+ seq: ad.seq,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Get all ads grouped by ad type.
|
|
|
* Returns a list of all ad types (from SysParamsService.getAdTypes)
|
|
|
@@ -467,117 +511,12 @@ export class AdService {
|
|
|
* Returns the cached ad data if found in Redis, otherwise queries MongoDB.
|
|
|
* Note: Cache validation (status, date range) is handled during cache rebuild or query.
|
|
|
*/
|
|
|
- async getAdByIdValidated(adsId: string): Promise<{
|
|
|
- id: string;
|
|
|
- adType: string;
|
|
|
- adsUrl: string | null;
|
|
|
- advertiser: string;
|
|
|
- title: string;
|
|
|
- } | null> {
|
|
|
- const adKey = tsCacheKeys.ad.byId(adsId);
|
|
|
-
|
|
|
+ async getAdByIdValidated(adsId: string): Promise<CachedAd | null> {
|
|
|
try {
|
|
|
- // Try Redis cache first
|
|
|
- const cachedAd = await this.redis.getJson<CachedAd | null>(adKey);
|
|
|
-
|
|
|
- if (cachedAd) {
|
|
|
- // Cache hit - return cached data
|
|
|
- return {
|
|
|
- id: cachedAd.id,
|
|
|
- adType: cachedAd.adType,
|
|
|
- adsUrl: cachedAd.adsUrl,
|
|
|
- advertiser: cachedAd.advertiser ?? '',
|
|
|
- title: cachedAd.title ?? '',
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // Cache miss - fallback to MongoDB
|
|
|
- this.logger.debug(
|
|
|
- `Ad cache miss: adsId=${adsId}, key=${adKey}, falling back to MongoDB`,
|
|
|
- );
|
|
|
-
|
|
|
- const now = nowSecBigInt();
|
|
|
- const ad = await this.mongoPrisma.ads.findUnique({
|
|
|
- where: { id: adsId },
|
|
|
- select: {
|
|
|
- id: true,
|
|
|
- adType: true,
|
|
|
- advertiser: true,
|
|
|
- title: true,
|
|
|
- adsContent: true,
|
|
|
- adsCoverImg: true,
|
|
|
- adsUrl: true,
|
|
|
- imgSource: true,
|
|
|
- startDt: true,
|
|
|
- expiryDt: true,
|
|
|
- seq: true,
|
|
|
- status: true,
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- if (!ad) {
|
|
|
- this.logger.debug(`Ad not found in MongoDB: adsId=${adsId}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // Validate status (1 = enabled)
|
|
|
- if (ad.status !== 1) {
|
|
|
- this.logger.debug(`Ad is disabled: adsId=${adsId}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // Validate date range
|
|
|
- if (ad.startDt > now) {
|
|
|
- this.logger.debug(`Ad not started yet: adsId=${adsId}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // If expiryDt is 0, it means no expiry; otherwise check if expired
|
|
|
- if (ad.expiryDt !== BigInt(0) && ad.expiryDt < now) {
|
|
|
- this.logger.debug(`Ad expired: adsId=${adsId}`);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // Cache the ad for future requests (fire-and-forget)
|
|
|
- const cacheData: CachedAd = {
|
|
|
- id: ad.id,
|
|
|
- adType: ad.adType,
|
|
|
- advertiser: ad.advertiser,
|
|
|
- title: ad.title,
|
|
|
- adsContent: ad.adsContent,
|
|
|
- adsCoverImg: ad.adsCoverImg,
|
|
|
- adsUrl: ad.adsUrl,
|
|
|
- imgSource: ad.imgSource,
|
|
|
- startDt: ad.startDt,
|
|
|
- expiryDt: ad.expiryDt,
|
|
|
- seq: ad.seq,
|
|
|
- };
|
|
|
-
|
|
|
- // Warm cache in Redis (fire-and-forget)
|
|
|
- this.redis.setJson(adKey, cacheData, 300).catch((err) => {
|
|
|
- this.logger.warn(
|
|
|
- `Failed to warm Redis cache for adsId=${adsId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- // Also notify mgnt-api to persist cache rebuild for durability (fire-and-forget)
|
|
|
- // This ensures the ad remains cached even if Redis is cleared
|
|
|
- this.notifyCacheSyncForAdRefresh(ad.id, ad.adType).catch((err) => {
|
|
|
- this.logger.debug(
|
|
|
- `Failed to notify mgnt-api for cache rebuild: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- return {
|
|
|
- id: ad.id,
|
|
|
- adType: ad.adType,
|
|
|
- adsUrl: ad.adsUrl,
|
|
|
- advertiser: ad.advertiser ?? '',
|
|
|
- title: ad.title ?? '',
|
|
|
- };
|
|
|
+ return await this.fetchActiveAd(adsId);
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
- `Error fetching ad: adsId=${adsId}, key=${adKey}`,
|
|
|
+ `Error fetching ad: adsId=${adsId}`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
return null;
|
|
|
@@ -738,43 +677,4 @@ export class AdService {
|
|
|
`Initiated stats.ad.impression publish for adId=${body.adId}, uid=${uid}`,
|
|
|
);
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * Notify mgnt-api to schedule a cache rebuild for a specific ad.
|
|
|
- * This is called when an ad is loaded from MongoDB (cache miss),
|
|
|
- * so that mgnt-api can persist the cache rebuild for durability.
|
|
|
- * Uses fire-and-forget with timeout to avoid blocking requests.
|
|
|
- *
|
|
|
- * @param adId - The ad ID to refresh
|
|
|
- * @param adType - The ad type for pool rebuild
|
|
|
- */
|
|
|
- private async notifyCacheSyncForAdRefresh(
|
|
|
- adId: string,
|
|
|
- adType: string,
|
|
|
- ): Promise<void> {
|
|
|
- try {
|
|
|
- const url = `${this.mgntApiBaseUrl}/mgnt-debug/cache-sync/ad/refresh`;
|
|
|
- const timeout = 5000; // 5 second timeout
|
|
|
- const controller = new AbortController();
|
|
|
- const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
-
|
|
|
- const response = await this.httpService.axiosRef.post(
|
|
|
- url,
|
|
|
- { adId, adType },
|
|
|
- { signal: controller.signal },
|
|
|
- );
|
|
|
-
|
|
|
- clearTimeout(timeoutId);
|
|
|
-
|
|
|
- this.logger.debug(
|
|
|
- `Successfully notified mgnt-api to rebuild cache for adId=${adId}`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- // Log but don't fail - this is best-effort
|
|
|
- const errorMsg = err instanceof Error ? err.message : String(err);
|
|
|
- this.logger.debug(
|
|
|
- `Failed to notify mgnt-api for cache rebuild (adId=${adId}): ${errorMsg}`,
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
}
|