Files
tyapi-server/internal/infrastructure/external/haiyuapi/haiyuapi_service.go

179 lines
4.8 KiB
Go
Raw Normal View History

2026-06-01 14:39:45 +08:00
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)
}
}