Pārlūkot izejas kodu

refactor: rename channelId to uChannelId across the application

- Updated LoginDto to use uChannelId instead of channelId.
- Refactored JwtStrategy to return the entire AppJwtPayload as CurrentAppUser.
- Created CurrentAppUser type alias for better type management.
- Removed channelId references from AdRecommendationContextDto and related controllers.
- Updated RecommendationService and its methods to eliminate channelId usage.
- Refactored AdClickDto, AdImpressionDto, and VideoClickDto to use uChannelId.
- Updated stats events and user login services to reflect the new uChannelId naming.
- Adjusted Prisma schema to replace channelId with uChannelId in relevant models.
- Cleaned up commented-out code related to channelId in various services and controllers.
Dave 3 mēneši atpakaļ
vecāks
revīzija
51d4cb2212
34 mainītis faili ar 377 papildinājumiem un 304 dzēšanām
  1. 2 2
      apps/box-app-api/src/feature/ads/ad.service.ts
  2. 1 1
      apps/box-app-api/src/feature/ads/dto/ad-click.dto.ts
  3. 1 1
      apps/box-app-api/src/feature/ads/dto/ad-impression.dto.ts
  4. 1 1
      apps/box-app-api/src/feature/auth/auth.controller.ts
  5. 17 11
      apps/box-app-api/src/feature/auth/auth.service.ts
  6. 48 0
      apps/box-app-api/src/feature/auth/decorators/current-user.decorator.ts
  7. 96 0
      apps/box-app-api/src/feature/auth/interfaces/app-jwt-payload.ts
  8. 80 0
      apps/box-app-api/src/feature/auth/jwt-utils.ts
  9. 1 1
      apps/box-app-api/src/feature/auth/login.dto.ts
  10. 18 16
      apps/box-app-api/src/feature/auth/strategies/jwt.strategy.ts
  11. 12 0
      apps/box-app-api/src/feature/auth/types/current-app-user.ts
  12. 5 5
      apps/box-app-api/src/feature/recommendation/dto/ad-recommendation.dto.ts
  13. 7 9
      apps/box-app-api/src/feature/recommendation/recommend-public.controller.ts
  14. 7 10
      apps/box-app-api/src/feature/recommendation/recommendation.controller.ts
  15. 13 13
      apps/box-app-api/src/feature/recommendation/recommendation.service.ts
  16. 1 1
      apps/box-app-api/src/feature/stats/dto/ad-click.dto.ts
  17. 1 1
      apps/box-app-api/src/feature/stats/dto/ad-impression.dto.ts
  18. 4 4
      apps/box-app-api/src/feature/stats/dto/video-click.dto.ts
  19. 3 3
      apps/box-app-api/src/feature/stats/stats-events.service.ts
  20. 12 169
      apps/box-app-api/src/feature/video/video.controller.ts
  21. 5 4
      apps/box-app-api/src/feature/video/video.service.ts
  22. 2 2
      apps/box-app-api/src/rabbitmq/rabbitmq-publisher.service.ts
  23. 4 2
      apps/box-mgnt-api/src/dev/services/video-cache-coverage.service.ts
  24. 4 2
      apps/box-mgnt-api/src/dev/services/video-stats.service.ts
  25. 0 19
      apps/box-mgnt-api/src/mgnt-backend/feature/tag/tag.dto.ts
  26. 6 6
      apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts
  27. 3 3
      apps/box-stats-api/src/feature/user-login/user-login.service.ts
  28. 1 1
      libs/common/src/events/user-login-event.dto.ts
  29. 4 2
      libs/core/src/cache/video/category/video-category-cache.builder.ts
  30. 6 3
      libs/core/src/cache/video/list/video-list-cache.builder.ts
  31. 2 2
      prisma/mongo-stats/schema/ads-click-history.prisma
  32. 6 6
      prisma/mongo-stats/schema/events.prisma
  33. 2 2
      prisma/mongo-stats/schema/user-login-history.prisma
  34. 2 2
      prisma/mongo-stats/schema/user.prisma

+ 2 - 2
apps/box-app-api/src/feature/ads/ad.service.ts

@@ -718,7 +718,7 @@ export class AdService {
       adType: body.adType,
       clickedAt,
       ip,
-      channelId: body.channelId,
+      uChannelId: body.uChannelId,
       machine: body.machine,
     };
 
@@ -763,7 +763,7 @@ export class AdService {
       impressionAt,
       visibleDurationMs: body.visibleDurationMs,
       ip,
-      channelId: body.channelId,
+      uChannelId: body.uChannelId,
       machine: body.machine,
     };
 

+ 1 - 1
apps/box-app-api/src/feature/ads/dto/ad-click.dto.ts

@@ -19,7 +19,7 @@ export class AdClickDto {
   })
   @IsNotEmpty()
   @IsString()
-  channelId: string;
+  uChannelId: string;
 
   @ApiProperty({
     description: '设备信息(品牌、系统版本等)',

+ 1 - 1
apps/box-app-api/src/feature/ads/dto/ad-impression.dto.ts

@@ -31,7 +31,7 @@ export class AdImpressionDto {
   })
   @IsNotEmpty()
   @IsString()
-  channelId: string;
+  uChannelId: string;
 
   @ApiProperty({
     description: '设备信息(品牌、系统版本等)',

+ 1 - 1
apps/box-app-api/src/feature/auth/auth.controller.ts

@@ -42,7 +42,7 @@ export class AuthController {
       userAgent,
       appVersion: body.appVersion,
       os: body.os,
-      channelId: body.channelId,
+      uChannelId: body.uChannelId,
       machine: body.machine,
       // add any other auth-related fields as needed
     });

+ 17 - 11
apps/box-app-api/src/feature/auth/auth.service.ts

@@ -1,12 +1,10 @@
-// apps/box-app-api/src/auth/auth.service.ts
+// apps/box-app-api/src/feature/auth/auth.service.ts
 import { Injectable, Logger } from '@nestjs/common';
 import { JwtService } from '@nestjs/jwt';
 import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto';
 import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service';
-import { AdPoolService, AdPayload } from '@box/core/ad/ad-pool.service';
-import { AdType } from '@prisma/mongo/client';
-import { LoginResponseDto } from './dto/login-response.dto';
 import { AdService } from '../ads/ad.service';
+import { AppJwtPayload } from './interfaces/app-jwt-payload';
 
 @Injectable()
 export class AuthService {
@@ -14,7 +12,6 @@ export class AuthService {
   constructor(
     private readonly jwtService: JwtService,
     private readonly rabbitmqPublisher: RabbitmqPublisherService,
-    private readonly adPoolService: AdPoolService,
     private readonly adService: AdService,
   ) {}
 
@@ -24,11 +21,11 @@ export class AuthService {
     userAgent?: string;
     appVersion?: string;
     os?: string;
-    channelId?: string;
+    uChannelId?: string;
     machine?: string;
     // plus whatever you need like account, password, etc.
   }): Promise<any> {
-    const { uid, ip, userAgent, appVersion, os, channelId, machine } = params;
+    const { uid, ip, userAgent, appVersion, os, uChannelId, machine } = params;
 
     // 1) Your existing auth logic (validate user, etc.)
     // const user = await this.validateUser(...);
@@ -38,11 +35,20 @@ export class AuthService {
 
     const now = Date.now(); // number (ms since epoch)
 
-    const payload = {
-      sub: uid, // or your userId
+    // Build JWT payload with required and optional tracking fields.
+    // Tracking fields (uChannelId, machine, ip, userAgent, appVersion, os) are optional
+    // to preserve backward compatibility with older tokens and minimize JWT size for stability.
+    const payload: AppJwtPayload = {
+      sub: uid,
       uid,
       jti: tokenId,
-      // ... other claims
+      uChannelId,
+      machine,
+      ip,
+      userAgent,
+      appVersion,
+      os,
+      iat: Math.floor(now / 1000), // issued at (in seconds, per JWT spec)
     };
 
     const accessToken = await this.jwtService.signAsync(payload);
@@ -54,7 +60,7 @@ export class AuthService {
       userAgent,
       appVersion,
       os,
-      channelId,
+      uChannelId,
       machine,
       tokenId,
       loginAt: now,

+ 48 - 0
apps/box-app-api/src/feature/auth/decorators/current-user.decorator.ts

@@ -0,0 +1,48 @@
+/**
+ * @CurrentUser() Decorator for Box App API
+ *
+ * Extracts the authenticated user from the request context.
+ * Returns the validated JWT payload with full type safety.
+ *
+ * The payload includes:
+ * - Core claims: sub, uid, jti
+ * - Optional tracking fields: uChannelId, machine, ip, userAgent, appVersion, os, iat
+ *
+ * Usage Example:
+ * ```typescript
+ * import { Controller, Get } from '@nestjs/common';
+ * import { CurrentUser } from './decorators/current-user.decorator';
+ * import { CurrentAppUser } from '../types/current-app-user';
+ *
+ * @Controller('users')
+ * export class UserController {
+ *   @Get('profile')
+ *   async getProfile(@CurrentUser() user: CurrentAppUser) {
+ *     const { uid, uChannelId, appVersion, os } = user;
+ *     // All fields are strongly typed
+ *     console.log(`User ${uid} from channel ${uChannelId} on ${os} v${appVersion}`);
+ *   }
+ * }
+ * ```
+ *
+ * For analytics and logging:
+ * - ip and userAgent are for analytics purposes only, not security enforcement
+ * - appVersion helps track client version usage
+ * - os enables platform-specific behavior
+ * - uChannelId tracks traffic source
+ * - machine identifies repeat visitors
+ */
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
+import type { FastifyRequest } from 'fastify';
+import { CurrentAppUser } from '../types/current-app-user';
+
+interface AuthenticatedRequest extends FastifyRequest {
+  user: CurrentAppUser;
+}
+
+export const CurrentUser = createParamDecorator(
+  (_data, ctx: ExecutionContext): CurrentAppUser => {
+    const req = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
+    return req.user;
+  },
+);

+ 96 - 0
apps/box-app-api/src/feature/auth/interfaces/app-jwt-payload.ts

@@ -0,0 +1,96 @@
+/**
+ * AppJwtPayload
+ *
+ * Represents the JWT payload for app-side login (device-based authentication).
+ * This interface defines all claims included in the access token issued after
+ * a successful login, enabling stateless verification and tracking across requests.
+ *
+ * Backward Compatibility:
+ * All non-critical tracking fields (ip, userAgent, appVersion, os, uChannelId, machine)
+ * are optional to support tokens issued before this interface was standardized and
+ * allow graceful upgrades.
+ */
+export interface AppJwtPayload {
+  /**
+   * Primary subject claim (standard JWT).
+   * Typically matches uid; identifies the authenticated user/device.
+   * Required for all tokens.
+   */
+  sub: string;
+
+  /**
+   * User or device unique identifier.
+   * Primary key for the authenticated entity.
+   * Required for all tokens.
+   */
+  uid: string;
+
+  /**
+   * JWT Token ID (jti claim, standard JWT).
+   * Unique identifier for this specific token instance.
+   * Used to prevent token reuse and for token revocation/blacklisting.
+   * Required for all tokens.
+   */
+  jti: string;
+
+  /**
+   * Optional: Channel/Traffic source identifier.
+   * Tracks which channel or promotion campaign drove this login.
+   * Used for analytics and conversion tracking.
+   * Example: "organic", "campaign_abc", "referrer_xyz"
+   */
+  uChannelId?: string;
+
+  /**
+   * Optional: Device/Machine identifier.
+   * Identifies the specific device or machine making the request.
+   * Can be used for device fingerprinting and multi-device tracking.
+   * Example: a device UUID or hardware ID.
+   */
+  machine?: string;
+
+  /**
+   * Optional: IP address at login time.
+   * For logging and analytics purposes only, NOT for hard security checks.
+   * IP-based geolocation and fraud detection should happen in dedicated services,
+   * not in token validation logic.
+   * Can vary on each request due to proxies, mobile networks, etc.
+   */
+  ip?: string;
+
+  /**
+   * Optional: User-Agent string from the login request.
+   * For analytics and debugging purposes only, NOT for security policy enforcement.
+   * Recorded for behavioral analysis and device type detection.
+   * Should not be used to reject requests since UA can vary or be spoofed.
+   */
+  userAgent?: string;
+
+  /**
+   * Optional: Application version at login time.
+   * Identifies the version of the app that issued this token.
+   * Useful for analytics, feature flags, and deprecation tracking.
+   * Example: "1.2.3", "2024.12.0"
+   */
+  appVersion?: string;
+
+  /**
+   * Optional: Operating system identifier.
+   * Identifies the OS of the device at login time.
+   * Examples: "Android", "iOS", "Web", "Windows", "macOS"
+   * Used for platform-specific analytics and API compatibility tracking.
+   */
+  os?: string;
+
+  /**
+   * Optional: Issued-at timestamp (standard JWT iat claim).
+   * Unix timestamp in seconds since epoch when this token was issued.
+   * Used to verify token age and enforce expiration.
+   * Stored as number (not bigint) to comply with JWT specification RFC 7519.
+   *
+   * Note: Database timestamps and audit logs elsewhere in the system use
+   * bigint epoch (milliseconds). This iat field explicitly uses seconds
+   * to match the JWT standard and prevent confusion.
+   */
+  iat?: number;
+}

+ 80 - 0
apps/box-app-api/src/feature/auth/jwt-utils.ts

@@ -0,0 +1,80 @@
+/**
+ * JWT Utility Functions for Box App API
+ *
+ * Helpers for safely extracting and working with JWT payloads
+ * in request contexts (Express/Fastify).
+ */
+
+import { AppJwtPayload } from './interfaces/app-jwt-payload';
+
+/**
+ * Safely extracts the AppJwtPayload from a request object.
+ *
+ * This function reads the `user` property that was injected by JwtAuthGuard
+ * after successful JWT verification and validation.
+ * It performs type-safe casting and returns null if the payload is missing
+ * or invalid, allowing safe usage without try-catch blocks.
+ *
+ * Typical usage in controllers, guards, or interceptors:
+ * ```typescript
+ * import { Request } from 'express';
+ * import { getAppJwtPayloadFromRequest } from './jwt-utils';
+ *
+ * @Controller('profile')
+ * export class ProfileController {
+ *   @Get()
+ *   async getProfile(@Req() req: Request) {
+ *     const claims = getAppJwtPayloadFromRequest(req);
+ *     if (claims) {
+ *       console.log(`User ${claims.uid} from channel ${claims.uChannelId} on ${claims.os} v${claims.appVersion}`);
+ *       // Use claims for logging, analytics, or business logic
+ *     } else {
+ *       // Payload missing or invalid
+ *       throw new UnauthorizedException('Invalid JWT payload');
+ *     }
+ *   }
+ * }
+ * ```
+ *
+ * Analytics logging example:
+ * ```typescript
+ * const claims = getAppJwtPayloadFromRequest(req);
+ * if (claims) {
+ *   logger.info('User login analytics', {
+ *     uid: claims.uid,
+ *     channel: claims.uChannelId,
+ *     appVersion: claims.appVersion,
+ *     os: claims.os,
+ *     machine: claims.machine,
+ *     ip: claims.ip,
+ *     userAgent: claims.userAgent,
+ *     tokenId: claims.jti,
+ *     issuedAt: claims.iat,
+ *   });
+ * }
+ * ```
+ *
+ * @param req The Express/Fastify request object with user property injected by JwtAuthGuard
+ * @returns The AppJwtPayload if present and valid, null otherwise
+ *
+ * @remarks
+ * - Returns null if req.user is undefined or null
+ * - Returns null if req.user is not an object
+ * - No exceptions are thrown; safe for optional chaining
+ * - The returned payload includes all optional tracking fields
+ */
+export function getAppJwtPayloadFromRequest(req: any): AppJwtPayload | null {
+  // Validate that req exists
+  if (!req) {
+    return null;
+  }
+
+  // Validate that req.user exists and is an object
+  if (!req.user || typeof req.user !== 'object') {
+    return null;
+  }
+
+  // Cast to AppJwtPayload and return
+  // The JwtStrategy.validate() method ensures this is the correct type
+  return req.user as AppJwtPayload;
+}

+ 1 - 1
apps/box-app-api/src/feature/auth/login.dto.ts

@@ -17,7 +17,7 @@ export class LoginDto {
   })
   @IsNotEmpty()
   @IsString()
-  channelId: string;
+  uChannelId: string;
 
   @ApiProperty({
     description: '机器型号',

+ 18 - 16
apps/box-app-api/src/feature/auth/strategies/jwt.strategy.ts

@@ -3,14 +3,8 @@ import { Injectable } from '@nestjs/common';
 import { PassportStrategy } from '@nestjs/passport';
 import { ExtractJwt, Strategy } from 'passport-jwt';
 import { ConfigService } from '@nestjs/config';
-
-export interface JwtPayload {
-  sub: string; // subject (uid)
-  uid: string;
-  jti: string; // token ID
-  iat?: number; // issued at
-  exp?: number; // expiry
-}
+import { AppJwtPayload } from '../interfaces/app-jwt-payload';
+import { CurrentAppUser } from '../types/current-app-user';
 
 @Injectable()
 export class JwtStrategy extends PassportStrategy(Strategy) {
@@ -26,14 +20,22 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
   /**
    * This method is called after JWT is verified.
    * The payload is already decoded and signature-checked.
-   * We return the user object that will be attached to req.user.
+   * The returned user object is attached to req.user and available via @CurrentUser() decorator.
+   *
+   * @param payload The decoded JWT payload containing all app claims including optional tracking fields
+   * @returns The current user as CurrentAppUser (AppJwtPayload) with full type hints
+   *
+   * Usage in controllers:
+   *   @Get('profile')
+   *   async getProfile(@CurrentUser() user: CurrentAppUser) {
+   *     const { uid, uChannelId, appVersion, os, machine, ip, userAgent } = user;
+   *     // All fields are now strongly typed
+   *   }
    */
-  async validate(payload: JwtPayload) {
-    // Return an object that will become req.user
-    return {
-      uid: payload.uid,
-      sub: payload.sub,
-      jti: payload.jti,
-    };
+  async validate(payload: AppJwtPayload): Promise<CurrentAppUser> {
+    // Return the entire payload as the current user object.
+    // This preserves all tracking fields (uChannelId, machine, ip, userAgent, appVersion, os)
+    // for use in controllers and services throughout the app.
+    return payload;
   }
 }

+ 12 - 0
apps/box-app-api/src/feature/auth/types/current-app-user.ts

@@ -0,0 +1,12 @@
+/**
+ * CurrentAppUser
+ *
+ * Type alias for the current authenticated user in the app context.
+ * This represents the JWT payload that has been validated by JwtStrategy
+ * and is now available in controllers via the @CurrentUser() decorator.
+ *
+ * All fields are strongly typed for use throughout the application.
+ */
+import { AppJwtPayload } from '../interfaces/app-jwt-payload';
+
+export type CurrentAppUser = AppJwtPayload;

+ 5 - 5
apps/box-app-api/src/feature/recommendation/dto/ad-recommendation.dto.ts

@@ -22,11 +22,11 @@ export class AdRecommendationDto {
 }
 
 export class AdRecommendationContextDto {
-  @ApiProperty({
-    description: '渠道ID(必须匹配)',
-    example: '6756channel123',
-  })
-  channelId: string;
+  // @ApiProperty({
+  //   description: '渠道ID(必须匹配)',
+  //   example: '6756channel123',
+  // })
+  // channelId: string;
 
   @ApiProperty({
     description: '广告模块ID(场景/槽位)',

+ 7 - 9
apps/box-app-api/src/feature/recommendation/recommend-public.controller.ts

@@ -75,12 +75,12 @@ export class RecommendPublicController {
     description: '当前广告ID',
     example: '6756def456ghi',
   })
-  @ApiQuery({
-    name: 'channelId',
-    required: true,
-    description: '渠道ID(必须匹配)',
-    example: '6756channel123',
-  })
+  // @ApiQuery({
+  //   name: 'channelId',
+  //   required: true,
+  //   description: '渠道ID(必须匹配)',
+  //   example: '6756channel123',
+  // })
   @ApiQuery({
     name: 'adsModuleId',
     required: true,
@@ -100,19 +100,17 @@ export class RecommendPublicController {
   })
   async getAdRecommendations(
     @Param('adId') adId: string,
-    @Query('channelId') channelId: string,
     @Query('adsModuleId') adsModuleId: string,
     @Query('limit') limit?: string,
   ): Promise<YouMayAlsoLikeAdResponseDto> {
     const limitNum = limit ? parseInt(limit, 10) : 3;
 
     this.logger.debug(
-      `GET /api/v1/recommend/ad/${adId}?channelId=${channelId}&adsModuleId=${adsModuleId}&limit=${limitNum}`,
+      `GET /api/v1/recommend/ad/${adId}?adsModuleId=${adsModuleId}&limit=${limitNum}`,
     );
 
     const recommendations =
       await this.recommendationService.getEnrichedAdRecommendations(adId, {
-        channelId,
         adsModuleId,
         limit: limitNum,
       });

+ 7 - 10
apps/box-app-api/src/feature/recommendation/recommendation.controller.ts

@@ -78,12 +78,12 @@ export class RecommendationController {
     description: '当前广告ID',
     example: '6756def456ghi',
   })
-  @ApiQuery({
-    name: 'channelId',
-    required: true,
-    description: '渠道ID(必须匹配)',
-    example: '6756channel123',
-  })
+  // @ApiQuery({
+  //   name: 'channelId',
+  //   required: true,
+  //   description: '渠道ID(必须匹配)',
+  //   example: '6756channel123',
+  // })
   @ApiQuery({
     name: 'adsModuleId',
     required: true,
@@ -103,20 +103,18 @@ export class RecommendationController {
   })
   async getSimilarAds(
     @Param('adId') adId: string,
-    @Query('channelId') channelId: string,
     @Query('adsModuleId') adsModuleId: string,
     @Query('limit') limit?: string,
   ): Promise<GetSimilarAdsResponseDto> {
     const limitNum = limit ? parseInt(limit, 10) : 5;
 
     this.logger.debug(
-      `GET /api/v1/recommendation/ads/${adId}/similar?channelId=${channelId}&adsModuleId=${adsModuleId}&limit=${limitNum}`,
+      `GET /api/v1/recommendation/ads/${adId}/similar?adsModuleId=${adsModuleId}&limit=${limitNum}`,
     );
 
     const recommendations = await this.recommendationService.getSimilarAds(
       adId,
       {
-        channelId,
         adsModuleId,
         limit: limitNum,
       },
@@ -127,7 +125,6 @@ export class RecommendationController {
       recommendations,
       count: recommendations.length,
       context: {
-        channelId,
         adsModuleId,
         limit: limitNum,
       },

+ 13 - 13
apps/box-app-api/src/feature/recommendation/recommendation.service.ts

@@ -23,7 +23,7 @@ interface AdCandidate {
 }
 
 export interface AdRecommendationContext {
-  channelId: string;
+  // channelId: string;
   adsModuleId: string;
   limit?: number;
 }
@@ -90,14 +90,14 @@ export class RecommendationService {
 
       const { tagIds } = currentVideo;
       // Use first category ID from categoryIds array
-      const channelId =
+      const categoryId =
         Array.isArray(currentVideo.categoryIds) &&
         currentVideo.categoryIds.length > 0
           ? currentVideo.categoryIds[0]
           : '';
 
       this.logger.debug(
-        `Video has ${tagIds?.length ?? 0} tags, categoryId=${channelId}`,
+        `Video has ${tagIds?.length ?? 0} tags, categoryId=${categoryId}`,
       );
 
       // 2. Collect candidates from tag-based sorted sets
@@ -124,8 +124,8 @@ export class RecommendationService {
       }
 
       // 5. Apply channel boost if available
-      if (channelId) {
-        await this.applyChannelBoost(candidates, channelId);
+      if (categoryId) {
+        await this.applyChannelBoost(candidates, categoryId);
       }
 
       // 6. Sort by boosted score and take top N
@@ -293,7 +293,7 @@ export class RecommendationService {
   /**
    * Get similar ads with strict channel and module filtering.
    * Algorithm:
-   * 1. Fetch eligible ads from Mongo (same channelId, adsModuleId, active, valid dates)
+   * 1. Fetch eligible ads from Mongo (same adsModuleId, active, valid dates)
    * 2. Get scores from Redis ads:global:score for eligible ads
    * 3. Sort by score descending and return top N
    * 4. Exclude current adId
@@ -302,10 +302,10 @@ export class RecommendationService {
     currentAdId: string,
     context: AdRecommendationContext,
   ): Promise<AdRecommendationDto[]> {
-    const { channelId, adsModuleId, limit = 5 } = context;
+    const { adsModuleId, limit = 5 } = context;
 
     this.logger.debug(
-      `Getting similar ads for adId=${currentAdId}, channelId=${channelId}, adsModuleId=${adsModuleId}, limit=${limit}`,
+      `Getting similar ads for adId=${currentAdId}, adsModuleId=${adsModuleId}, limit=${limit}`,
     );
 
     try {
@@ -327,7 +327,7 @@ export class RecommendationService {
 
       if (eligibleAds.length === 0) {
         this.logger.warn(
-          `No eligible ads found for channelId=${channelId}, adsModuleId=${adsModuleId}`,
+          `No eligible ads found for adsModuleId=${adsModuleId}`,
         );
         return [];
       }
@@ -474,7 +474,8 @@ export class RecommendationService {
       const videos = await this.prisma.videoMedia.findMany({
         where: {
           id: { in: videoIds },
-          listStatus: 1, // Only on-shelf videos
+          status: 'Completed',
+          // listStatus: 1, // Only on-shelf videos
         },
         select: {
           id: true,
@@ -526,16 +527,15 @@ export class RecommendationService {
     currentAdId: string,
     context: AdRecommendationContext,
   ): Promise<EnrichedAdRecommendationDto[]> {
-    const { channelId, adsModuleId, limit = 3 } = context;
+    const { adsModuleId, limit = 3 } = context;
 
     this.logger.debug(
-      `Getting enriched ad recommendations for adId=${currentAdId}, channelId=${channelId}, adsModuleId=${adsModuleId}, limit=${limit}`,
+      `Getting enriched ad recommendations for adId=${currentAdId}, adsModuleId=${adsModuleId}, limit=${limit}`,
     );
 
     try {
       // 1. Get basic recommendations from existing logic
       const recommendations = await this.getSimilarAds(currentAdId, {
-        channelId,
         adsModuleId,
         limit,
       });

+ 1 - 1
apps/box-app-api/src/feature/stats/dto/ad-click.dto.ts

@@ -18,7 +18,7 @@ export class AdClickDto {
   @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
   @IsNotEmpty()
   @IsString()
-  channelId: string;
+  uChannelId: string;
 
   @ApiProperty({ description: '业务场景(如 home, detail)', example: 'home' })
   @IsNotEmpty()

+ 1 - 1
apps/box-app-api/src/feature/stats/dto/ad-impression.dto.ts

@@ -19,7 +19,7 @@ export class AdImpressionDto {
   @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
   @IsNotEmpty()
   @IsString()
-  channelId: string;
+  uChannelId: string;
 
   @ApiProperty({ description: '业务场景(如 home, detail)', example: 'home' })
   @IsNotEmpty()

+ 4 - 4
apps/box-app-api/src/feature/stats/dto/video-click.dto.ts

@@ -7,10 +7,10 @@ export class VideoClickDto {
   @IsString()
   videoId: string;
 
-  @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
-  @IsNotEmpty()
-  @IsString()
-  channelId: string;
+  // @ApiProperty({ description: '渠道 ID', example: '652e7bcf4f1a2b4f98ad7777' })
+  // @IsNotEmpty()
+  // @IsString()
+  // channelId: string;
 
   @ApiProperty({
     description: '分类 ID',

+ 3 - 3
apps/box-app-api/src/feature/stats/stats-events.service.ts

@@ -11,7 +11,7 @@ export interface AdClickEvent {
   uid: string;
   adId: string;
   adsModuleId: string;
-  channelId: string;
+  uChannelId: string;
   scene: string;
   slot: string;
   adType: string;
@@ -27,7 +27,7 @@ export interface AdClickEvent {
 export interface VideoClickEvent {
   uid: string;
   videoId: string;
-  channelId: string;
+  // uChannelId: string;
   categoryId?: string;
   scene: string;
   clickedAt: bigint;
@@ -43,7 +43,7 @@ export interface AdImpressionEvent {
   uid: string;
   adId: string;
   adsModuleId: string;
-  channelId: string;
+  // uChannelId: string;
   scene: string;
   slot: string;
   adType: string;

+ 12 - 169
apps/box-app-api/src/feature/video/video.controller.ts

@@ -19,10 +19,7 @@ import {
 import { Request } from 'express';
 import { VideoService } from './video.service';
 import {
-  VideoPageDto,
   VideoDetailDto,
-  VideoCategoryDto,
-  VideoTagDto,
   VideoCategoryWithTagsResponseDto,
   VideoListRequestDto,
   VideoListResponseDto,
@@ -62,7 +59,17 @@ export class VideoController {
     description: '推荐视频列表',
     type: RecommendedVideosDto,
   })
-  async getRecommendedVideos(): Promise<RecommendedVideosDto> {
+  async getRecommendedVideos(
+    @Req() req: RequestWithUser,
+  ): Promise<RecommendedVideosDto> {
+    const uid = req.user?.uid;
+
+    if (!uid) {
+      throw new UnauthorizedException('Missing uid in JWT payload');
+    }
+
+    const ip = this.getClientIp(req);
+    const userAgent = this.getUserAgent(req);
     return this.videoService.getRecommendedVideos();
   }
 
@@ -86,174 +93,10 @@ export class VideoController {
     @Param('channelId') channelId: string,
     @Param('categoryId') categoryId: string,
   ): Promise<VideoDetailDto[]> {
-    return this.videoService.getLatestVideosByCategory(channelId, categoryId);
+    return this.videoService.getLatestVideosByCategory(categoryId);
   }
 
   /**
-   * Get categories for a channel from Redis cache.
-   */
-  // @Get('categories/:channelId')
-  // @ApiOperation({
-  //   summary: 'Get video categories for channel',
-  //   description: 'Returns list of video categories from prebuilt Redis cache.',
-  // })
-  // @ApiResponse({
-  //   status: 200,
-  //   description: 'List of categories',
-  //   type: VideoCategoryDto,
-  //   isArray: true,
-  // })
-  // async getCategories(
-  //   @Param('channelId') channelId: string,
-  // ): Promise<VideoCategoryDto[]> {
-  //   return this.videoService.getCategoryListForChannel(channelId);
-  // }
-
-  /**
-   * Get tags for a category.
-   * Note: channelId is kept in URL for backward compatibility but not used.
-   */
-  // @Get('tags/:channelId/:categoryId')
-  // @ApiOperation({
-  //   summary: 'Get video tags for category',
-  //   description:
-  //     'Returns list of tags in a specific category from Redis cache.',
-  // })
-  // @ApiResponse({
-  //   status: 200,
-  //   description: 'List of tags',
-  //   type: VideoTagDto,
-  //   isArray: true,
-  // })
-  // async getTags(
-  //   @Param('channelId') channelId: string,
-  //   @Param('categoryId') categoryId: string,
-  // ): Promise<VideoTagDto[]> {
-  //   return this.videoService.getTagListForCategory(categoryId);
-  // }
-
-  /**
-   * Get videos in a category with pagination.
-   */
-  // @Get('category/:channelId/:categoryId')
-  // @ApiOperation({
-  //   summary: 'Get videos by category',
-  //   description:
-  //     'Returns paginated videos for a specific category from Redis cache.',
-  // })
-  // @ApiQuery({
-  //   name: 'page',
-  //   required: false,
-  //   description: 'Page number (default: 1)',
-  //   example: 1,
-  // })
-  // @ApiQuery({
-  //   name: 'pageSize',
-  //   required: false,
-  //   description: 'Items per page (default: 20)',
-  //   example: 20,
-  // })
-  // @ApiResponse({
-  //   status: 200,
-  //   description: 'Paginated video list',
-  //   type: VideoPageDto,
-  // })
-  // async getVideosByCategory(
-  //   @Param('channelId') channelId: string,
-  //   @Param('categoryId') categoryId: string,
-  //   @Query('page') page?: string,
-  //   @Query('pageSize') pageSize?: string,
-  // ): Promise<VideoPageDto<VideoDetailDto>> {
-  //   const parsedPage = page ? Math.max(1, Number.parseInt(page, 10)) : 1;
-  //   const parsedPageSize = pageSize
-  //     ? Math.min(100, Number.parseInt(pageSize, 10))
-  //     : 20;
-
-  //   return this.videoService.getVideosByCategoryWithPaging({
-  //     channelId,
-  //     categoryId,
-  //     page: parsedPage,
-  //     pageSize: parsedPageSize,
-  //   });
-  // }
-
-  /**
-   * Get videos for a tag with pagination.
-   * Note: Need categoryId to use new cache semantics for tag list fallback.
-   */
-  // @Get('tag/:channelId/:categoryId/:tagId')
-  // @ApiOperation({
-  //   summary: 'Get videos by tag',
-  //   description:
-  //     'Returns paginated videos for a specific tag from Redis cache.',
-  // })
-  // @ApiQuery({
-  //   name: 'page',
-  //   required: false,
-  //   description: 'Page number (default: 1)',
-  //   example: 1,
-  // })
-  // @ApiQuery({
-  //   name: 'pageSize',
-  //   required: false,
-  //   description: 'Items per page (default: 20)',
-  //   example: 20,
-  // })
-  // @ApiResponse({
-  //   status: 200,
-  //   description: 'Paginated video list',
-  //   type: VideoPageDto,
-  // })
-  // async getVideosByTag(
-  //   @Param('channelId') channelId: string,
-  //   @Param('categoryId') categoryId: string,
-  //   @Param('tagId') tagId: string,
-  //   @Query('page') page?: string,
-  //   @Query('pageSize') pageSize?: string,
-  // ): Promise<VideoPageDto<VideoDetailDto>> {
-  //   const parsedPage = page ? Math.max(1, Number.parseInt(page, 10)) : 1;
-  //   const parsedPageSize = pageSize
-  //     ? Math.min(100, Number.parseInt(pageSize, 10))
-  //     : 20;
-
-  //   return this.videoService.getVideosByTagWithPaging({
-  //     channelId,
-  //     categoryId,
-  //     tagId,
-  //     page: parsedPage,
-  //     pageSize: parsedPageSize,
-  //   });
-  // }
-
-  /**
-   * Get home section videos (e.g., featured, latest, editorPick).
-   */
-  // @Get('home/:channelId/:section')
-  // @ApiOperation({
-  //   summary: 'Get home section videos',
-  //   description:
-  //     'Returns videos for home page sections (featured, latest, editorPick) from Redis cache.',
-  // })
-  // @ApiResponse({
-  //   status: 200,
-  //   description: 'List of videos in section',
-  //   type: VideoDetailDto,
-  //   isArray: true,
-  // })
-  // async getHomeSectionVideos(
-  //   @Param('channelId') channelId: string,
-  //   @Param('section') section: string,
-  // ): Promise<VideoDetailDto[]> {
-  //   // Validate section is a known type
-  //   const validSections = ['featured', 'latest', 'editorPick'];
-  //   if (!validSections.includes(section)) {
-  //     return [];
-  //   }
-
-  //   return this.videoService.getHomeSectionVideos(channelId, section as any);
-  // }
-
-  /**
    * GET /api/v1/video/categories-with-tags
    *
    * Get all video categories with their associated tags.

+ 5 - 4
apps/box-app-api/src/feature/video/video.service.ts

@@ -47,12 +47,12 @@ export class VideoService {
    * Uses key: box:app:video:list:category:{channelId}:{categoryId}:latest
    */
   async getLatestVideosByCategory(
-    channelId: string,
+    // channelId: string,
     categoryId: string,
   ): Promise<VideoDetailDto[]> {
     try {
       // Compose Redis key for latest videos
-      const key = `box:app:video:list:category:${channelId}:${categoryId}:latest`;
+      const key = `box:app:video:list:category:${categoryId}:latest`;
       // Get video IDs from Redis (LIST)
       const videoIds: string[] = await this.redis.lrange(key, 0, -1);
       if (!videoIds || videoIds.length === 0) {
@@ -80,7 +80,7 @@ export class VideoService {
         .filter((v): v is VideoDetailDto => v !== null);
     } catch (err) {
       this.logger.error(
-        `Error fetching latest videos for channelId=${channelId}, categoryId=${categoryId}`,
+        `Error fetching latest videos for categoryId=${categoryId}`,
         err instanceof Error ? err.stack : String(err),
       );
       return [];
@@ -548,7 +548,8 @@ export class VideoService {
       const videos = await this.mongoPrisma.videoMedia.findMany({
         where: {
           categoryIds: { has: categoryId },
-          listStatus: 1,
+          status: 'Completed',
+          // listStatus: 1,
           tagIds: { has: tagId },
         },
         orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],

+ 2 - 2
apps/box-app-api/src/rabbitmq/rabbitmq-publisher.service.ts

@@ -24,7 +24,7 @@ export interface StatsAdClickEventPayload {
   adType: string;
   clickedAt: bigint;
   ip: string;
-  channelId?: string;
+  uChannelId?: string;
   machine?: string;
 }
 
@@ -44,7 +44,7 @@ export interface StatsAdImpressionEventPayload {
   impressionAt: bigint;
   visibleDurationMs?: number;
   ip: string;
-  channelId?: string;
+  uChannelId?: string;
   machine?: string;
 }
 

+ 4 - 2
apps/box-mgnt-api/src/dev/services/video-cache-coverage.service.ts

@@ -77,7 +77,8 @@ export class VideoCacheCoverageService {
         const categoryVideoCount = await this.mongoPrisma.videoMedia.count({
           where: {
             categoryIds: { has: category.id },
-            listStatus: 1, // Only "on shelf" videos
+            status: 'Completed',
+            // listStatus: 1, // Only "on shelf" videos
           },
         });
 
@@ -122,7 +123,8 @@ export class VideoCacheCoverageService {
           const tagVideoCount = await this.mongoPrisma.videoMedia.count({
             where: {
               categoryIds: { has: category.id },
-              listStatus: 1, // Only "on shelf" videos
+              status: 'Completed',
+              // listStatus: 1, // Only "on shelf" videos
               tagIds: { has: tag.id }, // Has this specific tag
             },
           });

+ 4 - 2
apps/box-mgnt-api/src/dev/services/video-stats.service.ts

@@ -90,7 +90,8 @@ export class VideoStatsService {
         const videoCount = await this.mongoPrisma.videoMedia.count({
           where: {
             categoryIds: { has: category.id },
-            listStatus: 1, // Only "on shelf" videos
+            status: 'Completed',
+            // listStatus: 1, // Only "on shelf" videos
           },
         });
 
@@ -116,7 +117,8 @@ export class VideoStatsService {
           const tagVideoCount = await this.mongoPrisma.videoMedia.count({
             where: {
               categoryIds: { has: category.id },
-              listStatus: 1, // Only "on shelf" videos (matches builder)
+              status: 'Completed',
+              // listStatus: 1, // Only "on shelf" videos (matches builder)
               tagIds: { has: tag.id }, // Videos with this tag (matches builder)
             },
           });

+ 0 - 19
apps/box-mgnt-api/src/mgnt-backend/feature/tag/tag.dto.ts

@@ -27,13 +27,6 @@ export class TagDto {
   name: string;
 
   @ApiProperty({
-    description: '渠道ID (Mongo ObjectId)',
-    example: '664f9b5b8e4ff3f4c0c00001',
-  })
-  @IsMongoId()
-  channelId: string;
-
-  @ApiProperty({
     description: '分类ID (Mongo ObjectId)',
     example: '6650a0c28e4ff3f4c0c00111',
   })
@@ -74,13 +67,6 @@ export class CreateTagDto {
   name: string;
 
   @ApiProperty({
-    description: '渠道ID (Mongo ObjectId)',
-    example: '664f9b5b8e4ff3f4c0c00001',
-  })
-  @IsMongoId()
-  channelId: string;
-
-  @ApiProperty({
     description: '分类ID (Mongo ObjectId)',
     example: '6650a0c28e4ff3f4c0c00111',
   })
@@ -119,11 +105,6 @@ export class ListTagDto extends PageListDto {
   @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
   name?: string;
 
-  @ApiPropertyOptional({ description: '渠道ID (ObjectId)' })
-  @IsOptional()
-  @IsMongoId()
-  channelId?: string;
-
   @ApiPropertyOptional({ description: '分类ID (ObjectId)' })
   @IsOptional()
   @IsMongoId()

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

@@ -36,7 +36,7 @@ interface AdClickMessage extends BaseStatsMessage {
 
 interface VideoClickMessage extends BaseStatsMessage {
   videoId: string;
-  channelId: string;
+  // channelId: string;
   machine: string; // Device info: brand and system version
   categoryId?: string;
   scene: string;
@@ -46,7 +46,7 @@ interface VideoClickMessage extends BaseStatsMessage {
 interface AdImpressionMessage extends BaseStatsMessage {
   adId: string;
   adsModuleId: string;
-  channelId: string;
+  // channelId: string;
   scene: string;
   slot: string;
   adType: string;
@@ -304,7 +304,7 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
       !payload.messageId ||
       !payload.uid ||
       !payload.videoId ||
-      !payload.channelId ||
+      //!payload.channelId ||
       !payload.machine
     ) {
       this.logger.warn(
@@ -333,7 +333,7 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
         data: {
           uid: payload.uid,
           videoId: payload.videoId,
-          channelId: payload.channelId,
+          // channelId: payload.channelId,
           machine: payload.machine,
           categoryId: payload.categoryId ?? null,
           scene: payload.scene,
@@ -366,7 +366,7 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
       !payload.messageId ||
       !payload.uid ||
       !payload.adId ||
-      !payload.channelId ||
+      // !payload.channelId ||
       !payload.machine
     ) {
       this.logger.warn(
@@ -401,7 +401,7 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
             ? BigInt(payload.visibleDurationMs)
             : null,
           ip: payload.ip,
-          channelId: payload.channelId,
+          // uChannelId: payload.uChannelId ?? null,
           machine: payload.machine,
           createAt: this.toBigInt(payload.createAt),
           updateAt: this.toBigInt(payload.updateAt),

+ 3 - 3
apps/box-stats-api/src/feature/user-login/user-login.service.ts

@@ -22,13 +22,13 @@ export class UserLoginService {
               : BigInt(event.loginAt),
           ip: event.ip,
           os: event.os ?? null,
-          channelId: event.channelId,
+          uChannelId: event.uChannelId ?? null,
           machine: event.machine,
         },
         create: {
           uid: event.uid,
           os: event.os ?? null,
-          channelId: event.channelId,
+          uChannelId: event.uChannelId ?? null,
           machine: event.machine,
           createAt:
             typeof event.loginAt === 'bigint'
@@ -67,7 +67,7 @@ export class UserLoginService {
           userAgent: event.userAgent ?? null,
           appVersion: event.appVersion ?? null,
           os: event.os ?? null,
-          channelId: event.channelId,
+          uChannelId: event.uChannelId ?? null,
           machine: event.machine,
           createAt,
           tokenId: event.tokenId ?? null,

+ 1 - 1
libs/common/src/events/user-login-event.dto.ts

@@ -6,7 +6,7 @@ export interface UserLoginEventPayload {
   userAgent?: string; // optional, but useful
   appVersion?: string; // optional
   os?: string; // iOS / Android / Browser / Web
-  channelId: string; // required channel ID
+  uChannelId: string; // required channel ID
   machine: string; // required machine model
 
   tokenId?: string; // JWT jti or session token ID (optional for now)

+ 4 - 2
libs/core/src/cache/video/category/video-category-cache.builder.ts

@@ -218,7 +218,8 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
       const videos = await this.mongoPrisma.videoMedia.findMany({
         where: {
           categoryIds: { has: categoryId },
-          listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
+          status: 'Completed',
+          // listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
         },
         orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
         select: { id: true }, // Only fetch IDs, not full documents
@@ -301,7 +302,8 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
       const videos = await this.mongoPrisma.videoMedia.findMany({
         where: {
           categoryIds: { has: categoryId },
-          listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
+          status: 'Completed',
+          // listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
           tagIds: { has: tagId }, // ✅ Has this specific tag (matches stats endpoint)
         },
         orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],

+ 6 - 3
libs/core/src/cache/video/list/video-list-cache.builder.ts

@@ -101,7 +101,8 @@ export class VideoListCacheBuilder extends BaseCacheBuilder {
       const videos = await this.mongoPrisma.videoMedia.findMany({
         where: {
           categoryIds: { has: category.id },
-          listStatus: 1,
+          status: 'Completed',
+          // listStatus: 1,
         },
       });
 
@@ -154,7 +155,8 @@ export class VideoListCacheBuilder extends BaseCacheBuilder {
       // Fetch all on-shelf videos that have this tag
       const videos = await this.mongoPrisma.videoMedia.findMany({
         where: {
-          listStatus: 1,
+          status: 'Completed',
+          // listStatus: 1,
           tagIds: { has: tag.id },
         },
       });
@@ -214,7 +216,8 @@ export class VideoListCacheBuilder extends BaseCacheBuilder {
     const videos = await this.mongoPrisma.videoMedia.findMany({
       where: {
         categoryIds: { hasSome: categoryIds },
-        listStatus: 1,
+        status: 'Completed',
+        // listStatus: 1,
       },
       orderBy: [{ editedAt: 'desc' }, { updatedAt: 'desc' }],
       take: this.HOME_SECTION_LIMIT,

+ 2 - 2
prisma/mongo-stats/schema/ads-click-history.prisma

@@ -8,7 +8,7 @@ model AdsClickHistory {
   ip           String                                   // 点击 IP
   appVersion   String?                                  // 客户端版本 (optional)
   os           String?                                  // iOS / Android / Web (optional)
-  channelId    String                                   // 渠道 Id (required)
+  uChannelId   String                                   // 用户自带渠道 Id (required)
   machine      String                                   // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   clickAt      BigInt                                   // 点击时间 (epoch millis)
@@ -21,7 +21,7 @@ model AdsClickHistory {
   @@index([uid, clickAt])
   
   // 3. Query clicks by channel + user (for reporting)
-  @@index([channelId, uid, clickAt])
+  @@index([uChannelId, uid, clickAt])
   
   // 4. Query clicks by IP (fraud detection)
   @@index([ip, clickAt])

+ 6 - 6
prisma/mongo-stats/schema/events.prisma

@@ -7,7 +7,7 @@ model AdClickEvents {
 
   clickedAt    BigInt                          // 点击时间 (epoch)
   ip           String                          // 点击 IP
-  channelId    String                          // 渠道 Id (required)
+  uChannelId   String                          // 用户自带渠道 Id (required)
   machine      String                          // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   createAt     BigInt                          // 记录创建时间
@@ -19,7 +19,7 @@ model AdClickEvents {
   // 2. 查某设备的点击轨迹
   @@index([uid, clickedAt])
   // 3. 按渠道+设备分析(报表)
-  @@index([channelId, uid, clickedAt])
+  @@index([uChannelId, uid, clickedAt])
   // 4. 按广告类型/时间分析
   @@index([adType, clickedAt])
   // 5. 全局按时间片
@@ -36,7 +36,7 @@ model VideoClickEvents {
 
   clickedAt   BigInt                          // 点击时间 (epoch)
   ip          String                          // 点击 IP
-  channelId   String                          // 渠道 Id (required)
+  uChannelId  String                          // 用户自带渠道 Id (required)
   machine     String                          // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   createAt    BigInt                          // 记录创建时间
@@ -48,7 +48,7 @@ model VideoClickEvents {
   // 2. 查设备点击
   @@index([uid, clickedAt])
   // 3. 按渠道+设备分析(报表)
-  @@index([channelId, uid, clickedAt])
+  @@index([uChannelId, uid, clickedAt])
   // 4. 全局时间窗口
   @@index([clickedAt])
 
@@ -65,7 +65,7 @@ model AdImpressionEvents {
   impressionAt      BigInt                          // 曝光时间 (epoch)
   visibleDurationMs BigInt?                         // 可见时长(毫秒,optional)
   ip                String                          // IP
-  channelId         String                          // 渠道 Id (required)
+  uChannelId        String                          // 用户自带渠道 Id (required)
   machine           String                          // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   createAt          BigInt                          // 记录创建时间
@@ -77,7 +77,7 @@ model AdImpressionEvents {
   // 2. 设备曝光轨迹
   @@index([uid, impressionAt])
   // 3. 按渠道+设备分析(报表)
-  @@index([channelId, uid, impressionAt])
+  @@index([uChannelId, uid, impressionAt])
   // 4. 按广告类型
   @@index([adType, impressionAt])
   // 5. 时间片

+ 2 - 2
prisma/mongo-stats/schema/user-login-history.prisma

@@ -6,7 +6,7 @@ model UserLoginHistory {
   userAgent   String?                         // UA (optional but useful)
   appVersion  String?                         // 客户端版本 (optional)
   os          String?                         // iOS / Android / Browser
-  channelId   String                          // 渠道 Id (required)
+  uChannelId  String                          // 用户自带渠道 Id (required)
   machine     String                          // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   createAt    BigInt                          // 登录时间 (epoch)
@@ -18,7 +18,7 @@ model UserLoginHistory {
   @@index([uid, createAt])
   
   // 2. 按渠道查登陆情况
-  @@index([channelId, createAt])
+  @@index([uChannelId, createAt])
   
   // 3. 查某 IP 的登陆情况(反刷)
   @@index([ip, createAt])

+ 2 - 2
prisma/mongo-stats/schema/user.prisma

@@ -3,7 +3,7 @@ model User {
   uid           String      @unique         // 唯一设备码
   ip            String                      // 最近登录 IP
   os            String?                     // iOS / Android / Browser
-  channelId     String                      // 渠道 Id (required)
+  uChannelId    String                      // 用户自带渠道 Id (required)
   machine       String                      // 客户端提供 : 设备的信息,品牌及系统版本什么的 (required)
 
   createAt      BigInt      @default(0)     // 注册/创建时间
@@ -13,7 +13,7 @@ model User {
   // 1. 查某设备的登录情况
   @@index([uid, createAt])
   // 2. 按渠道分组统计
-  @@index([channelId, createAt])
+  @@index([uChannelId, createAt])
   // 3. 全局统计(按时间分片)
   @@index([createAt])