数据引擎重构
This commit is contained in:
+95
-22
@@ -25,11 +25,27 @@ type ApiResult struct {
|
||||
DurationMs int64
|
||||
}
|
||||
|
||||
// TokenRefreshFunc token 刷新回调函数
|
||||
// 由具体平台的 RefreshToken 函数实现,在检测到 401/token 过期时自动调用
|
||||
type TokenRefreshFunc func(ctx context.Context, config *PlatformConfig) error
|
||||
|
||||
// ApiClient 通用 API 客户端
|
||||
type ApiClient struct {
|
||||
config *PlatformConfig
|
||||
client *http.Client
|
||||
rateLimiter *time.Ticker // 限流 ticker,可被 GC
|
||||
config *PlatformConfig
|
||||
client *http.Client
|
||||
rateLimiter *time.Ticker // 限流 ticker,可被 GC
|
||||
tokenRefreshFunc TokenRefreshFunc
|
||||
bodyType string // 请求体序列化方式: ""=form-encoded, "json"=JSON
|
||||
}
|
||||
|
||||
// SetBodyType 设置请求体序列化方式
|
||||
func (c *ApiClient) SetBodyType(t string) {
|
||||
c.bodyType = t
|
||||
}
|
||||
|
||||
// SetTokenRefreshFunc 设置 token 刷新回调
|
||||
func (c *ApiClient) SetTokenRefreshFunc(fn TokenRefreshFunc) {
|
||||
c.tokenRefreshFunc = fn
|
||||
}
|
||||
|
||||
// NewApiClient 创建客户端
|
||||
@@ -91,11 +107,27 @@ func (c *ApiClient) doRequest(ctx context.Context, method, path string, body int
|
||||
retryDelay = 1 * time.Second
|
||||
}
|
||||
|
||||
refreshed := false // 标记是否已刷新过 token(每个 doRequest 最多刷新一次)
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
result, err = c.execute(ctx, method, path, body, paramsInQuery)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// token 过期 → 刷新一次,立即重试(不占退避重试次数)
|
||||
if isTokenExpiredError(err) && c.tokenRefreshFunc != nil && !refreshed {
|
||||
logrus.Warnf("检测到 token 过期 [%s],尝试自动刷新...", path)
|
||||
if refreshErr := c.tokenRefreshFunc(ctx, c.config); refreshErr != nil {
|
||||
logrus.Errorf("Token 刷新失败: %v", refreshErr)
|
||||
} else {
|
||||
refreshed = true
|
||||
logrus.Info("Token 刷新成功,立即重试请求")
|
||||
// 不递减 attempt,直接用新 token 重试
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Warnf("请求失败 (attempt %d/%d): %v", attempt+1, maxRetries+1, err)
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(retryDelay * time.Duration(attempt+1))
|
||||
@@ -122,28 +154,50 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
fullURL = c.applyAuthURL(fullURL)
|
||||
|
||||
// 将 URL 认证参数注入 body 并清除 URL(避免重复参数)
|
||||
// 注意:token_in_query=true 的认证方式(如腾讯广告),token 必须留在 URL 中,不移入 body
|
||||
var reqBody io.Reader
|
||||
var reqBodyBytes []byte
|
||||
if body != nil && !paramsInQuery {
|
||||
if paramsMap, ok := body.(map[string]interface{}); ok {
|
||||
// 从 URL 注入认证参数到 body
|
||||
if parsed, _ := url.Parse(fullURL); parsed != nil {
|
||||
q := parsed.Query()
|
||||
for k, vs := range q {
|
||||
if len(vs) > 0 {
|
||||
if _, exists := paramsMap[k]; !exists {
|
||||
paramsMap[k] = vs[0]
|
||||
// token_in_query=true 时认证参数必须留在 URL 中,不注入 body
|
||||
tokenInQuery := false
|
||||
if c.config.AuthConfig != nil {
|
||||
if tiq, _ := c.config.AuthConfig["token_in_query"].(bool); tiq {
|
||||
tokenInQuery = true
|
||||
}
|
||||
}
|
||||
if !tokenInQuery {
|
||||
if paramsMap, ok := body.(map[string]interface{}); ok {
|
||||
// 从 URL 注入认证参数到 body
|
||||
if parsed, _ := url.Parse(fullURL); parsed != nil {
|
||||
q := parsed.Query()
|
||||
for k, vs := range q {
|
||||
if len(vs) > 0 {
|
||||
if _, exists := paramsMap[k]; !exists {
|
||||
paramsMap[k] = vs[0]
|
||||
}
|
||||
q.Del(k)
|
||||
}
|
||||
q.Del(k)
|
||||
}
|
||||
parsed.RawQuery = q.Encode()
|
||||
fullURL = parsed.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if paramsMap, ok := body.(map[string]interface{}); ok {
|
||||
if c.bodyType == "json" {
|
||||
// JSON body(如腾讯广告 POST 接口)
|
||||
b, err := json.Marshal(paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("JSON序列化请求体失败: %w", err)
|
||||
}
|
||||
parsed.RawQuery = q.Encode()
|
||||
fullURL = parsed.String()
|
||||
reqBodyBytes = b
|
||||
reqBody = bytes.NewBuffer(b)
|
||||
} else {
|
||||
// Form body
|
||||
formStr := c.buildFormBody(paramsMap)
|
||||
reqBodyBytes = []byte(formStr)
|
||||
reqBody = strings.NewReader(formStr)
|
||||
}
|
||||
// Form body
|
||||
formStr := c.buildFormBody(paramsMap)
|
||||
reqBodyBytes = []byte(formStr)
|
||||
reqBody = strings.NewReader(formStr)
|
||||
} else {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -181,9 +235,13 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
// 打印等效 curl
|
||||
curlCmd := fmt.Sprintf("curl -X %s '%s'", method, fullURL)
|
||||
if reqBodyBytes != nil && len(reqBodyBytes) > 0 {
|
||||
for _, pair := range strings.Split(string(reqBodyBytes), "&") {
|
||||
if pair != "" {
|
||||
curlCmd += fmt.Sprintf(" --data-urlencode '%s'", pair)
|
||||
if c.bodyType == "json" {
|
||||
curlCmd += fmt.Sprintf(" -H 'Content-Type: application/json' -d '%s'", string(reqBodyBytes))
|
||||
} else {
|
||||
for _, pair := range strings.Split(string(reqBodyBytes), "&") {
|
||||
if pair != "" {
|
||||
curlCmd += fmt.Sprintf(" --data-urlencode '%s'", pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +256,11 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
req.Header.Set("User-Agent", "data-engine/1.0")
|
||||
if body != nil && !paramsInQuery {
|
||||
if _, ok := body.(map[string]interface{}); ok {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if c.bodyType == "json" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
} else {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
@@ -219,6 +281,13 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
if resp.StatusCode >= 400 {
|
||||
return result, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
// 检查腾讯API业务级token错误(HTTP 200但code=11002,access_token无效)
|
||||
var apiResp struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &apiResp); err == nil && apiResp.Code == 11002 {
|
||||
return result, fmt.Errorf("token expired: code=11002, msg=%s", string(respBody))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -283,6 +352,10 @@ func (c *ApiClient) buildFormBody(params map[string]interface{}) string {
|
||||
}
|
||||
case int, int8, int16, int32, int64:
|
||||
q.Set(k, fmt.Sprintf("%d", val))
|
||||
case []interface{}, map[string]interface{}:
|
||||
// 数组或对象需要 JSON 序列化
|
||||
b, _ := json.Marshal(v)
|
||||
q.Set(k, string(b))
|
||||
default:
|
||||
q.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user