This commit is contained in:
Mrx
2026-01-30 18:25:30 +08:00
parent 3158bf8c04
commit dbcecde4e0
19 changed files with 1000 additions and 14 deletions

View File

@@ -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"` // 开发环境服务地址

View File

@@ -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,

View File

@@ -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"`
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View 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
}

View 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
}

View 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)
}

View 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 调用数据宝 APIPOST。最终请求地址 = 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
}