291 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			291 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package httphelper
 | ||
| 
 | ||
| import (
 | ||
| 	"bytes"
 | ||
| 	"compress/gzip"
 | ||
| 	"encoding/json"
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"net/http"
 | ||
| 	"time"
 | ||
| )
 | ||
| 
 | ||
| // HTTPClient 定义一个通用的 HTTP 客户端结构
 | ||
| type HTTPClient struct {
 | ||
| 	Client  *http.Client      // 底层的 http.Client 实例
 | ||
| 	BaseURL string            // 基础 URL,所有请求将基于此 URL
 | ||
| 	Headers map[string]string // 默认请求头
 | ||
| }
 | ||
| 
 | ||
| // NewHTTPClient 创建一个新的 HTTPClient 实例
 | ||
| // timeout: 请求超时时间,例如 10 * time.Second
 | ||
| // baseURL: 基础 URL,例如 "https://api.example.com"
 | ||
| // defaultHeaders: 默认请求头,例如 {"Content-Type": "application/json"}
 | ||
| func NewHTTPClient(timeout time.Duration, baseURL string, defaultHeaders map[string]string) *HTTPClient {
 | ||
| 	return &HTTPClient{
 | ||
| 		Client: &http.Client{
 | ||
| 			Timeout: timeout,
 | ||
| 		},
 | ||
| 		BaseURL: baseURL,
 | ||
| 		Headers: defaultHeaders,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // applyHeaders 为请求应用默认和自定义请求头
 | ||
| func (c *HTTPClient) applyHeaders(req *http.Request, customHeaders map[string]string) {
 | ||
| 	// 应用默认请求头
 | ||
| 	for key, value := range c.Headers {
 | ||
| 		req.Header.Set(key, value)
 | ||
| 	}
 | ||
| 	// 应用自定义请求头(覆盖默认请求头)
 | ||
| 	for key, value := range customHeaders {
 | ||
| 		req.Header.Set(key, value)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // doRequest 执行实际的 HTTP 请求
 | ||
| // method: HTTP 方法 (GET, POST, PUT, DELETE等)
 | ||
| // path: 请求路径,将与 BaseURL 拼接
 | ||
| // requestBody: 请求体数据,如果为 GET/DELETE 请求则为 nil
 | ||
| // customHeaders: 自定义请求头,将覆盖默认请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型),如果为 nil 则表示不需要 JSON 解码
 | ||
| // rawResponse: 用于存储原始响应体字节切片(*[]byte),如果为 nil 则表示不需要原始响应
 | ||
| func (c *HTTPClient) DoRequest(
 | ||
| 	method, path string,
 | ||
| 	requestBody interface{},
 | ||
| 	customHeaders map[string]string,
 | ||
| 	responseData interface{},
 | ||
| 	rawResponse *[]byte, // 新增参数:指向字节切片的指针,用于存储原始响应
 | ||
| ) (int, error) {
 | ||
| 	// 拼接完整的 URL
 | ||
| 	url := c.BaseURL + path
 | ||
| 
 | ||
| 	var reqBodyReader io.Reader
 | ||
| 	if requestBody != nil {
 | ||
| 		// 将请求体编码为 JSON
 | ||
| 		jsonBody, err := json.Marshal(requestBody)
 | ||
| 		if err != nil {
 | ||
| 			return -1, fmt.Errorf("json marshal request body failed: %w", err)
 | ||
| 		}
 | ||
| 		reqBodyReader = bytes.NewBuffer(jsonBody)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建新的 HTTP 请求
 | ||
| 	req, err := http.NewRequest(method, url, reqBodyReader)
 | ||
| 	if err != nil {
 | ||
| 		return -1, fmt.Errorf("create http request failed: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 应用请求头
 | ||
| 	c.applyHeaders(req, customHeaders)
 | ||
| 
 | ||
| 	// 默认添加 gzip 接收头
 | ||
| 	req.Header.Set("Accept-Encoding", "gzip")
 | ||
| 
 | ||
| 	// 发送请求
 | ||
| 	resp, err := c.Client.Do(req)
 | ||
| 	if err != nil {
 | ||
| 		return -1, fmt.Errorf("send http request failed: %w", err)
 | ||
| 	}
 | ||
| 	defer resp.Body.Close()
 | ||
| 
 | ||
| 	// 检查 HTTP 状态码
 | ||
| 	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
 | ||
| 		bodyBytes, _ := io.ReadAll(resp.Body) // 读取错误响应体
 | ||
| 		return resp.StatusCode, fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes))
 | ||
| 	}
 | ||
| 
 | ||
| 	// 解码响应(支持 gzip)
 | ||
| 	var reader io.Reader = resp.Body
 | ||
| 	if resp.Header.Get("Content-Encoding") == "gzip" {
 | ||
| 		gzipReader, err := gzip.NewReader(resp.Body)
 | ||
| 		if err != nil {
 | ||
| 			return http.StatusOK, fmt.Errorf("create gzip reader failed: %w", err)
 | ||
| 		}
 | ||
| 		defer gzipReader.Close()
 | ||
| 		reader = gzipReader
 | ||
| 	}
 | ||
| 
 | ||
| 	// 首先读取整个响应体,然后决定如何处理
 | ||
| 	bodyBytes, err := io.ReadAll(reader)
 | ||
| 	if err != nil {
 | ||
| 		return http.StatusOK, fmt.Errorf("read response body failed: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果提供了原始响应目标,则填充它
 | ||
| 	if rawResponse != nil {
 | ||
| 		*rawResponse = bodyBytes
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果提供了 JSON 解码目标,则尝试解码
 | ||
| 	if responseData != nil {
 | ||
| 		err = json.Unmarshal(bodyBytes, responseData) // 直接对字节切片使用 Unmarshal
 | ||
| 		if err != nil {
 | ||
| 			return http.StatusOK, fmt.Errorf("json decode response body failed: %w", err)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return http.StatusOK, nil
 | ||
| }
 | ||
| 
 | ||
| // Get 发送 GET 请求
 | ||
| // path: 请求路径
 | ||
| // customHeaders: 自定义请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型)
 | ||
| func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseData interface{}) (int, error) {
 | ||
| 	return c.DoRequest(http.MethodGet, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil
 | ||
| }
 | ||
| 
 | ||
| // Post 发送 POST 请求
 | ||
| // path: 请求路径
 | ||
| // requestBody: 请求体数据
 | ||
| // customHeaders: 自定义请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型)
 | ||
| func (c *HTTPClient) Post(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
 | ||
| 	return c.DoRequest(http.MethodPost, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
 | ||
| }
 | ||
| 
 | ||
| // PostWithContentType 发送 POST 请求,支持自定义 Content-Type(如 application/json 或 multipart/form-data)
 | ||
| // contentType: 请求体类型(如 "application/json", "multipart/form-data")
 | ||
| // requestBody: 请求体(可以是结构体、map、或 multipart/form 格式)
 | ||
| // 注意:如果是 multipart/form-data,请确保 requestBody 是 io.Reader 和 contentType 是完整的包含 boundary 的值。
 | ||
| func (c *HTTPClient) PostWithContentType(path string, requestBody interface{}, contentType string, customHeaders map[string]string, responseData interface{}) error {
 | ||
| 	// 拼接 URL
 | ||
| 	url := c.BaseURL + path
 | ||
| 
 | ||
| 	var reqBodyReader io.Reader
 | ||
| 	switch body := requestBody.(type) {
 | ||
| 	case io.Reader:
 | ||
| 		reqBodyReader = body
 | ||
| 	default:
 | ||
| 		// 默认处理为 JSON
 | ||
| 		jsonBody, err := json.Marshal(body)
 | ||
| 		if err != nil {
 | ||
| 			return fmt.Errorf("json marshal request body failed: %w", err)
 | ||
| 		}
 | ||
| 		reqBodyReader = bytes.NewBuffer(jsonBody)
 | ||
| 		if contentType == "" {
 | ||
| 			contentType = "application/json"
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建请求
 | ||
| 	req, err := http.NewRequest(http.MethodPost, url, reqBodyReader)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("create http request failed: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 设置 Content-Type
 | ||
| 	if contentType != "" {
 | ||
| 		req.Header.Set("Content-Type", contentType)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 应用其他头部
 | ||
| 	c.applyHeaders(req, customHeaders)
 | ||
| 
 | ||
| 	// gzip 支持
 | ||
| 	req.Header.Set("Accept-Encoding", "gzip")
 | ||
| 
 | ||
| 	// 发出请求
 | ||
| 	resp, err := c.Client.Do(req)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("send http request failed: %w", err)
 | ||
| 	}
 | ||
| 	defer resp.Body.Close()
 | ||
| 
 | ||
| 	// 检查状态码
 | ||
| 	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
 | ||
| 		bodyBytes, _ := io.ReadAll(resp.Body)
 | ||
| 		return fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes))
 | ||
| 	}
 | ||
| 
 | ||
| 	// 读取响应
 | ||
| 	var reader io.Reader = resp.Body
 | ||
| 	if resp.Header.Get("Content-Encoding") == "gzip" {
 | ||
| 		gzipReader, err := gzip.NewReader(resp.Body)
 | ||
| 		if err != nil {
 | ||
| 			return fmt.Errorf("create gzip reader failed: %w", err)
 | ||
| 		}
 | ||
| 		defer gzipReader.Close()
 | ||
| 		reader = gzipReader
 | ||
| 	}
 | ||
| 
 | ||
| 	// 首先读取整个响应体,然后决定如何处理
 | ||
| 	bodyBytes, err := io.ReadAll(reader)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("read response body failed: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果提供了 JSON 解码目标,则尝试解码
 | ||
| 	if responseData != nil {
 | ||
| 		err = json.Unmarshal(bodyBytes, responseData)
 | ||
| 		if err != nil {
 | ||
| 			return fmt.Errorf("json decode response body failed: %w", err)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| // Put 发送 PUT 请求
 | ||
| // path: 请求路径
 | ||
| // requestBody: 请求体数据
 | ||
| // customHeaders: 自定义请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型)
 | ||
| // returns 状态码 和 错误信息
 | ||
| func (c *HTTPClient) Put(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
 | ||
| 	return c.DoRequest(http.MethodPut, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
 | ||
| }
 | ||
| 
 | ||
| // Delete 发送 DELETE 请求
 | ||
| // path: 请求路径
 | ||
| // customHeaders: 自定义请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型)
 | ||
| // returns 状态码 和 错误信息
 | ||
| func (c *HTTPClient) Delete(path string, customHeaders map[string]string, responseData interface{}) (int, error) {
 | ||
| 	// DELETE 请求通常没有请求体,但某些 RESTful API 可能支持
 | ||
| 	return c.DoRequest(http.MethodDelete, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil
 | ||
| }
 | ||
| 
 | ||
| // Patch 发送 PATCH 请求
 | ||
| // path: 请求路径
 | ||
| // requestBody: 请求体数据
 | ||
| // customHeaders: 自定义请求头
 | ||
| // responseData: 用于存储响应数据的目标结构体(指针类型)
 | ||
| // returns 状态码 和 错误信息
 | ||
| func (c *HTTPClient) Patch(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
 | ||
| 	return c.DoRequest(http.MethodPatch, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
 | ||
| }
 | ||
| 
 | ||
| // GetRaw 发送 GET 请求并返回原始响应体
 | ||
| // path: 请求路径
 | ||
| // customHeaders: 自定义请求头
 | ||
| // 返回值: 原始响应体字节切片或错误
 | ||
| // 状态码
 | ||
| // 错误信息
 | ||
| func (c *HTTPClient) GetRaw(path string, customHeaders map[string]string) ([]byte, int, error) {
 | ||
| 	var raw []byte
 | ||
| 	// responseData 传递 nil,rawResponse 传递 &raw
 | ||
| 	statusCode, err := c.DoRequest(http.MethodGet, path, nil, customHeaders, nil, &raw)
 | ||
| 	if err != nil {
 | ||
| 		return nil, statusCode, err
 | ||
| 	}
 | ||
| 	return raw, statusCode, nil
 | ||
| }
 | ||
| 
 | ||
| // PostRaw 发送 POST 请求并返回原始响应体
 | ||
| // path: 请求路径
 | ||
| // customHeaders: 自定义请求头
 | ||
| // 返回值: 原始响应体字节切片或错误
 | ||
| // 状态码
 | ||
| // 错误信息
 | ||
| func (c *HTTPClient) PostRaw(path string, customHeaders map[string]string) ([]byte, int, error) {
 | ||
| 	var raw []byte
 | ||
| 	// responseData 传递 nil,rawResponse 传递 &raw
 | ||
| 	statusCode, err := c.DoRequest(http.MethodPost, path, nil, customHeaders, nil, &raw)
 | ||
| 	if err != nil {
 | ||
| 		return nil, statusCode, err
 | ||
| 	}
 | ||
| 	return raw, statusCode, nil
 | ||
| }
 |