Quellcode durchsuchen

refactor: update Channel DTO and service for channelNo handling and logging

Dave vor 1 Monat
Ursprung
Commit
4bc2a79fd2

+ 12 - 12
apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.dto.ts

@@ -106,12 +106,12 @@ export class ChannelDto {
   @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
   remark?: string | null;
 
-  @ApiProperty({ description: '创建时间 epoch (ms)' })
+  @ApiProperty({ description: '创建时间 epoch (seconds)' })
   @Type(() => Number)
   @IsInt()
   createAt: number;
 
-  @ApiProperty({ description: '更新时间 epoch (ms)' })
+  @ApiProperty({ description: '更新时间 epoch (seconds)' })
   @Type(() => Number)
   @IsInt()
   updateAt: number;
@@ -142,16 +142,16 @@ export class ChannelDto {
 }
 
 export class CreateChannelDto {
-  @ApiProperty({
-    description: '渠道唯一标识符(业务侧自定义,必须唯一)',
-    maxLength: 100,
-    example: 'channel-us-001',
-  })
-  @IsString()
-  @MaxLength(100)
-  @IsNotEmpty()
-  @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
-  channelId: string;
+  // @ApiProperty({
+  //   description: '渠道唯一标识符(业务侧自定义,必须唯一)',
+  //   maxLength: 100,
+  //   example: 'channel-us-001',
+  // })
+  // @IsString()
+  // @MaxLength(100)
+  // @IsNotEmpty()
+  // @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
+  // channelId?: string;
 
   @ApiProperty({
     description: '渠道名称',

+ 130 - 53
apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts

@@ -3,6 +3,7 @@ import {
   Injectable,
   BadRequestException,
   NotFoundException,
+  Logger,
 } from '@nestjs/common';
 import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
@@ -20,17 +21,14 @@ import { nowSecBigInt } from '@box/common/time/time.util';
 
 @Injectable()
 export class ChannelService {
+  private readonly logger = new Logger(ChannelService.name);
+  private isBackfilling = false;
+
   constructor(
     private readonly mongoPrismaService: MongoPrismaService,
     private readonly cacheSyncService: CacheSyncService,
   ) {}
 
-  /**
-   * Current epoch seconds (BigInt) for persisted timestamps.
-   *
-   * NOTE:
-   *  - We now keep `createAt`/`updateAt` as BigInt seconds.
-   */
   private now(): bigint {
     return nowSecBigInt();
   }
@@ -69,10 +67,90 @@ export class ChannelService {
     }));
   }
 
+  private async generateNextChannelNo(): Promise<number> {
+    const last = await this.mongoPrismaService.channel.findFirst({
+      where: { channelNo: { isSet: true } },
+      orderBy: { channelNo: 'desc' },
+      select: { channelNo: true },
+    });
+
+    return (last?.channelNo ?? 0) + 1;
+  }
+
+  private async backfillIds(): Promise<void> {
+    if (this.isBackfilling) {
+      this.logger.warn('backfill is already running, skipping.');
+      return;
+    }
+
+    this.isBackfilling = true;
+    this.logger.log('Starting backfill of id...');
+    try {
+      const withoutId = await this.mongoPrismaService.channel.findMany({
+        where: {
+          OR: [{ channelNo: { isSet: false } }, { channelNo: null }],
+        },
+        orderBy: { createAt: 'asc' },
+        select: { id: true },
+      });
+
+      if (withoutId.length === 0) {
+        this.logger.log('No channels need backfilling channelNo.');
+        return;
+      }
+
+      this.logger.log(
+        `Found ${withoutId.length} channels without channelNo. Starting backfill...`,
+      );
+
+      let nextUid = await this.generateNextChannelNo();
+
+      for (const channel of withoutId) {
+        let assigned = false;
+
+        while (!assigned) {
+          try {
+            await this.mongoPrismaService.channel.update({
+              where: { id: channel.id },
+              data: { channelNo: nextUid },
+            });
+
+            this.logger.log(
+              `Backfilled channelNo ${nextUid} for channel id ${channel.id}`,
+            );
+
+            nextUid += 1;
+            assigned = true;
+          } catch (e: any) {
+            // Unique constraint violation → retry with a fresh number
+            if (e?.code === 'P2002') {
+              this.logger.warn(`Duplicate channelNo ${nextUid}, retrying...`);
+              nextUid = await this.generateNextChannelNo();
+            } else {
+              throw e;
+            }
+          }
+        }
+      }
+
+      this.logger.log(
+        `Backfilled ${withoutId.length} channelNos successfully.`,
+      );
+    } finally {
+      this.isBackfilling = false;
+      this.logger.log('Finished backfilling process.');
+    }
+  }
+
   async create(dto: CreateChannelDto) {
+    await this.backfillIds().catch((e) => {
+      this.logger.error('Error during backfillIds:', e);
+    });
+
     // Check for duplicate channel name
     const existingChannel = await this.mongoPrismaService.channel.findFirst({
       where: { name: dto.name },
+      select: { id: true },
     });
 
     if (existingChannel) {
@@ -81,20 +159,6 @@ export class ChannelService {
       );
     }
 
-    // Check for duplicate channelId
-    const existingChannelId = await this.mongoPrismaService.channel.findFirst({
-      where: { channelId: dto.channelId },
-    });
-
-    if (existingChannelId) {
-      throw new BadRequestException(
-        `Channel with channelId "${dto.channelId}" already exists`,
-      );
-    }
-
-    const isDefault = (await this.mongoPrismaService.channel.count()) === 0;
-
-    const now = this.now();
     const categoryIds = dto.categories?.map((c) => c.id) ?? [];
     const tagIds = dto.tags?.map((t) => t.id) ?? [];
 
@@ -104,24 +168,49 @@ export class ChannelService {
     ]);
     const tagNames = tags.map((t) => t.name.trim());
 
-    const channel = await this.mongoPrismaService.channel.create({
-      data: {
-        channelId: dto.channelId,
-        name: dto.name,
-        landingUrl: dto.landingUrl,
-        videoCdn: this.trimOptional(dto.videoCdn) ?? null,
-        coverCdn: this.trimOptional(dto.coverCdn) ?? null,
-        clientName: this.trimOptional(dto.clientName) ?? null,
-        clientNotice: this.trimOptional(dto.clientNotice) ?? null,
-        remark: this.trimOptional(dto.remark) ?? null,
-        isDefault,
-        categories: categories.length ? categories : null,
-        tags: tags.length ? tags : null,
-        tagNames: tagNames,
-        createAt: now,
-        updateAt: now,
-      },
-    });
+    const isDefault = (await this.mongoPrismaService.channel.count()) === 0;
+    const now = this.now();
+
+    // Generate and create with retry to avoid duplicate channelNo under concurrency
+    let nextNo = await this.generateNextChannelNo();
+    let channel: Awaited<
+      ReturnType<typeof this.mongoPrismaService.channel.create>
+    > | null = null;
+
+    while (!channel) {
+      try {
+        channel = await this.mongoPrismaService.channel.create({
+          data: {
+            channelNo: nextNo,
+            channelId: String(nextNo), // channelId mirrors channelNo on create
+            name: dto.name,
+            landingUrl: dto.landingUrl,
+            videoCdn: this.trimOptional(dto.videoCdn) ?? null,
+            coverCdn: this.trimOptional(dto.coverCdn) ?? null,
+            clientName: this.trimOptional(dto.clientName) ?? null,
+            clientNotice: this.trimOptional(dto.clientNotice) ?? null,
+            remark: this.trimOptional(dto.remark) ?? null,
+            isDefault,
+            categories: categories.length ? categories : null,
+            tags: tags.length ? tags : null,
+            tagNames,
+            createAt: now,
+            updateAt: now,
+          },
+        });
+      } catch (e: any) {
+        const isP2002 =
+          e?.code === 'P2002' ||
+          (e instanceof PrismaClientKnownRequestError && e.code === 'P2002');
+
+        if (isP2002) {
+          this.logger.warn(`Duplicate channelNo ${nextNo}, retrying...`);
+          nextNo = await this.generateNextChannelNo();
+          continue;
+        }
+        throw e;
+      }
+    }
 
     // Auto-schedule cache refresh
     await this.cacheSyncService.scheduleChannelRefreshAll();
@@ -150,20 +239,6 @@ export class ChannelService {
       );
     }
 
-    // check for duplicate channelId (excluding current channel)
-    const duplicateChannelId = await this.mongoPrismaService.channel.findFirst({
-      where: {
-        channelId: dto.channelId,
-        id: { not: dto.id },
-      },
-    });
-
-    if (duplicateChannelId) {
-      throw new BadRequestException(
-        `Channel with channelId "${dto.channelId}" already exists`,
-      );
-    }
-
     const now = this.now();
     const categoryIds = dto.categories?.map((c) => c.id) ?? [];
     const tagIds = dto.tags?.map((t) => t.id) ?? [];
@@ -178,7 +253,6 @@ export class ChannelService {
       const channel = await this.mongoPrismaService.channel.update({
         where: { id: dto.id },
         data: {
-          channelId: dto.channelId,
           name: dto.name,
           landingUrl: dto.landingUrl,
           videoCdn: this.trimOptional(dto.videoCdn) ?? null,
@@ -227,6 +301,9 @@ export class ChannelService {
   }
 
   async list(dto: ListChannelDto) {
+    await this.backfillIds().catch((e) => {
+      this.logger.error('Error during backfillIds:', e);
+    });
     const where: any = {};
 
     if (dto.name) {

+ 3 - 3
apps/box-mgnt-api/src/mgnt-backend/feature/uploader/uploader.service.ts

@@ -230,9 +230,9 @@ export class UploaderService {
     if (!source) {
       const maybeBuffer = toBuffer(
         (file as any).buffer ??
-        (file as any).value ??
-        (file as any).data ??
-        undefined,
+          (file as any).value ??
+          (file as any).data ??
+          undefined,
       );
       if (maybeBuffer) {
         source = Readable.from(maybeBuffer);

+ 2 - 0
prisma/mongo/schema/channel.prisma

@@ -1,6 +1,8 @@
 model Channel {
   id           String   @id @map("_id") @default(auto()) @db.ObjectId
   channelId    String   @unique
+  channelNo    Int?     @unique
+
   name         String                             // 渠道名称
   landingUrl   String                             // 最新网址
   videoCdn     String?                            // 视频CDN