Files
tyapi-server/internal/infrastructure/external/haiyuapi/haiyuapi_service.go
2026-06-01 14:39:45 +08:00

179 lines
4.8 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 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 调用海宇APIapiPath 如 /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)
}
}