package dx import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // HTTPStatusError preserves the telecom response body for non-200 responses. // RespCode is the raw carrier error code when the gateway supplies one. type HTTPStatusError struct { StatusCode int Body string RespCode string } func (e *HTTPStatusError) Error() string { if e == nil { return "telecom HTTP status error" } return fmt.Sprintf("http status %d: %s", e.StatusCode, e.Body) } // Config 网关鉴权与请求配置 type Config struct { BaseURL string // 默认 https://cmp-api.ctwing.cn:20164 AppKey string AppSecret string Timeout time.Duration UppercaseSign bool } // Client API 客户端 type Client struct { baseURL string appKey string appSecret string http *http.Client uppercaseSign bool } // New 创建客户端 func NewClient(cfg Config) *Client { base := cfg.BaseURL if base == "" { base = DefaultBaseURL } timeout := cfg.Timeout if timeout <= 0 { timeout = 10 * time.Second } return &Client{ baseURL: strings.TrimSuffix(base, "/"), appKey: cfg.AppKey, appSecret: cfg.AppSecret, uppercaseSign: cfg.UppercaseSign, http: &http.Client{ Timeout: timeout, }, } } // DoGetRequest 通用GET请求(自动处理签名) // endpoint: API路径,如 "/api/v1/openbill/queryTraffic" // params: 查询参数,需调用方自行构建 func (c *Client) DoGetRequest(ctx context.Context, endpoint string, params url.Values) ([]byte, error) { req, err := c.doGetRequest(ctx, endpoint, params) if err != nil { return nil, err } return c.doRequest(req) } func (c *Client) DoPostJSONRequest(ctx context.Context, endpoint string, body []byte) ([]byte, error) { timestamp := GetTimestampGMT() signStr, err := BuildSign("", string(body), c.appSecret, timestamp) if c.uppercaseSign { signStr, err = BuildSignV3("", string(body), c.appSecret, timestamp) } if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+endpoint, strings.NewReader(string(body))) if err != nil { return nil, err } req.Header.Set("AppKey", c.appKey) req.Header.Set("Sign", c.formatSign(signStr)) req.Header.Set("Timestamp", timestamp) req.Header.Set("Content-Type", "application/json") return c.doRequest(req) } // doGetRequest 封装通用的 GET 请求构建逻辑 func (c *Client) doGetRequest(ctx context.Context, endpoint string, params url.Values) (*http.Request, error) { queryString := params.Encode() timestamp := GetTimestampGMT() signStr, err := BuildSign(queryString, "", c.appSecret, timestamp) if c.uppercaseSign { signStr, err = BuildSignV3(queryString, "", c.appSecret, timestamp) } if err != nil { return nil, err } reqURL := c.baseURL + endpoint + "?" + queryString req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("AppKey", c.appKey) req.Header.Set("Sign", c.formatSign(signStr)) req.Header.Set("Timestamp", timestamp) return req, nil } func (c *Client) formatSign(sign string) string { if c.uppercaseSign { return strings.ToUpper(sign) } return sign } // doRequest 执行HTTP请求 func (c *Client) doRequest(req *http.Request) ([]byte, error) { resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { var errorBody struct { RespCode string `json:"respCode"` } _ = json.Unmarshal(body, &errorBody) if c.uppercaseSign && strings.TrimSpace(errorBody.RespCode) == "" { errorBody.RespCode = resp.Header.Get("gw-errorCode") } return nil, &HTTPStatusError{ StatusCode: resp.StatusCode, Body: string(body), RespCode: strings.TrimSpace(errorBody.RespCode), } } return body, nil }