Files
谢宇宁andClaude Opus 5.5 3639d2184a Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:47:17 +07:00

182 lines
4.7 KiB
Go

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
}
// 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] + "..."
}