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) } }