From 6c191d1b0da46b7115530a02169c111b691e77d0 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 12 Jun 2026 23:08:53 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD=E6=80=A7=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=AD=E7=82=B9=E7=BB=AD=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 Gin 默认 Static 为自定义处理器 - 支持 Range 请求头,实现断点续传 - 使用 1MB 大缓冲区,减少系统调用次数 - 添加 Accept-Ranges 和精确 Content-Length 响应头 - 设置正确的 MIME 类型映射 Co-Authored-By: Claude --- backend/cmd/main.go | 199 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index cd42b88..773f42c 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -4,10 +4,14 @@ 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" @@ -238,7 +242,8 @@ func startServer() { c.Next() }) - r.Static("/uploads", "uploads") + // 优化的静态文件服务,支持 Range 请求和断点续传 + r.GET("/uploads/*filepath", handleOptimizedStaticFile) router.SetupRoutes(r) setupEmbeddedFrontend(r) @@ -437,3 +442,195 @@ func (w *gzipResponseWriter) WriteString(s string) (int, error) { } return w.ResponseWriter.WriteString(s) } + +// 32KB 缓冲区用于文件传输 +// 1MB 缓冲区用于高速文件传输,适合 G 口带宽和大文件 +const fileBufferSize = 1024 * 1024 + +// handleOptimizedStaticFile 优化的静态文件处理,支持 Range 请求和断点续传 +func handleOptimizedStaticFile(c *gin.Context) { + // 获取请求的文件路径 + filepathParam := c.Param("filepath") + if filepathParam == "" { + c.Status(http.StatusBadRequest) + return + } + + // 安全处理路径,防止目录遍历攻击 + filepathParam = strings.TrimPrefix(filepathParam, "/") + fullPath := filepath.Join("uploads", filepathParam) + + // 清理路径,防止 .. 攻击 + cleanPath := filepath.Clean(fullPath) + if !strings.HasPrefix(cleanPath, "uploads"+string(os.PathSeparator)) && cleanPath != "uploads" { + c.Status(http.StatusForbidden) + 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", +}