package service import ( "errors" "fmt" "io" "mime/multipart" "os" "path/filepath" "time" "seeyon-filesystem/config" "seeyon-filesystem/dto" "seeyon-filesystem/model" "seeyon-filesystem/repository" "seeyon-filesystem/storage" "seeyon-filesystem/utils" ) // FileService 文件业务服务 type FileService struct { fileRepo *repository.FileRepository folderRepo *repository.FolderRepository userRepo *repository.UserRepository assignService *StorageAssignService } // NewFileService 创建文件服务实例 func NewFileService() *FileService { return &FileService{ fileRepo: repository.NewFileRepository(), folderRepo: repository.NewFolderRepository(), userRepo: repository.NewUserRepository(), assignService: NewStorageAssignService(), } } // Upload 上传文件 // 流程: 计算MD5 -> 检查秒传 -> 存储文件 -> 保存元数据 // storagePolicyID: 存储策略ID, 0=使用默认策略 func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64) (*dto.FileVO, error) { // 验证文件夹 if folderID > 0 { folder, err := s.folderRepo.FindByID(folderID) if err != nil { return nil, errors.New("目标文件夹不存在") } if folder.OwnerID != ownerID { return nil, errors.New("无权上传到此文件夹") } } // 确定存储策略: 优先使用指定策略, 否则根据用户分配自动确定 var engine storage.Engine var policyID uint64 if storagePolicyID > 0 { policyID = storagePolicyID } else { policyID = s.assignService.ResolvePolicyID(ownerID) } engine, engineErr := storage.GetEngineByPolicyID(policyID) if engineErr != nil { engine = storage.GetDefaultEngine() policyID = uint64(config.AppConfig.Storage.DefaultPolicyID) } if engine == nil { return nil, errors.New("存储引擎未初始化") } // 打开上传文件 src, err := fileHeader.Open() if err != nil { return nil, fmt.Errorf("打开上传文件失败: %w", err) } defer src.Close() // 读取文件内容到临时文件(用于计算MD5) tempFile, err := os.CreateTemp("", "upload_*") if err != nil { return nil, fmt.Errorf("创建临时文件失败: %w", err) } defer os.Remove(tempFile.Name()) size, err := io.Copy(tempFile, src) if err != nil { tempFile.Close() return nil, fmt.Errorf("保存临时文件失败: %w", err) } tempFile.Close() // 计算文件MD5 md5, err := utils.FileMD5(tempFile.Name()) if err != nil { return nil, fmt.Errorf("计算MD5失败: %w", err) } // 秒传检查: 如果MD5相同的文件已存在, 直接创建引用 existing, _ := s.fileRepo.FindByMD5(md5) if existing != nil { newFile := &model.File{ Name: fileHeader.Filename, Extension: utils.GetExtension(fileHeader.Filename), FolderID: folderID, OwnerID: ownerID, Size: size, MD5: md5, StorageKey: existing.StorageKey, StoragePolicyID: existing.StoragePolicyID, MimeType: utils.GetMimeType(utils.GetExtension(fileHeader.Filename)), Status: 1, } if err := s.fileRepo.Create(newFile); err != nil { return nil, fmt.Errorf("创建文件记录失败: %w", err) } s.userRepo.UpdateStorageUsed(ownerID, size) return s.toFileVO(newFile), nil } // 正常上传: 生成存储路径 -> 上传到存储引擎 -> 保存元数据 ext := utils.GetExtension(fileHeader.Filename) storageKey := storage.GenerateStorageKey(md5, ext) reader, err := os.Open(tempFile.Name()) if err != nil { return nil, fmt.Errorf("打开临时文件失败: %w", err) } defer reader.Close() if err := engine.Upload(storageKey, reader, size); err != nil { return nil, fmt.Errorf("存储文件失败: %w", err) } newFile := &model.File{ Name: fileHeader.Filename, Extension: ext, FolderID: folderID, OwnerID: ownerID, Size: size, MD5: md5, StorageKey: storageKey, StoragePolicyID: policyID, MimeType: utils.GetMimeType(ext), Status: 1, } if err := s.fileRepo.Create(newFile); err != nil { engine.Delete(storageKey) return nil, fmt.Errorf("创建文件记录失败: %w", err) } s.userRepo.UpdateStorageUsed(ownerID, size) return s.toFileVO(newFile), nil } // GetByID 根据ID获取文件 func (s *FileService) GetByID(id uint64, ownerID uint64) (*dto.FileVO, error) { file, err := s.fileRepo.FindByID(id) if err != nil { return nil, errors.New("文件不存在") } if file.OwnerID != ownerID { return nil, errors.New("无权访问此文件") } return s.toFileVO(file), nil } // Download 下载文件(检查归属) func (s *FileService) Download(id uint64, ownerID uint64) (io.ReadCloser, *model.File, error) { file, err := s.fileRepo.FindByID(id) if err != nil { return nil, nil, errors.New("文件不存在") } if file.OwnerID != ownerID { return nil, nil, errors.New("无权下载此文件") } return s.downloadFile(file, id) } // DownloadByFileID 下载文件(不检查归属, 用于分享下载) func (s *FileService) DownloadByFileID(fileID uint64) (io.ReadCloser, *model.File, error) { file, err := s.fileRepo.FindByID(fileID) if err != nil { return nil, nil, errors.New("文件不存在") } return s.downloadFile(file, fileID) } // downloadFile 内部下载实现 func (s *FileService) downloadFile(file *model.File, id uint64) (io.ReadCloser, *model.File, error) { engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { return nil, nil, fmt.Errorf("获取存储引擎失败: %w", err) } reader, err := engine.Download(file.StorageKey) if err != nil { return nil, nil, fmt.Errorf("读取文件失败: %w", err) } s.fileRepo.IncrementDownloadCount(id) return reader, file, nil } // Rename 重命名文件 // Rename 重命名文件 // version: 乐观锁版本号, 0=不检查版本 func (s *FileService) Rename(id uint64, newName string, ownerID uint64, version int) error { file, err := s.fileRepo.FindByID(id) if err != nil { return errors.New("文件不存在") } if file.OwnerID != ownerID { return errors.New("无权操作此文件") } // 乐观锁检查 if version > 0 && file.Version != version { return fmt.Errorf("文件已被其他人修改, 当前版本: %d, 请刷新后重试", file.Version) } file.Name = newName file.Extension = utils.GetExtension(newName) file.Version++ result := config.DB.Model(&model.File{}). Where("id = ? AND version = ?", id, file.Version-1). Updates(map[string]interface{}{ "name": file.Name, "extension": file.Extension, "version": file.Version, }) if result.RowsAffected == 0 { return fmt.Errorf("文件已被其他人修改, 请刷新后重试") } return result.Error } // Move 移动文件到目标文件夹 func (s *FileService) Move(id uint64, targetFolderID uint64, ownerID uint64) error { file, err := s.fileRepo.FindByID(id) if err != nil { return errors.New("文件不存在") } if file.OwnerID != ownerID { return errors.New("无权操作此文件") } if targetFolderID > 0 { folder, err := s.folderRepo.FindByID(targetFolderID) if err != nil { return errors.New("目标文件夹不存在") } if folder.OwnerID != ownerID { return errors.New("无权移动到此文件夹") } } file.FolderID = targetFolderID return s.fileRepo.Update(file) } // SoftDelete 移入回收站 func (s *FileService) SoftDelete(id uint64, ownerID uint64) error { file, err := s.fileRepo.FindByID(id) if err != nil { return errors.New("文件不存在") } if file.OwnerID != ownerID { return errors.New("无权操作此文件") } return s.fileRepo.SoftDelete(id) } // Restore 从回收站恢复 func (s *FileService) Restore(id uint64, ownerID uint64) error { file, err := s.fileRepo.FindByID(id) if err != nil { return errors.New("文件不存在") } if file.OwnerID != ownerID { return errors.New("无权操作此文件") } return s.fileRepo.Restore(id) } // HardDelete 永久删除(同时删除存储文件) func (s *FileService) HardDelete(id uint64, ownerID uint64) error { file, err := s.fileRepo.FindByID(id) if err != nil { return errors.New("文件不存在") } if file.OwnerID != ownerID { return errors.New("无权操作此文件") } // 根据文件的存储策略获取引擎并删除 engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err == nil && engine != nil { engine.Delete(file.StorageKey) } s.userRepo.UpdateStorageUsed(ownerID, -file.Size) return s.fileRepo.HardDelete(id) } // GetDownloadURL 生成文件下载链接 func (s *FileService) GetDownloadURL(id uint64) (string, error) { file, err := s.fileRepo.FindByID(id) if err != nil { return "", errors.New("文件不存在") } return s.generateFileURL(file, "download") } // GetPreviewURL 生成文件预览链接 func (s *FileService) GetPreviewURL(id uint64) (string, error) { file, err := s.fileRepo.FindByID(id) if err != nil { return "", errors.New("文件不存在") } return s.generateFileURL(file, "preview") } // generateFileURL 统一生成文件URL func (s *FileService) generateFileURL(file *model.File, urlType string) (string, error) { engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil } // 本地存储: 使用CDN地址 if localEngine, ok := engine.(*storage.LocalEngine); ok { if localEngine.CDNHost != "" { return localEngine.GetURL(file.StorageKey), nil } return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil } // MinIO存储: 生成预签名URL并替换为Nginx代理地址 if minioEngine, ok := engine.(*storage.MinIOEngine); ok { presignedURL, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if err != nil { return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil } // 替换为Nginx代理地址 policy := getStoragePolicyConfig(file.StoragePolicyID) if policy != nil && policy.NginxEndpoint != "" { presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.NginxEndpoint) } return presignedURL, nil } return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil } // getStoragePolicyConfig 获取存储策略配置 func getStoragePolicyConfig(policyID uint64) *model.PolicyConfig { var policy model.StoragePolicy if err := config.DB.First(&policy, policyID).Error; err != nil { return nil } return &policy.Config } // replaceMinIOEndpoint 替换URL中的MinIO地址为Nginx代理地址 func replaceMinIOEndpoint(url, minioEndpoint, nginxEndpoint string) string { oldHTTP := "http://" + minioEndpoint if len(url) > len(oldHTTP) && url[:len(oldHTTP)] == oldHTTP { return nginxEndpoint + url[len(oldHTTP):] } oldHTTPS := "https://" + minioEndpoint if len(url) > len(oldHTTPS) && url[:len(oldHTTPS)] == oldHTTPS { return nginxEndpoint + url[len(oldHTTPS):] } return url } // GetByKey 根据fileKey(storageKey)查询文件信息 func (s *FileService) GetByKey(storageKey string) (*dto.FileVO, error) { var file model.File err := config.DB.Where("storage_key = ?", storageKey).First(&file).Error if err != nil { return nil, errors.New("文件不存在") } return s.toFileVO(&file), nil } // Search 搜索文件(基于权限: 自己的 + 被授权的) func (s *FileService) Search(userID uint64, keyword string) ([]dto.FileVO, error) { roleIDs := s.getUserRoleIDs(userID) files, err := s.fileRepo.SearchByName(userID, roleIDs, keyword) if err != nil { return nil, err } vos := make([]dto.FileVO, 0, len(files)) for _, f := range files { vo := s.toFileVO(&f) vos = append(vos, *vo) } return vos, nil } // SearchByType 按文件类型搜索 func (s *FileService) SearchByType(ownerID uint64, extensions []string) ([]dto.FileVO, error) { files, err := s.fileRepo.SearchByType(ownerID, extensions) if err != nil { return nil, err } vos := make([]dto.FileVO, 0, len(files)) for _, f := range files { vos = append(vos, *s.toFileVO(&f)) } return vos, nil } // ListTrashed 查询回收站 func (s *FileService) ListTrashed(ownerID uint64) ([]dto.FileVO, error) { files, err := s.fileRepo.ListTrashed(ownerID) if err != nil { return nil, err } vos := make([]dto.FileVO, 0, len(files)) for _, f := range files { vos = append(vos, *s.toFileVO(&f)) } return vos, nil } // GetDownloadPath 获取文件的本地存储完整路径(用于静态文件服务) func (s *FileService) GetDownloadPath(storageKey string) string { return filepath.Join(config.AppConfig.Storage.LocalBasePath, storageKey) } // toFileVO 转换为视图对象 func (s *FileService) toFileVO(file *model.File) *dto.FileVO { vo := &dto.FileVO{ ID: file.ID, Name: file.Name, Extension: file.Extension, FolderID: file.FolderID, Size: file.Size, StorageKey: file.StorageKey, StoragePolicyID: file.StoragePolicyID, MimeType: file.MimeType, IsFavorite: file.IsFavorite, DownloadCount: file.DownloadCount, Version: file.Version, CreatedAt: file.CreatedAt, UpdatedAt: file.UpdatedAt, } // 生成下载/预览链接 if file.StorageKey != "" { downloadURL, _ := s.generateFileURL(file, "download") previewURL, _ := s.generateFileURL(file, "preview") vo.DownloadURL = downloadURL vo.PreviewURL = previewURL } return vo } // getUserRoleIDs 获取用户的角色ID列表 func (s *FileService) getUserRoleIDs(userID uint64) []uint64 { var userRoles []model.UserRole config.DB.Where("user_id = ?", userID).Find(&userRoles) roleIDs := make([]uint64, 0, len(userRoles)) for _, ur := range userRoles { roleIDs = append(roleIDs, ur.RoleID) } return roleIDs }