Files
tyapi-server/internal/infrastructure/external/nuoer/nuoer_service.go
2026-05-29 15:41:52 +08:00

268 lines
7.7 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 nuoer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
)
const defaultRequestTimeout = 4 * time.Second
// queryBillingAPIKeys 查询计费接口:数据未查得(busiCode=1000)仍按成功计费,返回空数据
var queryBillingAPIKeys = map[string]struct{}{
"idRiskTagV106": {}, // 身份风险V106
"personalLawsuit_cv2": {}, // 企业诉讼定制版
"personalLawsuit_cv1": {}, // 个人诉讼定制版
"loanRiskTagV11": {}, // 借贷意向查询
"loanRiskTagV5": {}, // 风险变量V5
"loanRiskTagV12": {}, // 特殊名单
"blackListV121_3_1": {}, // 债务逾期黑名单V3_1
"blackListV110": {}, // 特殊名单V110
"zhiTongModelG": {}, // 智瞳-通用版
"zhitong_ultra_v4_score": {}, // 智瞳分尊享版
"zhixiangScore": {}, // 智享分
}
func isQueryBillingAPIKey(apiKey string) bool {
_, ok := queryBillingAPIKeys[apiKey]
return ok
}
// 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) logResponse(transactionID, apiKey string, statusCode int, duration time.Duration, seqNo string) {
if s.logger == nil {
return
}
s.logger.LogResponse(seqNo, transactionID, apiKey, statusCode, duration)
}
func (s *NuoerService) logError(transactionID, apiKey, seqNo string, err error, payload interface{}) {
if s.logger == nil {
return
}
s.logger.LogError(seqNo, transactionID, apiKey, err, payload)
}
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
}
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("", transactionID, apiKey, requestURL)
}
bodyBytes, err := json.Marshal(requestPayload)
if err != nil {
err = errors.Join(ErrSystem, err)
s.logError(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)
s.logError(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)
s.logError(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)
s.logError(transactionID, apiKey, "", err, requestPayload)
return nil, err
}
duration := time.Since(startTime)
if resp.StatusCode != http.StatusOK {
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode))
s.logError(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))
s.logError(transactionID, apiKey, "", err, requestPayload)
return nil, err
}
if nuoerResp.Code != CodeSuccess {
if nuoerResp.Code == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) {
nuoerResp.Data = map[string]interface{}{}
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
return &nuoerResp, nil
}
nuoerErr := NewNuoerError(nuoerResp.Code, nuoerResp.Msg)
err = errors.Join(GetErrByPlatformCode(nuoerResp.Code), nuoerErr)
s.logError(transactionID, apiKey, nuoerResp.SeqNo, nuoerErr, requestPayload)
return nil, err
}
if nuoerResp.Data == nil {
err = errors.Join(ErrSystem, errors.New("响应 data 为空"))
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
return nil, err
}
busiCode, busiMsg, ok := parseDataBusiInfo(nuoerResp.Data)
if !ok {
err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode"))
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
return nil, err
}
if busiCode != BusiCodeSuccess {
if busiCode == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) {
nuoerResp.Data = map[string]interface{}{}
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
return &nuoerResp, nil
}
busiErr := NewNuoerBusiError(busiCode, busiMsg)
err = errors.Join(GetErrByBusiCode(busiCode), busiErr)
s.logError(transactionID, apiKey, nuoerResp.SeqNo, busiErr, requestPayload)
return nil, err
}
cleanedData, err := stripBusiMetaFromData(nuoerResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err))
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
return nil, err
}
nuoerResp.Data = cleanedData
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
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)
}
}