254 lines
6.6 KiB
Go
254 lines
6.6 KiB
Go
|
|
package nuoer
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"crypto/md5"
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"hyapi-server/internal/shared/external_logger"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const defaultRequestTimeout = 4 * time.Second
|
|||
|
|
|
|||
|
|
// nuoerResponse 诺尔智汇通用响应
|
|||
|
|
type nuoerResponse struct {
|
|||
|
|
Code int `json:"code"`
|
|||
|
|
Msg string `json:"msg"`
|
|||
|
|
SeqNo string `json:"seqNo"`
|
|||
|
|
Data interface{} `json:"data"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// serviceConfig 诺尔智汇服务运行时配置
|
|||
|
|
type serviceConfig struct {
|
|||
|
|
URL string
|
|||
|
|
AppID string
|
|||
|
|
AppSecret string
|
|||
|
|
Timeout time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NuoerService 诺尔智汇服务
|
|||
|
|
type NuoerService struct {
|
|||
|
|
config serviceConfig
|
|||
|
|
logger *external_logger.ExternalServiceLogger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewNuoerService 创建诺尔智汇服务实例
|
|||
|
|
func NewNuoerService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *NuoerService {
|
|||
|
|
if timeout <= 0 {
|
|||
|
|
timeout = defaultRequestTimeout
|
|||
|
|
}
|
|||
|
|
return &NuoerService{
|
|||
|
|
config: serviceConfig{
|
|||
|
|
URL: url,
|
|||
|
|
AppID: appID,
|
|||
|
|
AppSecret: appSecret,
|
|||
|
|
Timeout: timeout,
|
|||
|
|
},
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *NuoerService) generateRequestID() string {
|
|||
|
|
timestamp := time.Now().UnixNano()
|
|||
|
|
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppID)))
|
|||
|
|
return fmt.Sprintf("nuoer_%x", hash[:8])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *NuoerService) CallAPI(ctx context.Context, apiKey, apiPath string, body map[string]string) (*nuoerResponse, error) {
|
|||
|
|
requestURL := strings.TrimSuffix(s.config.URL, "/")
|
|||
|
|
if apiPath != "" {
|
|||
|
|
if !strings.HasPrefix(apiPath, "/") {
|
|||
|
|
apiPath = "/" + apiPath
|
|||
|
|
}
|
|||
|
|
requestURL += apiPath
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
requestID := s.generateRequestID()
|
|||
|
|
startTime := time.Now()
|
|||
|
|
|
|||
|
|
var transactionID string
|
|||
|
|
if id, ok := ctx.Value("transaction_id").(string); ok {
|
|||
|
|
transactionID = id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 对调用方传入的 body 全量参与加签(排除空值,按 key 升序,见 Sign)
|
|||
|
|
sign := Sign(body, s.config.AppSecret)
|
|||
|
|
|
|||
|
|
requestPayload := map[string]interface{}{
|
|||
|
|
"appId": s.config.AppID,
|
|||
|
|
"sign": sign,
|
|||
|
|
"apiKey": apiKey,
|
|||
|
|
"body": body,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogRequest(requestID, transactionID, apiKey, requestURL)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bodyBytes, err := json.Marshal(requestPayload)
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Content-Type", "application/json")
|
|||
|
|
|
|||
|
|
client := &http.Client{Timeout: s.config.Timeout}
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
err = wrapHTTPError(err)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
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, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogResponse(requestID, transactionID, apiKey, resp.StatusCode, 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, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var nuoerResp nuoerResponse
|
|||
|
|
if err := json.Unmarshal(respBody, &nuoerResp); err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if nuoerResp.Code != CodeSuccess {
|
|||
|
|
nuoerErr := NewNuoerError(nuoerResp.Code, nuoerResp.Msg)
|
|||
|
|
err = errors.Join(GetErrByPlatformCode(nuoerResp.Code), nuoerErr)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, nuoerErr, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if nuoerResp.Data == nil {
|
|||
|
|
err = errors.Join(ErrSystem, errors.New("响应 data 为空"))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
busiCode, busiMsg, ok := parseDataBusiInfo(nuoerResp.Data)
|
|||
|
|
if !ok {
|
|||
|
|
err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode"))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if busiCode != BusiCodeSuccess {
|
|||
|
|
busiErr := NewNuoerBusiError(busiCode, busiMsg)
|
|||
|
|
err = errors.Join(GetErrByBusiCode(busiCode), busiErr)
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, busiErr, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cleanedData, err := stripBusiMetaFromData(nuoerResp.Data)
|
|||
|
|
if err != nil {
|
|||
|
|
err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err))
|
|||
|
|
if s.logger != nil {
|
|||
|
|
s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload)
|
|||
|
|
}
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
nuoerResp.Data = cleanedData
|
|||
|
|
|
|||
|
|
return &nuoerResp, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// nuoerDataBusiMeta 业务层状态字段,仅用于解析校验,不对外返回
|
|||
|
|
type nuoerDataBusiMeta struct {
|
|||
|
|
BusiCode int `json:"busiCode"`
|
|||
|
|
BusiMsg string `json:"busiMsg"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// parseDataBusiInfo 从各接口不同的 data 结构中解析 busiCode、busiMsg
|
|||
|
|
func parseDataBusiInfo(data interface{}) (busiCode int, busiMsg string, ok bool) {
|
|||
|
|
if data == nil {
|
|||
|
|
return 0, "", false
|
|||
|
|
}
|
|||
|
|
raw, err := json.Marshal(data)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, "", false
|
|||
|
|
}
|
|||
|
|
var meta nuoerDataBusiMeta
|
|||
|
|
if err := json.Unmarshal(raw, &meta); err != nil {
|
|||
|
|
return 0, "", false
|
|||
|
|
}
|
|||
|
|
return meta.BusiCode, meta.BusiMsg, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// stripBusiMetaFromData 去掉 data 中的 busiCode、busiMsg,仅保留业务载荷
|
|||
|
|
func stripBusiMetaFromData(data interface{}) (interface{}, error) {
|
|||
|
|
raw, err := json.Marshal(data)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
var payload map[string]interface{}
|
|||
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
delete(payload, "busiCode")
|
|||
|
|
delete(payload, "busiMsg")
|
|||
|
|
return payload, 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)
|
|||
|
|
}
|
|||
|
|
}
|