|
|
@@ -6,11 +6,17 @@ import { RedisService } from '@box/db/redis/redis.service';
|
|
|
import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
|
|
|
import { CacheKeys } from '@box/common/cache/cache-keys';
|
|
|
import { AdDto } from './dto/ad.dto';
|
|
|
-import { AdListResponseDto, AdItemDto } from './dto';
|
|
|
+import {
|
|
|
+ AdListResponseDto,
|
|
|
+ AdItemDto,
|
|
|
+ AllAdsResponseDto,
|
|
|
+ AdsByTypeDto,
|
|
|
+} from './dto';
|
|
|
import { AdType } from '@box/common/ads/ad-types';
|
|
|
import { AdUrlResponseDto } from './dto/ad-url-response.dto';
|
|
|
import { AdClickDto } from './dto/ad-click.dto';
|
|
|
import { AdImpressionDto } from './dto/ad-impression.dto';
|
|
|
+import { SysParamsService } from '../sys-params/sys-params.service';
|
|
|
import {
|
|
|
RabbitmqPublisherService,
|
|
|
StatsAdClickEventPayload,
|
|
|
@@ -68,6 +74,7 @@ export class AdService {
|
|
|
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 =
|
|
|
@@ -216,9 +223,150 @@ export class AdService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Get paginated list of ads by type from Redis pool.
|
|
|
+ * Get all ads grouped by ad type.
|
|
|
+ * Returns a list of all ad types (from SysParamsService.getAdTypes)
|
|
|
+ * and ads grouped by each ad type.
|
|
|
+ *
|
|
|
+ * Flow:
|
|
|
+ * 1. Fetch all ad types from SysParamsService
|
|
|
+ * 2. For each ad type, fetch ads from Redis pool with pagination
|
|
|
+ * 3. Return adTypes list and adsList grouped by type
|
|
|
+ */
|
|
|
+ async listAdsByType(page: number, size: number): Promise<AllAdsResponseDto> {
|
|
|
+ // Step 1: Get all ad types
|
|
|
+ const adTypes = await this.sysParamsService.getAdTypes();
|
|
|
+
|
|
|
+ // Step 2: For each ad type, fetch ads
|
|
|
+ const adsList: AdsByTypeDto[] = [];
|
|
|
+
|
|
|
+ for (const adTypeInfo of adTypes) {
|
|
|
+ const poolKey = CacheKeys.appAdPoolByType(adTypeInfo.adType);
|
|
|
+
|
|
|
+ // Get the entire pool from Redis
|
|
|
+ let poolEntries: AdPoolEntry[] = [];
|
|
|
+ try {
|
|
|
+ const jsonData = await this.redis.getJson<AdPoolEntry[]>(poolKey);
|
|
|
+ if (jsonData && Array.isArray(jsonData)) {
|
|
|
+ poolEntries = jsonData;
|
|
|
+ } else {
|
|
|
+ this.logger.warn(
|
|
|
+ `Ad pool cache miss or invalid for adType=${adTypeInfo.adType}, key=${poolKey}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ if (err instanceof Error && err.message?.includes('WRONGTYPE')) {
|
|
|
+ this.logger.warn(
|
|
|
+ `Ad pool key ${poolKey} has wrong type, deleting incompatible key`,
|
|
|
+ );
|
|
|
+ try {
|
|
|
+ await this.redis.del(poolKey);
|
|
|
+ this.logger.log(
|
|
|
+ `Deleted incompatible ad pool key ${poolKey}. It will be rebuilt on next cache warmup.`,
|
|
|
+ );
|
|
|
+ } catch (delErr) {
|
|
|
+ this.logger.error(
|
|
|
+ `Failed to delete incompatible key ${poolKey}`,
|
|
|
+ delErr instanceof Error ? delErr.stack : String(delErr),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.logger.error(
|
|
|
+ `Failed to read ad pool for adType=${adTypeInfo.adType}, key=${poolKey}`,
|
|
|
+ err instanceof Error ? err.stack : String(err),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Array.isArray(poolEntries) || poolEntries.length === 0) {
|
|
|
+ // No ads for this type, add empty entry
|
|
|
+ adsList.push({
|
|
|
+ adType: adTypeInfo.adType,
|
|
|
+ items: [],
|
|
|
+ total: 0,
|
|
|
+ });
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const total = poolEntries.length;
|
|
|
+
|
|
|
+ // Apply pagination
|
|
|
+ const start = (page - 1) * size;
|
|
|
+ const stop = start + size - 1;
|
|
|
+
|
|
|
+ const items: AdItemDto[] = [];
|
|
|
+
|
|
|
+ if (start < total) {
|
|
|
+ // Slice the pool entries for this page
|
|
|
+ const pagedEntries = poolEntries.slice(start, stop + 1);
|
|
|
+ const adIds = pagedEntries.map((entry) => entry.id);
|
|
|
+
|
|
|
+ // Query MongoDB for full ad details
|
|
|
+ try {
|
|
|
+ const now = BigInt(Date.now());
|
|
|
+ const ads = await this.mongoPrisma.ads.findMany({
|
|
|
+ where: {
|
|
|
+ id: { in: adIds },
|
|
|
+ status: 1,
|
|
|
+ startDt: { lte: now },
|
|
|
+ OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // Create a map of ads by ID for fast lookup
|
|
|
+ const adMap = new Map(ads.map((ad) => [ad.id, ad]));
|
|
|
+
|
|
|
+ // Reorder results to match the pool order and map to AdItemDto
|
|
|
+ for (const entry of pagedEntries) {
|
|
|
+ const ad = adMap.get(entry.id);
|
|
|
+ if (!ad) {
|
|
|
+ this.logger.debug(
|
|
|
+ `Ad not found in MongoDB for adId=${entry.id} from pool`,
|
|
|
+ );
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ items.push({
|
|
|
+ id: ad.id,
|
|
|
+ advertiser: ad.advertiser ?? '',
|
|
|
+ title: ad.title ?? '',
|
|
|
+ adsContent: ad.adsContent ?? null,
|
|
|
+ adsCoverImg: ad.adsCoverImg ?? null,
|
|
|
+ adsUrl: ad.adsUrl ?? null,
|
|
|
+ startDt: ad.startDt.toString(),
|
|
|
+ expiryDt: ad.expiryDt.toString(),
|
|
|
+ seq: ad.seq ?? 0,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.logger.error(
|
|
|
+ `Failed to query ads from MongoDB for adIds=${adIds.join(',')}`,
|
|
|
+ err instanceof Error ? err.stack : String(err),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ adsList.push({
|
|
|
+ adType: adTypeInfo.adType,
|
|
|
+ items,
|
|
|
+ total,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ adTypes,
|
|
|
+ adsList,
|
|
|
+ page,
|
|
|
+ size,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get paginated list of ads by specific type from Redis pool.
|
|
|
* Reads the prebuilt ad pool from Redis, applies pagination, and fetches full ad details.
|
|
|
*
|
|
|
+ * @deprecated Use listAdsByType() instead for getting all ads grouped by type.
|
|
|
+ * This method is kept for backward compatibility.
|
|
|
+ *
|
|
|
* Flow:
|
|
|
* 1. Get total count from pool
|
|
|
* 2. Compute start/stop indices for LRANGE
|
|
|
@@ -227,7 +375,7 @@ export class AdService {
|
|
|
* 5. Reorder results to match Redis pool order
|
|
|
* 6. Map to AdItemDto and return response
|
|
|
*/
|
|
|
- async listAdsByType(
|
|
|
+ async listAdsBySpecificType(
|
|
|
adType: string,
|
|
|
page: number,
|
|
|
size: number,
|
|
|
@@ -584,6 +732,8 @@ export class AdService {
|
|
|
adType: body.adType,
|
|
|
clickedAt,
|
|
|
ip,
|
|
|
+ channelId: body.channelId,
|
|
|
+ machine: body.machine,
|
|
|
};
|
|
|
|
|
|
// Fire-and-forget: don't await, log errors asynchronously
|
|
|
@@ -627,6 +777,8 @@ export class AdService {
|
|
|
impressionAt,
|
|
|
visibleDurationMs: body.visibleDurationMs,
|
|
|
ip,
|
|
|
+ channelId: body.channelId,
|
|
|
+ machine: body.machine,
|
|
|
};
|
|
|
|
|
|
// Fire-and-forget: don't await, log errors asynchronously
|