2
0
Эх сурвалжийг харах

✨ feat(file): 增强分片上传功能,支持文件名传递和用户关联

- 添加fileName参数到getMultipartUploadPolicy函数,默认值为'unnamed-file'
- 在上传策略请求中增加name字段,传递文件名信息
- 添加文件类型检查,非File类型时抛出明确错误
- 关联上传用户ID到分片上传策略,增强安全性和可追溯性

♻️ refactor(file): 优化分片上传完成逻辑和日志记录

- 重命名parts数组属性,将PartNumber改为partNumber,ETag改为etag,统一使用小驼峰命名
- 添加分片上传开始和完成的详细日志记录,便于问题排查
- 修正MinIO分片完成参数格式转换,确保与API要求一致
yourname 4 сар өмнө
parent
commit
260c4297af

+ 12 - 3
src/client/utils/minio.ts

@@ -329,13 +329,14 @@ export async function getUploadPolicy(key: string, fileName: string, fileType?:
   return policyResponse.json();
 }
 
-export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string) {
+export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string, fileName: string = 'unnamed-file') {
   const policyResponse = await fileClient["multipart-policy"].$post({
     json: {
       totalSize,
       partSize: PART_SIZE,
       fileKey,
-      type: fileType
+      type: fileType,
+      name: fileName
     }
   });
   if (!policyResponse.ok) {
@@ -359,7 +360,15 @@ export async function uploadMinIOWithPolicy(
   
   
   if( file.size > PART_SIZE ){
-    const policy = await getMultipartUploadPolicy(file.size, `${uploadPath}${fileKey}`, file instanceof File ? file.type : undefined)
+    if (!(file instanceof File)) {
+      throw new Error('不支持的文件类型,无法获取文件名');
+    }
+    const policy = await getMultipartUploadPolicy(
+      file.size,
+      `${uploadPath}${fileKey}`,
+      file.type,
+      file.name
+    );
     return MinIOXHRMultipartUploader.upload(
       policy,
       file,

+ 41 - 33
src/server/api/files/multipart-policy/post.ts

@@ -8,22 +8,26 @@ import { authMiddleware } from '@/server/middleware/auth.middleware';
 
 // 创建分片上传策略请求Schema
 const CreateMultipartUploadPolicyDto = z.object({
-  fileKey: z.string().openapi({
-    description: '文件键名',
-    example: 'documents/report.pdf'
-  }),
-  totalSize: z.coerce.number().int().positive().openapi({
-    description: '文件总大小(字节)',
-    example: 10485760
-  }),
-  partSize: z.coerce.number().int().positive().openapi({
-    description: '分片大小(字节)',
-    example: 5242880
-  }),
-  type: z.string().max(50).nullable().optional().openapi({
-    description: '文件类型',
-    example: 'application/pdf'
-  })
+fileKey: z.string().openapi({
+  description: '文件键名',
+  example: 'documents/report.pdf'
+}),
+totalSize: z.coerce.number().int().positive().openapi({
+  description: '文件总大小(字节)',
+  example: 10485760
+}),
+partSize: z.coerce.number().int().positive().openapi({
+  description: '分片大小(字节)',
+  example: 5242880
+}),
+type: z.string().max(50).nullable().optional().openapi({
+  description: '文件类型',
+  example: 'application/pdf'
+}),
+name: z.string().max(255).openapi({
+  description: '文件名称',
+  example: '项目计划书.pdf'
+})
 });
 
 // 创建分片上传策略路由定义
@@ -87,23 +91,27 @@ const fileService = new FileService(AppDataSource);
 
 // 创建路由实例
 const app = new OpenAPIHono<AuthContext>().openapi(createMultipartUploadPolicyRoute, async (c) => {
-  try {
-    const data = await c.req.json();
-    // 计算分片数量
-    const partCount = Math.ceil(data.totalSize / data.partSize);
-    const result = await fileService.createMultipartUploadPolicy(data, partCount);
-    
-    return c.json({
-      uploadId: result.uploadId,
-      bucket: result.bucket,
-      key: result.key,
-      host: `${process.env.MINIO_USE_SSL ? 'https' : 'http'}://${process.env.MINIO_ENDPOINT}:${process.env.MINIO_PORT}`,
-      partUrls: result.uploadUrls
-    }, 200);
-  } catch (error) {
-    const message = error instanceof Error ? error.message : '生成分片上传策略失败';
-    return c.json({ code: 500, message }, 500);
-  }
+try {
+  const data = await c.req.json();
+  const user = c.get('user');
+  // 计算分片数量
+  const partCount = Math.ceil(data.totalSize / data.partSize);
+  const result = await fileService.createMultipartUploadPolicy({
+    ...data,
+    uploadUserId: user.id
+  }, partCount);
+  
+  return c.json({
+    uploadId: result.uploadId,
+    bucket: result.bucket,
+    key: result.key,
+    host: `${process.env.MINIO_USE_SSL ? 'https' : 'http'}://${process.env.MINIO_ENDPOINT}:${process.env.MINIO_PORT}`,
+    partUrls: result.uploadUrls
+  }, 200);
+} catch (error) {
+  const message = error instanceof Error ? error.message : '生成分片上传策略失败';
+  return c.json({ code: 500, message }, 500);
+}
 });
 
 export default app;

+ 16 - 3
src/server/modules/files/file.service.ts

@@ -167,15 +167,22 @@ export class FileService extends GenericCrudService<File> {
     uploadId: string;
     bucket: string;
     key: string;
-    parts: Array<{ PartNumber: number; ETag: string }>;
+    parts: Array<{ partNumber: number; etag: string }>;
   }) {
     try {
-      // 完成MinIO分片上传
+      logger.db('Starting multipart upload completion:', {
+        uploadId: data.uploadId,
+        bucket: data.bucket,
+        key: data.key,
+        partsCount: data.parts.length
+      });
+
+      // 完成MinIO分片上传 - 注意格式转换
       const result = await this.minioService.completeMultipartUpload(
         data.bucket,
         data.key,
         data.uploadId,
-        data.parts.map(part => ({ PartNumber: part.PartNumber, ETag: part.ETag }))
+        data.parts.map(part => ({ PartNumber: part.partNumber, ETag: part.etag }))
       );
       
       // 查找文件记录并更新
@@ -192,6 +199,12 @@ export class FileService extends GenericCrudService<File> {
       // 生成文件访问URL
       const url = this.minioService.getFileUrl(data.bucket, data.key);
       
+      logger.db('Multipart upload completed successfully:', {
+        fileId: file.id,
+        size: result.size,
+        key: data.key
+      });
+      
       return {
         fileId: file.id,
         url,