package controller import ( "fmt" "net/http" "strconv" "time" "seeyon-filesystem/config" "seeyon-filesystem/dto" "seeyon-filesystem/middleware" "seeyon-filesystem/model" "seeyon-filesystem/service" "seeyon-filesystem/storage" "seeyon-filesystem/utils" "github.com/gin-gonic/gin" ) // OpenAPIFileController 对外文件API控制器 type OpenAPIFileController struct { fileService *service.FileService folderService *service.FolderService chunkService *service.ChunkUploadService } func NewOpenAPIFileController() *OpenAPIFileController { return &OpenAPIFileController{ fileService: service.NewFileService(), folderService: service.NewFolderService(), chunkService: service.NewChunkUploadService(), } } // ========== 1. 根据父文件夹ID分页查询 ========== // ListByFolder 分页查询文件夹下的文件和子文件夹 // GET /api/openapi/files?folder_id=0&page=1&page_size=20 // 返回树状结构: 文件夹 + 文件 func (c *OpenAPIFileController) ListByFolder(ctx *gin.Context) { userID := middleware.GetCurrentUserID(ctx) folderIDStr := ctx.DefaultQuery("folder_id", "0") folderID, _ := strconv.ParseUint(folderIDStr, 10, 64) page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 50 } // 获取用户角色 roleIDs := getUserRoleIDs(userID) // 查询文件夹 folders, err := getFoldersByParent(folderID, userID, page, pageSize) if err != nil { utils.ServerError(ctx, "查询文件夹失败") return } // 查询文件 files, totalFiles, err := getFilesByFolder(folderID, userID, roleIDs, page, pageSize) if err != nil { utils.ServerError(ctx, "查询文件失败") return } // 组装树状结构 result := dto.OpenAPIListResult{ FolderID: folderID, TotalFiles: totalFiles, Page: page, PageSize: pageSize, } for _, f := range folders { childCount := getFolderChildCount(f.ID) result.Folders = append(result.Folders, dto.OpenAPIFolderVO{ ID: dto.StringID(f.ID), Name: f.Name, ParentID: dto.StringID(f.ParentID), ChildCount: childCount, CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"), }) } for _, f := range files { result.Files = append(result.Files, dto.OpenAPIFileVO{ ID: dto.StringID(f.ID), Name: f.Name, Extension: f.Extension, Size: f.Size, MD5: f.MD5, MimeType: f.MimeType, DownloadURL: getCDNOrPresignedURL(&f), CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"), }) } utils.Success(ctx, result) } // ========== 2. 按名称搜索返回树状结构 ========== // SearchTree 按文件名/文件夹名搜索, 返回树状结构 // GET /api/openapi/files/search?keyword=报告&page=1&page_size=20 func (c *OpenAPIFileController) SearchTree(ctx *gin.Context) { userID := middleware.GetCurrentUserID(ctx) keyword := ctx.Query("keyword") if keyword == "" { utils.BadRequest(ctx, "请输入搜索关键词") return } page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 50 } roleIDs := getUserRoleIDs(userID) // 搜索文件 files, total, err := searchFilesByName(userID, roleIDs, keyword, page, pageSize) if err != nil { utils.ServerError(ctx, "搜索失败") return } // 搜索文件夹 folders, err := searchFoldersByName(userID, keyword, page, pageSize) if err != nil { utils.ServerError(ctx, "搜索失败") return } result := dto.OpenAPISearchResult{ Keyword: keyword, TotalFiles: total, Page: page, PageSize: pageSize, } for _, f := range folders { result.Folders = append(result.Folders, dto.OpenAPIFolderVO{ ID: dto.StringID(f.ID), Name: f.Name, ParentID: dto.StringID(f.ParentID), CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"), }) } for _, f := range files { result.Files = append(result.Files, dto.OpenAPIFileVO{ ID: dto.StringID(f.ID), Name: f.Name, Extension: f.Extension, Size: f.Size, MD5: f.MD5, MimeType: f.MimeType, DownloadURL: getCDNOrPresignedURL(&f), CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"), }) } utils.Success(ctx, result) } // ========== 3. 分片上传/下载 ========== // ChunkUploadInit 初始化分片上传 // POST /api/openapi/files/upload/init func (c *OpenAPIFileController) ChunkUploadInit(ctx *gin.Context) { userID := middleware.GetCurrentUserID(ctx) var req dto.ChunkUploadInitRequest if err := ctx.ShouldBindJSON(&req); err != nil { utils.BadRequest(ctx, "参数错误: "+err.Error()) return } result, err := c.chunkService.InitUpload(req.FileName, req.FileSize, req.MD5, uint64(req.FolderID), userID, req.PolicyID) if err != nil { utils.ServerError(ctx, err.Error()) return } utils.Success(ctx, result) } // ChunkUpload 上传分片 // PUT /api/openapi/files/upload/chunk/:uploadId/:index func (c *OpenAPIFileController) ChunkUpload(ctx *gin.Context) { uploadID := ctx.Param("uploadId") indexStr := ctx.Param("index") index, err := strconv.Atoi(indexStr) if err != nil { utils.BadRequest(ctx, "分片索引无效") return } checksum := ctx.GetHeader("X-Checksum") file, err := ctx.FormFile("chunk") if err != nil { utils.BadRequest(ctx, "请上传分片文件") return } reader, err := file.Open() if err != nil { utils.BadRequest(ctx, "读取分片失败") return } defer reader.Close() result, err := c.chunkService.UploadChunk(uploadID, index, reader, checksum) if err != nil { utils.BadRequest(ctx, err.Error()) return } utils.Success(ctx, result) } // ChunkMerge 合并分片 // POST /api/openapi/files/upload/merge/:uploadId func (c *OpenAPIFileController) ChunkMerge(ctx *gin.Context) { uploadID := ctx.Param("uploadId") result, err := c.chunkService.MergeChunks(uploadID) if err != nil { utils.ServerError(ctx, err.Error()) return } utils.Success(ctx, result) } // ChunkProgress 查询上传进度 // GET /api/openapi/files/upload/progress/:uploadId func (c *OpenAPIFileController) ChunkProgress(ctx *gin.Context) { uploadID := ctx.Param("uploadId") result, err := c.chunkService.GetUploadProgress(uploadID) if err != nil { utils.NotFound(ctx, err.Error()) return } utils.Success(ctx, result) } // Download 下载文件 // GET /api/openapi/files/:id/download func (c *OpenAPIFileController) Download(ctx *gin.Context) { userID := middleware.GetCurrentUserID(ctx) fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64) if err != nil { utils.BadRequest(ctx, "无效的文件ID") return } reader, file, err := c.fileService.Download(fileID, userID) if err != nil { utils.ServerError(ctx, err.Error()) return } defer reader.Close() ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Name)) ctx.Header("Content-Type", file.MimeType) ctx.Header("Content-Length", strconv.FormatInt(file.Size, 10)) ctx.DataFromReader(http.StatusOK, file.Size, file.MimeType, reader, nil) } // Preview 预览文件 // GET /api/openapi/files/:id/preview func (c *OpenAPIFileController) Preview(ctx *gin.Context) { userID := middleware.GetCurrentUserID(ctx) fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64) if err != nil { utils.BadRequest(ctx, "无效的文件ID") return } reader, file, err := c.fileService.Download(fileID, userID) if err != nil { utils.ServerError(ctx, err.Error()) return } defer reader.Close() ctx.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Name)) ctx.Header("Content-Type", file.MimeType) ctx.Header("Content-Length", strconv.FormatInt(file.Size, 10)) ctx.DataFromReader(http.StatusOK, file.Size, file.MimeType, reader, nil) } // ========== 辅助函数 ========== func getUserRoleIDs(userID uint64) []uint64 { var userRoles []model.UserRole config.DB.Where("user_id = ?", userID).Find(&userRoles) ids := make([]uint64, 0, len(userRoles)) for _, ur := range userRoles { ids = append(ids, ur.RoleID) } return ids } func getFoldersByParent(parentID, ownerID uint64, page, pageSize int) ([]model.Folder, error) { var folders []model.Folder err := config.DB.Where("parent_id = ? AND owner_id = ? AND status = 1", parentID, ownerID). Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&folders).Error return folders, err } func getFilesByFolder(folderID, userID uint64, roleIDs []uint64, page, pageSize int) ([]model.File, int64, error) { var files []model.File query := config.DB.Where("folder_id = ? AND owner_id = ? AND status = 1", folderID, userID) var total int64 query.Model(&model.File{}).Count(&total) err := query.Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&files).Error return files, total, err } func getFolderChildCount(folderID uint64) int64 { var count int64 config.DB.Model(&model.Folder{}).Where("parent_id = ? AND status = 1", folderID).Count(&count) return count } func searchFilesByName(userID uint64, roleIDs []uint64, keyword string, page, pageSize int) ([]model.File, int64, error) { var files []model.File query := config.DB.Where("owner_id = ? AND status = 1 AND name LIKE ?", userID, "%"+keyword+"%") var total int64 query.Model(&model.File{}).Count(&total) err := query.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&files).Error return files, total, err } func searchFoldersByName(ownerID uint64, keyword string, page, pageSize int) ([]model.Folder, error) { var folders []model.Folder err := config.DB.Where("owner_id = ? AND status = 1 AND name LIKE ?", ownerID, "%"+keyword+"%"). Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&folders).Error return folders, err } func getCDNOrPresignedURL(file *model.File) string { if file.StorageKey == "" { return "" } engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { return "" } if localEngine, ok := engine.(*storage.LocalEngine); ok { return localEngine.GetURL(file.StorageKey) } if minioEngine, ok := engine.(*storage.MinIOEngine); ok { url, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if err == nil { return url } } return "" }