This commit is contained in:
2026-07-21 15:53:29 +08:00
parent 024b6614ba
commit c943f575d5
40 changed files with 5362 additions and 275 deletions

View File

@@ -0,0 +1,187 @@
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 发送业务 APIPOST 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)
}