package httpClient import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" "go.uber.org/zap" ) const ( DefaultTimeout time.Duration = 5 // 默认超时时间5s ) type HttpClient struct { reLogin *func() string } type HttpClientOptions struct { Timeout *time.Duration // 单位:s Header *http.Header } // PostBody 包含转换后的 Reader 和推荐的 Content-Type type PostBody struct { Reader io.Reader ContentType string } type ResponseData struct { Code int `json:"code"` } // 封装模块的响应结构数据 type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 // Header maps header keys to values. If the response had multiple // headers with the same key, they may be concatenated, with comma // delimiters. (RFC 7230, section 3.2.2 requires that multiple headers // be semantically equivalent to a comma-delimited sequence.) When // Header values are duplicated by other fields in this struct (e.g., // ContentLength, TransferEncoding, Trailer), the field values are // authoritative. // // Keys in the map are canonicalized (see CanonicalHeaderKey). Header http.Header // Body represents the response body. Body []byte // ContentLength records the length of the associated content. The // value -1 indicates that the length is unknown. Unless Request.Method // is "HEAD", values >= 0 indicate that the given number of bytes may // be read from Body. ContentLength int64 } // var reLogin = func() string { // // empty function // return "" // } func (c *HttpClient) responseInterceptor(response *Response, retry func(*string) (*Response, error)) *Response { // 判断是否回复code 401 // var data ResponseData // if err := json.Unmarshal(response.Body, &data); err != nil { // zap.L().Warn("ResponseData 解析失败", zap.String("Status", response.Status), zap.String("body", string(response.Body)), zap.String("err", err.Error())) // return response // } // if data.Code == 401 { // TODO 调整401判断逻辑,待观察是否有问题 if response.StatusCode == 401 { if c.reLogin == nil { return response } tokenStr := (*c.reLogin)() retryBody, err := retry(&tokenStr) if err != nil { zap.L().Warn("http retry 失败", zap.String("token", tokenStr), zap.String("err", err.Error())) return response } return retryBody } return response } func (c *HttpClient) RegisterLogin(login func() string) { c.reLogin = &login } func (c *HttpClient) HttpGet(url string, params *url.Values, opt *HttpClientOptions) (*Response, error) { // 拼接参数到 URL finalURL := url if params != nil { finalURL = finalURL + "?" + params.Encode() } zap.L().Debug("HttpGet", zap.String("url", finalURL)) // 创建请求 req, err := http.NewRequest( http.MethodGet, finalURL, nil) if err != nil { zap.L().Error("NewRequest failed.", zap.String("err", err.Error())) return nil, err } // 设置 Header if opt != nil && opt.Header != nil { for key, values := range *opt.Header { for _, v := range values { req.Header.Add(key, v) } } } // 设置 Timeout timeout := DefaultTimeout if opt != nil && opt.Timeout != nil { timeout = *opt.Timeout } // 发送请求 - 封装成函数用于重试 var reqFunc = func(token *string) (*Response, error) { client := &http.Client{Timeout: timeout * time.Second} resp, err := client.Do(req) if err != nil { zap.L().Error("Do Send failed.", zap.String("err", err.Error())) return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return &Response{ Status: resp.Status, StatusCode: resp.StatusCode, Header: resp.Header, Body: respBody, ContentLength: resp.ContentLength, }, nil } response, err := reqFunc(nil) if err != nil { return nil, err } // 应答失败,直接返回 if response.StatusCode != 200 { zap.L().Warn("[HttpGet] Response failed", zap.String("Status", response.Status), zap.String("Body", string(response.Body))) return response, fmt.Errorf("[HttpGet] Response failed") } return c.responseInterceptor(response, reqFunc), nil } // toPostBody 将任意 body 转换为 http POST 可用的 io.Reader func toPostBody(body any) (*PostBody, error) { if body == nil { return &PostBody{Reader: nil, ContentType: ""}, nil } switch v := body.(type) { case io.Reader: // 直接传 io.Reader return &PostBody{Reader: v}, nil case string: // 字符串默认 text/plain return &PostBody{Reader: strings.NewReader(v), ContentType: "text/plain"}, nil case []byte: // []byte 默认 application/octet-stream return &PostBody{Reader: bytes.NewReader(v), ContentType: "application/octet-stream"}, nil case url.Values: // 表单类型 application/x-www-form-urlencoded return &PostBody{ Reader: strings.NewReader(v.Encode()), ContentType: "application/x-www-form-urlencoded; charset=UTF-8", }, nil default: // 尝试序列化为 JSON jsonData, err := json.Marshal(v) if err != nil { return nil, errors.New("unsupported body type and failed to marshal to JSON") } return &PostBody{ Reader: bytes.NewReader(jsonData), ContentType: "application/json", }, nil } } func (c *HttpClient) HttpPost(url string, body any, opt *HttpClientOptions) (*Response, error) { // 转换body数据 payload, err := toPostBody(body) if err != nil { zap.L().Error("to PostBody failed.", zap.String("err", err.Error())) return nil, err } zap.L().Debug("HttpPost", zap.String("url", url)) // 创建请求 req, err := http.NewRequest( http.MethodPost, url, payload.Reader) if err != nil { zap.L().Error("NewRequest failed.", zap.String("err", err.Error())) return nil, err } // 设置 Content-Type req.Header.Set("Content-Type", payload.ContentType) // 设置 Header if opt != nil && opt.Header != nil { for key, values := range *opt.Header { for _, v := range values { req.Header.Add(key, v) } } } // 设置 Timeout timeout := DefaultTimeout if opt != nil && opt.Timeout != nil { timeout = *opt.Timeout } // 发送请求 - 封装成函数用于重试 var reqFunc = func(token *string) (*Response, error) { client := &http.Client{Timeout: timeout * time.Second} resp, err := client.Do(req) if err != nil { zap.L().Error("Do Send failed.", zap.String("err", err.Error())) return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return &Response{ Status: resp.Status, StatusCode: resp.StatusCode, Header: resp.Header, Body: respBody, ContentLength: resp.ContentLength, }, nil } response, err := reqFunc(nil) if err != nil { return nil, err } // 应答失败,直接返回 if response.StatusCode != 200 { zap.L().Warn("[HttpPost] Response failed", zap.String("Status", response.Status), zap.String("Body", string(response.Body))) return response, fmt.Errorf("[HttpPost] Response failed") } return c.responseInterceptor(response, reqFunc), nil }