diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 773f42c..9df6393 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -4,14 +4,12 @@ import ( "compress/gzip" "embed" "fmt" - "io" "io/fs" "log" "net/http" "net/url" "os" "path/filepath" - "strconv" "strings" "verification-platform-backend/internal/config" "verification-platform-backend/internal/database" @@ -388,6 +386,12 @@ func isCompressibleContentType(contentType string) bool { func gzipMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + // 静态文件下载路径跳过 gzip 处理,避免影响下载性能 + if strings.HasPrefix(c.Request.URL.Path, "/uploads/") { + c.Next() + return + } + if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") { c.Next() return @@ -443,11 +447,7 @@ func (w *gzipResponseWriter) WriteString(s string) (int, error) { return w.ResponseWriter.WriteString(s) } -// 32KB 缓冲区用于文件传输 -// 1MB 缓冲区用于高速文件传输,适合 G 口带宽和大文件 -const fileBufferSize = 1024 * 1024 - -// handleOptimizedStaticFile 优化的静态文件处理,支持 Range 请求和断点续传 +// handleOptimizedStaticFile 优化的静态文件处理,使用 http.ServeFile 实现最高性能 func handleOptimizedStaticFile(c *gin.Context) { // 获取请求的文件路径 filepathParam := c.Param("filepath") @@ -467,170 +467,13 @@ func handleOptimizedStaticFile(c *gin.Context) { return } - // 打开文件 - file, err := os.Open(cleanPath) - if err != nil { - if os.IsNotExist(err) { - c.Status(http.StatusNotFound) - } else { - c.Status(http.StatusInternalServerError) - } - return - } - defer file.Close() - - // 获取文件信息 - fileInfo, err := file.Stat() - if err != nil { - c.Status(http.StatusInternalServerError) - return - } - - // 如果是目录,返回 404 - if fileInfo.IsDir() { - c.Status(http.StatusNotFound) - return - } - - // 获取文件大小 - fileSize := fileInfo.Size() - - // 设置响应头 - c.Header("Accept-Ranges", "bytes") + // 设置缓存头 c.Header("Cache-Control", "public, max-age=31536000, immutable") - // 获取 MIME 类型 - ext := strings.ToLower(filepath.Ext(cleanPath)) - if mimeType, ok := mimeTypes[ext]; ok { - c.Header("Content-Type", mimeType) - } else { - c.Header("Content-Type", "application/octet-stream") - } - - // 处理 Range 请求(断点续传) - rangeHeader := c.GetHeader("Range") - if rangeHeader != "" { - // 解析 Range 头 - // 格式: bytes=start-end 或 bytes=start- - rangeParts := strings.TrimPrefix(rangeHeader, "bytes=") - if rangeParts != rangeHeader { - // 解析范围 - var start, end int64 - if strings.Contains(rangeParts, "-") { - parts := strings.Split(rangeParts, "-") - if len(parts) == 2 { - if parts[0] != "" { - start, _ = strconv.ParseInt(parts[0], 10, 64) - } - if parts[1] != "" { - end, _ = strconv.ParseInt(parts[1], 10, 64) - } else { - end = fileSize - 1 - } - } - } - - // 验证范围 - if start >= fileSize || start < 0 || end < start || end >= fileSize { - c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) - c.Status(http.StatusRequestedRangeNotSatisfiable) - return - } - - // 设置部分内容响应 - c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) - c.Header("Content-Length", strconv.FormatInt(end-start+1, 10)) - c.Status(http.StatusPartialContent) - - // 定位到起始位置 - file.Seek(start, io.SeekStart) - - // 使用缓冲区传输 - buf := make([]byte, fileBufferSize) - remaining := end - start + 1 - for remaining > 0 { - toRead := int64(fileBufferSize) - if remaining < toRead { - toRead = remaining - } - n, err := file.Read(buf[:toRead]) - if err != nil && err != io.EOF { - break - } - if n == 0 { - break - } - c.Writer.Write(buf[:n]) - remaining -= int64(n) - } - return - } - } - - // 普通请求,发送整个文件 - c.Header("Content-Length", strconv.FormatInt(fileSize, 10)) - c.Status(http.StatusOK) - - // 使用缓冲区高效传输 - buf := make([]byte, fileBufferSize) - _, err = io.CopyBuffer(c.Writer, file, buf) - if err != nil { - log.Printf("Error sending file %s: %v", cleanPath, err) - } -} - -// mimeTypes 常见 MIME 类型映射 -var mimeTypes = map[string]string{ - ".zip": "application/zip", - ".exe": "application/vnd.microsoft.windows.executable", - ".dll": "application/vnd.microsoft.windows.dll", - ".apk": "application/vnd.android.package-archive", - ".ipa": "application/vnd.apple.ipa", - ".dmg": "application/vnd.apple.dmg", - ".pdf": "application/pdf", - ".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", - ".mp3": "audio/mpeg", - ".wav": "audio/wav", - ".ogg": "audio/ogg", - ".flac": "audio/flac", - ".avi": "video/x-msvideo", - ".mkv": "video/x-matroska", - ".mov": "video/quicktime", - ".wmv": "video/x-ms-wmv", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".bmp": "image/bmp", - ".webp": "image/webp", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".html": "text/html; charset=utf-8", - ".htm": "text/html; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".mjs": "application/javascript; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".xml": "application/xml; charset=utf-8", - ".txt": "text/plain; charset=utf-8", - ".md": "text/markdown; charset=utf-8", - ".log": "text/plain; charset=utf-8", - ".csv": "text/csv; charset=utf-8", - ".tar": "application/x-tar", - ".gz": "application/gzip", - ".tgz": "application/gzip", - ".rar": "application/vnd.rar", - ".7z": "application/x-7z-compressed", - ".iso": "application/x-iso9660-image", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".eot": "application/vnd.ms-fontobject", - ".otf": "font/otf", + // 使用 http.ServeFile,它已经: + // 1. 支持 Range 请求(断点续传) + // 2. 使用 sendfile 系统调用(零拷贝) + // 3. 自动处理 MIME 类型 + // 4. 高效处理大文件 + http.ServeFile(c.Writer, c.Request, cleanPath) }