f
This commit is contained in:
37
config.yaml
37
config.yaml
@@ -593,3 +593,40 @@ shumai:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数据宝配置走实时接口
|
||||
# ===========================================
|
||||
shujubao:
|
||||
url: "https://api.chinadatapay.com"
|
||||
app_secret: "iOk0ALBX0BSdTSTf"
|
||||
sign_method: "md5" # 签名方法:md5 或 hmac,默认 hmac
|
||||
timeout: 60s # 请求超时时间,默认 60 秒
|
||||
# 数据宝日志配置
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
service_name: "shujubao"
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ type Config struct {
|
||||
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
}
|
||||
|
||||
@@ -582,6 +583,33 @@ type ShumaiLevelFileConfig struct {
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// ShujubaoConfig 数据宝配置
|
||||
type ShujubaoConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac,默认 hmac
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
|
||||
Logging ShujubaoLoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
// ShujubaoLoggingConfig 数据宝日志配置
|
||||
type ShujubaoLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]ShujubaoLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
// ShujubaoLevelFileConfig 数据宝级别文件配置
|
||||
type ShujubaoLevelFileConfig struct {
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// PDFGenConfig PDF生成服务配置
|
||||
type PDFGenConfig struct {
|
||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/ocr"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
@@ -375,6 +376,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*shumai.ShumaiService, error) {
|
||||
return shumai.NewShumaiServiceWithConfig(cfg)
|
||||
},
|
||||
// ShujubaoService - 数据宝服务
|
||||
func(cfg *config.Config) (*shujubao.ShujubaoService, error) {
|
||||
return shujubao.NewShujubaoServiceWithConfig(cfg)
|
||||
},
|
||||
func(cfg *config.Config) *yushan.YushanService {
|
||||
return yushan.NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
|
||||
@@ -100,11 +100,44 @@ type JRZQDCBEReq struct {
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
type JRZQACABReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
// shujubao
|
||||
type QYGL2ACDReq struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
}
|
||||
|
||||
type YYSYK9R4Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type QCXG9F5CReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXG3B8ZReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXM7R9Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXGP1W3Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXG5U0ZReq struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
}
|
||||
type QCXGY7F2Req struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
}
|
||||
type QYGL6F2DReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
@@ -53,6 +54,7 @@ type ApiRequestService struct {
|
||||
|
||||
func NewApiRequestService(
|
||||
westDexService *westdex.WestDexService,
|
||||
shujubaoService *shujubao.ShujubaoService,
|
||||
muziService *muzi.MuziService,
|
||||
yushanService *yushan.YushanService,
|
||||
tianYanChaService *tianyancha.TianYanChaService,
|
||||
@@ -69,7 +71,7 @@ func NewApiRequestService(
|
||||
combService := comb.NewCombService(productManagementService)
|
||||
|
||||
// 创建处理器依赖容器
|
||||
processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, shumaiService, validator, combService)
|
||||
processorDeps := processors.NewProcessorDependencies(westDexService, shujubaoService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, shumaiService, validator, combService)
|
||||
|
||||
// 统一注册所有处理器
|
||||
registerAllProcessors(combService)
|
||||
@@ -121,6 +123,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
|
||||
"JRZQDCBE": jrzq.ProcessJRZQDCBERequest,
|
||||
"JRZQACAB": jrzq.ProcessJRZQACABERequest, // 银行卡四要素
|
||||
"JRZQ09J8": jrzq.ProcessJRZQ09J8Request,
|
||||
"JRZQ1D09": jrzq.ProcessJRZQ1D09Request,
|
||||
"JRZQ3C7B": jrzq.ProcessJRZQ3C7BRequest,
|
||||
@@ -266,7 +269,14 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询
|
||||
"QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询
|
||||
"QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询)
|
||||
"QCXG1U4U": qcxg.ProcessQCXG1U4URequest, // 车辆里程记录(混合查询)
|
||||
"QCXG1U4U": qcxg.ProcessQCXG1U4URequest, //
|
||||
"QCXG9F5C": qcxg.ProcessQCXG9F5CERequest, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": qcxg.ProcessQCXG3B8ZRequest, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": qcxg.ProcessQCXGP1W3Request, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXM7R9": qcxg.ProcessQCXM7R9Request, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGH1E6": qcxg.ProcessQCXGH1E6Request, // 全国车辆VIN基础信息查验 9054
|
||||
"QCXG5U0Z": qcxg.ProcessQCXG5U0ZRequest, // 车辆静态信息查询 10479
|
||||
"QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443
|
||||
|
||||
// DWBG系列处理器 - 多维报告
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
|
||||
@@ -244,6 +244,15 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
||||
"PDFG01GZ": &dto.PDFG01GZReq{}, //
|
||||
"QYGL5S1I": &dto.QYGL5S1IReq{}, //企业司法涉诉V2
|
||||
"JRZQACAB": &dto.JRZQACABReq{}, //银行卡四要素
|
||||
"QCXG9F5C": &dto.QCXG9F5CReq{}, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": &dto.QCXG3B8ZReq{}, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": &dto.QCXGP1W3Req{}, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXM7R9": &dto.QCXM7R9Req{}, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGH1E6": &dto.QCXGH1E6Req{}, //全国车辆VIN基础信息查验 9054
|
||||
"QCXG5U0Z": &dto.QCXG5U0ZReq{}, //车辆静态信息查询 10479
|
||||
"QCXGY7F2": &dto.QCXGY7F2Req{}, //二手车VIN估值 10443
|
||||
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
@@ -28,6 +29,7 @@ type CallContext struct {
|
||||
// ProcessorDependencies 处理器依赖容器
|
||||
type ProcessorDependencies struct {
|
||||
WestDexService *westdex.WestDexService
|
||||
ShujubaoService *shujubao.ShujubaoService
|
||||
MuziService *muzi.MuziService
|
||||
YushanService *yushan.YushanService
|
||||
TianYanChaService *tianyancha.TianYanChaService
|
||||
@@ -45,6 +47,7 @@ type ProcessorDependencies struct {
|
||||
// NewProcessorDependencies 创建处理器依赖容器
|
||||
func NewProcessorDependencies(
|
||||
westDexService *westdex.WestDexService,
|
||||
shujubaoService *shujubao.ShujubaoService,
|
||||
muziService *muzi.MuziService,
|
||||
yushanService *yushan.YushanService,
|
||||
tianYanChaService *tianyancha.TianYanChaService,
|
||||
@@ -58,6 +61,7 @@ func NewProcessorDependencies(
|
||||
) *ProcessorDependencies {
|
||||
return &ProcessorDependencies{
|
||||
WestDexService: westDexService,
|
||||
ShujubaoService: shujubaoService,
|
||||
MuziService: muziService,
|
||||
YushanService: yushanService,
|
||||
TianYanChaService: tianYanChaService,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessJRZQACABERequest JRZQACAB 银行卡四要素 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessJRZQACABERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQACABReq
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "7eb69f73a855e41875e22f139b934c3c",
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
"acc_no": paramsDto.BankCard,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/9442"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG3B8ZRequest QCXG3B8Z 疑似运营车辆查询(月度里程)10268 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXG3B8ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG3B8ZReq
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c94605174cfe29bb2a62e2600b7d1596",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10268"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG5U0ZRequest QCXG5U0Z 车辆静态信息查询 10479 API 处理方法(使用数据宝服务)
|
||||
func ProcessQCXG5U0ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG5U0ZReq
|
||||
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)
|
||||
}
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "7c8122677476dd2621f574976f1a9fde",
|
||||
"vin": paramsDto.VinCode,
|
||||
}
|
||||
|
||||
apiPath := "/communication/personal/10479"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG9F5CERequest QCXG9F5C 疑似营运车辆注册平台数 10386 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXG9F5CERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG9F5CReq
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "27ab7048dda23d9a56178a2e5d4300ec",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10386"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXM7R9Request QCXGM7R9 疑似运营车辆查询(半年度里程)10270 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXM7R9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXM7R9Req
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "fc335ea4308add7454ac0858b08bef72",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/personal/10270"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGP1W3Request QCXGP1W3 疑似运营车辆查询(季度里程)10269 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXGP1W3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGP1W3Req
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "ecd6f3485322b0c706fc1dce330fe26e",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10269"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGY7F2Request QCXGY7F2 二手车VIN估值 10443 API 处理方法(使用数据宝服务)
|
||||
func ProcessQCXGY7F2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGY7F2Req
|
||||
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)
|
||||
}
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "463cea654a0a99d5d04c62f98ac882c0",
|
||||
"vin": paramsDto.VinCode,
|
||||
}
|
||||
|
||||
apiPath := "/government/traffic/10443"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessYYSYK9R4Request JRZQACAB 全网手机三要素验证1979周更新版 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessYYSYK9R4Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSYK9R4Req
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c115708d915451da8f34a23e144dda6b",
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/1979"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignMethod 签名方法
|
||||
type SignMethod string
|
||||
|
||||
const (
|
||||
SignMethodMD5 SignMethod = "md5"
|
||||
SignMethodHMACMD5 SignMethod = "hmac"
|
||||
)
|
||||
|
||||
// GenerateSignMD5 使用 MD5 生成签名:md5(app_secret + timestamp),32 位小写
|
||||
func GenerateSignMD5(appSecret, timestamp string) string {
|
||||
h := md5.Sum([]byte(appSecret + timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignHMAC 使用 HMAC-MD5 生成签名(仅 timestamp,兼容旧逻辑)
|
||||
func GenerateSignHMAC(appSecret, timestamp string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsMD5 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 MD5。
|
||||
// sortedParamStr 格式为 key1=value1&key2=value2&...(key 按字母序)。
|
||||
func GenerateSignFromParamsMD5(appSecret, sortedParamStr string) string {
|
||||
h := md5.Sum([]byte(appSecret + sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsHMAC 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 HMAC-MD5。
|
||||
func GenerateSignFromParamsHMAC(appSecret, sortedParamStr string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
121
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
121
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ShujubaoError 数据宝服务错误
|
||||
type ShujubaoError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *ShujubaoError) Error() string {
|
||||
return fmt.Sprintf("数据宝错误 [%s]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否成功
|
||||
func (e *ShujubaoError) IsSuccess() bool {
|
||||
return e.Code == "200" || e.Code == "0" || e.Code == "10000"
|
||||
}
|
||||
|
||||
// NewShujubaoError 创建新的数据宝错误
|
||||
func NewShujubaoError(code, message string) *ShujubaoError {
|
||||
return &ShujubaoError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// 数据宝全系统错误码与描述映射(Code -> Desc)
|
||||
var systemErrorCodeDesc = map[string]string{
|
||||
"10000": "成功",
|
||||
"10001": "参数传入有误",
|
||||
"10002": "查询失败",
|
||||
"10003": "系统处理异常",
|
||||
"10004": "系统处理超时",
|
||||
"10005": "服务异常",
|
||||
"10006": "查无",
|
||||
"10020": "同一参数请求次数超限",
|
||||
"99999": "其他错误",
|
||||
"999": "接口处理异常",
|
||||
"000": "key参数不能为空",
|
||||
"001": "找不到这个key",
|
||||
"002": "调用次数已用完",
|
||||
"003": "用户该接口状态不可用",
|
||||
"004": "接口信息不存在",
|
||||
"005": "你没有认证信息",
|
||||
"008": "当前接口只允许“企业认证”通过的账户进行调用,请在数据宝官网个人中心进行企业认证后再进行调用,谢谢!",
|
||||
"009": "触发风控",
|
||||
"011": "接口缺少参数",
|
||||
"012": "没有ip访问权限",
|
||||
"013": "接口模板不存在",
|
||||
"015": "该接口已下架",
|
||||
"020": "调用第三方产生异常",
|
||||
"022": "调用第三方返回的数据格式错误",
|
||||
"025": "你没有购买此接口",
|
||||
"026": "用户信息不存在",
|
||||
"027": "请求第三方地址超时,请稍后再试",
|
||||
"028": "请求第三方地址被拒绝,请稍后再试",
|
||||
"034": "签名不合法",
|
||||
"035": "请求参数加密有误",
|
||||
"036": "验签失败",
|
||||
"037": "timestamp不能为空",
|
||||
"038": "请求繁忙,请稍后联系管理员再试",
|
||||
"039": "请在个人中心接口设置加密状态",
|
||||
"040": "timestamp不合法",
|
||||
"041": "timestamp已过期",
|
||||
"042": "身份证手机号姓名银行卡等不符合规则",
|
||||
"043": "该号段不支持验证",
|
||||
"047": "请在个人中心获取密钥",
|
||||
"048": "找不到这个secretKey",
|
||||
"049": "用户还未申购该产品",
|
||||
"050": "请联系客服开启验签",
|
||||
"051": "超过当日调用次数",
|
||||
"052": "机房限制调用,请联系客服切换其他机房",
|
||||
"053": "系统错误",
|
||||
"054": "token无效",
|
||||
"055": "配置信息未完善,请联系数据宝工作人员",
|
||||
"056": "apiName参数不能为空",
|
||||
"057": "并发量超过限制,请联系客服",
|
||||
"058": "撞库风控预警,请联系客服",
|
||||
}
|
||||
|
||||
// GetSystemErrorDesc 根据错误码获取系统错误描述(支持带 SYSTEM_ 前缀或纯数字)
|
||||
func GetSystemErrorDesc(code string) string {
|
||||
// 去掉 SYSTEM_ 前缀
|
||||
key := code
|
||||
if len(code) > 7 && code[:7] == "SYSTEM_" {
|
||||
key = code[7:]
|
||||
}
|
||||
if desc, ok := systemErrorCodeDesc[key]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewShujubaoErrorFromCode 根据状态码创建错误
|
||||
func NewShujubaoErrorFromCode(code, message string) *ShujubaoError {
|
||||
if message != "" {
|
||||
return NewShujubaoError(code, message)
|
||||
}
|
||||
if desc := GetSystemErrorDesc(code); desc != "" {
|
||||
return NewShujubaoError(code, desc)
|
||||
}
|
||||
return NewShujubaoError(code, "未知错误")
|
||||
}
|
||||
|
||||
// IsShujubaoError 检查是否是数据宝错误
|
||||
func IsShujubaoError(err error) bool {
|
||||
_, ok := err.(*ShujubaoError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetShujubaoError 获取数据宝错误
|
||||
func GetShujubaoError(err error) *ShujubaoError {
|
||||
if shujubaoErr, ok := err.(*ShujubaoError); ok {
|
||||
return shujubaoErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewShujubaoServiceWithConfig 使用配置创建数据宝服务
|
||||
func NewShujubaoServiceWithConfig(cfg *config.Config) (*ShujubaoService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Shujubao.Logging.Enabled,
|
||||
LogDir: cfg.Shujubao.Logging.LogDir,
|
||||
ServiceName: "shujubao",
|
||||
UseDaily: cfg.Shujubao.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Shujubao.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
for k, v := range cfg.Shujubao.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[k] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: v.MaxSize,
|
||||
MaxBackups: v.MaxBackups,
|
||||
MaxAge: v.MaxAge,
|
||||
Compress: v.Compress,
|
||||
}
|
||||
}
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var signMethod SignMethod
|
||||
if cfg.Shujubao.SignMethod == "md5" {
|
||||
signMethod = SignMethodMD5
|
||||
} else {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
timeout := 60 * time.Second
|
||||
if cfg.Shujubao.Timeout > 0 {
|
||||
timeout = cfg.Shujubao.Timeout
|
||||
}
|
||||
|
||||
return NewShujubaoService(
|
||||
cfg.Shujubao.URL,
|
||||
cfg.Shujubao.AppSecret,
|
||||
signMethod,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceWithLogging 使用自定义日志配置创建数据宝服务
|
||||
func NewShujubaoServiceWithLogging(url, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*ShujubaoService, error) {
|
||||
loggingConfig.ServiceName = "shujubao"
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceSimple 创建无日志的数据宝服务
|
||||
func NewShujubaoServiceSimple(url, appSecret string, signMethod SignMethod, timeout time.Duration) *ShujubaoService {
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, nil)
|
||||
}
|
||||
265
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
265
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
)
|
||||
|
||||
// ShujubaoResp 数据宝 API 通用响应(按实际文档调整)
|
||||
type ShujubaoResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ShujubaoConfig 数据宝服务配置
|
||||
type ShujubaoConfig struct {
|
||||
URL string
|
||||
AppSecret string
|
||||
SignMethod SignMethod
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// ShujubaoService 数据宝服务
|
||||
type ShujubaoService struct {
|
||||
config ShujubaoConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewShujubaoService 创建数据宝服务实例
|
||||
func NewShujubaoService(url, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *ShujubaoService {
|
||||
if signMethod == "" {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
return &ShujubaoService{
|
||||
config: ShujubaoConfig{
|
||||
URL: url,
|
||||
AppSecret: appSecret,
|
||||
SignMethod: signMethod,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求 ID
|
||||
func (s *ShujubaoService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppSecret)))
|
||||
return fmt.Sprintf("shujubao_%x", hash[:8])
|
||||
}
|
||||
|
||||
// buildSortedParamStr 将入参按 key 的 ASCII 排序组合为 key1=value1&key2=value2&...
|
||||
func buildSortedParamStr(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(vs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// buildFormUrlEncodedBody 按 key 的 ASCII 排序构建 application/x-www-form-urlencoded 请求体(键与值均已 URL 编码)
|
||||
func buildFormUrlEncodedBody(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(url.QueryEscape(k))
|
||||
b.WriteByte('=')
|
||||
b.WriteString(url.QueryEscape(vs))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// generateSign 根据入参与时间戳生成签名。入参按 ASCII 排序组合后与 app_secret 做 MD5/HMAC。
|
||||
// 对于开启了加密的接口需传 sign 与 timestamp;明文传输的接口则无需传这两个参数。
|
||||
func (s *ShujubaoService) generateSign(timestamp string, params map[string]interface{}) string {
|
||||
// 合并 timestamp 到入参后参与排序
|
||||
merged := make(map[string]interface{}, len(params)+1)
|
||||
for k, v := range params {
|
||||
merged[k] = v
|
||||
}
|
||||
merged["timestamp"] = timestamp
|
||||
sortedStr := buildSortedParamStr(merged)
|
||||
switch s.config.SignMethod {
|
||||
case SignMethodMD5:
|
||||
return GenerateSignFromParamsMD5(s.config.AppSecret, sortedStr)
|
||||
default:
|
||||
return GenerateSignFromParamsHMAC(s.config.AppSecret, sortedStr)
|
||||
}
|
||||
}
|
||||
|
||||
// buildRequestURL 拼接接口地址得到最终请求 URL,如 https://api.chinadatapay.com/communication/personal/197
|
||||
func (s *ShujubaoService) buildRequestURL(apiPath string) string {
|
||||
base := strings.TrimSuffix(s.config.URL, "/")
|
||||
if apiPath == "" {
|
||||
return base
|
||||
}
|
||||
return base + "/" + strings.TrimPrefix(apiPath, "/")
|
||||
}
|
||||
|
||||
// CallAPI 调用数据宝 API(POST)。最终请求地址 = url + 拼接接口地址值;body 为业务参数;sign、timestamp 按原样传 header。
|
||||
func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) (data interface{}, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := s.generateRequestID()
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 /personal/197
|
||||
requestURL := s.buildRequestURL(apiPath)
|
||||
|
||||
var transactionID string
|
||||
if id, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = id
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogRequest(requestID, transactionID, apiPath, requestURL)
|
||||
}
|
||||
|
||||
// 使用 application/x-www-form-urlencoded,贵司接口暂不支持 JSON 入参
|
||||
formBody := buildFormUrlEncodedBody(params)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, strings.NewReader(formBody))
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("timestamp", timestamp)
|
||||
req.Header.Set("sign", s.generateSign(timestamp, params))
|
||||
|
||||
client := &http.Client{Timeout: s.config.Timeout}
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
|
||||
errStr == "timeout" ||
|
||||
errStr == "Client.Timeout exceeded" ||
|
||||
errStr == "net/http: request canceled" {
|
||||
isTimeout = true
|
||||
}
|
||||
if isTimeout {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", err))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
s.logger.LogResponse(requestID, transactionID, apiPath, response.StatusCode, duration)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", response.StatusCode))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var shujubaoResp ShujubaoResp
|
||||
if err := json.Unmarshal(respBody, &shujubaoResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := shujubaoResp.Code
|
||||
if code != "10000" && code != "10006" {
|
||||
shujubaoErr := NewShujubaoErrorFromCode(code, shujubaoResp.Message)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shujubaoErr, params)
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, shujubaoErr)
|
||||
}
|
||||
|
||||
return shujubaoResp.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user