Files
tyapi-server/internal/infrastructure/external/nuoer/nuoer_service.go

268 lines
7.7 KiB
Go
Raw Normal View History

2026-05-28 10:55:28 +08:00
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
2026-05-29 15:41:52 +08:00
// 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
}
2026-05-28 10:55:28 +08:00
// 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,
}
}
2026-05-29 15:41:52 +08:00
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)
2026-05-28 10:55:28 +08:00
}
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 {
2026-05-29 15:41:52 +08:00
s.logger.LogRequest("", transactionID, apiKey, requestURL)
2026-05-28 10:55:28 +08:00
}
bodyBytes, err := json.Marshal(requestPayload)
if err != nil {
err = errors.Join(ErrSystem, err)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
err = errors.Join(ErrSystem, err)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
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)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Join(ErrSystem, err)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
2026-05-29 15:41:52 +08:00
duration := time.Since(startTime)
2026-05-28 10:55:28 +08:00
if resp.StatusCode != http.StatusOK {
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode))
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
var nuoerResp nuoerResponse
if err := json.Unmarshal(respBody, &nuoerResp); err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, "", err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
if nuoerResp.Code != CodeSuccess {
2026-05-29 15:41:52 +08:00
if nuoerResp.Code == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) {
nuoerResp.Data = map[string]interface{}{}
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
return &nuoerResp, nil
}
2026-05-28 10:55:28 +08:00
nuoerErr := NewNuoerError(nuoerResp.Code, nuoerResp.Msg)
err = errors.Join(GetErrByPlatformCode(nuoerResp.Code), nuoerErr)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, nuoerResp.SeqNo, nuoerErr, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
if nuoerResp.Data == nil {
err = errors.Join(ErrSystem, errors.New("响应 data 为空"))
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
busiCode, busiMsg, ok := parseDataBusiInfo(nuoerResp.Data)
if !ok {
err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode"))
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
if busiCode != BusiCodeSuccess {
2026-05-29 15:41:52 +08:00
if busiCode == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) {
nuoerResp.Data = map[string]interface{}{}
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
return &nuoerResp, nil
}
2026-05-28 10:55:28 +08:00
busiErr := NewNuoerBusiError(busiCode, busiMsg)
err = errors.Join(GetErrByBusiCode(busiCode), busiErr)
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, nuoerResp.SeqNo, busiErr, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
cleanedData, err := stripBusiMetaFromData(nuoerResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err))
2026-05-29 15:41:52 +08:00
s.logError(transactionID, apiKey, nuoerResp.SeqNo, err, requestPayload)
2026-05-28 10:55:28 +08:00
return nil, err
}
nuoerResp.Data = cleanedData
2026-05-29 15:41:52 +08:00
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, nuoerResp.SeqNo)
2026-05-28 10:55:28 +08:00
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)
}
}