add jiyi
This commit is contained in:
31
config.yaml
31
config.yaml
@@ -716,6 +716,37 @@ nuoer:
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
# ===========================================
|
||||
# ✨ 集奕(JIYI)配置
|
||||
# ===========================================
|
||||
jiyi:
|
||||
url: "https://api.jiyidata.cn"
|
||||
app_id: "5DS2yMY9"
|
||||
app_secret: "16aa682d9b66e17def6aca82e14afc8931fca15d"
|
||||
timeout: 4s
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
service_name: "jiyi"
|
||||
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
|
||||
|
||||
# ===========================================
|
||||
# ✨ 汇博(BHSC)配置
|
||||
# ===========================================
|
||||
|
||||
@@ -197,6 +197,12 @@ nuoer:
|
||||
url: "https://api.enolfax.com/enol/api"
|
||||
app_id: "t4qO2mR3"
|
||||
app_secret: "d1515bf9ed2f2fe063b5f4f7e2c50f0ec65bfd58"
|
||||
|
||||
# 集奕配置
|
||||
jiyi:
|
||||
url: "https://api.jiyidata.cn"
|
||||
app_id: "5DS2yMY9"
|
||||
app_secret: "16aa682d9b66e17def6aca82e14afc8931fca15d"
|
||||
development:
|
||||
enable_cors: true
|
||||
cors_allowed_origins: "http://localhost:5173,http://localhost:8080"
|
||||
|
||||
@@ -1087,6 +1087,10 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||
"JRZQK9P2": &dto.JRZQK9P2Req{}, //洞侦1.0
|
||||
"JRZQR4N7": &dto.JRZQR4N7Req{}, //借贷意向验证3.0
|
||||
"JRZQH6M3": &dto.JRZQH6M3Req{}, //无间司南-纯黑A版
|
||||
"JRZQW3L8": &dto.JRZQW3L8Req{}, //信用司南
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ type Config struct {
|
||||
Tianyuanapi TianyuanapiConfig `mapstructure:"tianyuanapi"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
||||
Jiyi JiyiConfig `mapstructure:"jiyi"`
|
||||
Huibo HuiboConfig `mapstructure:"huibo"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
}
|
||||
@@ -738,6 +739,33 @@ type NuoerLevelFileConfig struct {
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// JiyiConfig 集奕配置
|
||||
type JiyiConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
|
||||
Logging JiyiLoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
// JiyiLoggingConfig 集奕日志配置
|
||||
type JiyiLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]JiyiLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
// JiyiLevelFileConfig 集奕日志级别配置
|
||||
type JiyiLevelFileConfig struct {
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// HuiboConfig 汇博配置
|
||||
type HuiboConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
"hyapi-server/internal/infrastructure/external/email"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||
"hyapi-server/internal/infrastructure/external/ocr"
|
||||
@@ -415,6 +416,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*nuoer.NuoerService, error) {
|
||||
return nuoer.NewNuoerServiceWithConfig(cfg)
|
||||
},
|
||||
// JiyiService - 集奕服务
|
||||
func(cfg *config.Config) (*jiyi.JiyiService, error) {
|
||||
return jiyi.NewJiyiServiceWithConfig(cfg)
|
||||
},
|
||||
// HuiboService - 汇博服务
|
||||
func(cfg *config.Config) (*huibo.HuiboService, error) {
|
||||
return huibo.NewHuiboServiceWithConfig(cfg)
|
||||
|
||||
@@ -211,6 +211,35 @@ type JRZQTH74Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQK9P2Req 洞侦1.0
|
||||
type JRZQK9P2Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQR4N7Req 借贷意向验证3.0
|
||||
type JRZQR4N7Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQH6M3Req 无间司南-纯黑A版
|
||||
type JRZQH6M3Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQW3L8Req 信用司南
|
||||
type JRZQW3L8Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type QCXG76VAReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
jiyiext "hyapi-server/internal/infrastructure/external/jiyi"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
nuoerext "hyapi-server/internal/infrastructure/external/nuoer"
|
||||
"hyapi-server/internal/infrastructure/external/shujubao"
|
||||
@@ -66,6 +67,7 @@ func NewApiRequestService(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||
nuoerService *nuoerext.NuoerService,
|
||||
jiyiService *jiyiext.JiyiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
@@ -83,6 +85,7 @@ func NewApiRequestService(
|
||||
jiguangService,
|
||||
tianyuanapiService,
|
||||
nuoerService,
|
||||
jiyiService,
|
||||
huiboService,
|
||||
validator,
|
||||
productManagementService,
|
||||
@@ -105,6 +108,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||
nuoerService *nuoerext.NuoerService,
|
||||
jiyiService *jiyiext.JiyiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
@@ -133,6 +137,7 @@ func NewApiRequestServiceWithRepos(
|
||||
jiguangService,
|
||||
tianyuanapiService,
|
||||
nuoerService,
|
||||
jiyiService,
|
||||
huiboService,
|
||||
validator,
|
||||
combService,
|
||||
@@ -193,6 +198,10 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQTH74": jrzq.ProcessJRZQTH74Request, //团伙欺诈
|
||||
"JRZQGJ99": jrzq.ProcessJRZQGJ99Request, //公积金核验
|
||||
"JRZQFC59": jrzq.ProcessJRZQFC59Request, //房产核验
|
||||
"JRZQK9P2": jrzq.ProcessJRZQK9P2Request, //洞侦1.0
|
||||
"JRZQR4N7": jrzq.ProcessJRZQR4N7Request, //借贷意向验证3.0
|
||||
"JRZQH6M3": jrzq.ProcessJRZQH6M3Request, //无间司南-纯黑A版
|
||||
"JRZQW3L8": jrzq.ProcessJRZQW3L8Request, //信用司南
|
||||
|
||||
// QYGL系列处理器
|
||||
"QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
||||
|
||||
@@ -312,6 +312,10 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"JRZQTH74": &dto.JRZQTH74Req{}, //团伙欺诈
|
||||
"JRZQGJ99": &dto.JRZQGJ99Req{}, //公积金核验
|
||||
"JRZQFC59": &dto.JRZQFC59Req{}, //房产核验
|
||||
"JRZQK9P2": &dto.JRZQK9P2Req{}, //洞侦1.0
|
||||
"JRZQR4N7": &dto.JRZQR4N7Req{}, //借贷意向验证3.0
|
||||
"JRZQH6M3": &dto.JRZQH6M3Req{}, //无间司南-纯黑A版
|
||||
"JRZQW3L8": &dto.JRZQW3L8Req{}, //信用司南
|
||||
"IVYZX7J9": &dto.IVYZX7J9Req{}, //学籍核验
|
||||
"QCXG6U5G": &dto.QCXG6U5GReq{}, //车辆核验
|
||||
"QCXG7K2N": &dto.QCXG7K2NReq{}, //人车核验加强版
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||
"hyapi-server/internal/infrastructure/external/huibo"
|
||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
"hyapi-server/internal/infrastructure/external/muzi"
|
||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||
"hyapi-server/internal/infrastructure/external/shujubao"
|
||||
@@ -43,6 +44,7 @@ type ProcessorDependencies struct {
|
||||
JiguangService *jiguang.JiguangService
|
||||
TianyuanapiService *tianyuanapi.TianyuanapiService
|
||||
NuoerService *nuoer.NuoerService
|
||||
JiyiService *jiyi.JiyiService
|
||||
HuiboService *huibo.HuiboService
|
||||
Validator interfaces.RequestValidator
|
||||
CombService CombServiceInterface // Changed to interface to break import cycle
|
||||
@@ -72,6 +74,7 @@ func NewProcessorDependencies(
|
||||
jiguangService *jiguang.JiguangService,
|
||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||
nuoerService *nuoer.NuoerService,
|
||||
jiyiService *jiyi.JiyiService,
|
||||
huiboService *huibo.HuiboService,
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
@@ -91,6 +94,7 @@ func NewProcessorDependencies(
|
||||
JiguangService: jiguangService,
|
||||
TianyuanapiService: tianyuanapiService,
|
||||
NuoerService: nuoerService,
|
||||
JiyiService: jiyiService,
|
||||
HuiboService: huiboService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQH6M3Request 无间司南-纯黑A版(上游 jy000052)
|
||||
func ProcessJRZQH6M3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQH6M3Req
|
||||
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{
|
||||
"name": paramsDto.Name,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000052"
|
||||
apiPath := "/api/v1/wujsinan/blacka"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, 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
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQK9P2Request 洞侦1.0(上游 jy000022)
|
||||
func ProcessJRZQK9P2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQK9P2Req
|
||||
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{
|
||||
"name": paramsDto.Name,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000022"
|
||||
apiPath := "/api/v1/cave/invest"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.CaveInvestOptions(0))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, 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
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQR4N7Request 借贷意向验证3.0(上游 jy000042)
|
||||
func ProcessJRZQR4N7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQR4N7Req
|
||||
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{
|
||||
"name": paramsDto.Name,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000042"
|
||||
apiPath := "/api/v1/loaninten/ver3"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, 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
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQW3L8Request 信用司南(上游 jy000004,查空不计费)
|
||||
func ProcessJRZQW3L8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQW3L8Req
|
||||
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{
|
||||
"name": paramsDto.Name,
|
||||
"idCard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000004"
|
||||
apiPath := "/api/v1/credit/panorama"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, 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
|
||||
}
|
||||
40
internal/infrastructure/external/jiyi/call_options.go
vendored
Normal file
40
internal/infrastructure/external/jiyi/call_options.go
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package jiyi
|
||||
|
||||
// CallMode 请求组装模式(不同产品加密字段位置不同)
|
||||
type CallMode int
|
||||
|
||||
const (
|
||||
// CallModePlain 明文,不附加 encryptType/encryptionType/timestamp
|
||||
CallModePlain CallMode = iota
|
||||
// CallModeBodyEncrypt 洞侦类:body.encryptType,0 明文 / 1 MD5,并自动补 timestamp
|
||||
CallModeBodyEncrypt
|
||||
// CallModeTopEncrypt 借贷意向类:顶层 encryptionType,1 明文 / 2 MD5
|
||||
CallModeTopEncrypt
|
||||
)
|
||||
|
||||
// CallOptions 调用选项
|
||||
type CallOptions struct {
|
||||
Mode CallMode
|
||||
// EncryptValue:
|
||||
// - CallModeBodyEncrypt:0 明文,1 MD5(默认 0)
|
||||
// - CallModeTopEncrypt:1 明文,2 MD5(默认 1)
|
||||
EncryptValue int
|
||||
}
|
||||
|
||||
// DefaultCallOptions 默认:明文且不注入额外字段
|
||||
func DefaultCallOptions() CallOptions {
|
||||
return CallOptions{Mode: CallModePlain}
|
||||
}
|
||||
|
||||
// CaveInvestOptions 洞侦1.0 等:body 内 encryptType + timestamp
|
||||
func CaveInvestOptions(encryptType int) CallOptions {
|
||||
return CallOptions{Mode: CallModeBodyEncrypt, EncryptValue: encryptType}
|
||||
}
|
||||
|
||||
// TopEncryptionOptions 借贷意向验证3.0 等:顶层 encryptionType
|
||||
func TopEncryptionOptions(encryptionType int) CallOptions {
|
||||
if encryptionType <= 0 {
|
||||
encryptionType = 1
|
||||
}
|
||||
return CallOptions{Mode: CallModeTopEncrypt, EncryptValue: encryptionType}
|
||||
}
|
||||
67
internal/infrastructure/external/jiyi/crypto.go
vendored
Normal file
67
internal/infrastructure/external/jiyi/crypto.go
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
package jiyi
|
||||
|
||||
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[:])
|
||||
}
|
||||
|
||||
// MD5Encrypt 对业务入参做 MD5 加密(小写十六进制),用于 encryptType=1。
|
||||
func MD5Encrypt(plaintext string) string {
|
||||
return genMD5(plaintext)
|
||||
}
|
||||
|
||||
// encryptBodyMD5 将三要素等敏感字段值转为 MD5 密文(保留 encryptType、timestamp)。
|
||||
func encryptBodyMD5(body map[string]string) map[string]string {
|
||||
if len(body) == 0 {
|
||||
return body
|
||||
}
|
||||
skip := map[string]struct{}{
|
||||
"encryptType": {},
|
||||
"timestamp": {},
|
||||
}
|
||||
encrypted := make(map[string]string, len(body))
|
||||
for k, v := range body {
|
||||
if _, ok := skip[k]; ok {
|
||||
encrypted[k] = v
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(v) == "" {
|
||||
encrypted[k] = v
|
||||
continue
|
||||
}
|
||||
encrypted[k] = MD5Encrypt(v)
|
||||
}
|
||||
return encrypted
|
||||
}
|
||||
43
internal/infrastructure/external/jiyi/crypto_test.go
vendored
Normal file
43
internal/infrastructure/external/jiyi/crypto_test.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package jiyi
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
// 与文档 Java 示例一致的待签字符串顺序
|
||||
body := map[string]string{
|
||||
"idNo": "440181199807041111",
|
||||
"mobile": "13000000000",
|
||||
"name": "黄雅莉",
|
||||
"encryptType": "0",
|
||||
"timestamp": "1720940866832",
|
||||
}
|
||||
secret := "a661a2728f2a54b71601d3e8a7f6210d650c2018"
|
||||
got := Sign(body, secret)
|
||||
want := genMD5("encryptType0idNo440181199807041111mobile13000000000name黄雅莉timestamp1720940866832" + secret)
|
||||
if got != want {
|
||||
t.Fatalf("sign mismatch: got %s want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptBodyMD5(t *testing.T) {
|
||||
body := map[string]string{
|
||||
"idNo": "440181199807041111",
|
||||
"mobile": "13000000000",
|
||||
"name": "黄雅莉",
|
||||
"encryptType": "1",
|
||||
"timestamp": "1720940866832",
|
||||
}
|
||||
got := encryptBodyMD5(body)
|
||||
if got["encryptType"] != "1" || got["timestamp"] != "1720940866832" {
|
||||
t.Fatalf("meta fields should keep plaintext: %+v", got)
|
||||
}
|
||||
if got["idNo"] != MD5Encrypt("440181199807041111") {
|
||||
t.Fatalf("idNo not encrypted: %s", got["idNo"])
|
||||
}
|
||||
if got["mobile"] != MD5Encrypt("13000000000") {
|
||||
t.Fatalf("mobile not encrypted: %s", got["mobile"])
|
||||
}
|
||||
if got["name"] != MD5Encrypt("黄雅莉") {
|
||||
t.Fatalf("name not encrypted: %s", got["name"])
|
||||
}
|
||||
}
|
||||
53
internal/infrastructure/external/jiyi/empty_result_test.go
vendored
Normal file
53
internal/infrastructure/external/jiyi/empty_result_test.go
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
package jiyi
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsEmptyBusinessResult(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
empty bool
|
||||
}{
|
||||
{"nil_map", map[string]interface{}{}, true},
|
||||
{"missing_result", map[string]interface{}{"foo": 1}, true},
|
||||
{"nil_result", map[string]interface{}{"result": nil}, true},
|
||||
{"empty_result_map", map[string]interface{}{"result": map[string]interface{}{}}, true},
|
||||
{"black_tags_zero_not_empty", map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"black_list": "0",
|
||||
"black_tag04": "0",
|
||||
},
|
||||
}, false},
|
||||
{"score_present", map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"score": "617",
|
||||
"reason": "",
|
||||
"contents": map[string]interface{}{"TC_A001": "0"},
|
||||
},
|
||||
}, false},
|
||||
{"all_blank_strings", map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"score": "",
|
||||
"reason": "",
|
||||
},
|
||||
}, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := isEmptyBusinessResult(tc.data)
|
||||
if got != tc.empty {
|
||||
t.Fatalf("got %v want %v", got, tc.empty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsQueryBillingAPIKey(t *testing.T) {
|
||||
if !isQueryBillingAPIKey("jy000022") || !isQueryBillingAPIKey("jy000042") || !isQueryBillingAPIKey("jy000052") {
|
||||
t.Fatal("查空计费产品应在白名单中")
|
||||
}
|
||||
if isQueryBillingAPIKey("jy000004") {
|
||||
t.Fatal("信用司南查空不计费,不应在白名单中")
|
||||
}
|
||||
}
|
||||
119
internal/infrastructure/external/jiyi/jiyi_errors.go
vendored
Normal file
119
internal/infrastructure/external/jiyi/jiyi_errors.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package jiyi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 平台层 code 返回码
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeResponseError = -1 // 异常
|
||||
)
|
||||
|
||||
// 业务层 busiCode 返回码(洞侦等接口通用)
|
||||
const (
|
||||
BusiCodeSuccess = 10 // 查询成功
|
||||
BusiCodeNotFound = 1000 // 未查得
|
||||
BusiCodeSignError = 1005 // 签名异常
|
||||
BusiCodeDataRequestError = 1007 // 数据请求异常
|
||||
BusiCodeParamError = 1010 // 参数错误
|
||||
BusiCodeUnknown = -1 // 未知错误
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
var platformCodeDesc = map[int]string{
|
||||
CodeSuccess: "成功",
|
||||
CodeResponseError: "异常",
|
||||
}
|
||||
|
||||
var busiCodeDesc = map[int]string{
|
||||
BusiCodeSuccess: "查询成功",
|
||||
BusiCodeNotFound: "未查得",
|
||||
BusiCodeSignError: "签名异常",
|
||||
BusiCodeDataRequestError: "数据请求异常",
|
||||
BusiCodeParamError: "参数错误",
|
||||
BusiCodeUnknown: "未知错误",
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
type jiyiError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *jiyiError) Error() string {
|
||||
return fmt.Sprintf("集奕返回错误,code: %d,msg: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// NewJiyiError 创建平台层错误
|
||||
func NewJiyiError(code int, message string) *jiyiError {
|
||||
if message == "" {
|
||||
if desc := GetPlatformCodeDesc(code); desc != "" {
|
||||
message = desc
|
||||
} else {
|
||||
message = "集奕返回未知错误"
|
||||
}
|
||||
}
|
||||
return &jiyiError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
type jiyiBusiError struct {
|
||||
BusiCode int
|
||||
BusiMsg string
|
||||
}
|
||||
|
||||
func (e *jiyiBusiError) Error() string {
|
||||
return fmt.Sprintf("集奕业务错误,busiCode: %d,busiMsg: %s", e.BusiCode, e.BusiMsg)
|
||||
}
|
||||
|
||||
// NewJiyiBusiError 创建业务层错误
|
||||
func NewJiyiBusiError(busiCode int, busiMsg string) *jiyiBusiError {
|
||||
if busiMsg == "" {
|
||||
if desc := GetBusiCodeDesc(busiCode); desc != "" {
|
||||
busiMsg = desc
|
||||
} else {
|
||||
busiMsg = "集奕业务返回未知错误"
|
||||
}
|
||||
}
|
||||
return &jiyiBusiError{BusiCode: busiCode, BusiMsg: busiMsg}
|
||||
}
|
||||
|
||||
// GetErrByBusiCode 将 busiCode 映射为内部哨兵错误
|
||||
func GetErrByBusiCode(busiCode int) error {
|
||||
if busiCode == BusiCodeSuccess {
|
||||
return nil
|
||||
}
|
||||
if busiCode == BusiCodeNotFound {
|
||||
return ErrNotFound
|
||||
}
|
||||
return ErrDatasource
|
||||
}
|
||||
|
||||
// GetErrByPlatformCode 将平台 code 映射为内部哨兵错误
|
||||
func GetErrByPlatformCode(code int) error {
|
||||
if code == CodeSuccess {
|
||||
return nil
|
||||
}
|
||||
return ErrDatasource
|
||||
}
|
||||
64
internal/infrastructure/external/jiyi/jiyi_factory.go
vendored
Normal file
64
internal/infrastructure/external/jiyi/jiyi_factory.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package jiyi
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewJiyiServiceWithConfig 使用配置创建集奕服务
|
||||
func NewJiyiServiceWithConfig(cfg *config.Config) (*JiyiService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Jiyi.Logging.Enabled,
|
||||
LogDir: cfg.Jiyi.Logging.LogDir,
|
||||
ServiceName: "jiyi",
|
||||
UseDaily: cfg.Jiyi.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Jiyi.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
for level, levelCfg := range cfg.Jiyi.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.Jiyi.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultRequestTimeout
|
||||
}
|
||||
|
||||
return NewJiyiService(
|
||||
cfg.Jiyi.URL,
|
||||
cfg.Jiyi.AppID,
|
||||
cfg.Jiyi.AppSecret,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewJiyiServiceWithLogging 使用自定义日志配置创建集奕服务
|
||||
func NewJiyiServiceWithLogging(url, appID, appSecret string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*JiyiService, error) {
|
||||
loggingConfig.ServiceName = "jiyi"
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewJiyiService(url, appID, appSecret, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewJiyiServiceSimple 创建无日志的集奕服务
|
||||
func NewJiyiServiceSimple(url, appID, appSecret string, timeout time.Duration) *JiyiService {
|
||||
return NewJiyiService(url, appID, appSecret, timeout, nil)
|
||||
}
|
||||
351
internal/infrastructure/external/jiyi/jiyi_service.go
vendored
Normal file
351
internal/infrastructure/external/jiyi/jiyi_service.go
vendored
Normal file
@@ -0,0 +1,351 @@
|
||||
package jiyi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const defaultRequestTimeout = 4 * time.Second
|
||||
|
||||
// queryBillingAPIKeys 查询计费接口:未查得/空结果仍按成功返回空数据,由平台侧计费
|
||||
var queryBillingAPIKeys = map[string]struct{}{
|
||||
"jy000022": {}, // 洞侦1.0
|
||||
"jy000042": {}, // 借贷意向验证3.0
|
||||
"jy000052": {}, // 无间司南-纯黑A版
|
||||
}
|
||||
|
||||
func isQueryBillingAPIKey(apiKey string) bool {
|
||||
_, ok := queryBillingAPIKeys[apiKey]
|
||||
return ok
|
||||
}
|
||||
|
||||
// jiyiResponse 集奕通用响应
|
||||
type jiyiResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Fee string `json:"fee"`
|
||||
SeqNo string `json:"seqNo"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// Response 对外暴露的响应别名,便于处理器取值
|
||||
type Response = jiyiResponse
|
||||
|
||||
type serviceConfig struct {
|
||||
URL string
|
||||
AppID string
|
||||
AppSecret string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// JiyiService 集奕数据服务
|
||||
type JiyiService struct {
|
||||
config serviceConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewJiyiService 创建集奕服务实例
|
||||
func NewJiyiService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *JiyiService {
|
||||
if timeout <= 0 {
|
||||
timeout = defaultRequestTimeout
|
||||
}
|
||||
return &JiyiService{
|
||||
config: serviceConfig{
|
||||
URL: url,
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JiyiService) 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 *JiyiService) logError(transactionID, apiKey, seqNo string, err error, payload interface{}) {
|
||||
if s.logger == nil {
|
||||
return
|
||||
}
|
||||
s.logger.LogError(seqNo, transactionID, apiKey, err, payload)
|
||||
}
|
||||
|
||||
// CallAPI 调用集奕接口。opts 为空时按明文、不注入额外字段处理。
|
||||
func (s *JiyiService) CallAPI(ctx context.Context, apiKey, apiPath string, body map[string]string, opts ...CallOptions) (*Response, 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
|
||||
}
|
||||
|
||||
opt := DefaultCallOptions()
|
||||
if len(opts) > 0 {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
requestBody := cloneBody(body)
|
||||
switch opt.Mode {
|
||||
case CallModeBodyEncrypt:
|
||||
encType := opt.EncryptValue
|
||||
if _, ok := requestBody["timestamp"]; !ok || strings.TrimSpace(requestBody["timestamp"]) == "" {
|
||||
requestBody["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
}
|
||||
requestBody["encryptType"] = strconv.Itoa(encType)
|
||||
if encType == 1 {
|
||||
requestBody = encryptBodyMD5(requestBody)
|
||||
}
|
||||
case CallModeTopEncrypt:
|
||||
if opt.EncryptValue == 2 {
|
||||
requestBody = encryptBodyMD5(requestBody)
|
||||
}
|
||||
}
|
||||
|
||||
sign := Sign(requestBody, s.config.AppSecret)
|
||||
|
||||
requestPayload := map[string]interface{}{
|
||||
"appId": s.config.AppID,
|
||||
"sign": sign,
|
||||
"apiKey": apiKey,
|
||||
"body": requestBody,
|
||||
}
|
||||
if opt.Mode == CallModeTopEncrypt {
|
||||
encVal := opt.EncryptValue
|
||||
if encVal <= 0 {
|
||||
encVal = 1
|
||||
}
|
||||
// 顶层 encryptionType:部分接口要求 JSON number,统一传整型
|
||||
requestPayload["encryptionType"] = encVal
|
||||
}
|
||||
|
||||
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 jiyiResp jiyiResponse
|
||||
if err := json.Unmarshal(respBody, &jiyiResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
s.logError(transactionID, apiKey, "", err, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if jiyiResp.Code != CodeSuccess {
|
||||
jiyiErr := NewJiyiError(jiyiResp.Code, jiyiResp.Msg)
|
||||
err = errors.Join(GetErrByPlatformCode(jiyiResp.Code), jiyiErr)
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, jiyiErr, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if jiyiResp.Data == nil {
|
||||
// 部分接口查空时 data 可能为 null
|
||||
if isQueryBillingAPIKey(apiKey) {
|
||||
jiyiResp.Data = map[string]interface{}{}
|
||||
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo)
|
||||
return &jiyiResp, nil
|
||||
}
|
||||
err = errors.Join(ErrNotFound, errors.New("响应 data 为空"))
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
busiCode, busiMsg, ok := parseDataBusiInfo(jiyiResp.Data)
|
||||
if !ok || (busiCode == 0 && strings.TrimSpace(busiMsg) == "") {
|
||||
err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode"))
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if busiCode != BusiCodeSuccess {
|
||||
// 查询计费产品:未查得仍返回空数据,由平台计费
|
||||
if busiCode == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) {
|
||||
jiyiResp.Data = map[string]interface{}{}
|
||||
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo)
|
||||
return &jiyiResp, nil
|
||||
}
|
||||
busiErr := NewJiyiBusiError(busiCode, busiMsg)
|
||||
err = errors.Join(GetErrByBusiCode(busiCode), busiErr)
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, busiErr, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cleanedData, err := stripBusiMetaFromData(jiyiResp.Data)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err))
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
if isEmptyBusinessResult(cleanedData) {
|
||||
if isQueryBillingAPIKey(apiKey) {
|
||||
jiyiResp.Data = map[string]interface{}{}
|
||||
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo)
|
||||
return &jiyiResp, nil
|
||||
}
|
||||
emptyErr := NewJiyiBusiError(BusiCodeNotFound, "未查得")
|
||||
err = errors.Join(ErrNotFound, emptyErr)
|
||||
s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload)
|
||||
return nil, err
|
||||
}
|
||||
jiyiResp.Data = cleanedData
|
||||
|
||||
s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo)
|
||||
|
||||
return &jiyiResp, nil
|
||||
}
|
||||
|
||||
type jiyiDataBusiMeta struct {
|
||||
BusiCode int `json:"busiCode"`
|
||||
BusiMsg string `json:"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 jiyiDataBusiMeta
|
||||
if err := json.Unmarshal(raw, &meta); err != nil {
|
||||
return 0, "", false
|
||||
}
|
||||
return meta.BusiCode, meta.BusiMsg, true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// isEmptyBusinessResult 判断业务载荷是否为空(无 result,或 result 无有效内容)
|
||||
func isEmptyBusinessResult(data interface{}) bool {
|
||||
payload, ok := data.(map[string]interface{})
|
||||
if !ok || len(payload) == 0 {
|
||||
return true
|
||||
}
|
||||
result, exists := payload["result"]
|
||||
if !exists || result == nil {
|
||||
return true
|
||||
}
|
||||
return isEmptyValue(result)
|
||||
}
|
||||
|
||||
func isEmptyValue(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(x) == ""
|
||||
case map[string]interface{}:
|
||||
if len(x) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range x {
|
||||
if !isEmptyValue(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case []interface{}:
|
||||
return len(x) == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func cloneBody(body map[string]string) map[string]string {
|
||||
if body == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
cloned := make(map[string]string, len(body)+2)
|
||||
for k, v := range body {
|
||||
cloned[k] = v
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
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