// Package gateway 提供 Gateway API 的统一客户端封装 // 实现 AES-128-ECB 加密 + MD5 签名认证机制,支持请求日志和网络级错误重试 package gateway import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/bytedance/sonic" "go.uber.org/zap" ) const ( defaultTimeout = 30 * time.Second maxIdleConns = 100 maxIdleConnsPerHost = 10 idleConnTimeout = 90 * time.Second contentTypeJSON = "application/json;charset=utf-8" gatewaySuccessCode = 200 defaultMaxRetries = 2 retryBaseDelay = 100 * time.Millisecond ) // Client 是 Gateway API 的 HTTP 客户端 type Client struct { baseURL string appID string appSecret string httpClient *http.Client timeout time.Duration logger *zap.Logger maxRetries int } // requestWrapper 用于将请求参数包装为 Gateway 的 {"params": ...} 格式 type requestWrapper struct { Params interface{} `json:"params"` } // NewClient 创建 Gateway 客户端实例 // baseURL: Gateway 服务基础地址 // appID: 应用 ID // appSecret: 应用密钥(用于加密和签名) // logger: Zap 日志记录器 func NewClient(baseURL, appID, appSecret string, logger *zap.Logger) *Client { return &Client{ baseURL: baseURL, appID: appID, appSecret: appSecret, httpClient: &http.Client{ Transport: &http.Transport{ MaxIdleConns: maxIdleConns, MaxIdleConnsPerHost: maxIdleConnsPerHost, IdleConnTimeout: idleConnTimeout, }, }, timeout: defaultTimeout, logger: logger, maxRetries: defaultMaxRetries, } } // WithTimeout 设置请求超时时间(支持链式调用) func (c *Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return c } // WithRetry 设置最大重试次数(支持链式调用) // maxRetries=0 表示不重试,maxRetries=2 表示最多重试 2 次(共 3 次尝试) func (c *Client) WithRetry(maxRetries int) *Client { c.maxRetries = maxRetries return c } // doRequest 执行 Gateway API 请求的统一方法 // 流程:包装参数 → 序列化 → 加密 → 签名 → HTTP POST(带重试)→ 解析响应 → 检查业务状态码 // params: 请求参数结构体,内部自动包装为 {"params": } 格式 func (c *Client) doRequest(ctx context.Context, path string, params interface{}) (json.RawMessage, error) { startTime := time.Now() // 将参数包装为 {"params": ...} 格式后序列化 wrapper := requestWrapper{Params: params} dataBytes, err := sonic.Marshal(wrapper) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化业务数据失败") } // 加密业务数据(加密结果不变,可在重试间复用) encryptedData, err := aesEncrypt(dataBytes, c.appSecret) if err != nil { return nil, err } // 带重试的 HTTP 请求 var lastErr error for attempt := 0; attempt <= c.maxRetries; attempt++ { if attempt > 0 { // 检查用户 Context 是否已取消 if ctx.Err() != nil { break } // 指数退避等待:100ms → 200ms → 300ms(封顶 3 倍基础延迟) delay := retryBaseDelay * time.Duration(1< retryBaseDelay*3 { delay = retryBaseDelay * 3 } c.logger.Warn("Gateway 请求重试", zap.String("path", path), zap.Int("attempt", attempt+1), zap.Duration("delay", delay), ) time.Sleep(delay) } result, retryable, err := c.executeHTTPRequest(ctx, path, encryptedData) if err != nil { lastErr = err // 仅对网络级错误重试 if retryable && ctx.Err() == nil { continue } break } // 成功 duration := time.Since(startTime) c.logger.Debug("Gateway 请求成功", zap.String("path", path), zap.Duration("duration", duration), ) return result, nil } // 所有尝试都失败 duration := time.Since(startTime) c.logger.Error("Gateway 请求失败", zap.String("path", path), zap.Duration("duration", duration), zap.Error(lastErr), ) return nil, lastErr } // executeHTTPRequest 执行单次 HTTP 请求(无重试逻辑) // 返回值:响应数据、是否可重试、错误 func (c *Client) executeHTTPRequest(ctx context.Context, path string, encryptedData string) (json.RawMessage, bool, error) { // 每次重试使用新的时间戳和签名 timestamp := time.Now().Unix() sign := generateSign(c.appID, encryptedData, timestamp, c.appSecret) reqBody := map[string]interface{}{ "appId": c.appID, "data": encryptedData, "sign": sign, "timestamp": timestamp, } reqBodyBytes, err := sonic.Marshal(reqBody) if err != nil { return nil, false, errors.Wrap(errors.CodeInternalError, err, "序列化请求体失败") } reqCtx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.baseURL+path, bytes.NewReader(reqBodyBytes)) if err != nil { return nil, false, errors.Wrap(errors.CodeGatewayError, err, "创建 HTTP 请求失败") } req.Header.Set("Content-Type", contentTypeJSON) resp, err := c.httpClient.Do(req) if err != nil { // 用户 Context 已取消 — 不可重试 if ctx.Err() != nil { return nil, false, errors.Wrap(errors.CodeGatewayError, ctx.Err(), "请求被取消") } // Client 超时 — 可重试 if reqCtx.Err() == context.DeadlineExceeded { return nil, true, errors.Wrap(errors.CodeGatewayTimeout, err, "Gateway 请求超时") } // 其他网络错误(连接失败、DNS 解析等)— 可重试 return nil, true, errors.Wrap(errors.CodeGatewayError, err, "发送 HTTP 请求失败") } defer resp.Body.Close() // HTTP 状态码错误 — 不可重试 if resp.StatusCode != http.StatusOK { return nil, false, errors.New(errors.CodeGatewayError, fmt.Sprintf("HTTP 状态码异常: %d", resp.StatusCode)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, false, errors.Wrap(errors.CodeGatewayInvalidResp, err, "读取响应体失败") } var gatewayResp GatewayResponse if err := sonic.Unmarshal(body, &gatewayResp); err != nil { return nil, false, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败") } // Gateway 业务错误 — 不可重试 if gatewayResp.Code != gatewaySuccessCode { c.logger.Warn("Gateway 业务错误", zap.String("path", path), zap.Int("gateway_code", gatewayResp.Code), zap.String("gateway_msg", gatewayResp.Msg), ) return nil, false, errors.New(errors.CodeGatewayError, fmt.Sprintf("Gateway 业务错误: code=%d, msg=%s", gatewayResp.Code, gatewayResp.Msg)) } return gatewayResp.Data, false, nil } // doRequestWithResponse 执行 Gateway API 请求并自动反序列化响应为目标类型 func doRequestWithResponse[T any](c *Client, ctx context.Context, path string, params interface{}) (*T, error) { data, err := c.doRequest(ctx, path, params) if err != nil { return nil, err } var result T if err := sonic.Unmarshal(data, &result); err != nil { return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败") } return &result, nil }