Files
erpgo/internal/huohanhan/client.go
T

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]
}