package controllers import ( "crypto/md5" "encoding/hex" "encoding/json" "io" "net/http" "net/url" "os" "path" "path/filepath" "server/models" "strconv" "strings" "time" beego "github.com/beego/beego/v2/server/web" ) // FileController 处理文件相关请求 type FileController struct { beego.Controller } // GetAllFiles 获取所有文件信息 func (c *FileController) GetAllFiles() { files, err := models.GetAllFiles() if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件列表成功", "data": files, } } c.ServeJSON() } // GetFileById 根据ID获取文件信息 func (c *FileController) GetFileById() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } file, err := models.GetFileById(id) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在", } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件成功", "data": file, } } c.ServeJSON() } // GetFilesByTenant 根据租户ID获取文件信息 func (c *FileController) GetFilesByTenant() { tenantID := c.GetString("tenant_id") if tenantID == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "租户ID不能为空", } c.ServeJSON() return } files, err := models.GetFilesByTenant(tenantID) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件列表成功", "data": files, } } c.ServeJSON() } // GetFilesByCategory 根据分类获取文件信息 func (c *FileController) GetFilesByCategory() { category := c.GetString("category") if category == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "分类不能为空", } c.ServeJSON() return } files, err := models.GetFilesByCategory(category) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件列表成功", "data": files, } } c.ServeJSON() } // GetFilesByStatus 根据状态获取文件信息 func (c *FileController) GetFilesByStatus() { statusStr := c.GetString("status") status, err := strconv.Atoi(statusStr) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "状态参数错误", } c.ServeJSON() return } files, err := models.GetFilesByStatus(int8(status)) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件列表成功", "data": files, } } c.ServeJSON() } // CreateFile 创建新文件信息 func (c *FileController) CreateFile() { var file models.FileInfo // 解析请求体 if err := json.Unmarshal(c.Ctx.Input.RequestBody, &file); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "请求参数错误", "error": err.Error(), } c.ServeJSON() return } // 从JWT中间件获取用户信息 if userID, ok := c.Ctx.Input.GetData("userId").(int); ok && userID > 0 { file.UserID = userID } if username, ok := c.Ctx.Input.GetData("username").(string); ok && username != "" { file.UploadBy = username } // 验证必填字段 if file.TenantID == "" || file.FileName == "" || file.OriginalName == "" || file.FilePath == "" || file.FileType == "" || file.FileExt == "" || file.Category == "" || file.UploadBy == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "必填字段不能为空", } c.ServeJSON() return } // 添加文件信息 if _, err := models.AddFile(&file); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "创建文件失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "创建文件成功", "data": file, } } c.ServeJSON() } // UpdateFile 更新文件信息 func (c *FileController) UpdateFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } var file models.FileInfo file.ID = id // 解析请求体 if err := json.Unmarshal(c.Ctx.Input.RequestBody, &file); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "请求参数错误", "error": err.Error(), } c.ServeJSON() return } // 更新文件信息 if err := models.UpdateFile(&file); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "更新文件失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "更新文件成功", "data": file, } } c.ServeJSON() } // DeleteFile 删除文件信息(软删除) func (c *FileController) DeleteFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } if err := models.DeleteFile(id); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "删除文件失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "删除文件成功", } } c.ServeJSON() } // HardDeleteFile 硬删除文件信息 func (c *FileController) HardDeleteFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } if err := models.HardDeleteFile(id); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "删除文件失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "删除文件成功", } } c.ServeJSON() } // GetFileStatistics 获取文件统计信息 func (c *FileController) GetFileStatistics() { tenantID := c.GetString("tenant_id") if tenantID == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "租户ID不能为空", } c.ServeJSON() return } stats, err := models.GetFileStatistics(tenantID) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取统计信息失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取统计信息成功", "data": stats, } } c.ServeJSON() } // SearchFiles 搜索文件 func (c *FileController) SearchFiles() { keyword := c.GetString("keyword") tenantID := c.GetString("tenant_id") if keyword == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "搜索关键词不能为空", } c.ServeJSON() return } if tenantID == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "租户ID不能为空", } c.ServeJSON() return } files, err := models.SearchFiles(keyword, tenantID) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "搜索文件失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "搜索文件成功", "data": files, } } c.ServeJSON() } // GetMyFiles 获取当前用户的文件列表 func (c *FileController) GetMyFiles() { // 从JWT中间件获取用户信息 userID, ok := c.Ctx.Input.GetData("userId").(int) if !ok || userID <= 0 { c.Data["json"] = map[string]interface{}{ "success": false, "message": "用户未登录或登录已过期", } c.ServeJSON() return } files, err := models.GetFilesByUserID(userID) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败: " + err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取成功", "data": files, } } c.ServeJSON() } // GetFilesPublic 不需要认证的获取文件信息接口 func (c *FileController) GetFilesPublic() { files, err := models.GetAllFiles() if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件列表失败", "error": err.Error(), } } else { c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取文件列表成功", "data": files, } } c.ServeJSON() } // Post 处理文件上传 func (c *FileController) Post() { // 从JWT中间件获取用户信息 userID, ok := c.Ctx.Input.GetData("userId").(int) if !ok || userID <= 0 { c.Data["json"] = map[string]interface{}{ "success": false, "message": "用户未登录或登录已过期", } c.ServeJSON() return } username, _ := c.Ctx.Input.GetData("username").(string) if username == "" { username = "unknown" } // 从JWT中间件获取租户ID tenantID := "default" if tid, ok := c.Ctx.Input.GetData("tenantId").(int); ok && tid > 0 { tenantID = strconv.Itoa(tid) } // 获取上传的文件 file, header, err := c.GetFile("file") if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取上传文件失败: " + err.Error(), } c.ServeJSON() return } defer file.Close() // 获取文件基本信息 originalName := header.Filename fileSize := header.Size fileExt := strings.ToLower(filepath.Ext(originalName)) fileName := strings.TrimSuffix(originalName, fileExt) // 读取文件内容到内存以计算MD5(对于大文件可能需要优化) fileData := make([]byte, fileSize) n, err := file.Read(fileData) if err != nil && err != io.EOF { c.Data["json"] = map[string]interface{}{ "success": false, "message": "读取文件失败: " + err.Error(), } c.ServeJSON() return } if int64(n) != fileSize { c.Data["json"] = map[string]interface{}{ "success": false, "message": "读取文件不完整", } c.ServeJSON() return } // 计算文件MD5值 hash := md5.New() hash.Write(fileData) fileMD5 := hex.EncodeToString(hash.Sum(nil)) // 检查是否已存在相同MD5的文件 existingFile, err := models.GetFileByMD5AndTenant(fileMD5, tenantID) if err == nil && existingFile != nil { // 文件已存在,不保存文件,只创建数据库记录 // 生成日期路径(年/月/日) now := time.Now() // 创建文件信息记录(使用已存在的文件路径) fileInfo := models.FileInfo{ TenantID: tenantID, UserID: userID, FileName: fileName, OriginalName: originalName, FilePath: existingFile.FilePath, // 使用已存在文件的路径 FileURL: existingFile.FileURL, // 使用已存在文件的URL FileSize: fileSize, FileType: getFileTypeByExt(fileExt), FileExt: fileExt, MD5: fileMD5, Category: c.GetString("category"), Status: 1, UploadBy: username, UploadTime: now, } // 如果分类为空,使用默认分类 if fileInfo.Category == "" { fileInfo.Category = "未分类" } // 保存到数据库(只保存记录,不保存文件) id, err := models.AddFile(&fileInfo) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "保存文件信息失败: " + err.Error(), } c.ServeJSON() return } fileInfo.ID = id // 返回成功响应 c.Data["json"] = map[string]interface{}{ "success": true, "message": "文件上传成功(重复文件,使用已有文件)", "data": fileInfo, } c.ServeJSON() return } // 文件不存在,正常上传流程 // 获取分类(可选) category := c.GetString("category") if category == "" { category = "未分类" } // 生成日期路径(年/月/日) now := time.Now() datePath := now.Format("2006/01/02") // 目录改成 server 文件夹目录下的 uploads uploadDir := filepath.Join("uploads", datePath) // 清理路径 uploadDir = filepath.Clean(uploadDir) // 确保目录存在 if err := os.MkdirAll(uploadDir, 0755); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "创建上传目录失败: " + err.Error(), } c.ServeJSON() return } // 生成唯一文件名(时间戳 + 原始文件名) timestamp := now.Format("20060102150405") uniqueFileName := timestamp + "_" + originalName savePath := path.Join(uploadDir, uniqueFileName) // 计算相对路径(用于存储到数据库) relativePath := path.Join("uploads", datePath, uniqueFileName) // 保存文件(将已读取的数据写入文件) if err := os.WriteFile(savePath, fileData, 0644); err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "保存文件失败: " + err.Error(), } c.ServeJSON() return } // 获取文件类型 fileType := getFileTypeByExt(fileExt) // 构造文件URL(相对路径) fileURL := "/" + relativePath // 创建文件信息记录 fileInfo := models.FileInfo{ TenantID: tenantID, UserID: userID, FileName: fileName, OriginalName: originalName, FilePath: relativePath, FileURL: fileURL, FileSize: fileSize, FileType: fileType, FileExt: fileExt, MD5: fileMD5, Category: category, Status: 1, // 设置为正常状态 UploadBy: username, UploadTime: now, } // 保存到数据库 id, err := models.AddFile(&fileInfo) if err != nil { // 如果数据库保存失败,删除已上传的文件 os.Remove(savePath) c.Data["json"] = map[string]interface{}{ "success": false, "message": "保存文件信息失败: " + err.Error(), } c.ServeJSON() return } fileInfo.ID = id // 返回成功响应 c.Data["json"] = map[string]interface{}{ "success": true, "message": "文件上传成功", "data": fileInfo, } c.ServeJSON() } // DownloadFile 下载文件 func (c *FileController) DownloadFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } // 获取文件信息 file, err := models.GetFileById(id) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在", } c.ServeJSON() return } // 获取实际的文件记录(如果文件不存在,通过MD5查找) actualFile, err := getActualFile(file) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件信息失败", } c.ServeJSON() return } // 检查文件是否存在(尝试多个可能的路径) var filePath string possiblePaths := []string{ actualFile.FilePath, // 直接使用相对路径 filepath.Join("server", actualFile.FilePath), // server目录前缀 filepath.Join(".", actualFile.FilePath), // 当前目录 } for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { filePath = path break } } if filePath == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在于服务器", } c.ServeJSON() return } // 设置响应头 c.Ctx.Output.Header("Content-Description", "File Transfer") c.Ctx.Output.Header("Content-Type", "application/octet-stream") c.Ctx.Output.Header("Content-Disposition", "attachment; filename="+url.QueryEscape(actualFile.OriginalName)) c.Ctx.Output.Header("Content-Transfer-Encoding", "binary") c.Ctx.Output.Header("Expires", "0") c.Ctx.Output.Header("Cache-Control", "must-revalidate") c.Ctx.Output.Header("Pragma", "public") // 输出文件 http.ServeFile(c.Ctx.ResponseWriter, c.Ctx.Request, filePath) } // PreviewFile 预览文件 func (c *FileController) PreviewFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } // 获取文件信息 file, err := models.GetFileById(id) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在", } c.ServeJSON() return } // 获取实际的文件记录(如果文件不存在,通过MD5查找) actualFile, err := getActualFile(file) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件信息失败", } c.ServeJSON() return } // 检查文件是否可预览 if !actualFile.CanPreview() { c.Data["json"] = map[string]interface{}{ "success": false, "message": "该文件类型不支持预览", } c.ServeJSON() return } // 检查文件是否存在(尝试多个可能的路径) var filePath string possiblePaths := []string{ actualFile.FilePath, // 直接使用相对路径 filepath.Join("server", actualFile.FilePath), // server目录前缀 filepath.Join(".", actualFile.FilePath), // 当前目录 } for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { filePath = path break } } if filePath == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在于服务器", } c.ServeJSON() return } // 设置正确的 Content-Type contentType := getContentType(actualFile.FileExt) c.Ctx.Output.Header("Content-Type", contentType) c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName)) // 打开文件 fileHandle, err := os.Open(filePath) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "打开文件失败: " + err.Error(), } c.ServeJSON() return } defer fileHandle.Close() // 复制文件内容到响应 io.Copy(c.Ctx.ResponseWriter, fileHandle) } // PublicPreviewFile 公开预览文件(用于 Office Online Viewer,无需认证但仅用于预览) func (c *FileController) PublicPreviewFile() { idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "参数错误", } c.ServeJSON() return } // 获取文件信息 file, err := models.GetFileById(id) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在", } c.ServeJSON() return } // 获取实际的文件记录 actualFile, err := getActualFile(file) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "获取文件信息失败", } c.ServeJSON() return } // 检查文件是否可预览 if !actualFile.CanPreview() { c.Data["json"] = map[string]interface{}{ "success": false, "message": "该文件类型不支持预览", } c.ServeJSON() return } // 检查文件是否存在 var filePath string possiblePaths := []string{ actualFile.FilePath, filepath.Join("server", actualFile.FilePath), filepath.Join(".", actualFile.FilePath), } for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { filePath = path break } } if filePath == "" { c.Data["json"] = map[string]interface{}{ "success": false, "message": "文件不存在于服务器", } c.ServeJSON() return } // 设置正确的 Content-Type contentType := getContentType(actualFile.FileExt) c.Ctx.Output.Header("Content-Type", contentType) c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName)) // 允许跨域访问(Office Online Viewer 需要) c.Ctx.Output.Header("Access-Control-Allow-Origin", "*") // 打开文件 fileHandle, err := os.Open(filePath) if err != nil { c.Data["json"] = map[string]interface{}{ "success": false, "message": "打开文件失败: " + err.Error(), } c.ServeJSON() return } defer fileHandle.Close() // 复制文件内容到响应 io.Copy(c.Ctx.ResponseWriter, fileHandle) } // getFileTypeByExt 根据文件扩展名获取文件类型 func getFileTypeByExt(ext string) string { ext = strings.ToLower(ext) switch ext { case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp": return "image" case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt": return "document" case ".mp4", ".avi", ".mov", ".wmv": return "video" case ".mp3", ".wav", ".flac": return "audio" case ".zip", ".rar", ".7z": return "archive" default: return "other" } } // getContentType 根据文件扩展名获取 Content-Type func getContentType(ext string) string { ext = strings.ToLower(ext) contentTypes := map[string]string{ // 图片 ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".bmp": "image/bmp", ".webp": "image/webp", ".svg": "image/svg+xml", // 文档 ".pdf": "application/pdf", ".txt": "text/plain; charset=utf-8", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", // 视频 ".mp4": "video/mp4", ".webm": "video/webm", // 音频 ".mp3": "audio/mpeg", ".wav": "audio/wav", } if contentType, ok := contentTypes[ext]; ok { return contentType } return "application/octet-stream" } // getActualFile 获取实际的文件记录(如果当前记录的文件不存在,通过MD5查找唯一文件) func getActualFile(file *models.FileInfo) (*models.FileInfo, error) { // 检查当前记录的文件是否存在(尝试多个可能的路径) possiblePaths := []string{ file.FilePath, // 直接使用相对路径 filepath.Join("server", file.FilePath), // server目录前缀 filepath.Join(".", file.FilePath), // 当前目录 } for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { // 文件存在,返回当前记录 return file, nil } } // 文件不存在,通过MD5查找唯一文件 if file.MD5 == "" { return file, nil } actualFile, err := models.GetFileByMD5(file.MD5) if err == nil && actualFile != nil { // 检查找到的文件是否存在 possiblePaths = []string{ actualFile.FilePath, filepath.Join("server", actualFile.FilePath), filepath.Join(".", actualFile.FilePath), } for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { return actualFile, nil } } } // 如果找不到,返回原始记录 return file, nil }