// 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" ) const ( defaultTimeout = 30 * time.Second maxIdleConns = 100 maxIdleConnsPerHost = 10 idleConnTimeout = 90 * time.Second contentTypeJSON = "application/json;charset=utf-8" gatewaySuccessCode = 200 ) // Client 是 Gateway API 的 HTTP 客户端 type Client struct { baseURL string appID string appSecret string httpClient *http.Client timeout time.Duration } // NewClient 创建 Gateway 客户端实例 // baseURL: Gateway 服务基础地址 // appID: 应用 ID // appSecret: 应用密钥(用于加密和签名) func NewClient(baseURL, appID, appSecret string) *Client { return &Client{ baseURL: baseURL, appID: appID, appSecret: appSecret, httpClient: &http.Client{ Transport: &http.Transport{ MaxIdleConns: maxIdleConns, MaxIdleConnsPerHost: maxIdleConnsPerHost, IdleConnTimeout: idleConnTimeout, }, }, timeout: defaultTimeout, } } // WithTimeout 设置请求超时时间(支持链式调用) func (c *Client) WithTimeout(timeout time.Duration) *Client { c.timeout = timeout return c } // doRequest 执行 Gateway API 请求的统一方法 // 流程:序列化 → 加密 → 签名 → HTTP POST → 解析响应 → 检查业务状态码 func (c *Client) doRequest(ctx context.Context, path string, businessData interface{}) (json.RawMessage, error) { dataBytes, err := sonic.Marshal(businessData) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化业务数据失败") } encryptedData, err := aesEncrypt(dataBytes, c.appSecret) if err != nil { return nil, err } timestamp := time.Now().UnixMilli() 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, 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, errors.Wrap(errors.CodeGatewayError, err, "创建 HTTP 请求失败") } req.Header.Set("Content-Type", contentTypeJSON) resp, err := c.httpClient.Do(req) if err != nil { if reqCtx.Err() == context.DeadlineExceeded { return nil, errors.Wrap(errors.CodeGatewayTimeout, err, "Gateway 请求超时") } if ctx.Err() != nil { return nil, errors.Wrap(errors.CodeGatewayError, ctx.Err(), "请求被取消") } return nil, errors.Wrap(errors.CodeGatewayError, err, "发送 HTTP 请求失败") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("HTTP 状态码异常: %d", resp.StatusCode)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "读取响应体失败") } var gatewayResp GatewayResponse if err := sonic.Unmarshal(body, &gatewayResp); err != nil { return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败") } if gatewayResp.Code != gatewaySuccessCode { return nil, errors.New(errors.CodeGatewayError, fmt.Sprintf("Gateway 业务错误: code=%d, msg=%s", gatewayResp.Code, gatewayResp.Msg)) } return gatewayResp.Data, nil }