188 lines
5.1 KiB
Go
188 lines
5.1 KiB
Go
|
|
package fadada
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"crypto/rand"
|
|||
|
|
"encoding/hex"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"net/url"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// HTTPClient 法大大 OpenAPI HTTP 客户端(不依赖官方 SDK)
|
|||
|
|
type HTTPClient struct {
|
|||
|
|
config *Config
|
|||
|
|
client *http.Client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewHTTPClient(config *Config) *HTTPClient {
|
|||
|
|
return &HTTPClient{
|
|||
|
|
config: config,
|
|||
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// apiResponse 通用响应外壳
|
|||
|
|
type apiResponse struct {
|
|||
|
|
Code string `json:"code"`
|
|||
|
|
Msg string `json:"msg"`
|
|||
|
|
Data json.RawMessage `json:"data"`
|
|||
|
|
RequestID string `json:"requestId,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PostBiz 发送业务 API:POST form bizContent=<JSON>,带 AccessToken 签名
|
|||
|
|
func (h *HTTPClient) PostBiz(path, accessToken string, biz any) (*apiResponse, error) {
|
|||
|
|
bizJSON, err := marshalBizContent(biz)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
headers := h.buildBizHeaders(accessToken, bizJSON)
|
|||
|
|
return h.doPost(path, headers, bizJSON)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PostToken 获取 accessToken:无 AccessToken,带 Grant-Type
|
|||
|
|
func (h *HTTPClient) PostToken(path string) (*apiResponse, error) {
|
|||
|
|
headers := h.buildTokenHeaders()
|
|||
|
|
return h.doPost(path, headers, "")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (h *HTTPClient) doPost(path string, headers map[string]string, bizJSON string) (*apiResponse, error) {
|
|||
|
|
fullURL := joinURL(h.config.ServerURL, path)
|
|||
|
|
|
|||
|
|
form := url.Values{}
|
|||
|
|
form.Set("bizContent", bizJSON)
|
|||
|
|
body := form.Encode()
|
|||
|
|
|
|||
|
|
req, err := http.NewRequest(http.MethodPost, fullURL, strings.NewReader(body))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("创建法大大请求失败: %w", err)
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|||
|
|
for k, v := range headers {
|
|||
|
|
req.Header.Set(k, v)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resp, err := h.client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
// 网络错误也记录一次,便于排查连接问题
|
|||
|
|
writeDebugLog(http.MethodPost, fullURL, headers, bizJSON, -1, fmt.Sprintf("HTTP ERROR: %v", err), "")
|
|||
|
|
return nil, fmt.Errorf("调用法大大接口失败 %s: %w", path, err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
respBody, err := io.ReadAll(resp.Body)
|
|||
|
|
if err != nil {
|
|||
|
|
writeDebugLog(http.MethodPost, fullURL, headers, bizJSON, resp.StatusCode, fmt.Sprintf("READ BODY ERROR: %v", err), "")
|
|||
|
|
return nil, fmt.Errorf("读取法大大响应失败 %s: %w", path, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
requestID := ""
|
|||
|
|
if vals := resp.Header.Values("X-FASC-Request-Id"); len(vals) > 0 {
|
|||
|
|
requestID = vals[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 落盘 cURL + 响应(含 X-FASC-* 头与 bizContent),便于排查免验证签等问题
|
|||
|
|
writeDebugLog(http.MethodPost, fullURL, headers, bizJSON, resp.StatusCode, string(respBody), requestID)
|
|||
|
|
|
|||
|
|
var out apiResponse
|
|||
|
|
if err := json.Unmarshal(respBody, &out); err != nil {
|
|||
|
|
return nil, fmt.Errorf("解析法大大响应失败 %s: %w body=%s", path, err, truncate(string(respBody), 512))
|
|||
|
|
}
|
|||
|
|
if out.RequestID == "" {
|
|||
|
|
out.RequestID = requestID
|
|||
|
|
}
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (h *HTTPClient) buildTokenHeaders() map[string]string {
|
|||
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|||
|
|
nonce := randomNonce()
|
|||
|
|
headMap := map[string]string{
|
|||
|
|
"X-FASC-App-Id": h.config.AppID,
|
|||
|
|
"X-FASC-Sign-Type": signTypeHMACSHA256,
|
|||
|
|
"X-FASC-Timestamp": timestamp,
|
|||
|
|
"X-FASC-Nonce": nonce,
|
|||
|
|
"X-FASC-Grant-Type": "client_credential",
|
|||
|
|
"X-FASC-Api-SubVersion": apiSubVersion,
|
|||
|
|
}
|
|||
|
|
headMap["X-FASC-Sign"] = SignByMap(headMap, timestamp, h.config.AppSecret)
|
|||
|
|
return headMap
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (h *HTTPClient) buildBizHeaders(accessToken, bizJSON string) map[string]string {
|
|||
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|||
|
|
nonce := randomNonce()
|
|||
|
|
headMap := map[string]string{
|
|||
|
|
"X-FASC-App-Id": h.config.AppID,
|
|||
|
|
"X-FASC-Sign-Type": signTypeHMACSHA256,
|
|||
|
|
"X-FASC-Timestamp": timestamp,
|
|||
|
|
"X-FASC-Nonce": nonce,
|
|||
|
|
"X-FASC-AccessToken": accessToken,
|
|||
|
|
"X-FASC-Api-SubVersion": apiSubVersion,
|
|||
|
|
"bizContent": bizJSON,
|
|||
|
|
}
|
|||
|
|
sign := SignByMap(headMap, timestamp, h.config.AppSecret)
|
|||
|
|
delete(headMap, "bizContent")
|
|||
|
|
headMap["X-FASC-Sign"] = sign
|
|||
|
|
return headMap
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func marshalBizContent(biz any) (string, error) {
|
|||
|
|
if biz == nil {
|
|||
|
|
return "", nil
|
|||
|
|
}
|
|||
|
|
switch v := biz.(type) {
|
|||
|
|
case string:
|
|||
|
|
return v, nil
|
|||
|
|
case []byte:
|
|||
|
|
return string(v), nil
|
|||
|
|
default:
|
|||
|
|
b, err := json.Marshal(biz)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("序列化 bizContent 失败: %w", err)
|
|||
|
|
}
|
|||
|
|
// 紧凑 JSON,避免多余空格影响签名
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if err := json.Compact(&buf, b); err == nil {
|
|||
|
|
return buf.String(), nil
|
|||
|
|
}
|
|||
|
|
return string(b), nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func joinURL(base, path string) string {
|
|||
|
|
base = strings.TrimRight(base, "/")
|
|||
|
|
if !strings.HasPrefix(path, "/") {
|
|||
|
|
path = "/" + path
|
|||
|
|
}
|
|||
|
|
return base + path
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func randomNonce() string {
|
|||
|
|
b := make([]byte, 16)
|
|||
|
|
if _, err := rand.Read(b); err != nil {
|
|||
|
|
return strconv.FormatInt(time.Now().UnixNano(), 16)
|
|||
|
|
}
|
|||
|
|
return hex.EncodeToString(b)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func truncate(s string, n int) string {
|
|||
|
|
if len(s) <= n {
|
|||
|
|
return s
|
|||
|
|
}
|
|||
|
|
return s[:n] + "..."
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// decodeData 将 apiResponse.Data 反序列化到目标结构
|
|||
|
|
func decodeData(raw json.RawMessage, dest any) error {
|
|||
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return json.Unmarshal(raw, dest)
|
|||
|
|
}
|