f
This commit is contained in:
34
config.yaml
34
config.yaml
@@ -679,3 +679,37 @@ huibo:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 🌐 诺尔智汇配置
|
||||
# ===========================================
|
||||
nuoer:
|
||||
url: "https://api.enolfax.com/enol/api"
|
||||
app_id: "t4qO2mR3"
|
||||
app_secret: "d1515bf9ed2f2fe063b5f4f7e2c50f0ec65bfd58"
|
||||
timeout: 4s
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
service_name: "nuoer"
|
||||
use_daily: true
|
||||
enable_level_separation: true
|
||||
# 各级别配置
|
||||
level_configs:
|
||||
info:
|
||||
max_size: 100
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
error:
|
||||
max_size: 200
|
||||
max_backups: 10
|
||||
max_age: 90
|
||||
compress: true
|
||||
warn:
|
||||
max_size: 100
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
8
go.mod
8
go.mod
@@ -7,6 +7,8 @@ require (
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
|
||||
github.com/chromedp/chromedp v0.13.2
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-playground/locales v0.14.1
|
||||
@@ -16,6 +18,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hibiken/asynq v0.25.1
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/qiniu/go-sdk/v7 v7.25.4
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
@@ -58,8 +61,6 @@ require (
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
|
||||
github.com/chromedp/chromedp v0.13.2 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
@@ -94,15 +95,12 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oschwald/geoip2-golang v1.13.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -244,6 +244,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -268,10 +269,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
||||
@@ -45,6 +45,7 @@ type Config struct {
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
Huibo HuiboConfig `mapstructure:"huibo"`
|
||||
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP服务器配置
|
||||
@@ -705,6 +706,34 @@ type HuiboLevelFileConfig struct {
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// NuoerConfig 诺尔智汇配置
|
||||
type NuoerConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
|
||||
Logging NuoerLoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
// NuoerLoggingConfig 诺尔智汇日志配置
|
||||
type NuoerLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
ServiceName string `mapstructure:"service_name"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]NuoerLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
// NuoerLevelFileConfig 诺尔智汇级别文件配置
|
||||
type NuoerLevelFileConfig struct {
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// DomainConfig 域名配置
|
||||
type DomainConfig struct {
|
||||
API string `mapstructure:"api"` // API域名
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/huibo"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/nuoer"
|
||||
"tyapi-server/internal/infrastructure/external/ocr"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
@@ -405,6 +406,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*shujubao.ShujubaoService, error) {
|
||||
return shujubao.NewShujubaoServiceWithConfig(cfg)
|
||||
},
|
||||
// NuoerService - 诺尔智汇服务
|
||||
func(cfg *config.Config) (*nuoer.NuoerService, error) {
|
||||
return nuoer.NewNuoerServiceWithConfig(cfg)
|
||||
},
|
||||
func(cfg *config.Config) *yushan.YushanService {
|
||||
return yushan.NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
|
||||
@@ -358,6 +358,11 @@ type QCXGGB2QReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
CarPlateType string `json:"carplate_type" validate:"required"`
|
||||
}
|
||||
|
||||
type QCXGM4CLReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type QCXGJJ2AReq struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
EngineNumber string `json:"engine_number" validate:"omitempty"`
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/huibo"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/nuoer"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
@@ -68,6 +69,7 @@ func NewApiRequestService(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
nuoerService *nuoer.NuoerService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *appconfig.Config,
|
||||
@@ -84,6 +86,7 @@ func NewApiRequestService(
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
huiboService,
|
||||
nuoerService,
|
||||
validator,
|
||||
productManagementService,
|
||||
cfg,
|
||||
@@ -105,6 +108,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
nuoerService *nuoer.NuoerService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *appconfig.Config,
|
||||
@@ -132,6 +136,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
huiboService,
|
||||
nuoerService,
|
||||
validator,
|
||||
combService,
|
||||
reportRepo,
|
||||
@@ -378,7 +383,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QCXG5U0Z": qcxg.ProcessQCXG5U0ZRequest, // 车辆静态信息查询 10479
|
||||
"QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443
|
||||
"QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验(ETC)10093 月更
|
||||
|
||||
"QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔
|
||||
// DWBG系列处理器 - 多维报告
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,
|
||||
|
||||
@@ -287,6 +287,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai
|
||||
"QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai
|
||||
"QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai
|
||||
"QCXGM4CL": &dto.QCXGM4CLReq{}, //名下车辆诺尔
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/huibo"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/nuoer"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
@@ -42,6 +43,7 @@ type ProcessorDependencies struct {
|
||||
JiguangService *jiguang.JiguangService
|
||||
ShumaiService *shumai.ShumaiService
|
||||
HuiboService *huibo.HuiboService
|
||||
NuoerService *nuoer.NuoerService
|
||||
Validator interfaces.RequestValidator
|
||||
CombService CombServiceInterface // Changed to interface to break import cycle
|
||||
Options *commands.ApiCallOptions // 添加Options支持
|
||||
@@ -70,6 +72,7 @@ func NewProcessorDependencies(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
nuoerService *nuoer.NuoerService,
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
reportRepo repositories.ReportRepository,
|
||||
@@ -88,6 +91,7 @@ func NewProcessorDependencies(
|
||||
JiguangService: jiguangService,
|
||||
ShumaiService: shumaiService,
|
||||
HuiboService: huiboService,
|
||||
NuoerService: nuoerService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
)
|
||||
|
||||
// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 极光名下车辆数量查询
|
||||
// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 名下车辆数量(委托诺尔 QCXGM4CL)
|
||||
func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG4D2EReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -21,27 +20,15 @@ func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
reqData := map[string]interface{}{
|
||||
"idNum": paramsDto.IDCard,
|
||||
"userType": paramsDto.UserType,
|
||||
}
|
||||
|
||||
// 调用极光API
|
||||
// apiCode: vehicle-inquiry-under-name (用于请求头)
|
||||
// apiPath: vehicle/inquiry-under-name (用于URL路径)
|
||||
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData)
|
||||
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
|
||||
if err != nil {
|
||||
// 根据错误类型返回相应的错误
|
||||
if errors.Is(err, jiguang.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, jiguang.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
return respBytes, nil
|
||||
return transformQCXG5F3AResponse(raw)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光名下车辆车牌查询 以替换数量
|
||||
// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 名下车辆(委托诺尔 QCXGM4CL,响应格式兼容极光)
|
||||
func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG5F3AReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -21,28 +23,53 @@ func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
reqData := map[string]interface{}{
|
||||
"idNum": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"userType": "1",
|
||||
}
|
||||
|
||||
// 调用极光API
|
||||
// apiCode: vehicle-inquiry-under-name (用于请求头)
|
||||
// apiPath: vehicle/inquiry-under-name (用于URL路径)
|
||||
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData)
|
||||
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
|
||||
if err != nil {
|
||||
// 根据错误类型返回相应的错误
|
||||
if errors.Is(err, jiguang.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, jiguang.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
return respBytes, nil
|
||||
return transformQCXG5F3AResponse(raw)
|
||||
}
|
||||
|
||||
// transformQCXG5F3AResponse 将诺尔响应转为 QCXG5F3A 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为字符串
|
||||
func transformQCXG5F3AResponse(raw []byte) ([]byte, error) {
|
||||
base := gjson.GetBytes(raw, "result")
|
||||
if !base.Exists() {
|
||||
base = gjson.ParseBytes(raw)
|
||||
}
|
||||
|
||||
list := base.Get("list").Value()
|
||||
if list == nil {
|
||||
list = []interface{}{}
|
||||
}
|
||||
|
||||
countStr, err := formatVehicleCountAsString(base.Get("vehicleCount"))
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
out := map[string]interface{}{
|
||||
"vehicleCount": countStr,
|
||||
"list": list,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func formatVehicleCountAsString(v gjson.Result) (string, error) {
|
||||
if !v.Exists() {
|
||||
return "0", nil
|
||||
}
|
||||
switch v.Type {
|
||||
case gjson.String:
|
||||
return v.String(), nil
|
||||
case gjson.Number:
|
||||
return strconv.FormatInt(v.Int(), 10), nil
|
||||
default:
|
||||
return "", errors.New("vehicleCount 类型无效")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 兼容旧版 极光名下车牌查询数量
|
||||
// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 - 名下车辆详版(委托诺尔 QCXGM4CL)
|
||||
func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG9P1CReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -24,54 +23,53 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
null := ""
|
||||
// 构建请求参数
|
||||
reqData := map[string]interface{}{
|
||||
"idNum": paramsDto.IDCard,
|
||||
"name": null,
|
||||
"userType": "1",
|
||||
}
|
||||
|
||||
// 调用极光API
|
||||
// apiCode: vehicle-inquiry-under-name (用于请求头)
|
||||
// apiPath: vehicle/inquiry-under-name (用于URL路径)
|
||||
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData)
|
||||
if err != nil {
|
||||
// 根据错误类型返回相应的错误
|
||||
if errors.Is(err, jiguang.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, jiguang.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 极光服务已经返回了 data 字段的 JSON,直接返回即可
|
||||
// return respBytes, nil
|
||||
|
||||
// 使用 gjson 检查并转换 vehicleCount 字段
|
||||
vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount")
|
||||
if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String {
|
||||
// 如果是字符串类型,转换为整数
|
||||
vehicleCountInt, err := strconv.Atoi(vehicleCountResult.String())
|
||||
m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard})
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
// 解析 JSON 并修改 vehicleCount 字段
|
||||
var respData map[string]interface{}
|
||||
if err := json.Unmarshal(respBytes, &respData); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respData["vehicleCount"] = vehicleCountInt
|
||||
// 重新序列化为JSON并返回
|
||||
resultBytes, err := json.Marshal(respData)
|
||||
|
||||
return transformQCXG9P1CResponse(raw)
|
||||
}
|
||||
|
||||
// transformQCXG9P1CResponse 将诺尔响应转为 QCXG9P1C 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为整数
|
||||
func transformQCXG9P1CResponse(raw []byte) ([]byte, error) {
|
||||
base := gjson.GetBytes(raw, "result")
|
||||
if !base.Exists() {
|
||||
base = gjson.ParseBytes(raw)
|
||||
}
|
||||
|
||||
list := base.Get("list").Value()
|
||||
if list == nil {
|
||||
list = []interface{}{}
|
||||
}
|
||||
|
||||
countInt, err := formatVehicleCountAsInt(base.Get("vehicleCount"))
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return resultBytes, nil
|
||||
|
||||
out := map[string]interface{}{
|
||||
"vehicleCount": countInt,
|
||||
"list": list,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// 如果 vehicleCount 不存在或不是字符串,直接返回原始响应
|
||||
return respBytes, nil
|
||||
func formatVehicleCountAsInt(v gjson.Result) (int, error) {
|
||||
if !v.Exists() {
|
||||
return 0, nil
|
||||
}
|
||||
switch v.Type {
|
||||
case gjson.String:
|
||||
return strconv.Atoi(v.String())
|
||||
case gjson.Number:
|
||||
return int(v.Int()), nil
|
||||
default:
|
||||
return 0, errors.New("vehicleCount 类型无效")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/nuoer"
|
||||
)
|
||||
|
||||
// ProcessQCXGM4CLRequest QCXGM4CL API处理方法 - 名下车辆诺尔
|
||||
func ProcessQCXGM4CLRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGM4CLReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"idCard": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
nuoerDoCheckAPIKey := "id_vehicle_query_102"
|
||||
ApiPath := "/v1/doCheck"
|
||||
|
||||
resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body)
|
||||
if err != nil {
|
||||
if errors.Is(err, nuoer.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
if errors.Is(err, nuoer.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
38
internal/infrastructure/external/nuoer/crypto.go
vendored
Normal file
38
internal/infrastructure/external/nuoer/crypto.go
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package nuoer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sign 根据 body 业务参数与 secret 生成 MD5 签名。
|
||||
// 规则:排除空值参数,按 key 的 ASCII 升序排序,拼接「参数名+参数值」后追加 secret,再 MD5(小写十六进制)。
|
||||
func Sign(body map[string]string, secret string) string {
|
||||
if len(body) == 0 {
|
||||
return genMD5(secret)
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(body))
|
||||
for k, v := range body {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var sb strings.Builder
|
||||
for _, k := range keys {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString(body[k])
|
||||
}
|
||||
sb.WriteString(secret)
|
||||
return genMD5(sb.String())
|
||||
}
|
||||
|
||||
func genMD5(s string) string {
|
||||
sum := md5.Sum([]byte(s))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
21
internal/infrastructure/external/nuoer/crypto_test.go
vendored
Normal file
21
internal/infrastructure/external/nuoer/crypto_test.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package nuoer
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"name": "张三",
|
||||
"mobile": "13290879000",
|
||||
"idCard": "330129199511153412",
|
||||
}
|
||||
secret := "secret"
|
||||
got := Sign(body, secret)
|
||||
if got == "" {
|
||||
t.Fatal("sign should not be empty")
|
||||
}
|
||||
// 文档示例:name张三mobile13290879000idCard330129199511153412secret
|
||||
want := genMD5("idCard330129199511153412mobile13290879000name张三secret")
|
||||
if got != want {
|
||||
t.Fatalf("sign mismatch: got %s want %s", got, want)
|
||||
}
|
||||
}
|
||||
141
internal/infrastructure/external/nuoer/nuoer_errors.go
vendored
Normal file
141
internal/infrastructure/external/nuoer/nuoer_errors.go
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
package nuoer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 平台层 code 返回码(见文档2)
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeResponseError = -1 // 响应异常
|
||||
)
|
||||
|
||||
// 业务层 busiCode 返回码(见文档2)
|
||||
const (
|
||||
BusiCodeSuccess = 10 // 查询成功【计费】
|
||||
BusiCodeNotFound = 1000 // 数据未查得
|
||||
BusiCodeInsufficientFund = 1001 // 账户余额不足
|
||||
BusiCodeAccountNotFound = 1002 // 账户信息不存在
|
||||
BusiCodeAppIDError = 1003 // appId异常
|
||||
BusiCodeProductError = 1004 // 产品编号异常
|
||||
BusiCodeAccountError = 1005 // 账号信息异常
|
||||
BusiCodeOverdraftLimit = 1006 // 透支余额已达上限
|
||||
BusiCodeDataRequestError = 1007 // 数据请求异常
|
||||
BusiCodeServiceNotOpen = 1009 // 服务尚未开通
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// platformCodeDesc 平台层 code -> 描述
|
||||
var platformCodeDesc = map[int]string{
|
||||
CodeSuccess: "成功",
|
||||
CodeResponseError: "响应异常",
|
||||
}
|
||||
|
||||
// busiCodeDesc 业务层 busiCode -> 描述
|
||||
var busiCodeDesc = map[int]string{
|
||||
BusiCodeSuccess: "查询成功【计费】",
|
||||
BusiCodeNotFound: "数据未查得",
|
||||
BusiCodeInsufficientFund: "账户余额不足",
|
||||
BusiCodeAccountNotFound: "账户信息不存在",
|
||||
BusiCodeAppIDError: "appId异常",
|
||||
BusiCodeProductError: "产品编号异常",
|
||||
BusiCodeAccountError: "账号信息异常",
|
||||
BusiCodeOverdraftLimit: "透支余额已达上限",
|
||||
BusiCodeDataRequestError: "数据请求异常",
|
||||
BusiCodeServiceNotOpen: "服务尚未开通",
|
||||
}
|
||||
|
||||
// GetPlatformCodeDesc 根据平台 code 获取描述
|
||||
func GetPlatformCodeDesc(code int) string {
|
||||
if desc, ok := platformCodeDesc[code]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetBusiCodeDesc 根据 busiCode 获取描述
|
||||
func GetBusiCodeDesc(busiCode int) string {
|
||||
if desc, ok := busiCodeDesc[busiCode]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// nuoerError 诺尔智汇平台层错误(响应 code 字段)
|
||||
type nuoerError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *nuoerError) Error() string {
|
||||
return fmt.Sprintf("诺尔智汇返回错误,code: %d,msg: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// NewNuoerError 创建平台层错误
|
||||
func NewNuoerError(code int, message string) *nuoerError {
|
||||
if message == "" {
|
||||
if desc := GetPlatformCodeDesc(code); desc != "" {
|
||||
message = desc
|
||||
} else {
|
||||
message = "诺尔智汇返回未知错误"
|
||||
}
|
||||
}
|
||||
return &nuoerError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
// nuoerBusiError 诺尔智汇业务层错误(data.busiCode 字段)
|
||||
type nuoerBusiError struct {
|
||||
BusiCode int
|
||||
BusiMsg string
|
||||
}
|
||||
|
||||
func (e *nuoerBusiError) Error() string {
|
||||
return fmt.Sprintf("诺尔智汇业务错误,busiCode: %d,busiMsg: %s", e.BusiCode, e.BusiMsg)
|
||||
}
|
||||
|
||||
// NewNuoerBusiError 创建业务层错误
|
||||
func NewNuoerBusiError(busiCode int, busiMsg string) *nuoerBusiError {
|
||||
if busiMsg == "" {
|
||||
if desc := GetBusiCodeDesc(busiCode); desc != "" {
|
||||
busiMsg = desc
|
||||
} else {
|
||||
busiMsg = "诺尔智汇业务返回未知错误"
|
||||
}
|
||||
}
|
||||
return &nuoerBusiError{BusiCode: busiCode, BusiMsg: busiMsg}
|
||||
}
|
||||
|
||||
// GetNotFoundErrByBusiCode 将 busiCode 映射为「查询为空」类错误(不扣费场景)
|
||||
func GetNotFoundErrByBusiCode(busiCode int) error {
|
||||
switch busiCode {
|
||||
case BusiCodeNotFound:
|
||||
return ErrNotFound
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetErrByBusiCode 将 busiCode 映射为内部哨兵错误,供处理器 errors.Is 判断
|
||||
func GetErrByBusiCode(busiCode int) error {
|
||||
if busiCode == BusiCodeSuccess {
|
||||
return nil
|
||||
}
|
||||
if notFound := GetNotFoundErrByBusiCode(busiCode); notFound != nil {
|
||||
return notFound
|
||||
}
|
||||
return ErrDatasource
|
||||
}
|
||||
|
||||
// GetErrByPlatformCode 将平台 code 映射为内部哨兵错误
|
||||
func GetErrByPlatformCode(code int) error {
|
||||
if code == CodeSuccess {
|
||||
return nil
|
||||
}
|
||||
return ErrDatasource
|
||||
}
|
||||
64
internal/infrastructure/external/nuoer/nuoer_factory.go
vendored
Normal file
64
internal/infrastructure/external/nuoer/nuoer_factory.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package nuoer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewNuoerServiceWithConfig 使用配置创建诺尔智汇服务
|
||||
func NewNuoerServiceWithConfig(cfg *config.Config) (*NuoerService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Nuoer.Logging.Enabled,
|
||||
LogDir: cfg.Nuoer.Logging.LogDir,
|
||||
ServiceName: "nuoer",
|
||||
UseDaily: cfg.Nuoer.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Nuoer.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
for level, levelCfg := range cfg.Nuoer.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[level] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: levelCfg.MaxSize,
|
||||
MaxBackups: levelCfg.MaxBackups,
|
||||
MaxAge: levelCfg.MaxAge,
|
||||
Compress: levelCfg.Compress,
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := cfg.Nuoer.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultRequestTimeout
|
||||
}
|
||||
|
||||
return NewNuoerService(
|
||||
cfg.Nuoer.URL,
|
||||
cfg.Nuoer.AppID,
|
||||
cfg.Nuoer.AppSecret,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewNuoerServiceWithLogging 使用自定义日志配置创建诺尔智汇服务
|
||||
func NewNuoerServiceWithLogging(url, appID, appSecret string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*NuoerService, error) {
|
||||
loggingConfig.ServiceName = "nuoer"
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewNuoerService(url, appID, appSecret, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewNuoerServiceSimple 创建无日志的诺尔智汇服务
|
||||
func NewNuoerServiceSimple(url, appID, appSecret string, timeout time.Duration) *NuoerService {
|
||||
return NewNuoerService(url, appID, appSecret, timeout, nil)
|
||||
}
|
||||
253
internal/infrastructure/external/nuoer/nuoer_service.go
vendored
Normal file
253
internal/infrastructure/external/nuoer/nuoer_service.go
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
package nuoer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user