重构数据引擎和报表引擎
This commit is contained in:
@@ -29,7 +29,7 @@ type ApiResult struct {
|
||||
type ApiClient struct {
|
||||
config *PlatformConfig
|
||||
client *http.Client
|
||||
rateLimiter <-chan time.Time // 限流 ticker
|
||||
rateLimiter *time.Ticker // 限流 ticker,可被 GC
|
||||
}
|
||||
|
||||
// NewApiClient 创建客户端
|
||||
@@ -38,14 +38,22 @@ func NewApiClient(config *PlatformConfig) *ApiClient {
|
||||
if config.RequestTimeoutMs > 0 {
|
||||
timeout = time.Duration(config.RequestTimeoutMs) * time.Millisecond
|
||||
}
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
}
|
||||
ac := &ApiClient{
|
||||
config: config,
|
||||
client: &http.Client{Timeout: timeout},
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
// 初始化限流
|
||||
if config.RateLimitPerMinute > 0 {
|
||||
interval := time.Minute / time.Duration(config.RateLimitPerMinute)
|
||||
ac.rateLimiter = time.Tick(interval)
|
||||
ac.rateLimiter = time.NewTicker(interval)
|
||||
logrus.Infof("限流已启用: %d 次/分钟, 间隔 %v", config.RateLimitPerMinute, interval)
|
||||
}
|
||||
return ac
|
||||
@@ -61,6 +69,13 @@ func (c *ApiClient) PostJSON(ctx context.Context, path string, body interface{})
|
||||
return c.doRequest(ctx, "POST", path, body, false)
|
||||
}
|
||||
|
||||
// Close 释放客户端资源(限流 ticker)
|
||||
func (c *ApiClient) Close() {
|
||||
if c.rateLimiter != nil {
|
||||
c.rateLimiter.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -99,8 +114,9 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
// 限流等待
|
||||
if c.rateLimiter != nil {
|
||||
select {
|
||||
case <-c.rateLimiter:
|
||||
case <-c.rateLimiter.C:
|
||||
case <-ctx.Done():
|
||||
c.rateLimiter.Stop()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -112,8 +128,13 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
fullURL = c.applyAuthURL(fullURL)
|
||||
|
||||
var reqBody io.Reader
|
||||
var reqBodyBytes []byte
|
||||
if body != nil && !paramsInQuery {
|
||||
b, _ := json.Marshal(body)
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("JSON序列化请求体失败: %w", err)
|
||||
}
|
||||
reqBodyBytes = b
|
||||
reqBody = bytes.NewBuffer(b)
|
||||
}
|
||||
|
||||
@@ -133,7 +154,7 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
c.applyAuthHeader(req)
|
||||
c.applyAuthHeader(req, reqBodyBytes)
|
||||
req.Header.Set("User-Agent", "data-engine/1.0")
|
||||
if body != nil && !paramsInQuery {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -145,7 +166,10 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体失败: %w", err)
|
||||
}
|
||||
result := &ApiResult{Body: respBody, DurationMs: time.Since(start).Milliseconds()}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
@@ -157,7 +181,11 @@ func (c *ApiClient) execute(ctx context.Context, method, path string, body inter
|
||||
// buildQueryURL 将 params 拼接到 URL 查询参数中
|
||||
// 支持数组/对象类型的值自动 JSON 序列化 + URL 编码
|
||||
func (c *ApiClient) buildQueryURL(rawURL string, params map[string]interface{}) string {
|
||||
parsed, _ := url.Parse(rawURL)
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed == nil {
|
||||
logrus.Errorf("buildQueryURL: 解析 URL 失败: %v", err)
|
||||
return rawURL
|
||||
}
|
||||
q := parsed.Query()
|
||||
|
||||
for k, v := range params {
|
||||
@@ -224,7 +252,11 @@ func (c *ApiClient) applyAuthURL(rawURL string) string {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parsed, _ := url.Parse(rawURL)
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed == nil {
|
||||
logrus.Errorf("applyAuthURL: 解析 URL 失败: %v", err)
|
||||
return rawURL
|
||||
}
|
||||
q := parsed.Query()
|
||||
if tokenInQuery && token != "" {
|
||||
q.Set(queryKey, token)
|
||||
@@ -236,10 +268,16 @@ func (c *ApiClient) applyAuthURL(rawURL string) string {
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func (c *ApiClient) applyAuthHeader(req *http.Request) {
|
||||
func (c *ApiClient) applyAuthHeader(req *http.Request, bodyBytes []byte) {
|
||||
cfg := c.config.AuthConfig
|
||||
token := c.config.AccessToken
|
||||
|
||||
// APP_SIGNATURE 认证:app-id + signature 头部(如钉钉智能薪酬)
|
||||
if c.config.AuthType == "APP_SIGNATURE" {
|
||||
c.applyAppSignatureAuth(req, bodyBytes)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
if tiq, _ := cfg["token_in_query"].(bool); tiq {
|
||||
return
|
||||
@@ -251,9 +289,9 @@ func (c *ApiClient) applyAuthHeader(req *http.Request) {
|
||||
|
||||
if cfg != nil {
|
||||
if h, ok := cfg["header_name"].(string); ok {
|
||||
f := cfg["header_format"].(string)
|
||||
if f == "" {
|
||||
f = "{token}"
|
||||
f := "{token}"
|
||||
if fv, ok2 := cfg["header_format"].(string); ok2 {
|
||||
f = fv
|
||||
}
|
||||
req.Header.Set(h, strings.ReplaceAll(f, "{token}", token))
|
||||
return
|
||||
@@ -268,6 +306,73 @@ func (c *ApiClient) applyAuthHeader(req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func generateNonce() string {
|
||||
nanoPart := time.Now().UnixNano() % 1000000000000
|
||||
r, _ := rand.Int(rand.Reader, big.NewInt(10000))
|
||||
@@ -293,7 +398,11 @@ func (c *ApiClient) applySignature(rawURL string, body interface{}, paramsInQuer
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parsed, _ := url.Parse(rawURL)
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed == nil {
|
||||
logrus.Errorf("applySignature: 解析 URL 失败: %v", err)
|
||||
return rawURL
|
||||
}
|
||||
q := parsed.Query()
|
||||
|
||||
// 收集所有参数并按 key 排序
|
||||
|
||||
Reference in New Issue
Block a user