179 lines
4.8 KiB
Go
179 lines
4.8 KiB
Go
|
|
package haiyuapi
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"crypto/md5"
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"tyapi-server/internal/shared/external_logger"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const defaultRequestTimeout = 60 * time.Second
|
|||
|
|
|
|||
|
|
// serviceConfig 海宇API服务运行时配置(Access Key 为 16 进制 AES-128 密钥)
|
|||
|
|
type serviceConfig struct {
|
|||
|
|
BaseURL string
|
|||
|
|
AccessID string
|
|||
|
|
SecretKey string
|
|||
|
|
Timeout time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HaiyuapiService 海宇API上游服务客户端
|
|||
|
|
type HaiyuapiService struct {
|
|||
|
|
config serviceConfig
|
|||
|
|
logger *external_logger.ExternalServiceLogger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewHaiyuapiService 创建海宇API服务实例
|
|||
|
|
func NewHaiyuapiService(baseURL, accessID, secretKey string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *HaiyuapiService {
|
|||
|
|
if timeout <= 0 {
|
|||
|
|
timeout = defaultRequestTimeout
|
|||
|
|
}
|
|||
|
|
return &HaiyuapiService{
|
|||
|
|
config: serviceConfig{
|
|||
|
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
|||
|
|
AccessID: accessID,
|
|||
|
|
SecretKey: secretKey,
|
|||
|
|
Timeout: timeout,
|
|||
|
|
},
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CallAPI 调用海宇API:apiPath 如 /api/v1/FLXGHB4F,自动拼接 base_url 与 ?t=13位毫秒时间戳,返回解密后的明文 JSON
|
|||
|
|
func (s *HaiyuapiService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) ([]byte, error) {
|
|||
|
|
startTime := time.Now()
|
|||
|
|
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", time.Now().UnixNano(), s.config.SecretKey)))
|
|||
|
|
requestID := fmt.Sprintf("haiyuapi_%x", hash[:8])
|
|||
|
|
|
|||
|
|
var transactionID string
|
|||
|
|
if id, ok := ctx.Value("transaction_id").(string); ok {
|
|||
|
|
transactionID = id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
path := apiPath
|
|||
|
|
if !strings.HasPrefix(path, "/") {
|
|||
|
|
path = "/" + path
|
|||
|
|
}
|
|||
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|||
|
|
reqURL := s.config.BaseURL + path + "?t=" + timestamp
|
|||
|
|
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogRequest(requestID, transactionID, apiPath, reqURL)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encryptedData, err := EncryptParams(params, s.config.SecretKey)
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, fmt.Errorf("请求加密失败: %w", err))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bodyBytes, err := json.Marshal(RequestPayload{Data: encryptedData})
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewBuffer(bodyBytes))
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
req.Header.Set(HeaderContentType, ContentTypeJSON)
|
|||
|
|
req.Header.Set(HeaderAccessID, s.config.AccessID)
|
|||
|
|
|
|||
|
|
resp, err := (&http.Client{Timeout: s.config.Timeout}).Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
err = wrapHTTPError(err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
respBody, err := io.ReadAll(resp.Body)
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
duration := time.Since(startTime)
|
|||
|
|
|
|||
|
|
if resp.StatusCode != http.StatusOK {
|
|||
|
|
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var apiResp APIResponse
|
|||
|
|
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if apiResp.Code != CodeSuccess {
|
|||
|
|
apiErr := NewHaiyuapiAPIError(apiResp.Code, apiResp.Message)
|
|||
|
|
err = errors.Join(GetErrByCode(apiResp.Code), apiErr)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, apiErr, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
plainResp, err := DecryptData(apiResp.Data, s.config.SecretKey)
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, fmt.Errorf("响应解密失败: %w", err))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogResponse(requestID, transactionID, apiPath, resp.StatusCode, duration)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return plainResp, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func wrapHTTPError(err error) error {
|
|||
|
|
if err == context.DeadlineExceeded {
|
|||
|
|
return errors.Join(ErrDatasource, err)
|
|||
|
|
}
|
|||
|
|
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
|||
|
|
return errors.Join(ErrDatasource, err)
|
|||
|
|
}
|
|||
|
|
switch err.Error() {
|
|||
|
|
case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled":
|
|||
|
|
return errors.Join(ErrDatasource, err)
|
|||
|
|
default:
|
|||
|
|
return errors.Join(ErrSystem, err)
|
|||
|
|
}
|
|||
|
|
}
|