Files
huang b5147d1acb
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m34s
设备的部分改造
2026-03-10 10:34:08 +08:00

236 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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": <JSON>} 格式
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<<uint(attempt-1))
if delay > 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
}