134 lines
3.9 KiB
Go
134 lines
3.9 KiB
Go
package huohanhan
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"go-admin/internal/config"
|
|
"go-admin/internal/logx"
|
|
)
|
|
|
|
var authFailureCodes = map[string]bool{
|
|
"authentication_required": true,
|
|
"invalid_token": true,
|
|
"invalid_token_expired": true,
|
|
}
|
|
|
|
// Client 是货憨憨业务接口的统一 HTTP 客户端。
|
|
//
|
|
// Request 会自动添加 Authorization 和登录 cookies。只有服务端明确表示
|
|
// 认证失效时才重新登录并重放一次;超时、HTTP 500 和普通业务错误不会重试。
|
|
// 请求体使用 []byte,是为了让认证失败后的唯一一次重放不依赖可回卷的 Reader。
|
|
type Client struct {
|
|
baseURL *url.URL
|
|
auth *AuthManager
|
|
httpClient *http.Client
|
|
log *logx.Logger
|
|
}
|
|
|
|
// NewClient 创建业务请求客户端。
|
|
func NewClient(cfg config.HuohanhanConfig, auth *AuthManager, logger *logx.Logger, httpClient *http.Client) (*Client, error) {
|
|
baseURL, err := url.Parse(strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/"))
|
|
if err != nil || baseURL.Scheme == "" || baseURL.Hostname() == "" {
|
|
return nil, fmt.Errorf("货憨憨网址不正确")
|
|
}
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("货憨憨认证管理器不能为空")
|
|
}
|
|
if httpClient == nil {
|
|
httpClient = auth.httpClient
|
|
}
|
|
if logger == nil {
|
|
logger = logx.New(1000)
|
|
}
|
|
return &Client{baseURL: baseURL, auth: auth, httpClient: httpClient, log: logger}, nil
|
|
}
|
|
|
|
// Request 请求一个相对于 /api/ 的货憨憨接口。
|
|
//
|
|
// 返回的 response 由调用方关闭。非 2xx 状态会返回中文错误;认证失败
|
|
// 的 response 在内部关闭后重试,不会泄漏给调用方。
|
|
func (c *Client) Request(ctx context.Context, method, path string, body []byte, contentType string) (*http.Response, error) {
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
state, err := c.auth.GetValidAuth(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, payload, err := c.do(ctx, method, path, body, contentType, state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isAuthFailure(resp.StatusCode, payload) {
|
|
resp.Body.Close()
|
|
if attempt == 1 {
|
|
return nil, fmt.Errorf("重新登录后认证仍然失效")
|
|
}
|
|
c.log.Warn("货憨憨认证已失效,正在重新登录后重试一次")
|
|
if err := c.auth.Invalidate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := c.auth.ForceLogin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
|
|
resp.Body.Close()
|
|
resp.Body = io.NopCloser(bytes.NewReader(payload))
|
|
resp.ContentLength = int64(len(payload))
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("货憨憨接口请求失败:HTTP %d", resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|
|
return nil, fmt.Errorf("货憨憨接口请求失败")
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, body []byte, contentType string, state AuthState) (*http.Response, []byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, apiURL(c.baseURL, path), bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("创建货憨憨接口请求失败:%w", err)
|
|
}
|
|
applyCommonHeaders(req)
|
|
if contentType != "" {
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
req.Header.Set("Authorization", state.AuthorizationValue())
|
|
for name, value := range state.Cookies {
|
|
req.AddCookie(&http.Cookie{Name: name, Value: value})
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("请求货憨憨接口失败:%w", err)
|
|
}
|
|
payload, err := readBody(resp.Body)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return nil, nil, fmt.Errorf("读取货憨憨接口响应失败:%w", err)
|
|
}
|
|
return resp, payload, nil
|
|
}
|
|
|
|
func isAuthFailure(statusCode int, body []byte) bool {
|
|
if statusCode == http.StatusUnauthorized {
|
|
return true
|
|
}
|
|
var payload struct {
|
|
Code string `json:"code"`
|
|
}
|
|
if json.Unmarshal(body, &payload) != nil {
|
|
return false
|
|
}
|
|
return authFailureCodes[payload.Code]
|
|
}
|
|
|