diff --git a/config.yaml b/config.yaml index dd9c8ac..af615cd 100644 --- a/config.yaml +++ b/config.yaml @@ -530,3 +530,46 @@ jiguang: max_backups: 5 max_age: 30 compress: true + +# =========================================== +# ✨ 数脉配置走实时接口 +# =========================================== +shumai: + url: "https://api.shumaidata.com" + app_id: "pIfqx8MsoTOjhbB762qi5BfkjJ4D7w0O" + app_secret: "BnJWo61hUgNEa5fqBCueiT1IZ1e0DxPU" + # =========================================== + # ✨ 数脉子账号配置走政务 + # =========================================== + # 走政务接口使用这个 + app_id2: "AwZZRzWkArtFDO2lDcT2jHfuoo9n35Tq" + app_secret2: "nCXN6fKLImjfvzI12hj8O1CMl1gJeaWh" + + sign_method: "md5" # 签名方法:md5 或 hmac,默认 hmac + timeout: 60s # 请求超时时间,默认 60 秒 + + # 数脉日志配置 + logging: + enabled: true + log_dir: "logs/external_services" + service_name: "shumai" + 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 20cab68..b6055ad 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,6 +39,7 @@ type Config struct { Alicloud AlicloudConfig `mapstructure:"alicloud"` Xingwei XingweiConfig `mapstructure:"xingwei"` Jiguang JiguangConfig `mapstructure:"jiguang"` + Shumai ShumaiConfig `mapstructure:"shumai"` } // ServerConfig HTTP服务器配置 @@ -550,6 +551,36 @@ type JiguangLevelFileConfig struct { Compress bool `mapstructure:"compress"` } +// ShumaiConfig 数脉配置 +type ShumaiConfig struct { + URL string `mapstructure:"url"` + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` + AppID2 string `mapstructure:"app_id2"` // 走政务接口使用这个 + AppSecret2 string `mapstructure:"app_secret2"` // 走政务接口使用这个 + SignMethod string `mapstructure:"sign_method"` // md5 或 hmac,默认 hmac + Timeout time.Duration `mapstructure:"timeout"` + + Logging ShumaiLoggingConfig `mapstructure:"logging"` +} + +// ShumaiLoggingConfig 数脉日志配置 +type ShumaiLoggingConfig struct { + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + LevelConfigs map[string]ShumaiLevelFileConfig `mapstructure:"level_configs"` +} + +// ShumaiLevelFileConfig 数脉级别文件配置 +type ShumaiLevelFileConfig struct { + MaxSize int `mapstructure:"max_size"` + MaxBackups int `mapstructure:"max_backups"` + MaxAge int `mapstructure:"max_age"` + Compress bool `mapstructure:"compress"` +} + // DomainConfig 域名配置 type DomainConfig struct { API string `mapstructure:"api"` // API域名 diff --git a/internal/container/container.go b/internal/container/container.go index bfb098e..dee7361 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -42,6 +42,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/shumai" "tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/storage" "tyapi-server/internal/infrastructure/external/tianyancha" @@ -371,6 +372,10 @@ func NewContainer() *Container { func(cfg *config.Config) (*jiguang.JiguangService, error) { return jiguang.NewJiguangServiceWithConfig(cfg) }, + // ShumaiService - 数脉服务 + func(cfg *config.Config) (*shumai.ShumaiService, error) { + return shumai.NewShumaiServiceWithConfig(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 a08320a..1a60837 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -853,3 +853,78 @@ type IVYZ9H2MReq struct { type YYSY9E4AReq struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` } + +// YYSY运营商相关API DTO +type YYSY3M8SReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYC4R9Req struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYH6D2Req struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYP0T4Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYE7V5Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYS9W1Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYK8R3Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type YYSYF2T7Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` + DateRange string `json:"date_range" validate:"required,validAuthDate" ` +} + +// 数脉 API +type IVYZ3M8SReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type IVYZ9K7FReq struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` +} + +type IVYZA1B3Req struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + PhotoData string `json:"photo_data" validate:"required,validBase64Image"` +} + +type IVYZC4R9Req struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type IVYZP0T4Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` +} + +type IVYZF2T7Req struct { + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` + DateRange string `json:"date_range" validate:"required,validAuthDate" ` +} + +type IVYZX5QZReq struct { + ReturnURL string `json:"return_url" validate:"required,validReturnURL"` +} diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 8c04c66..d616aaa 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -20,6 +20,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/shumai" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -56,6 +57,7 @@ func NewApiRequestService( zhichaService *zhicha.ZhichaService, xingweiService *xingwei.XingweiService, jiguangService *jiguang.JiguangService, + shumaiService *shumai.ShumaiService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, ) *ApiRequestService { @@ -63,7 +65,7 @@ func NewApiRequestService( combService := comb.NewCombService(productManagementService) // 创建处理器依赖容器 - processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, validator, combService) + processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, shumaiService, validator, combService) // 统一注册所有处理器 registerAllProcessors(combService) @@ -186,6 +188,14 @@ func registerAllProcessors(combService *comb.CombService) { "YYSY9E4A": yysy.ProcessYYSY9E4ARequest, "YYSY9F1B": yysy.ProcessYYSY9F1BYequest, "YYSY6F2B": yysy.ProcessYYSY6F2BRequest, + "YYSY3M8S": yysy.ProcessYYSY3M8SRequest, //运营商二要素查询 + "YYSYC4R9": yysy.ProcessYYSYC4R9Request, //运营商三要素详版查询 + "YYSYH6D2": yysy.ProcessYYSYH6D2Request, //运营商三要素简版查询 + "YYSYP0T4": yysy.ProcessYYSYP0T4Request, //在网时长查询 + "YYSYE7V5": yysy.ProcessYYSYE7V5Request, //手机在网状态查询 + "YYSYS9W1": yysy.ProcessYYSYS9W1Request, //手机携号转网查询 + "YYSYK8R3": yysy.ProcessYYSYK8R3Request, //手机空号检测查询 + "YYSYF2T7": yysy.ProcessYYSYF2T7Request, //手机二次放号检测查询 // IVYZ系列处理器 "IVYZ0B03": ivyz.ProcessIVYZ0B03Request, @@ -221,6 +231,9 @@ func registerAllProcessors(combService *comb.CombService) { "IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1 "IVYZ0S0D": ivyz.ProcessIVYZ0S0DRequest, //劳动仲裁信息查询(个人版) "IVYZ1J7H": ivyz.ProcessIVYZ1J7HRequest, //行驶证核查v2 + "IVYZ9K7F": ivyz.ProcessIVYZ9K7FRequest, //身份证实名认证 + "IVYZA1B3": ivyz.ProcessIVYZA1B3Request, //公安三要素人脸识别 + "IVYZN2P8": ivyz.ProcessIVYZN2P8Request, //身份证实名认证即时版本 // COMB系列处理器 - 只注册有自定义逻辑的组合包 "COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index abacd5c..5090329 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -228,6 +228,19 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "JRZQO6L7": &dto.JRZQO6L7Req{}, //全国自然人经济特征评分模型v3 简版 "JRZQO7L1": &dto.JRZQO7L1Req{}, //全国自然人经济特征评分模型v4 详版 "JRZQS7G0": &dto.JRZQS7G0Req{}, //社保综合评分V1 + "IVYZ9K7F": &dto.IVYZ9K7FReq{}, //身份证实名认证 + "YYSY3M8S": &dto.YYSY3M8SReq{}, //运营商二要素查询 + "YYSYC4R9": &dto.YYSYC4R9Req{}, //运营商三要素详版查询 + "YYSYH6D2": &dto.YYSYH6D2Req{}, //运营商三要素简版查询 + "YYSYP0T4": &dto.YYSYP0T4Req{}, //在网时长查询 + "YYSYE7V5": &dto.YYSYE7V5Req{}, //手机在网状态查询 + "YYSYS9W1": &dto.YYSYS9W1Req{}, //手机携号转网查询 + "YYSYK8R3": &dto.YYSYK8R3Req{}, //手机空号检测查询 + "YYSYF2T7": &dto.YYSYF2T7Req{}, //手机二次放号检测查询 + "IVYZA1B3": &dto.IVYZA1B3Req{}, //公安三要素人脸识别 + "IVYZX5QZ": &dto.IVYZX5QZReq{}, //活体识别 + "IVYZN2P8": &dto.IVYZ9K7FReq{}, //身份证实名认证即时版本 + } // 优先返回已配置的DTO @@ -408,6 +421,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "legal_person": "法人姓名", "ent_code": "企业代码", "auth_date": "授权日期", + "date_range": "数据范围", "time_range": "时间范围", "authorized": "是否授权", "authorization_url": "授权链接", @@ -463,6 +477,7 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "legal_person": "王五", "ent_code": "91110000123456789X", "auth_date": "20240101-20241231", + "date_range": "20240101-20241231", "time_range": "09:00-18:00", "authorized": "1", "years": "5", @@ -527,6 +542,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "legal_person": "请输入法人真实姓名", "ent_code": "请输入统一社会信用代码", "auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)", + "date_range": "请输入日期范围(YYYYMMDD-YYYYMMDD)", "time_range": "请输入时间范围(HH:MM-HH:MM)", "authorized": "请选择是否授权", "years": "请输入查询年数(0-100)", @@ -593,6 +609,7 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "legal_person": "请输入法人真实姓名", "ent_code": "请输入统一社会信用代码", "auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天", + "date_range": "请输入日期范围,格式:YYYYMMDD-YYYYMMDD", "time_range": "请输入时间范围,格式:HH:MM-HH:MM", "authorized": "请输入是否授权:0-未授权,1-已授权", "years": "请输入查询年数(0-100)", diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index f9160c5..43de6d7 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -4,8 +4,9 @@ import ( "context" "tyapi-server/internal/application/api/commands" "tyapi-server/internal/infrastructure/external/alicloud" - "tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/jiguang" + "tyapi-server/internal/infrastructure/external/muzi" + "tyapi-server/internal/infrastructure/external/shumai" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -34,6 +35,7 @@ type ProcessorDependencies struct { ZhichaService *zhicha.ZhichaService XingweiService *xingwei.XingweiService JiguangService *jiguang.JiguangService + ShumaiService *shumai.ShumaiService Validator interfaces.RequestValidator CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 @@ -50,6 +52,7 @@ func NewProcessorDependencies( zhichaService *zhicha.ZhichaService, xingweiService *xingwei.XingweiService, jiguangService *jiguang.JiguangService, + shumaiService *shumai.ShumaiService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface ) *ProcessorDependencies { @@ -62,6 +65,7 @@ func NewProcessorDependencies( ZhichaService: zhichaService, XingweiService: xingweiService, JiguangService: jiguangService, + ShumaiService: shumaiService, Validator: validator, CombService: combService, Options: nil, // 初始化为nil,在调用时设置 diff --git a/internal/domains/api/services/processors/ivyz/ivyz9k7f_processor.go b/internal/domains/api/services/processors/ivyz/ivyz9k7f_processor.go new file mode 100644 index 0000000..452080d --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyz9k7f_processor.go @@ -0,0 +1,48 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessIVYZ9K7FRequest IVYZ9K7F 身份证实名认证 API处理方法 +func ProcessIVYZ9K7FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZ9K7FReq + 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) + } + reqFormData := map[string]interface{}{ + "idcard": paramsDto.IDCard, + "name": paramsDto.Name, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/id_card/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrNotFound) { + // 查无记录情况 + return nil, errors.Join(processors.ErrNotFound, err) + } else if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/ivyz/ivyza1b3_processor.go b/internal/domains/api/services/processors/ivyz/ivyza1b3_processor.go new file mode 100644 index 0000000..5f7cea4 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyza1b3_processor.go @@ -0,0 +1,49 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessIVYZA1B3Request IVYZA1B3 公安三要素人脸识别API处理方法 +func ProcessIVYZA1B3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZA1B3Req + 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) + } + reqFormData := map[string]interface{}{ + "idcard": paramsDto.IDCard, + "name": paramsDto.Name, + "image": paramsDto.PhotoData, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/face_id_card/compare" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrNotFound) { + // 查无记录情况 + return nil, errors.Join(processors.ErrNotFound, err) + } else if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/ivyz/ivyzn2p8_processor.go b/internal/domains/api/services/processors/ivyz/ivyzn2p8_processor.go new file mode 100644 index 0000000..8e417c2 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyzn2p8_processor.go @@ -0,0 +1,50 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessIVYZN2P8Request IVYZN2P8 身份证实名认证政务版 API处理方法 +func ProcessIVYZN2P8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZ9K7FReq + 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) + } + reqFormData := map[string]interface{}{ + "idcard": paramsDto.IDCard, + "name": paramsDto.Name, + } + + //走政务接口 - 使用 app_id2 和 app_secret2 + deps.ShumaiService.UseGovernment() + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/id_card/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrNotFound) { + // 查无记录情况 + return nil, errors.Join(processors.ErrNotFound, err) + } else if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/ivyz/ivyzx5qz_processor.go b/internal/domains/api/services/processors/ivyz/ivyzx5qz_processor.go new file mode 100644 index 0000000..524b8ab --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyzx5qz_processor.go @@ -0,0 +1,48 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessIVYZx5qzRequest IVYZx5qz 活体识别API处理方法 +func ProcessIVYZX5QZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZX5QZReq + 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) + } + + reqFormData := map[string]interface{}{ + "return_url": paramsDto.ReturnURL, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/liveness/h5/v4/token" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrNotFound) { + // 查无记录情况 + return nil, errors.Join(processors.ErrNotFound, err) + } else if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/jrzq/jrzqo6l7_processor.go b/internal/domains/api/services/processors/jrzq/jrzqo6l7_processor.go index 6cf28b3..813347c 100644 --- a/internal/domains/api/services/processors/jrzq/jrzqo6l7_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzqo6l7_processor.go @@ -47,17 +47,13 @@ func ProcessJRZQO6L7Request(ctx context.Context, params []byte, deps *processors } // 使用 WithSkipCode201Check 不跳过 201 错误检查,当 Code == "201" 时返回错误 - ctx = zhicha.WithSkipCode201Check(ctx) + // ctx = zhicha.WithSkipCode201Check(ctx) respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI081", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, zhicha.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } // 将响应数据转换为JSON字节 diff --git a/internal/domains/api/services/processors/jrzq/jrzqo7l1_processor.go b/internal/domains/api/services/processors/jrzq/jrzqo7l1_processor.go index 9a3d581..dc4c2f0 100644 --- a/internal/domains/api/services/processors/jrzq/jrzqo7l1_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzqo7l1_processor.go @@ -48,17 +48,13 @@ func ProcessJRZQO7L1Request(ctx context.Context, params []byte, deps *processors } // 使用 WithSkipCode201Check 不跳过 201 错误检查,当 Code == "201" 时返回错误 - ctx = zhicha.WithSkipCode201Check(ctx) + // ctx = zhicha.WithSkipCode201Check(ctx) respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI080", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, zhicha.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } // 将响应数据转换为JSON字节 diff --git a/internal/domains/api/services/processors/jrzq/jrzqs7g0_processor.go b/internal/domains/api/services/processors/jrzq/jrzqs7g0_processor.go index c4dd8b4..b4de8a4 100644 --- a/internal/domains/api/services/processors/jrzq/jrzqs7g0_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzqs7g0_processor.go @@ -43,17 +43,13 @@ func ProcessJRZQS7G0Request(ctx context.Context, params []byte, deps *processors "authorized": paramsDto.Authorized, } // 使用 WithSkipCode201Check 不跳过 201 错误检查,当 Code == "201" 时返回错误 - ctx = zhicha.WithSkipCode201Check(ctx) + // ctx = zhicha.WithSkipCode201Check(ctx) respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI082", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, zhicha.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } // 将响应数据转换为JSON字节 respBytes, err := json.Marshal(respData) diff --git a/internal/domains/api/services/processors/yysy/yysy3m8s_processor.go b/internal/domains/api/services/processors/yysy/yysy3m8s_processor.go new file mode 100644 index 0000000..ca40bfc --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysy3m8s_processor.go @@ -0,0 +1,45 @@ +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/shumai" +) + +// ProcessYYSY3M8SRequest YYSY3M8S 运营商二要素 API处理方法 +func ProcessYYSY3M8SRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSY3M8SReq + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + "name": paramsDto.Name, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/mobile_two/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysyc4r9_processor.go b/internal/domains/api/services/processors/yysy/yysyc4r9_processor.go new file mode 100644 index 0000000..4fac5ab --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyc4r9_processor.go @@ -0,0 +1,46 @@ +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/shumai" +) + +// ProcessYYSYC4R9Request YYSYC4R9 运营商三要素详版API处理方法 +func ProcessYYSYC4R9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYC4R9Req + 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) + } + reqFormData := map[string]interface{}{ + "idcard": paramsDto.IDCard, + "name": paramsDto.Name, + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v2/mobile_three/check/detail" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysye7v5_processor.go b/internal/domains/api/services/processors/yysy/yysye7v5_processor.go new file mode 100644 index 0000000..d079d73 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysye7v5_processor.go @@ -0,0 +1,44 @@ +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/shumai" +) + +// ProcessYYSYE7V5Request YYSYE7V5 手机在网状态查询API处理方法 +func ProcessYYSYE7V5Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYE7V5Req + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v1/mobile_status/check" // 接口路径,根据数脉文档填写( + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysyf2t7_processor.go b/internal/domains/api/services/processors/yysy/yysyf2t7_processor.go new file mode 100644 index 0000000..5baa065 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyf2t7_processor.go @@ -0,0 +1,45 @@ +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/shumai" +) + +// ProcessYYSYF2T7Request YYSYF2T7 手机二次放号检测查询API处理方法 +func ProcessYYSYF2T7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYF2T7Req + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + "date": paramsDto.DateRange, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/mobile_twice/check" // 接口路径,根据数脉文档填写( + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysyh6d2_processor.go b/internal/domains/api/services/processors/yysy/yysyh6d2_processor.go new file mode 100644 index 0000000..4a90035 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyh6d2_processor.go @@ -0,0 +1,46 @@ +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/shumai" +) + +// ProcessYYSYH6D2Request YYSYH6D2 运营商三要素简版API处理方法 +func ProcessYYSYH6D2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYH6D2Req + 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) + } + reqFormData := map[string]interface{}{ + "idcard": paramsDto.IDCard, + "name": paramsDto.Name, + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/mobile_three/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysyk8r3_processor.go b/internal/domains/api/services/processors/yysy/yysyk8r3_processor.go new file mode 100644 index 0000000..b4fb817 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyk8r3_processor.go @@ -0,0 +1,44 @@ +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/shumai" +) + +// ProcessYYSYK8R3Request YYSYK8R3 手机空号检测查询API处理方法 +func ProcessYYSYK8R3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYK8R3Req + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/mobile_empty/check" // 接口路径,根据数脉文档填写( + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysyp0t4_processor.go b/internal/domains/api/services/processors/yysy/yysyp0t4_processor.go new file mode 100644 index 0000000..521f048 --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysyp0t4_processor.go @@ -0,0 +1,44 @@ +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/shumai" +) + +// ProcessYYSYP0T4Request YYSYP0T4 在网时长API处理方法 +func ProcessYYSYP0T4Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYP0T4Req + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v2/mobile_online/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/yysy/yysys9w1_processor.go b/internal/domains/api/services/processors/yysy/yysys9w1_processor.go new file mode 100644 index 0000000..71fe14b --- /dev/null +++ b/internal/domains/api/services/processors/yysy/yysys9w1_processor.go @@ -0,0 +1,44 @@ +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/shumai" +) + +// ProcessYYSYS9W1Request YYSYS9W1 手机携号转网查询API处理方法 +func ProcessYYSYS9W1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.YYSYS9W1Req + 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) + } + reqFormData := map[string]interface{}{ + "mobile": paramsDto.MobileNo, + } + + // 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded + apiPath := "/v4/mobile-transfer/query" // 接口路径,根据数脉文档填写( + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/infrastructure/external/shumai/crypto.go b/internal/infrastructure/external/shumai/crypto.go new file mode 100644 index 0000000..99e94a2 --- /dev/null +++ b/internal/infrastructure/external/shumai/crypto.go @@ -0,0 +1,199 @@ +package shumai + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + "strings" +) + +// SignMethod 签名方法 +type SignMethod string + +const ( + SignMethodMD5 SignMethod = "md5" + SignMethodHMACMD5 SignMethod = "hmac" +) + +// GenerateSignForm 生成表单接口签名(appid & timestamp & app_security) +// 拼接规则:appid + "&" + timestamp + "&" + app_security,对拼接串做 MD5,32 位小写十六进制; +// 不足 32 位左侧补 0。 +func GenerateSignForm(appid, timestamp, appSecret string) string { + str := appid + "&" + timestamp + "&" + appSecret + hash := md5.Sum([]byte(str)) + sign := strings.ToLower(hex.EncodeToString(hash[:])) + if n := 32 - len(sign); n > 0 { + sign = strings.Repeat("0", n) + sign + } + return sign +} + +// app_secret: "BnJWo61hUgNEa5fqBCueiT1IZ1e0DxPU" + +// Encrypt 使用 AES/ECB/PKCS5Padding 加密数据 +// 加密算法:AES,工作模式:ECB(无初始向量),填充方式:PKCS5Padding +// 加密 key 是服务商分配的 app_security,AES 加密之后再进行 base64 编码 +func Encrypt(data, appSecurity string) (string, error) { + key := prepareAESKey([]byte(appSecurity)) + ciphertext, err := aesEncryptECB([]byte(data), key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt 解密 base64 编码的 AES/ECB/PKCS5Padding 加密数据 +func Decrypt(encodedData, appSecurity string) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + return nil, err + } + key := prepareAESKey([]byte(appSecurity)) + plaintext, err := aesDecryptECB(ciphertext, key) + if err != nil { + return nil, err + } + return plaintext, nil +} + +// prepareAESKey 准备 AES 密钥,确保长度为 16/24/32 字节 +// 如果 key 长度不足,用 0 填充;如果过长,截取前 32 字节 +func prepareAESKey(key []byte) []byte { + keyLen := len(key) + if keyLen == 16 || keyLen == 24 || keyLen == 32 { + return key + } + if keyLen < 16 { + // 不足 16 字节,用 0 填充到 16 字节(AES-128) + padded := make([]byte, 16) + copy(padded, key) + return padded + } + if keyLen < 24 { + // 不足 24 字节,用 0 填充到 24 字节(AES-192) + padded := make([]byte, 24) + copy(padded, key) + return padded + } + if keyLen < 32 { + // 不足 32 字节,用 0 填充到 32 字节(AES-256) + padded := make([]byte, 32) + copy(padded, key) + return padded + } + // 超过 32 字节,截取前 32 字节(AES-256) + return key[:32] +} + +// aesEncryptECB 使用 AES ECB 模式加密,PKCS5 填充 +func aesEncryptECB(plaintext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + paddedPlaintext := pkcs5Padding(plaintext, block.BlockSize()) + ciphertext := make([]byte, len(paddedPlaintext)) + mode := newECBEncrypter(block) + mode.CryptBlocks(ciphertext, paddedPlaintext) + return ciphertext, nil +} + +// aesDecryptECB 使用 AES ECB 模式解密,PKCS5 去填充 +func aesDecryptECB(ciphertext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext)%block.BlockSize() != 0 { + return nil, errors.New("ciphertext length is not a multiple of block size") + } + plaintext := make([]byte, len(ciphertext)) + mode := newECBDecrypter(block) + mode.CryptBlocks(plaintext, ciphertext) + return pkcs5Unpadding(plaintext), nil +} + +// pkcs5Padding PKCS5 填充 +func pkcs5Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padtext...) +} + +// pkcs5Unpadding 去除 PKCS5 填充 +func pkcs5Unpadding(src []byte) []byte { + length := len(src) + if length == 0 { + return src + } + unpadding := int(src[length-1]) + if unpadding > length { + return src + } + return src[:length-unpadding] +} + +// ECB 模式加密/解密实现 +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +type ecbEncrypter ecb + +func newECBEncrypter(b cipher.Block) cipher.BlockMode { + return (*ecbEncrypter)(newECB(b)) +} + +func (x *ecbEncrypter) BlockSize() int { + return x.blockSize +} + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Encrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +type ecbDecrypter ecb + +func newECBDecrypter(b cipher.Block) cipher.BlockMode { + return (*ecbDecrypter)(newECB(b)) +} + +func (x *ecbDecrypter) BlockSize() int { + return x.blockSize +} + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Decrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} diff --git a/internal/infrastructure/external/shumai/shumai_errors.go b/internal/infrastructure/external/shumai/shumai_errors.go new file mode 100644 index 0000000..96e1494 --- /dev/null +++ b/internal/infrastructure/external/shumai/shumai_errors.go @@ -0,0 +1,108 @@ +package shumai + +import ( + "fmt" +) + +// ShumaiError 数脉服务错误 +type ShumaiError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// Error 实现 error 接口 +func (e *ShumaiError) Error() string { + return fmt.Sprintf("数脉错误 [%s]: %s", e.Code, e.Message) +} + +// IsSuccess 是否成功 +func (e *ShumaiError) IsSuccess() bool { + return e.Code == "0" || e.Code == "200" +} + +// IsNoRecord 是否查无记录 +func (e *ShumaiError) IsNoRecord() bool { + return e.Code == "404" +} + +// IsParamError 是否参数错误 +func (e *ShumaiError) IsParamError() bool { + return e.Code == "400" +} + +// IsAuthError 是否认证错误 +func (e *ShumaiError) IsAuthError() bool { + return e.Code == "601" || e.Code == "602" +} + +// IsSystemError 是否系统错误 +func (e *ShumaiError) IsSystemError() bool { + return e.Code == "500" || e.Code == "501" +} + +// 预定义错误 +var ( + ErrSuccess = &ShumaiError{Code: "200", Message: "成功"} + ErrParamError = &ShumaiError{Code: "400", Message: "参数错误"} + ErrNoRecord = &ShumaiError{Code: "404", Message: "请求资源不存在"} + ErrSystemError = &ShumaiError{Code: "500", Message: "系统内部错误,请联系服务商"} + ErrThirdPartyError = &ShumaiError{Code: "501", Message: "第三方服务异常"} + ErrNoPermission = &ShumaiError{Code: "601", Message: "服务商未开通接口权限"} + ErrAccountDisabled = &ShumaiError{Code: "602", Message: "账号停用"} + ErrInsufficientBalance = &ShumaiError{Code: "603", Message: "余额不足请充值"} + ErrInterfaceDisabled = &ShumaiError{Code: "604", Message: "接口停用"} + ErrInsufficientQuota = &ShumaiError{Code: "605", Message: "次数不足,请购买套餐"} + ErrRateLimitExceeded = &ShumaiError{Code: "606", Message: "调用超限,请联系服务商"} + ErrOther = &ShumaiError{Code: "1001", Message: "其他,以实际返回为准"} +) + +// NewShumaiError 创建数脉错误 +func NewShumaiError(code, message string) *ShumaiError { + return &ShumaiError{Code: code, Message: message} +} + +// NewShumaiErrorFromCode 根据状态码创建错误 +func NewShumaiErrorFromCode(code string) *ShumaiError { + switch code { + case "0", "200": + return ErrSuccess + case "400": + return ErrParamError + case "404": + return ErrNoRecord + case "500": + return ErrSystemError + case "501": + return ErrThirdPartyError + case "601": + return ErrNoPermission + case "602": + return ErrAccountDisabled + case "603": + return ErrInsufficientBalance + case "604": + return ErrInterfaceDisabled + case "605": + return ErrInsufficientQuota + case "606": + return ErrRateLimitExceeded + case "1001": + return ErrOther + default: + return &ShumaiError{Code: code, Message: "未知错误"} + } +} + +// IsShumaiError 是否为数脉错误 +func IsShumaiError(err error) bool { + _, ok := err.(*ShumaiError) + return ok +} + +// GetShumaiError 获取数脉错误 +func GetShumaiError(err error) *ShumaiError { + if e, ok := err.(*ShumaiError); ok { + return e + } + return nil +} diff --git a/internal/infrastructure/external/shumai/shumai_factory.go b/internal/infrastructure/external/shumai/shumai_factory.go new file mode 100644 index 0000000..5f80d7f --- /dev/null +++ b/internal/infrastructure/external/shumai/shumai_factory.go @@ -0,0 +1,69 @@ +package shumai + +import ( + "time" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/external_logger" +) + +// NewShumaiServiceWithConfig 使用 config 创建数脉服务 +func NewShumaiServiceWithConfig(cfg *config.Config) (*ShumaiService, error) { + loggingConfig := external_logger.ExternalServiceLoggingConfig{ + Enabled: cfg.Shumai.Logging.Enabled, + LogDir: cfg.Shumai.Logging.LogDir, + ServiceName: "shumai", + UseDaily: cfg.Shumai.Logging.UseDaily, + EnableLevelSeparation: cfg.Shumai.Logging.EnableLevelSeparation, + LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig), + } + for k, v := range cfg.Shumai.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.Shumai.SignMethod == "md5" { + signMethod = SignMethodMD5 + } else { + signMethod = SignMethodHMACMD5 + } + timeout := 60 * time.Second + if cfg.Shumai.Timeout > 0 { + timeout = cfg.Shumai.Timeout + } + + return NewShumaiService( + cfg.Shumai.URL, + cfg.Shumai.AppID, + cfg.Shumai.AppSecret, + signMethod, + timeout, + logger, + cfg.Shumai.AppID2, // 走政务接口使用这个 + cfg.Shumai.AppSecret2, // 走政务接口使用这个 + ), nil +} + +// NewShumaiServiceWithLogging 使用自定义日志配置创建数脉服务 +func NewShumaiServiceWithLogging(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig, appID2, appSecret2 string) (*ShumaiService, error) { + loggingConfig.ServiceName = "shumai" + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + return NewShumaiService(url, appID, appSecret, signMethod, timeout, logger, appID2, appSecret2), nil +} + +// NewShumaiServiceSimple 创建无数脉日志的数脉服务 +func NewShumaiServiceSimple(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, appID2, appSecret2 string) *ShumaiService { + return NewShumaiService(url, appID, appSecret, signMethod, timeout, nil, appID2, appSecret2) +} diff --git a/internal/infrastructure/external/shumai/shumai_service.go b/internal/infrastructure/external/shumai/shumai_service.go new file mode 100644 index 0000000..9f4d9cb --- /dev/null +++ b/internal/infrastructure/external/shumai/shumai_service.go @@ -0,0 +1,279 @@ +package shumai + +import ( + "context" + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "tyapi-server/internal/shared/external_logger" +) + +var ( + ErrDatasource = errors.New("数据源异常") + ErrSystem = errors.New("系统异常") + ErrNotFound = errors.New("查询为空") +) + +// ShumaiResponse 数脉 API 通用响应(占位,按实际文档调整) +type ShumaiResponse struct { + Code int `json:"code"` // 状态码 + Msg string `json:"msg"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// ShumaiConfig 数脉服务配置 +type ShumaiConfig struct { + URL string + AppID string + AppSecret string + AppID2 string // 走政务接口使用这个 + AppSecret2 string // 走政务接口使用这个 + SignMethod SignMethod + Timeout time.Duration +} + +// ShumaiService 数脉服务 +type ShumaiService struct { + config ShumaiConfig + logger *external_logger.ExternalServiceLogger + useGovernment bool // 是否使用政务接口(app_id2) +} + +// NewShumaiService 创建数脉服务实例 +// appID2 和 appSecret2 用于政务接口,如果为空则只使用普通接口 +func NewShumaiService(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger, appID2, appSecret2 string) *ShumaiService { + if signMethod == "" { + signMethod = SignMethodHMACMD5 + } + if timeout == 0 { + timeout = 60 * time.Second + } + return &ShumaiService{ + config: ShumaiConfig{ + URL: url, + AppID: appID, + AppSecret: appSecret, + AppID2: appID2, // 走政务接口使用这个 + AppSecret2: appSecret2, // 走政务接口使用这个 + SignMethod: signMethod, + Timeout: timeout, + }, + logger: logger, + useGovernment: false, + } +} + +func (s *ShumaiService) generateRequestID() string { + timestamp := time.Now().UnixNano() + appID := s.getCurrentAppID() + hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, appID))) + return fmt.Sprintf("shumai_%x", hash[:8]) +} + +// getCurrentAppID 获取当前使用的 AppID +func (s *ShumaiService) getCurrentAppID() string { + if s.useGovernment && s.config.AppID2 != "" { + return s.config.AppID2 + } + return s.config.AppID +} + +// getCurrentAppSecret 获取当前使用的 AppSecret +func (s *ShumaiService) getCurrentAppSecret() string { + if s.useGovernment && s.config.AppSecret2 != "" { + return s.config.AppSecret2 + } + return s.config.AppSecret +} + +// UseGovernment 切换到政务接口(使用 app_id2 和 app_secret2) +func (s *ShumaiService) UseGovernment() { + s.useGovernment = true +} + +// UseNormal 切换到普通接口(使用 app_id 和 app_secret) +func (s *ShumaiService) UseNormal() { + s.useGovernment = false +} + +// IsUsingGovernment 检查是否正在使用政务接口 +func (s *ShumaiService) IsUsingGovernment() bool { + return s.useGovernment +} + +// GetConfig 返回当前配置 +func (s *ShumaiService) GetConfig() ShumaiConfig { + return s.config +} + +// CallAPIForm 以表单方式调用数脉 API(application/x-www-form-urlencoded) +// 在方法内部将 reqFormData 转为表单:先写入业务参数,再追加 appid、timestamp、sign。 +// 签名算法:md5(appid×tamp&app_security),32 位小写,不足补 0。 +func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqFormData map[string]interface{}) ([]byte, error) { + startTime := time.Now() + requestID := s.generateRequestID() + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + appID := s.getCurrentAppID() + appSecret := s.getCurrentAppSecret() + sign := GenerateSignForm(appID, timestamp, appSecret) + + var transactionID string + if id, ok := ctx.Value("transaction_id").(string); ok { + transactionID = id + } + + form := url.Values{} + form.Set("appid", appID) + form.Set("timestamp", timestamp) + form.Set("sign", sign) + for k, v := range reqFormData { + if v == nil { + continue + } + form.Set(k, fmt.Sprint(v)) + } + body := form.Encode() + + baseURL := strings.TrimSuffix(s.config.URL, "/") + + reqURL := baseURL + if apiPath != "" { + reqURL = baseURL + "/" + strings.TrimPrefix(apiPath, "/") + } + if apiPath == "" { + apiPath = "shumai_form" + } + if s.logger != nil { + s.logger.LogRequest(requestID, transactionID, apiPath, reqURL) + } + + req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(body)) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData) + } + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: s.config.Timeout} + resp, err := client.Do(req) + if err != nil { + isTimeout := ctx.Err() == context.DeadlineExceeded + if !isTimeout { + if te, ok := err.(interface{ Timeout() bool }); ok && te.Timeout() { + isTimeout = true + } + } + if !isTimeout { + es := err.Error() + if strings.Contains(es, "deadline exceeded") || strings.Contains(es, "timeout") || strings.Contains(es, "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, reqFormData) + } + return nil, err + } + defer resp.Body.Close() + + duration := time.Since(startTime) + raw, err := io.ReadAll(resp.Body) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData) + } + return nil, err + } + + if resp.StatusCode != http.StatusOK { + err = errors.Join(ErrDatasource, fmt.Errorf("HTTP %d", resp.StatusCode)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData) + } + return nil, err + } + + if s.logger != nil { + s.logger.LogResponse(requestID, transactionID, apiPath, resp.StatusCode, duration) + } + + var shumaiResp ShumaiResponse + if err := json.Unmarshal(raw, &shumaiResp); err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData) + } + return nil, err + } + + codeStr := strconv.Itoa(shumaiResp.Code) + msg := shumaiResp.Msg + if msg == "" { + msg = shumaiResp.Message + } + + shumaiErr := NewShumaiErrorFromCode(codeStr) + if !shumaiErr.IsSuccess() { + if shumaiErr.Message == "未知错误" && msg != "" { + shumaiErr = NewShumaiError(codeStr, msg) + } + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, shumaiErr, reqFormData) + } + if shumaiErr.IsNoRecord() { + return nil, errors.Join(ErrNotFound, shumaiErr) + } + return nil, errors.Join(ErrDatasource, shumaiErr) + } + + if shumaiResp.Data == nil { + return []byte("{}"), nil + } + + dataBytes, err := json.Marshal(shumaiResp.Data) + if err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("data 序列化失败: %w", err)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData) + } + return nil, err + } + return dataBytes, nil +} + +func (s *ShumaiService) Encrypt(data string) (string, error) { + appSecret := s.getCurrentAppSecret() + encryptedValue, err := Encrypt(data, appSecret) + if err != nil { + return "", ErrSystem + } + return encryptedValue, nil +} + +func (s *ShumaiService) Decrypt(encodedData string) ([]byte, error) { + appSecret := s.getCurrentAppSecret() + decryptedValue, err := Decrypt(encodedData, appSecret) + if err != nil { + return nil, ErrSystem + } + return decryptedValue, nil +} diff --git a/internal/infrastructure/external/westdex/westdex_service.go b/internal/infrastructure/external/westdex/westdex_service.go index da9da38..9c2fc77 100644 --- a/internal/infrastructure/external/westdex/westdex_service.go +++ b/internal/infrastructure/external/westdex/westdex_service.go @@ -19,7 +19,7 @@ import ( var ( ErrDatasource = errors.New("数据源异常") ErrSystem = errors.New("系统异常") - ErrNotFound = errors.New("查询为空") + ErrNotFound = errors.New("查询为空") ) type WestResp struct { @@ -72,7 +72,6 @@ func (w *WestDexService) generateRequestID() string { return fmt.Sprintf("westdex_%x", hash[:8]) } - // buildRequestURL 构建请求URL func (w *WestDexService) buildRequestURL(code string) string { timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) @@ -132,14 +131,13 @@ func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[s isTimeout = true } else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() { isTimeout = true - } else if errStr := clientDoErr.Error(); - errStr == "context deadline exceeded" || - errStr == "timeout" || - errStr == "Client.Timeout exceeded" || - errStr == "net/http: request canceled" { + } else if errStr := clientDoErr.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", clientDoErr)) } else { @@ -185,7 +183,6 @@ func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[s } return nil, err } - // 记录响应日志(不记录具体响应数据) if w.logger != nil { w.logger.LogResponseWithID(requestID, transactionID, code, httpResp.StatusCode, duration, westDexResp.ID) @@ -305,14 +302,13 @@ func (w *WestDexService) G05HZ01CallAPI(ctx context.Context, code string, reqDat isTimeout = true } else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() { isTimeout = true - } else if errStr := clientDoErr.Error(); - errStr == "context deadline exceeded" || - errStr == "timeout" || - errStr == "Client.Timeout exceeded" || - errStr == "net/http: request canceled" { + } else if errStr := clientDoErr.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", clientDoErr)) } else { diff --git a/internal/infrastructure/external/zhicha/zhicha_service.go b/internal/infrastructure/external/zhicha/zhicha_service.go index 1a84c68..6027b39 100644 --- a/internal/infrastructure/external/zhicha/zhicha_service.go +++ b/internal/infrastructure/external/zhicha/zhicha_service.go @@ -22,7 +22,6 @@ import ( var ( ErrDatasource = errors.New("数据源异常") ErrSystem = errors.New("系统异常") - ErrNotFound = errors.New("数据为空") ) // contextKey 用于在 context 中存储不跳过 201 错误检查的标志 @@ -30,12 +29,6 @@ type contextKey string const dontSkipCode201CheckKey contextKey = "dont_skip_code_201_check" -// WithSkipCode201Check 返回一个设置了不跳过 201 错误检查标志的 context -// 默认情况下会跳过 201 检查(继续执行),使用此函数后会在 Code == "201" 时返回错误 -func WithSkipCode201Check(ctx context.Context) context.Context { - return context.WithValue(ctx, dontSkipCode201CheckKey, true) -} - type ZhichaResp struct { Code string `json:"code"` Message string `json:"message"` @@ -200,24 +193,8 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st return nil, err } - // 检查是否需要不跳过 201 错误检查(默认跳过,继续执行) - // 如果设置了 dontSkipCode201CheckKey,则返回错误 - dontSkipCode201Check := false - if dontSkip, ok := ctx.Value(dontSkipCode201CheckKey).(bool); ok { - dontSkipCode201Check = dontSkip - } - - // 如果设置了不跳过检查,当 Code == "201" 时返回错误 - if zhichaResp.Code == "201" && dontSkipCode201Check { - if z.logger != nil { - z.logger.LogError(requestID, transactionID, proID, ErrNotFound, params) - } - return nil, ErrNotFound - - } - // 检查业务状态码 - if zhichaResp.Code != "200" && zhichaResp.Code != "201" { + if zhichaResp.Code != "200" { // 创建智查金控错误用于日志记录 zhichaErr := NewZhichaErrorFromCode(zhichaResp.Code) if zhichaErr.Code == "未知错误" {