package upstream import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "strings" "time" "videoweb/app/appg" "videoweb/pub/common/useragent" "github.com/gin-gonic/gin" ) const maxBody = 4 << 20 // 4MB var client = &http.Client{Timeout: 20 * time.Second} // BaseURL 上游 API 根地址,来自 conf/app.yaml 的 serverAddr。 func BaseURL() string { return strings.TrimRight(strings.TrimSpace(appg.Conf.ServerAddr), "/") } // GetJSON 带上 X-Real-IP / User-Agent / X-User-Agent,并从 cookie 透传 Authorization。 func GetJSON(c *gin.Context, path string, out any) error { _, err := doJSON(c, http.MethodGet, path, nil, out) return err } // BinaryResult 上游非 JSON 响应(如 CSV 导出)。 type BinaryResult struct { StatusCode int ContentType string ContentDisposition string Body []byte } const maxBinaryBody = 16 << 20 // 16MB // GetBinary 拉取上游二进制/文本文件响应(不强制 JSON Accept)。 func GetBinary(c *gin.Context, path string) (*BinaryResult, error) { base := BaseURL() if base == "" { return nil, fmt.Errorf("upstream serverAddr is empty") } req, err := http.NewRequest(http.MethodGet, base+path, nil) if err != nil { return nil, err } applyCommonHeaders(c, req) req.Header.Set("Accept", "text/csv,application/octet-stream,*/*") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBinaryBody)) if err != nil { return nil, err } return &BinaryResult{ StatusCode: resp.StatusCode, ContentType: resp.Header.Get("Content-Type"), ContentDisposition: resp.Header.Get("Content-Disposition"), Body: raw, }, nil } // GetPublicJSON 请求无需登录的公共数据,不透传客户端 Authorization。 // 公共配置接口会在收到过期 token 时返回会话失效,因此必须显式与登录态隔离。 func GetPublicJSON(c *gin.Context, path string, out any) error { _, err := doJSONWithAuth(c, http.MethodGet, path, nil, out, false) return err } // PostJSON 向上游 POST JSON,解码到 out;返回 HTTP 状态码。 // 注意:上游业务错误常见为 HTTP 200 + code!=200,此时仍会成功解码。 func PostJSON(c *gin.Context, path string, body any, out any) (int, error) { return doJSON(c, http.MethodPost, path, body, out) } // MultipartFile 上游 multipart 文件字段。 type MultipartFile struct { FieldName string FileName string Reader io.Reader } // PostMultipart 向上游 POST multipart/form-data。 func PostMultipart(c *gin.Context, path string, fields map[string]string, file *MultipartFile, out any) (int, error) { base := BaseURL() if base == "" { return 0, fmt.Errorf("upstream serverAddr is empty") } var buf bytes.Buffer w := multipart.NewWriter(&buf) for k, v := range fields { if err := w.WriteField(k, v); err != nil { return 0, err } } if file != nil && file.Reader != nil { part, err := w.CreateFormFile(file.FieldName, file.FileName) if err != nil { return 0, err } if _, err := io.Copy(part, io.LimitReader(file.Reader, maxBody)); err != nil { return 0, err } } if err := w.Close(); err != nil { return 0, err } req, err := http.NewRequest(http.MethodPost, base+path, &buf) if err != nil { return 0, err } applyCommonHeaders(c, req) req.Header.Set("Content-Type", w.FormDataContentType()) resp, err := client.Do(req) if err != nil { return 0, err } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBody)) if err != nil { return resp.StatusCode, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return resp.StatusCode, fmt.Errorf("upstream %s status %d: %s", path, resp.StatusCode, truncate(string(raw), 200)) } if out != nil { if err := json.Unmarshal(raw, out); err != nil { return resp.StatusCode, fmt.Errorf("decode %s: %w", path, err) } } return resp.StatusCode, nil } func doJSON(c *gin.Context, method, path string, body any, out any) (int, error) { return doJSONWithAuth(c, method, path, body, out, true) } func doJSONWithAuth(c *gin.Context, method, path string, body any, out any, forwardAuth bool) (int, error) { base := BaseURL() if base == "" { return 0, fmt.Errorf("upstream serverAddr is empty") } fullURL := base + path var bodyReader io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return 0, err } bodyReader = strings.NewReader(string(b)) } req, err := http.NewRequest(method, fullURL, bodyReader) if err != nil { return 0, err } applyCommonHeaders(c, req) if !forwardAuth { req.Header.Del("Authorization") } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := client.Do(req) if err != nil { return 0, err } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBody)) if err != nil { return resp.StatusCode, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return resp.StatusCode, fmt.Errorf("upstream %s status %d: %s", path, resp.StatusCode, truncate(string(raw), 200)) } if out != nil { if err := json.Unmarshal(raw, out); err != nil { return resp.StatusCode, fmt.Errorf("decode %s: %w", path, err) } } return resp.StatusCode, nil } func applyCommonHeaders(c *gin.Context, req *http.Request) { headers := useragent.BuildUpstreamHeader(c, useragent.DefaultVer) for k, v := range headers { req.Header.Set(k, v) } req.Header.Set("Accept", "application/json") if token := useragent.TokenFromRequest(c.Request); token != "" { req.Header.Set("Authorization", token) } else if c != nil && c.Request != nil { if auth := strings.TrimSpace(c.GetHeader("Authorization")); auth != "" { req.Header.Set("Authorization", auth) } } } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }