2026-05-29 18:39:32 +08:00
|
|
|
|
package sync
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
2026-06-01 14:08:17 +08:00
|
|
|
|
"crypto/md5"
|
2026-05-29 18:39:32 +08:00
|
|
|
|
"crypto/rand"
|
2026-06-01 14:08:17 +08:00
|
|
|
|
"encoding/hex"
|
2026-05-29 18:39:32 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"math/big"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/url"
|
2026-06-01 14:08:17 +08:00
|
|
|
|
"sort"
|
2026-05-29 18:39:32 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ApiResult API 调用结果
|
|
|
|
|
|
type ApiResult struct {
|
|
|
|
|
|
Body []byte
|
|
|
|
|
|
DurationMs int64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ApiClient 通用 API 客户端
|
|
|
|
|
|
type ApiClient struct {
|
2026-06-01 14:08:17 +08:00
|
|
|
|
config *PlatformConfig
|
|
|
|
|
|
client *http.Client
|
2026-06-11 13:06:54 +08:00
|
|
|
|
rateLimiter *time.Ticker // 限流 ticker,可被 GC
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewApiClient 创建客户端
|
|
|
|
|
|
func NewApiClient(config *PlatformConfig) *ApiClient {
|
|
|
|
|
|
timeout := 30 * time.Second
|
|
|
|
|
|
if config.RequestTimeoutMs > 0 {
|
|
|
|
|
|
timeout = time.Duration(config.RequestTimeoutMs) * time.Millisecond
|
|
|
|
|
|
}
|
2026-06-11 13:06:54 +08:00
|
|
|
|
transport := &http.Transport{
|
|
|
|
|
|
MaxIdleConns: 100,
|
|
|
|
|
|
MaxIdleConnsPerHost: 20,
|
|
|
|
|
|
IdleConnTimeout: 90 * time.Second,
|
|
|
|
|
|
}
|
2026-06-01 14:08:17 +08:00
|
|
|
|
ac := &ApiClient{
|
2026-05-29 18:39:32 +08:00
|
|
|
|
config: config,
|
2026-06-11 13:06:54 +08:00
|
|
|
|
client: &http.Client{
|
|
|
|
|
|
Timeout: timeout,
|
|
|
|
|
|
Transport: transport,
|
|
|
|
|
|
},
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
2026-06-01 14:08:17 +08:00
|
|
|
|
// 初始化限流
|
|
|
|
|
|
if config.RateLimitPerMinute > 0 {
|
|
|
|
|
|
interval := time.Minute / time.Duration(config.RateLimitPerMinute)
|
2026-06-11 13:06:54 +08:00
|
|
|
|
ac.rateLimiter = time.NewTicker(interval)
|
2026-06-01 14:08:17 +08:00
|
|
|
|
logrus.Infof("限流已启用: %d 次/分钟, 间隔 %v", config.RateLimitPerMinute, interval)
|
|
|
|
|
|
}
|
|
|
|
|
|
return ac
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get 发送 GET 请求(无参数)
|
|
|
|
|
|
func (c *ApiClient) Get(ctx context.Context, path string) (*ApiResult, error) {
|
|
|
|
|
|
return c.doRequest(ctx, "GET", path, nil, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PostJSON 发送 POST JSON 请求
|
|
|
|
|
|
func (c *ApiClient) PostJSON(ctx context.Context, path string, body interface{}) (*ApiResult, error) {
|
|
|
|
|
|
return c.doRequest(ctx, "POST", path, body, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
// Close 释放客户端资源(限流 ticker)
|
|
|
|
|
|
func (c *ApiClient) Close() {
|
|
|
|
|
|
if c.rateLimiter != nil {
|
|
|
|
|
|
c.rateLimiter.Stop()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 18:39:32 +08:00
|
|
|
|
// Request 通用请求方法(支持 GET/POST,支持参数在 query 或 body)
|
|
|
|
|
|
func (c *ApiClient) Request(ctx context.Context, method, path string, params map[string]interface{}, paramsInQuery bool) (*ApiResult, error) {
|
|
|
|
|
|
if paramsInQuery {
|
|
|
|
|
|
return c.doRequest(ctx, method, path, params, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
if method == "GET" {
|
|
|
|
|
|
return c.doRequest(ctx, "GET", path, params, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
return c.doRequest(ctx, method, path, params, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *ApiClient) doRequest(ctx context.Context, method, path string, body interface{}, paramsInQuery bool) (result *ApiResult, err error) {
|
|
|
|
|
|
maxRetries := c.config.MaxRetries
|
|
|
|
|
|
if maxRetries <= 0 {
|
|
|
|
|
|
maxRetries = 3
|
|
|
|
|
|
}
|
|
|
|
|
|
retryDelay := time.Duration(c.config.RetryDelayMs) * time.Millisecond
|
|
|
|
|
|
if retryDelay <= 0 {
|
|
|
|
|
|
retryDelay = 1 * time.Second
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
|
|
|
|
result, err = c.execute(ctx, method, path, body, paramsInQuery)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
logrus.Warnf("请求失败 (attempt %d/%d): %v", attempt+1, maxRetries+1, err)
|
|
|
|
|
|
if attempt < maxRetries {
|
|
|
|
|
|
time.Sleep(retryDelay * time.Duration(attempt+1))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, fmt.Errorf("请求已重试 %d 次仍失败: %w", maxRetries, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *ApiClient) execute(ctx context.Context, method, path string, body interface{}, paramsInQuery bool) (*ApiResult, error) {
|
2026-06-01 14:08:17 +08:00
|
|
|
|
// 限流等待
|
|
|
|
|
|
if c.rateLimiter != nil {
|
|
|
|
|
|
select {
|
2026-06-11 13:06:54 +08:00
|
|
|
|
case <-c.rateLimiter.C:
|
2026-06-01 14:08:17 +08:00
|
|
|
|
case <-ctx.Done():
|
2026-06-11 13:06:54 +08:00
|
|
|
|
c.rateLimiter.Stop()
|
2026-06-01 14:08:17 +08:00
|
|
|
|
return nil, ctx.Err()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 18:39:32 +08:00
|
|
|
|
start := time.Now()
|
|
|
|
|
|
fullURL := c.config.GetApiUrl(path)
|
|
|
|
|
|
|
|
|
|
|
|
// 先注入认证参数
|
|
|
|
|
|
fullURL = c.applyAuthURL(fullURL)
|
|
|
|
|
|
|
|
|
|
|
|
var reqBody io.Reader
|
2026-06-11 13:06:54 +08:00
|
|
|
|
var reqBodyBytes []byte
|
2026-05-29 18:39:32 +08:00
|
|
|
|
if body != nil && !paramsInQuery {
|
2026-06-11 13:06:54 +08:00
|
|
|
|
b, err := json.Marshal(body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("JSON序列化请求体失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
reqBodyBytes = b
|
2026-05-29 18:39:32 +08:00
|
|
|
|
reqBody = bytes.NewBuffer(b)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果参数在查询字符串中,拼接到 URL
|
|
|
|
|
|
if body != nil && paramsInQuery {
|
|
|
|
|
|
if paramsMap, ok := body.(map[string]interface{}); ok {
|
|
|
|
|
|
fullURL = c.buildQueryURL(fullURL, paramsMap)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 14:08:17 +08:00
|
|
|
|
// 计算签名并追加(如快手 API 的 MD5 签名)
|
|
|
|
|
|
fullURL = c.applySignature(fullURL, body, paramsInQuery)
|
2026-05-29 18:39:32 +08:00
|
|
|
|
logrus.Infof("请求 URL: %s", fullURL)
|
|
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, reqBody)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
c.applyAuthHeader(req, reqBodyBytes)
|
2026-05-29 18:39:32 +08:00
|
|
|
|
req.Header.Set("User-Agent", "data-engine/1.0")
|
|
|
|
|
|
if body != nil && !paramsInQuery {
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Do(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("请求失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("读取响应体失败: %w", err)
|
|
|
|
|
|
}
|
2026-05-29 18:39:32 +08:00
|
|
|
|
result := &ApiResult{Body: respBody, DurationMs: time.Since(start).Milliseconds()}
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode >= 400 {
|
|
|
|
|
|
return result, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// buildQueryURL 将 params 拼接到 URL 查询参数中
|
|
|
|
|
|
// 支持数组/对象类型的值自动 JSON 序列化 + URL 编码
|
|
|
|
|
|
func (c *ApiClient) buildQueryURL(rawURL string, params map[string]interface{}) string {
|
2026-06-11 13:06:54 +08:00
|
|
|
|
parsed, err := url.Parse(rawURL)
|
|
|
|
|
|
if err != nil || parsed == nil {
|
|
|
|
|
|
logrus.Errorf("buildQueryURL: 解析 URL 失败: %v", err)
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
2026-05-29 18:39:32 +08:00
|
|
|
|
q := parsed.Query()
|
|
|
|
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
switch val := v.(type) {
|
|
|
|
|
|
case string:
|
|
|
|
|
|
q.Set(k, val)
|
|
|
|
|
|
case bool:
|
|
|
|
|
|
if val {
|
|
|
|
|
|
q.Set(k, "true")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q.Set(k, "false")
|
|
|
|
|
|
}
|
|
|
|
|
|
case float64:
|
|
|
|
|
|
// JSON 数字反序列化默认是 float64,转 int 避免科学计数法
|
|
|
|
|
|
if val == float64(int64(val)) {
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%d", int64(val)))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%v", val))
|
|
|
|
|
|
}
|
|
|
|
|
|
case float32:
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%v", val))
|
|
|
|
|
|
case int, int8, int16, int32, int64:
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%d", val))
|
|
|
|
|
|
case uint, uint8, uint16, uint32, uint64:
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%d", val))
|
|
|
|
|
|
case []interface{}, map[string]interface{}:
|
|
|
|
|
|
// 数组或对象需要 JSON 序列化后 URL 编码
|
|
|
|
|
|
b, _ := json.Marshal(v)
|
|
|
|
|
|
q.Set(k, string(b))
|
|
|
|
|
|
default:
|
|
|
|
|
|
q.Set(k, fmt.Sprintf("%v", v))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parsed.RawQuery = q.Encode()
|
|
|
|
|
|
return parsed.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *ApiClient) applyAuthURL(rawURL string) string {
|
|
|
|
|
|
cfg := c.config.AuthConfig
|
|
|
|
|
|
token := c.config.AccessToken
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokenInQuery, _ := cfg["token_in_query"].(bool)
|
|
|
|
|
|
queryKey, _ := cfg["query_key"].(string)
|
|
|
|
|
|
if queryKey == "" {
|
|
|
|
|
|
queryKey = "access_token"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extraParams := make(map[string]string)
|
|
|
|
|
|
if eq, ok := cfg["extra_query_params"].(map[string]interface{}); ok {
|
|
|
|
|
|
for k, v := range eq {
|
|
|
|
|
|
val := fmt.Sprintf("%v", v)
|
|
|
|
|
|
val = strings.ReplaceAll(val, "{timestamp}", fmt.Sprintf("%d", time.Now().Unix()))
|
2026-06-01 14:08:17 +08:00
|
|
|
|
val = strings.ReplaceAll(val, "{timestamp_ms}", fmt.Sprintf("%d", time.Now().UnixMilli()))
|
2026-05-29 18:39:32 +08:00
|
|
|
|
val = strings.ReplaceAll(val, "{nonce}", generateNonce())
|
|
|
|
|
|
extraParams[k] = val
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !tokenInQuery && len(extraParams) == 0 {
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
parsed, err := url.Parse(rawURL)
|
|
|
|
|
|
if err != nil || parsed == nil {
|
|
|
|
|
|
logrus.Errorf("applyAuthURL: 解析 URL 失败: %v", err)
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
2026-05-29 18:39:32 +08:00
|
|
|
|
q := parsed.Query()
|
|
|
|
|
|
if tokenInQuery && token != "" {
|
|
|
|
|
|
q.Set(queryKey, token)
|
|
|
|
|
|
}
|
|
|
|
|
|
for k, v := range extraParams {
|
|
|
|
|
|
q.Set(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
parsed.RawQuery = q.Encode()
|
|
|
|
|
|
return parsed.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
func (c *ApiClient) applyAuthHeader(req *http.Request, bodyBytes []byte) {
|
2026-05-29 18:39:32 +08:00
|
|
|
|
cfg := c.config.AuthConfig
|
|
|
|
|
|
token := c.config.AccessToken
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
// APP_SIGNATURE 认证:app-id + signature 头部(如钉钉智能薪酬)
|
|
|
|
|
|
if c.config.AuthType == "APP_SIGNATURE" {
|
|
|
|
|
|
c.applyAppSignatureAuth(req, bodyBytes)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 18:39:32 +08:00
|
|
|
|
if cfg != nil {
|
|
|
|
|
|
if tiq, _ := cfg["token_in_query"].(bool); tiq {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if token == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if cfg != nil {
|
|
|
|
|
|
if h, ok := cfg["header_name"].(string); ok {
|
2026-06-11 13:06:54 +08:00
|
|
|
|
f := "{token}"
|
|
|
|
|
|
if fv, ok2 := cfg["header_format"].(string); ok2 {
|
|
|
|
|
|
f = fv
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
req.Header.Set(h, strings.ReplaceAll(f, "{token}", token))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch c.config.AuthType {
|
|
|
|
|
|
case "OAUTH2", "TOKEN":
|
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
|
|
case "API_KEY":
|
|
|
|
|
|
req.Header.Set("X-API-Key", token)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
// applyAppSignatureAuth 设置 app-id + signature 认证头部
|
|
|
|
|
|
func (c *ApiClient) applyAppSignatureAuth(req *http.Request, bodyBytes []byte) {
|
|
|
|
|
|
cfg := c.config.AuthConfig
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 设置 app-id 头部
|
|
|
|
|
|
appIdHeader := "app-id"
|
|
|
|
|
|
if h, _ := cfg["app_id_header"].(string); h != "" {
|
|
|
|
|
|
appIdHeader = h
|
|
|
|
|
|
}
|
|
|
|
|
|
appId := c.config.AppKey
|
|
|
|
|
|
if appId == "" {
|
|
|
|
|
|
if aid, _ := cfg["app_id"].(string); aid != "" {
|
|
|
|
|
|
appId = aid
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if appId != "" {
|
|
|
|
|
|
req.Header.Set(appIdHeader, appId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 计算签名并设置 signature 头部
|
|
|
|
|
|
signHeader := "signature"
|
|
|
|
|
|
if h, _ := cfg["sign_header"].(string); h != "" {
|
|
|
|
|
|
signHeader = h
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
secret := c.config.AppSecret
|
|
|
|
|
|
|
|
|
|
|
|
signAlgo := "md5_upper_body"
|
|
|
|
|
|
if a, _ := cfg["sign_algorithm"].(string); a != "" {
|
|
|
|
|
|
signAlgo = a
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sig := computeBodySignature(bodyBytes, secret, signAlgo)
|
|
|
|
|
|
if sig != "" {
|
|
|
|
|
|
req.Header.Set(signHeader, sig)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// computeBodySignature 计算基于请求体的签名
|
|
|
|
|
|
// 支持的算法:
|
|
|
|
|
|
// - md5_upper_body: MD5(body_string + secret) 大写(默认,钉钉智能薪酬)
|
|
|
|
|
|
// - md5_body: MD5(body_string + secret) 小写
|
|
|
|
|
|
func computeBodySignature(bodyBytes []byte, secret, algo string) string {
|
|
|
|
|
|
if secret == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
bodyStr := ""
|
|
|
|
|
|
if len(bodyBytes) > 0 {
|
|
|
|
|
|
bodyStr = string(bodyBytes)
|
|
|
|
|
|
}
|
|
|
|
|
|
switch algo {
|
|
|
|
|
|
case "md5_body", "md5_upper_body":
|
|
|
|
|
|
h := md5.Sum([]byte(bodyStr + secret))
|
|
|
|
|
|
sig := hex.EncodeToString(h[:])
|
|
|
|
|
|
if algo == "md5_upper_body" {
|
|
|
|
|
|
sig = strings.ToUpper(sig)
|
|
|
|
|
|
}
|
|
|
|
|
|
return sig
|
|
|
|
|
|
default:
|
|
|
|
|
|
logrus.Warnf("未知签名算法: %s", algo)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 18:39:32 +08:00
|
|
|
|
func generateNonce() string {
|
|
|
|
|
|
nanoPart := time.Now().UnixNano() % 1000000000000
|
|
|
|
|
|
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
|
|
|
|
|
|
return fmt.Sprintf("%012d%04d", nanoPart, r.Int64())
|
|
|
|
|
|
}
|
2026-06-01 14:08:17 +08:00
|
|
|
|
|
|
|
|
|
|
// applySignature 计算签名并追加到 URL(支持快手等平台的 MD5 签名)
|
|
|
|
|
|
func (c *ApiClient) applySignature(rawURL string, body interface{}, paramsInQuery bool) string {
|
|
|
|
|
|
cfg := c.config.AuthConfig
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
signAlgo, _ := cfg["sign_algorithm"].(string)
|
|
|
|
|
|
if signAlgo == "" {
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
appSecret, _ := cfg["app_secret"].(string)
|
|
|
|
|
|
if appSecret == "" && c.config.AppSecret != "" {
|
|
|
|
|
|
appSecret = c.config.AppSecret
|
|
|
|
|
|
}
|
|
|
|
|
|
if appSecret == "" {
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:06:54 +08:00
|
|
|
|
parsed, err := url.Parse(rawURL)
|
|
|
|
|
|
if err != nil || parsed == nil {
|
|
|
|
|
|
logrus.Errorf("applySignature: 解析 URL 失败: %v", err)
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
2026-06-01 14:08:17 +08:00
|
|
|
|
q := parsed.Query()
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有参数并按 key 排序
|
|
|
|
|
|
keys := make([]string, 0, len(q))
|
|
|
|
|
|
for k := range q {
|
|
|
|
|
|
if k == "sign" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
keys = append(keys, k)
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
|
|
|
|
var signStr string
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
|
signStr += k + "=" + q.Get(k) + "&"
|
|
|
|
|
|
}
|
|
|
|
|
|
signStr += "key=" + appSecret
|
|
|
|
|
|
|
|
|
|
|
|
var sign string
|
|
|
|
|
|
switch signAlgo {
|
|
|
|
|
|
case "md5":
|
|
|
|
|
|
h := md5.Sum([]byte(signStr))
|
|
|
|
|
|
sign = hex.EncodeToString(h[:])
|
|
|
|
|
|
case "md5_upper":
|
|
|
|
|
|
h := md5.Sum([]byte(signStr))
|
|
|
|
|
|
sign = strings.ToUpper(hex.EncodeToString(h[:]))
|
|
|
|
|
|
default:
|
|
|
|
|
|
return rawURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
q.Set("sign", sign)
|
|
|
|
|
|
parsed.RawQuery = q.Encode()
|
|
|
|
|
|
return parsed.String()
|
|
|
|
|
|
}
|