瀏覽代碼

feat(stats-reporting): refactor ad metadata resolution to use Redis as the authoritative source

Dave 1 月之前
父節點
當前提交
0d2adf51b1
共有 1 個文件被更改,包括 39 次插入39 次删除
  1. 39 39
      apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts

+ 39 - 39
apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts

@@ -34,8 +34,6 @@ interface AdClickMessage extends BaseStatsMessage {
   machine?: string;
 }
 
-const AD_META_KEY_PREFIX = 'box:app:ad:meta:';
-
 @Injectable()
 export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
   private readonly logger = new Logger(StatsEventsConsumer.name);
@@ -332,11 +330,11 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
     }
 
     try {
-      const resolvedAdType = await this.resolveAdTypeForAdsId(adsId);
+      const resolvedMeta = await this.resolveAdMetadata(adsId);
 
-      if (!resolvedAdType) {
+      if (!resolvedMeta) {
         this.logger.error(
-          `Invalid ad metadata for adsId=${adsId}, uid=${payload.uid}, channelId=${payload.channelId}, messageId=${messageId}; dropping event per Task 2 – stats-consumer-enrichment-flow.md`,
+          `Invalid ad metadata for adsId=${adsId}, uid=${payload.uid}, channelId=${payload.channelId}, messageId=${messageId}; dropping event because Redis hash box:stats:ads is authoritative`,
         );
         this.counters.malformed++;
         this.ack(msg);
@@ -357,8 +355,8 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
         data: {
           uid: payload.uid,
           adsId: adsId,
-          adId: adId,
-          adType: resolvedAdType,
+          adId: resolvedMeta.adId ?? adId,
+          adType: resolvedMeta.adType,
           clickedAt: clickTime,
           ip: payload.ip,
           channelId: payload.channelId,
@@ -381,46 +379,48 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
     }
   }
 
-  private async resolveAdTypeForAdsId(adsId: string): Promise<string | null> {
-    // Task 2 – stats-consumer-enrichment-flow.md: Redis read-only, Mongo fallback, hostile adsId drop path.
-    const cacheKey = `${AD_META_KEY_PREFIX}${adsId}`;
-    let cachedValue: string | null = null;
+  private async resolveAdMetadata(
+    adsId: string,
+  ): Promise<{ adType: string; adId?: number } | null> {
+    const hashKey = 'box:stats:ads';
+    // Redis hash box:stats:ads is the single source of truth now; the Mongo fallback was removed per new architecture.
+    const ensureClient = (this.redis as any).ensureClient as
+      | (() => unknown)
+      | undefined;
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+    const redisClient =
+      ensureClient?.call(this.redis) ?? (this.redis as any).client ?? null;
+
+    if (!redisClient) {
+      this.logger.error(
+        'Redis client unavailable while resolving ads metadata',
+      );
+      return null;
+    }
 
     try {
-      cachedValue = await this.redis.get(cacheKey);
+      const rawValue = await redisClient.hget(hashKey, adsId);
+      if (!rawValue) return null;
+
+      const parsed = JSON.parse(rawValue);
+      if (
+        parsed &&
+        typeof parsed === 'object' &&
+        typeof parsed.adType === 'string'
+      ) {
+        return {
+          adType: parsed.adType,
+          adId: typeof parsed.adId === 'number' ? parsed.adId : undefined,
+        };
+      }
     } catch (err) {
       this.logger.debug(
-        `Redis lookup failed for key=${cacheKey}, falling back to Mongo`,
+        `Redis lookup/parsing failed for ${hashKey} field ${adsId}`,
         err instanceof Error ? err.stack : String(err),
       );
     }
 
-    if (cachedValue) {
-      const trimmed = cachedValue.trim();
-      if (trimmed) {
-        if (trimmed.startsWith('{')) {
-          try {
-            const parsed = JSON.parse(trimmed);
-            if (parsed && typeof parsed === 'object' && 'adType' in parsed) {
-              return String(parsed.adType);
-            }
-          } catch {
-            // ignore parse failures and treat value as raw string below
-          }
-        }
-        if (!trimmed.startsWith('{')) {
-          return trimmed;
-        }
-      }
-    }
-
-    const client = this.prisma as any;
-    const adRecord = await client.ads.findUnique({
-      where: { id: adsId },
-      select: { adType: true },
-    });
-
-    return adRecord?.adType ?? null;
+    return null;
   }
 
   async onModuleDestroy(): Promise<void> {