diff --git a/config.yaml b/config.yaml index 40b11fe..f7c39a7 100644 --- a/config.yaml +++ b/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 + + + diff --git a/internal/config/config.go b/internal/config/config.go index 498d71f..9c8d78e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` // 开发环境服务地址 diff --git a/internal/container/container.go b/internal/container/container.go index 1d84b1c..b5b631e 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -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, diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index cdb6046..5788824 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -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"` } diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 04a8b1d..292090d 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -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, @@ -254,19 +257,26 @@ func registerAllProcessors(combService *comb.CombService) { "QCXG8A3D": qcxg.ProcessQCXG8A3DRequest, "QCXG6B4E": qcxg.ProcessQCXG6B4ERequest, "QCXG4896": qcxg.ProcessQCXG4896Request, - "QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询 - "QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询 - "QCXGJJ2A": qcxg.ProcessQCXGJJ2ARequest, // vin码查车辆信息(一对多) - "QCXGGJ3A": qcxg.ProcessQCXGGJ3ARequest, // 车辆vin码查询号牌 - "QCXGYTS2": qcxg.ProcessQCXGYTS2Request, // 车辆二要素核验v2 - "QCXGP00W": qcxg.ProcessQCXGP00WRequest, // 车辆出险详版查询 - "QCXGGB2Q": qcxg.ProcessQCXGGB2QRequest, // 车辆二要素核验V1 - "QCXG4I1Z": qcxg.ProcessQCXG4I1ZRequest, // 车辆过户详版查询 - "QCXG1H7Y": qcxg.ProcessQCXG1H7YRequest, // 车辆过户简版查询 - "QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询 - "QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询 - "QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询) - "QCXG1U4U": qcxg.ProcessQCXG1U4URequest, // 车辆里程记录(混合查询) + "QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询 + "QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询 + "QCXGJJ2A": qcxg.ProcessQCXGJJ2ARequest, // vin码查车辆信息(一对多) + "QCXGGJ3A": qcxg.ProcessQCXGGJ3ARequest, // 车辆vin码查询号牌 + "QCXGYTS2": qcxg.ProcessQCXGYTS2Request, // 车辆二要素核验v2 + "QCXGP00W": qcxg.ProcessQCXGP00WRequest, // 车辆出险详版查询 + "QCXGGB2Q": qcxg.ProcessQCXGGB2QRequest, // 车辆二要素核验V1 + "QCXG4I1Z": qcxg.ProcessQCXG4I1ZRequest, // 车辆过户详版查询 + "QCXG1H7Y": qcxg.ProcessQCXG1H7YRequest, // 车辆过户简版查询 + "QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询 + "QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询 + "QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询) + "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, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index fb3f091..b195ef0 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -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 diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 43de6d7..07798ca 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -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, diff --git a/internal/domains/api/services/processors/jrzq/jrzqacab_processor.go b/internal/domains/api/services/processors/jrzq/jrzqacab_processor.go new file mode 100644 index 0000000..f029219 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzqacab_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxg3b8z_processor.go b/internal/domains/api/services/processors/qcxg/qcxg3b8z_processor.go new file mode 100644 index 0000000..917a6aa --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg3b8z_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxg5u0z_processor.go b/internal/domains/api/services/processors/qcxg/qcxg5u0z_processor.go new file mode 100644 index 0000000..c5e8446 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg5u0z_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxg9f5c_processor.go b/internal/domains/api/services/processors/qcxg/qcxg9f5c_processor.go new file mode 100644 index 0000000..09099c5 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg9f5c_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgm7r9_processor.go b/internal/domains/api/services/processors/qcxg/qcxgm7r9_processor.go new file mode 100644 index 0000000..5a86ec1 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgm7r9_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgp1w3_processor.go b/internal/domains/api/services/processors/qcxg/qcxgp1w3_processor.go new file mode 100644 index 0000000..d923231 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgp1w3_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgy7f2_processor.go b/internal/domains/api/services/processors/qcxg/qcxgy7f2_processor.go new file mode 100644 index 0000000..d5c9add --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgy7f2_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/yysy/yysyk9r4_processor.go b/internal/domains/api/services/processors/yysy/yysyk9r4_processor.go new file mode 100644 index 0000000..259a573 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyk9r4_processor.go @@ -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 +} diff --git a/internal/infrastructure/external/shujubao/crypto.go b/internal/infrastructure/external/shujubao/crypto.go new file mode 100644 index 0000000..554ff4a --- /dev/null +++ b/internal/infrastructure/external/shujubao/crypto.go @@ -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 +} diff --git a/internal/infrastructure/external/shujubao/shujubao_errors.go b/internal/infrastructure/external/shujubao/shujubao_errors.go new file mode 100644 index 0000000..ddf5ce9 --- /dev/null +++ b/internal/infrastructure/external/shujubao/shujubao_errors.go @@ -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 +} diff --git a/internal/infrastructure/external/shujubao/shujubao_factory.go b/internal/infrastructure/external/shujubao/shujubao_factory.go new file mode 100644 index 0000000..cf97ffb --- /dev/null +++ b/internal/infrastructure/external/shujubao/shujubao_factory.go @@ -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) +} diff --git a/internal/infrastructure/external/shujubao/shujubao_service.go b/internal/infrastructure/external/shujubao/shujubao_service.go new file mode 100644 index 0000000..06c10a7 --- /dev/null +++ b/internal/infrastructure/external/shujubao/shujubao_service.go @@ -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 +}