Переглянути джерело

refactor: add support for raw binary Excel tag imports; enhance importExcelTags method to handle buffer and filename

Dave 3 тижнів тому
батько
коміт
b9efa5f2a1

+ 8 - 0
apps/box-mgnt-api/src/main.ts

@@ -31,6 +31,14 @@ async function bootstrap() {
     },
     },
   });
   });
 
 
+  fastifyAdapter.getInstance().addContentTypeParser(
+    'application/octet-stream',
+    { parseAs: 'buffer' },
+    (_req, body, done) => {
+      done(null, body);
+    },
+  );
+
   // after creating fastifyAdapter but before NestFactory.create:
   // after creating fastifyAdapter but before NestFactory.create:
   // Read allowed origin from env (fallback to '*')
   // Read allowed origin from env (fallback to '*')
   const corsOrg = process.env.MGNT_CORS_ORIGIN || '*';
   const corsOrg = process.env.MGNT_CORS_ORIGIN || '*';

+ 41 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts

@@ -303,6 +303,47 @@ export class VideoMediaController {
   }
   }
 
 
   /**
   /**
+   * 导入 Excel 标签(raw binary)
+   * POST /video-media/import/excel-tags-raw
+   */
+  @ApiConsumes('application/octet-stream')
+  @Post('import/excel-tags-raw')
+  async importExcelTagsRaw(@Req() req: FastifyRequest) {
+    const reqAny = req as any;
+    const body = reqAny.body;
+    let buffer: Buffer | undefined;
+
+    if (Buffer.isBuffer(body)) {
+      buffer = body;
+    } else if (body instanceof Uint8Array) {
+      buffer = Buffer.from(body);
+    }
+
+    if (!buffer) {
+      const raw = reqAny.raw as NodeJS.ReadableStream | undefined;
+      if (!raw) {
+        throw new BadRequestException('No file uploaded');
+      }
+
+      const chunks: Buffer[] = [];
+      for await (const chunk of raw) {
+        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+      }
+
+      buffer = Buffer.concat(chunks);
+    }
+
+    if (!buffer.length) {
+      throw new BadRequestException('Empty file');
+    }
+
+    const filename =
+      (req.headers['x-filename'] as string | undefined) ?? 'upload.xlsx';
+
+    return this.videoMediaService.importExcelTagsFromBuffer(buffer, filename);
+  }
+
+  /**
    * 导出所有视频媒体为 Excel
    * 导出所有视频媒体为 Excel
    * GET /video-media/export/excel
    * GET /video-media/export/excel
    */
    */

+ 15 - 4
apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.service.ts

@@ -350,15 +350,20 @@ export class VideoMediaService {
 
 
   async importExcelTags(file: MultipartFile) {
   async importExcelTags(file: MultipartFile) {
     const buffer = await this.readUploadBuffer(file);
     const buffer = await this.readUploadBuffer(file);
+    return this.importExcelTagsFromBuffer(buffer, (file as any)?.filename);
+  }
+
+  async importExcelTagsFromBuffer(buffer: Buffer, filename?: string) {
     if (!buffer.length) {
     if (!buffer.length) {
       throw new BadRequestException('Empty file');
       throw new BadRequestException('Empty file');
     }
     }
     this.logger.log(
     this.logger.log(
-      `[importExcelTags] upload buffer bytes=${buffer.length} filename=${(file as any)?.filename ?? 'unknown'} mimetype=${(file as any)?.mimetype ?? 'unknown'}`,
+      `[importExcelTags] upload buffer bytes=${buffer.length} filename=${filename ?? 'unknown'}`,
     );
     );
 
 
     const rowsAH: Array<{ idRaw: unknown; tagsRaw: unknown }> = [];
     const rowsAH: Array<{ idRaw: unknown; tagsRaw: unknown }> = [];
-    const filename = (file as any)?.filename ?? '';
+    const maxRows = 100000;
+    const maxCols = 20;
     const looksLikeXlsx =
     const looksLikeXlsx =
       buffer.length >= 4 &&
       buffer.length >= 4 &&
       buffer[0] === 0x50 &&
       buffer[0] === 0x50 &&
@@ -366,7 +371,7 @@ export class VideoMediaService {
       buffer[2] === 0x03 &&
       buffer[2] === 0x03 &&
       buffer[3] === 0x04;
       buffer[3] === 0x04;
 
 
-    if (looksLikeXlsx || /\.xlsx$/i.test(filename)) {
+    if (looksLikeXlsx || /\.xlsx$/i.test(filename ?? '')) {
       const workbook = XLSX.read(buffer, { type: 'buffer' });
       const workbook = XLSX.read(buffer, { type: 'buffer' });
       const sheetName = workbook.SheetNames[0];
       const sheetName = workbook.SheetNames[0];
       const sheet = sheetName ? workbook.Sheets[sheetName] : undefined;
       const sheet = sheetName ? workbook.Sheets[sheetName] : undefined;
@@ -377,6 +382,8 @@ export class VideoMediaService {
 
 
       const rangeRef = sheet['!ref'] ?? 'A1:A1';
       const rangeRef = sheet['!ref'] ?? 'A1:A1';
       const range = XLSX.utils.decode_range(rangeRef);
       const range = XLSX.utils.decode_range(rangeRef);
+      range.e.r = Math.min(range.e.r, range.s.r + maxRows - 1);
+      range.e.c = Math.min(range.e.c, range.s.c + maxCols - 1);
 
 
       for (let r = range.s.r; r <= range.e.r; r += 1) {
       for (let r = range.s.r; r <= range.e.r; r += 1) {
         const idCell = sheet[XLSX.utils.encode_cell({ r, c: 0 })];
         const idCell = sheet[XLSX.utils.encode_cell({ r, c: 0 })];
@@ -415,10 +422,14 @@ export class VideoMediaService {
         return cells;
         return cells;
       };
       };
 
 
-      for (let i = 1; i < lines.length; i += 1) {
+      const maxLineIndex = Math.min(lines.length - 1, maxRows);
+      for (let i = 1; i <= maxLineIndex; i += 1) {
         const line = lines[i];
         const line = lines[i];
         if (!line) continue;
         if (!line) continue;
         const cells = parseCsvLine(line);
         const cells = parseCsvLine(line);
+        if (cells.length > maxCols) {
+          cells.length = maxCols;
+        }
         rowsAH.push({
         rowsAH.push({
           idRaw: cells[0] ?? '',
           idRaw: cells[0] ?? '',
           tagsRaw: cells[7] ?? '',
           tagsRaw: cells[7] ?? '',