瀏覽代碼

🗑️ chore(files): remove file module and related components

- 删除File实体类及相关数据模型定义
- 移除文件服务层代码(file.service.ts)
- 删除MinIO集成服务(minio.service.ts)
- 从数据源配置中移除File实体引用
- 清理文件上传、下载、管理相关功能实现
yourname 4 月之前
父節點
當前提交
5af00e1e54

+ 1 - 2
src/server/data-source.ts

@@ -5,7 +5,6 @@ import process from 'node:process'
 // 实体类导入
 import { UserEntity as User } from "./modules/users/user.entity"
 import { Role } from "./modules/users/role.entity"
-import { File } from "./modules/files/file.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -15,7 +14,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, File
+    User, Role, 
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 0 - 158
src/server/modules/files/file.entity.ts

@@ -1,158 +0,0 @@
-import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
-import { z } from '@hono/zod-openapi';
-import { UserEntity, UserSchema } from '@/server/modules/users/user.entity';
-
-@Entity('file')
-export class File {
-  @PrimaryGeneratedColumn({ name: 'id', type: 'int', unsigned: true })
-  id!: number;
-
-  @Column({ name: 'name', type: 'varchar', length: 255 })
-  name!: string;
-
-  @Column({ name: 'type', type: 'varchar', length: 50, nullable: true, comment: '文件类型' })
-  type!: string | null;
-
-  @Column({ name: 'size', type: 'int', unsigned: true, nullable: true, comment: '文件大小,单位字节' })
-  size!: number | null;
-
-  @Column({ name: 'path', type: 'varchar', length: 512, comment: '文件存储路径' })
-  path!: string;
-
-  @Column({ name: 'description', type: 'text', nullable: true, comment: '文件描述' })
-  description!: string | null;
-
-  @Column({ name: 'upload_user_id', type: 'int', unsigned: true })
-  uploadUserId!: number;
-
-  @ManyToOne(() => UserEntity)
-  @JoinColumn({ name: 'upload_user_id', referencedColumnName: 'id' })
-  uploadUser!: UserEntity;
-
-  @Column({ name: 'upload_time', type: 'datetime' })
-  uploadTime!: Date;
-
-  @Column({ name: 'last_updated', type: 'datetime', nullable: true, comment: '最后更新时间' })
-  lastUpdated!: Date | null;
-
-  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
-  createdAt!: Date;
-
-  @Column({ 
-    name: 'updated_at', 
-    type: 'timestamp', 
-    default: () => 'CURRENT_TIMESTAMP', 
-    onUpdate: 'CURRENT_TIMESTAMP' 
-  })
-  updatedAt!: Date;
-}
-
-export const FileSchema = z.object({
-  id: z.number().int().positive().openapi({
-    description: '文件ID',
-    example: 1
-  }),
-  name: z.string().max(255).openapi({
-    description: '文件名称',
-    example: '项目计划书.pdf'
-  }),
-  type: z.string().max(50).nullable().openapi({
-    description: '文件类型',
-    example: 'application/pdf'
-  }),
-  size: z.number().int().positive().nullable().openapi({
-    description: '文件大小,单位字节',
-    example: 102400
-  }),
-  path: z.string().max(512).openapi({
-    description: '文件存储路径',
-    example: '/uploads/documents/2023/project-plan.pdf'
-  }),
-  description: z.string().nullable().openapi({
-    description: '文件描述',
-    example: '2023年度项目计划书'
-  }),
-  uploadUserId: z.number().int().positive().openapi({
-    description: '上传用户ID',
-    example: 1
-  }),
-  uploadUser: UserSchema,
-  uploadTime: z.date().openapi({
-    description: '上传时间',
-    example: '2023-01-15T10:30:00Z'
-  }),
-  lastUpdated: z.date().nullable().openapi({
-    description: '最后更新时间',
-    example: '2023-01-16T14:20:00Z'
-  }),
-  createdAt: z.date().openapi({
-    description: '创建时间',
-    example: '2023-01-15T10:30:00Z'
-  }),
-  updatedAt: z.date().openapi({
-    description: '更新时间',
-    example: '2023-01-16T14:20:00Z'
-  })
-});
-
-export const CreateFileDto = z.object({
-  name: z.string().max(255).openapi({
-    description: '文件名称',
-    example: '项目计划书.pdf'
-  }),
-  type: z.string().max(50).nullable().optional().openapi({
-    description: '文件类型',
-    example: 'application/pdf'
-  }),
-  size: z.coerce.number().int().positive().nullable().optional().openapi({
-    description: '文件大小,单位字节',
-    example: 102400
-  }),
-  path: z.string().max(512).openapi({
-    description: '文件存储路径',
-    example: '/uploads/documents/2023/project-plan.pdf'
-  }),
-  description: z.string().nullable().optional().openapi({
-    description: '文件描述',
-    example: '2023年度项目计划书'
-  }),
-  lastUpdated: z.coerce.date().nullable().optional().openapi({
-    description: '最后更新时间',
-    example: '2023-01-16T14:20:00Z'
-  })
-});
-
-export const UpdateFileDto = z.object({
-  name: z.string().max(255).optional().openapi({ 
-    description: '文件名称',
-    example: '项目计划书_v2.pdf' 
-  }),
-  type: z.string().max(50).nullable().optional().openapi({ 
-    description: '文件类型',
-    example: 'application/pdf' 
-  }),
-  size: z.coerce.number().int().positive().nullable().optional().openapi({ 
-    description: '文件大小,单位字节',
-    example: 153600 
-  }),
-  path: z.string().max(512).optional().openapi({ 
-    description: '文件存储路径',
-    example: '/uploads/documents/2023/project-plan_v2.pdf' 
-  }),
-  description: z.string().nullable().optional().openapi({ 
-    description: '文件描述',
-    example: '2023年度项目计划书(修订版)' 
-  }),
-  uploadUserId: z.number().int().positive().optional().openapi({
-    description: '上传用户ID',
-    example: 1
-  }),
-  uploadTime: z.coerce.date().optional().openapi({ 
-    description: '上传时间',
-    example: '2023-01-15T10:30:00Z' 
-  }),
-  lastUpdated: z.coerce.date().nullable().optional().openapi({ 
-    description: '最后更新时间',
-    example: '2023-01-16T14:20:00Z' 
-  })
-});

+ 0 - 185
src/server/modules/files/file.service.ts

@@ -1,185 +0,0 @@
-import { GenericCrudService } from '@/server/utils/generic-crud.service';
-import { DataSource } from 'typeorm';
-import { File } from './file.entity';
-import { MinioService } from './minio.service';
-// import { AppError } from '@/server/utils/errorHandler';
-import { v4 as uuidv4 } from 'uuid';
-import { logger } from '@/server/utils/logger';
-
-export class FileService extends GenericCrudService<File> {
-  private readonly minioService: MinioService;
-
-  constructor(dataSource: DataSource) {
-    super(dataSource, File);
-    this.minioService = new MinioService();
-  }
-
-  /**
-   * 创建文件记录并生成预签名上传URL
-   */
-  async createFile(data: Partial<File>) {
-    try {
-      // 生成唯一文件存储路径
-      const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
-      
-      // 生成MinIO上传策略
-      const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey);
-      
-      // 准备文件记录数据
-      const fileData = {
-        ...data,
-        path: fileKey,
-        uploadTime: new Date(),
-        createdAt: new Date(),
-        updatedAt: new Date()
-      };
-      
-      // 保存文件记录到数据库
-      const savedFile = await this.create(fileData as File);
-      
-      // 返回文件记录和上传策略
-      return {
-        file: savedFile,
-        uploadPolicy
-      };
-    } catch (error) {
-      logger.error('Failed to create file:', error);
-      throw new Error('文件创建失败');
-    }
-  }
-
-  /**
-   * 删除文件记录及对应的MinIO文件
-   */
-  async deleteFile(id: number) {
-    try {
-      // 获取文件记录
-      const file = await this.getById(id);
-      if (!file) {
-        throw new Error('文件不存在');
-      }
-      
-      // 验证文件是否存在于MinIO
-      const fileExists = await this.minioService.objectExists(this.minioService.bucketName, file.path);
-      if (!fileExists) {
-        logger.error(`File not found in MinIO: ${this.minioService.bucketName}/${file.path}`);
-        // 仍然继续删除数据库记录,但记录警告日志
-      } else {
-        // 从MinIO删除文件
-        await this.minioService.deleteObject(this.minioService.bucketName, file.path);
-      }
-      
-      // 从数据库删除记录
-      await this.delete(id);
-      
-      return true;
-    } catch (error) {
-      logger.error('Failed to delete file:', error);
-      throw new Error('文件删除失败');
-    }
-  }
-
-  /**
-   * 获取文件访问URL
-   */
-  async getFileUrl(id: number) {
-    const file = await this.getById(id);
-    if (!file) {
-      throw new Error('文件不存在');
-    }
-    
-    return this.minioService.getFileUrl(this.minioService.bucketName, file.path);
-  }
-
-  /**
-   * 创建多部分上传策略
-   */
-  async createMultipartUploadPolicy(data: Partial<File>, partCount: number) {
-    try {
-      // 生成唯一文件存储路径
-      const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
-      
-      // 初始化多部分上传
-      const uploadId = await this.minioService.createMultipartUpload(
-        this.minioService.bucketName,
-        fileKey
-      );
-      
-      // 生成各部分上传URL
-      const uploadUrls = await this.minioService.generateMultipartUploadUrls(
-        this.minioService.bucketName,
-        fileKey,
-        uploadId,
-        partCount
-      );
-      
-      // 准备文件记录数据
-      const fileData = {
-        ...data,
-        path: fileKey,
-        uploadTime: new Date(),
-        createdAt: new Date(),
-        updatedAt: new Date()
-      };
-      
-      // 保存文件记录到数据库
-      const savedFile = await this.create(fileData as File);
-      
-      // 返回文件记录和上传策略
-      return {
-        file: savedFile,
-        uploadId,
-        uploadUrls,
-        bucket: this.minioService.bucketName,
-        key: fileKey
-      };
-    } catch (error) {
-      logger.error('Failed to create multipart upload policy:', error);
-      throw new Error('创建多部分上传策略失败');
-    }
-  }
-
-  /**
-   * 完成分片上传
-   */
-  async completeMultipartUpload(data: {
-    uploadId: string;
-    bucket: string;
-    key: string;
-    parts: Array<{ PartNumber: number; ETag: string }>;
-  }) {
-    try {
-      // 完成MinIO分片上传
-      const result = await this.minioService.completeMultipartUpload(
-        data.bucket,
-        data.key,
-        data.uploadId,
-        data.parts.map(part => ({ PartNumber: part.PartNumber, ETag: part.ETag }))
-      );
-      
-      // 查找文件记录并更新
-      const file = await this.repository.findOneBy({ path: data.key });
-      if (!file) {
-        throw new Error('文件记录不存在');
-      }
-      
-      // 更新文件大小等信息
-      file.size = result.size;
-      file.updatedAt = new Date();
-      await this.repository.save(file);
-      
-      // 生成文件访问URL
-      const url = this.minioService.getFileUrl(data.bucket, data.key);
-      
-      return {
-        fileId: file.id,
-        url,
-        key: data.key,
-        size: result.size
-      };
-    } catch (error) {
-      logger.error('Failed to complete multipart upload:', error);
-      throw new Error('完成分片上传失败');
-    }
-  }
-}

+ 0 - 204
src/server/modules/files/minio.service.ts

@@ -1,204 +0,0 @@
-import { Client } from 'minio';
-import { logger } from '@/server/utils/logger';
-import * as process from 'node:process';
-
-export class MinioService {
-  private readonly client: Client;
-  public readonly bucketName: string;
-
-  constructor() {
-    this.client = new Client({
-      endPoint: process.env.MINIO_HOST || 'localhost',
-      port: parseInt(process.env.MINIO_PORT || '443'),
-      useSSL: process.env.MINIO_USE_SSL !== 'false',
-      accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
-      secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
-    });
-    this.bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
-  }
-
-  // 设置桶策略为"公读私写"
-  async setPublicReadPolicy(bucketName: string = this.bucketName) {
-    const policy = {
-      "Version": "2012-10-17",
-      "Statement": [
-        {
-          "Effect": "Allow",
-          "Principal": {"AWS": "*"},
-          "Action": ["s3:GetObject"],
-          "Resource": [`arn:aws:s3:::${bucketName}/*`]
-        },
-        {
-          "Effect": "Allow",
-          "Principal": {"AWS": "*"},
-          "Action": ["s3:ListBucket"],
-          "Resource": [`arn:aws:s3:::${bucketName}`]
-        }
-      ]
-    };
-
-    try {
-      await this.client.setBucketPolicy(bucketName, JSON.stringify(policy));
-      logger.db(`Bucket policy set to public read for: ${bucketName}`);
-    } catch (error) {
-      logger.error(`Failed to set bucket policy for ${bucketName}:`, error);
-      throw error;
-    }
-  }
-
-  // 确保存储桶存在
-  async ensureBucketExists(bucketName: string = this.bucketName) {
-    try {
-      const exists = await this.client.bucketExists(bucketName);
-      if (!exists) {
-        await this.client.makeBucket(bucketName);
-        await this.setPublicReadPolicy(bucketName);
-        logger.db(`Created new bucket: ${bucketName}`);
-      }
-      return true;
-    } catch (error) {
-      logger.error(`Failed to ensure bucket exists: ${bucketName}`, error);
-      throw error;
-    }
-  }
-
-  // 生成上传策略
-  async generateUploadPolicy(fileKey: string) {
-    await this.ensureBucketExists();
-    
-    const expiresAt = new Date(Date.now() + 3600 * 1000);
-    const policy = this.client.newPostPolicy();
-    policy.setBucket(this.bucketName);
-    
-    policy.setKey(fileKey);
-    policy.setExpires(expiresAt);
-
-    const { postURL, formData } = await this.client.presignedPostPolicy(policy);
-
-    return {
-      'x-amz-algorithm': formData['x-amz-algorithm'],
-      'x-amz-credential': formData['x-amz-credential'],
-      'x-amz-date': formData['x-amz-date'],
-      'x-amz-security-token': formData['x-amz-security-token'] || undefined,
-      policy: formData['policy'],
-      'x-amz-signature': formData['x-amz-signature'],
-      host: postURL,
-      key: fileKey,
-      bucket: this.bucketName,
-    };
-  }
-
-  // 生成文件访问URL
-  getFileUrl(bucketName: string, fileKey: string) {
-    const protocol = process.env.MINIO_USE_SSL ? 'https' : 'http';
-    const port = process.env.MINIO_PORT ? `:${process.env.MINIO_PORT}` : '';
-    return `${protocol}://${process.env.MINIO_ENDPOINT}${port}/${bucketName}/${fileKey}`;
-  }
-
-  // 创建分段上传会话
-  async createMultipartUpload(bucketName: string, objectName: string) {
-    try {
-      const uploadId = await this.client.initiateNewMultipartUpload(bucketName, objectName, {});
-      logger.db(`Created multipart upload for ${objectName} with ID: ${uploadId}`);
-      return uploadId;
-    } catch (error) {
-      logger.error(`Failed to create multipart upload for ${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  // 生成分段上传预签名URL
-  async generateMultipartUploadUrls(
-    bucketName: string,
-    objectName: string,
-    uploadId: string,
-    partCount: number,
-    expiresInSeconds = 3600
-  ) {
-    try {
-      const partUrls = [];
-      for (let partNumber = 1; partNumber <= partCount; partNumber++) {
-        const url = await this.client.presignedUrl(
-          'put',
-          bucketName,
-          objectName,
-          expiresInSeconds,
-          {
-            uploadId,
-            partNumber: partNumber.toString()
-          }
-        );
-        partUrls.push(url);
-      }
-      return partUrls;
-    } catch (error) {
-      logger.error(`Failed to generate multipart upload URLs for ${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  // 完成分段上传
-  async completeMultipartUpload(
-    bucketName: string,
-    objectName: string,
-    uploadId: string,
-    parts: { ETag: string; PartNumber: number }[]
-  ): Promise<{ size: number }> {
-    try {
-      await this.client.completeMultipartUpload(
-        bucketName,
-        objectName,
-        uploadId,
-        parts.map(p => ({ part: p.PartNumber, etag: p.ETag }))
-      );
-      logger.db(`Completed multipart upload for ${objectName} with ID: ${uploadId}`);
-      
-      // 获取对象信息以获取文件大小
-      const stat = await this.client.statObject(bucketName, objectName);
-      return { size: stat.size };
-    } catch (error) {
-      logger.error(`Failed to complete multipart upload for ${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  // 上传文件
-  async createObject(bucketName: string, objectName: string, fileContent: Buffer, contentType: string = 'application/octet-stream') {
-    try {
-      await this.ensureBucketExists(bucketName);
-      await this.client.putObject(bucketName, objectName, fileContent, fileContent.length, {
-        'Content-Type': contentType
-      });
-      logger.db(`Created object: ${bucketName}/${objectName}`);
-      return this.getFileUrl(bucketName, objectName);
-    } catch (error) {
-      logger.error(`Failed to create object ${bucketName}/${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  // 检查文件是否存在
-  async objectExists(bucketName: string, objectName: string): Promise<boolean> {
-    try {
-      await this.client.statObject(bucketName, objectName);
-      return true;
-    } catch (error) {
-      if ((error as Error).message.includes('not found')) {
-        return false;
-      }
-      logger.error(`Error checking existence of object ${bucketName}/${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  // 删除文件
-  async deleteObject(bucketName: string, objectName: string) {
-    try {
-      await this.client.removeObject(bucketName, objectName);
-      logger.db(`Deleted object: ${bucketName}/${objectName}`);
-    } catch (error) {
-      logger.error(`Failed to delete object ${bucketName}/${objectName}:`, error);
-      throw error;
-    }
-  }
-}