Files
hyapi-server/internal/shared/fadada/http.go
2026-07-21 15:53:29 +08:00

188 lines
5.1 KiB
Go
Raw 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 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)
}