From 8ab2a6d81d0607c889e96ba527f813d35a22b344 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Mon, 1 Jun 2026 14:39:45 +0800 Subject: [PATCH] f --- config.yaml | 31 +++ internal/config/config.go | 29 +++ internal/container/container.go | 5 + internal/domains/api/dto/api_request_dto.go | 2 +- .../api/services/api_request_service.go | 11 +- .../api/services/form_config_service.go | 4 +- .../api/services/processors/dependencies.go | 4 + .../processors/flxg/flxghb4f_processor.go | 54 ++---- .../processors/qygl/qyglbh7y_processor.go | 42 ++--- .../external/haiyuapi/crypto.go | 24 +++ .../external/haiyuapi/haiyuapi_errors.go | 93 +++++++++ .../external/haiyuapi/haiyuapi_factory.go | 64 +++++++ .../external/haiyuapi/haiyuapi_service.go | 178 ++++++++++++++++++ .../external/haiyuapi/haiyuapi_types.go | 21 +++ 14 files changed, 495 insertions(+), 67 deletions(-) create mode 100644 internal/infrastructure/external/haiyuapi/crypto.go create mode 100644 internal/infrastructure/external/haiyuapi/haiyuapi_errors.go create mode 100644 internal/infrastructure/external/haiyuapi/haiyuapi_factory.go create mode 100644 internal/infrastructure/external/haiyuapi/haiyuapi_service.go create mode 100644 internal/infrastructure/external/haiyuapi/haiyuapi_types.go diff --git a/config.yaml b/config.yaml index d804bda..63ece92 100644 --- a/config.yaml +++ b/config.yaml @@ -697,6 +697,37 @@ nuoer: 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 + +# =========================================== +# 🌐 海宇API(上游数据源)配置 +# =========================================== +haiyuapi: + base_url: "https://api.haiyudata.com" + access_id: "2c136588d34b47fd" # 请求头 Access-Id + secret_key: "39c5c2c02ff13bf2be0ac0b705e21e28" # Access Key(16进制 AES-128 密钥) + timeout: 60s + logging: + enabled: true + log_dir: "logs/external_services" + service_name: "haiyuapi" + use_daily: true + enable_level_separation: true level_configs: info: max_size: 100 diff --git a/internal/config/config.go b/internal/config/config.go index e255e19..f8fc1d0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -46,6 +46,7 @@ type Config struct { PDFGen PDFGenConfig `mapstructure:"pdfgen"` Huibo HuiboConfig `mapstructure:"huibo"` Nuoer NuoerConfig `mapstructure:"nuoer"` + Haiyuapi HaiyuapiConfig `mapstructure:"haiyuapi"` } // ServerConfig HTTP服务器配置 @@ -734,6 +735,34 @@ type NuoerLevelFileConfig struct { Compress bool `mapstructure:"compress"` } +// HaiyuapiConfig 海宇API(上游数据源)配置 +type HaiyuapiConfig struct { + BaseURL string `mapstructure:"base_url"` + AccessID string `mapstructure:"access_id"` + SecretKey string `mapstructure:"secret_key"` + Timeout time.Duration `mapstructure:"timeout"` + + Logging HaiyuapiLoggingConfig `mapstructure:"logging"` +} + +// HaiyuapiLoggingConfig 海宇API日志配置 +type HaiyuapiLoggingConfig struct { + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + ServiceName string `mapstructure:"service_name"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + LevelConfigs map[string]HaiyuapiLevelFileConfig `mapstructure:"level_configs"` +} + +// HaiyuapiLevelFileConfig 海宇API级别文件配置 +type HaiyuapiLevelFileConfig 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 a0c99f8..1cc94d1 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -51,6 +51,7 @@ import ( "tyapi-server/internal/infrastructure/external/shumai" "tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/storage" + "tyapi-server/internal/infrastructure/external/haiyuapi" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -410,6 +411,10 @@ func NewContainer() *Container { func(cfg *config.Config) (*nuoer.NuoerService, error) { return nuoer.NewNuoerServiceWithConfig(cfg) }, + // HaiyuapiService - 海宇API上游服务 + func(cfg *config.Config) (*haiyuapi.HaiyuapiService, error) { + return haiyuapi.NewHaiyuapiServiceWithConfig(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 0cd109b..4f2a101 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -608,7 +608,7 @@ type QYGL5A3CReq struct { } type QYGLBH7YReq struct { - EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"` + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` } type QYGL2naoReq struct { diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index bec0c53..e3617f7 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -21,6 +21,7 @@ import ( "tyapi-server/internal/domains/api/services/processors/yysy" "tyapi-server/internal/domains/product/services" "tyapi-server/internal/infrastructure/external/alicloud" + "tyapi-server/internal/infrastructure/external/haiyuapi" "tyapi-server/internal/infrastructure/external/huibo" "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/muzi" @@ -70,6 +71,7 @@ func NewApiRequestService( shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, nuoerService *nuoer.NuoerService, + haiyuapiService *haiyuapi.HaiyuapiService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, cfg *appconfig.Config, @@ -87,6 +89,7 @@ func NewApiRequestService( shumaiService, huiboService, nuoerService, + haiyuapiService, validator, productManagementService, cfg, @@ -109,6 +112,7 @@ func NewApiRequestServiceWithRepos( shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, nuoerService *nuoer.NuoerService, + haiyuapiService *haiyuapi.HaiyuapiService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, cfg *appconfig.Config, @@ -137,6 +141,7 @@ func NewApiRequestServiceWithRepos( shumaiService, huiboService, nuoerService, + haiyuapiService, validator, combService, reportRepo, @@ -191,7 +196,8 @@ func registerAllProcessors(combService *comb.CombService) { "FLXG3A9B": flxg.ProcessFLXG3A9BRequest, "FLXGK5D2": flxg.ProcessFLXGK5D2Request, "FLXGDJG3": flxg.ProcessFLXGDJG3Request, //董监高司法综合信息核验 - "FLXGHB4F": flxg.ProcessFLXGHB4FRequest, //个人涉诉案件查询汇博 + "FLXGHB4F": flxg.ProcessFLXGHB4FRequest, //个人涉诉案件查询(海宇API) + // JRZQ系列处理器 "JRZQ8203": jrzq.ProcessJRZQ8203Request, "JRZQ0A03": jrzq.ProcessJRZQ0A03Request, @@ -260,11 +266,12 @@ func registerAllProcessors(combService *comb.CombService) { "QYGLDJ12": qygl.ProcessQYGLDJ12Request, //企业年报信息核验 "QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查 "QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验 - "QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询汇博 "QYGL4YAB": qygl.ProcessQYGL4YABRequest, //企业四要素认证shumai "QYGL3YSB": qygl.ProcessQYGL3YSBRequest, //企业三要素认证shumai "QYGL2YSB": qygl.ProcessQYGL2YSBRequest, //企业二要素认证shumai "QYGLDG77": qygl.ProcessQYGLDG77Request, //企业对公打款认证shumai + "QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询海宇 + // YYSY系列处理器 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 "YYSYD50F": yysy.ProcessYYSYD50FRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 070ae7e..d98759c 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -281,8 +281,8 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZRAX1": &dto.IVYZRAX1Req{}, //融安信用分 "IVYZRAX2": &dto.IVYZRAX1Req{}, //融御反欺诈 "IVYZ2MN7": &dto.IVYZ2MN6Req{}, //学历Bzhicha - "FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询汇博 - "QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询汇博 + "FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询(海宇API) + "QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询(海宇API) "QYGL4YAB": &dto.QYGL4YABReq{}, //企业四要素认证shumai "QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 39a8058..a15200f 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -12,6 +12,7 @@ import ( "tyapi-server/internal/infrastructure/external/nuoer" "tyapi-server/internal/infrastructure/external/shujubao" "tyapi-server/internal/infrastructure/external/shumai" + "tyapi-server/internal/infrastructure/external/haiyuapi" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -44,6 +45,7 @@ type ProcessorDependencies struct { ShumaiService *shumai.ShumaiService HuiboService *huibo.HuiboService NuoerService *nuoer.NuoerService + HaiyuapiService *haiyuapi.HaiyuapiService Validator interfaces.RequestValidator CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 @@ -73,6 +75,7 @@ func NewProcessorDependencies( shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, nuoerService *nuoer.NuoerService, + haiyuapiService *haiyuapi.HaiyuapiService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface reportRepo repositories.ReportRepository, @@ -92,6 +95,7 @@ func NewProcessorDependencies( ShumaiService: shumaiService, HuiboService: huiboService, NuoerService: nuoerService, + HaiyuapiService: haiyuapiService, Validator: validator, CombService: combService, Options: nil, // 初始化为nil,在调用时设置 diff --git a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go index 0518e00..f427896 100644 --- a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go @@ -7,54 +7,40 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/huibo" + "tyapi-server/internal/infrastructure/external/haiyuapi" ) -// ProcessFLXGHB4FRequest FLXGHB4F API处理方法 - 个人涉诉案件查询汇博 +// ProcessFLXGHB4FRequest FLXGHB4F API处理方法 - 个人涉诉案件查询(海宇API) func ProcessFLXGHB4FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.FLXGHB4FReq if err := json.Unmarshal(params, ¶msDto); err != nil { return nil, errors.Join(processors.ErrSystem, err) } - if deps.HuiboService == nil { - return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化")) + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) } - // 使用 MD5 加密 name 和 idCard - // encryptedName := "MD5:" + huibo.MD5Encrypt(paramsDto.Name, deps.HuiboService.GetConfig().AppKey) - // encryptedIDCard := "MD5:" + huibo.MD5Encrypt(paramsDto.IDCard, deps.HuiboService.GetConfig().AppKey) - - reqdata := map[string]interface{}{ - "name": paramsDto.Name, - "idCard": paramsDto.IDCard, + if deps.HaiyuapiService == nil { + return nil, errors.Join(processors.ErrSystem, errors.New("海宇API服务未初始化")) } - respBytes, err := deps.HuiboService.CallAPI2(ctx, "P_004_0271", reqdata) + reqParams := map[string]interface{}{ + "name": paramsDto.Name, + "id_card": paramsDto.IDCard, + } + + apiPath := "/api/v1/FLXGHB4F" + respBytes, err := deps.HaiyuapiService.CallAPI(ctx, apiPath, reqParams) if err != nil { - return nil, errors.Join(processors.ErrDatasource, err) - } - - // 解析响应 - var response huibo.CallAPI2Response - if err := json.Unmarshal(respBytes, &response); err != nil { + if errors.Is(err, haiyuapi.ErrNotFound) { + return nil, errors.Join(processors.ErrNotFound, err) + } + if errors.Is(err, haiyuapi.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } return nil, errors.Join(processors.ErrSystem, err) } - // 处理状态码 - switch response.Code { - case huibo.CallAPI2StatusSuccess: - // 查询成功 - if response.Data == nil { - return []byte("{}"), nil - } - return respBytes, nil - case huibo.CallAPI2StatusNoData: - // 查询成功,无数据 - 按产品约定按调用成功计费 - return []byte("{}"), nil - default: - // 其他错误状态码 - message := huibo.GetCallAPI2StatusMessage(response.Code) - return nil, errors.Join(processors.ErrDatasource, errors.New(message)) - } + return respBytes, nil } diff --git a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go index 8f4c41f..524d888 100644 --- a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go +++ b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go @@ -7,7 +7,7 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/huibo" + "tyapi-server/internal/infrastructure/external/haiyuapi" ) // ProcessQYGLBH7YRequest QYGLBH7Y API处理方法 - 企业案件查询汇博 @@ -17,39 +17,25 @@ func ProcessQYGLBH7YRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrSystem, err) } - if deps.HuiboService == nil { - return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化")) + if deps.HaiyuapiService == nil { + return nil, errors.Join(processors.ErrSystem, errors.New("海宇API服务未初始化")) } - reqdata := map[string]interface{}{ - "companyName": paramsDto.EntName, + reqParams := map[string]interface{}{ + "ent_name": paramsDto.EntName, } - respBytes, err := deps.HuiboService.CallAPI2(ctx, "E_004_0261", reqdata) + apiPath := "/api/v1/QYGLBH7Y" + respBytes, err := deps.HaiyuapiService.CallAPI(ctx, apiPath, reqParams) if err != nil { - return nil, errors.Join(processors.ErrDatasource, err) - } - - // 解析响应 - var response huibo.CallAPI2Response - if err := json.Unmarshal(respBytes, &response); err != nil { + if errors.Is(err, haiyuapi.ErrNotFound) { + return nil, errors.Join(processors.ErrNotFound, err) + } + if errors.Is(err, haiyuapi.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } return nil, errors.Join(processors.ErrSystem, err) } - // 处理状态码 - switch response.Code { - case huibo.CallAPI2StatusSuccess: - // 查询成功 - if response.Data == nil { - return []byte("{}"), nil - } - return respBytes, nil - case huibo.CallAPI2StatusNoData: - // 查询成功,无数据 - 按产品约定按调用成功计费 - return []byte("{}"), nil - default: - // 其他错误状态码 - message := huibo.GetCallAPI2StatusMessage(response.Code) - return nil, errors.Join(processors.ErrDatasource, errors.New(message)) - } + return respBytes, nil } diff --git a/internal/infrastructure/external/haiyuapi/crypto.go b/internal/infrastructure/external/haiyuapi/crypto.go new file mode 100644 index 0000000..315ddc6 --- /dev/null +++ b/internal/infrastructure/external/haiyuapi/crypto.go @@ -0,0 +1,24 @@ +package haiyuapi + +import ( + "encoding/json" + + "tyapi-server/internal/shared/crypto" +) + +// EncryptParams 将业务参数序列化为 JSON 后,使用 Access Key(16进制)AES-128-CBC 加密并 Base64 编码 +func EncryptParams(params map[string]interface{}, accessKey string) (string, error) { + plainJSON, err := json.Marshal(params) + if err != nil { + return "", err + } + return crypto.AesEncrypt(plainJSON, accessKey) +} + +// DecryptData 解密响应 data 字段(IV+密文 Base64);空字符串视为无数据,返回 {} +func DecryptData(encrypted, accessKey string) ([]byte, error) { + if encrypted == "" { + return []byte("{}"), nil + } + return crypto.AesDecrypt(encrypted, accessKey) +} diff --git a/internal/infrastructure/external/haiyuapi/haiyuapi_errors.go b/internal/infrastructure/external/haiyuapi/haiyuapi_errors.go new file mode 100644 index 0000000..bf8a968 --- /dev/null +++ b/internal/infrastructure/external/haiyuapi/haiyuapi_errors.go @@ -0,0 +1,93 @@ +package haiyuapi + +import ( + "errors" + "fmt" +) + +// 海宇API平台返回码 +const ( + CodeSuccess = 0 // 业务成功 + CodeQueryEmpty = 1000 // 查询为空 + CodeSystem = 1001 // 接口异常 + CodeDecryptFail = 1002 // 参数解密失败 + CodeRequestParam = 1003 // 基础参数校验不正确 + CodeInvalidIP = 1004 // 未经授权的IP + CodeMissingAccessID = 1005 // 缺少Access-Id + CodeInvalidAccessID = 1006 // 未经授权的AccessId + CodeInsufficientBalance = 1007 // 账户余额不足,无法请求 + CodeProductNotSubscribed = 1008 // 未开通此产品 + CodeBusiness = 2001 // 业务失败 +) + +var ( + ErrDatasource = errors.New("数据源异常") + ErrSystem = errors.New("系统异常") + ErrNotFound = errors.New("查询为空") +) + +// haiyuapiAPIError 海宇API平台错误 +type haiyuapiAPIError struct { + Code int + Message string +} + +func (e *haiyuapiAPIError) Error() string { + return fmt.Sprintf("海宇API返回错误,code: %d,message: %s", e.Code, e.Message) +} + +// NewHaiyuapiAPIError 创建平台错误 +func NewHaiyuapiAPIError(code int, message string) *haiyuapiAPIError { + if message == "" { + if desc := GetPlatformCodeDesc(code); desc != "" { + message = desc + } else { + message = "海宇API返回未知错误" + } + } + return &haiyuapiAPIError{Code: code, Message: message} +} + +// platformCodeDesc 平台 code -> 官方 message +var platformCodeDesc = map[int]string{ + CodeSuccess: "业务成功", + CodeQueryEmpty: "查询为空", + CodeSystem: "接口异常", + CodeDecryptFail: "参数解密失败", + CodeRequestParam: "基础参数校验不正确", + CodeInvalidIP: "未经授权的IP", + CodeMissingAccessID: "缺少Access-Id", + CodeInvalidAccessID: "未经授权的AccessId", + CodeInsufficientBalance: "账户余额不足,无法请求", + CodeProductNotSubscribed: "未开通此产品", + CodeBusiness: "业务失败", +} + +// GetPlatformCodeDesc 根据平台 code 获取描述 +func GetPlatformCodeDesc(code int) string { + if desc, ok := platformCodeDesc[code]; ok { + return desc + } + return "" +} + +// GetErrByCode 将海宇API code 映射为内部哨兵错误,供处理器 errors.Is 判断 +// +// 1000 -> ErrNotFound(查询为空,可按产品约定当成功处理) +// 1001, 1002, 1003 -> ErrSystem(接口/解密/参数校验异常) +// 1004~1008, 2001 -> ErrDatasource(鉴权、余额、产品、业务类上游错误) +func GetErrByCode(code int) error { + switch code { + case CodeSuccess: + return nil + case CodeQueryEmpty: + return ErrNotFound + case CodeSystem, CodeDecryptFail, CodeRequestParam: + return ErrSystem + case CodeInvalidIP, CodeMissingAccessID, CodeInvalidAccessID, + CodeInsufficientBalance, CodeProductNotSubscribed, CodeBusiness: + return ErrDatasource + default: + return ErrDatasource + } +} diff --git a/internal/infrastructure/external/haiyuapi/haiyuapi_factory.go b/internal/infrastructure/external/haiyuapi/haiyuapi_factory.go new file mode 100644 index 0000000..778d772 --- /dev/null +++ b/internal/infrastructure/external/haiyuapi/haiyuapi_factory.go @@ -0,0 +1,64 @@ +package haiyuapi + +import ( + "time" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/external_logger" +) + +// NewHaiyuapiServiceWithConfig 使用配置创建海宇API服务 +func NewHaiyuapiServiceWithConfig(cfg *config.Config) (*HaiyuapiService, error) { + loggingConfig := external_logger.ExternalServiceLoggingConfig{ + Enabled: cfg.Haiyuapi.Logging.Enabled, + LogDir: cfg.Haiyuapi.Logging.LogDir, + ServiceName: "haiyuapi", + UseDaily: cfg.Haiyuapi.Logging.UseDaily, + EnableLevelSeparation: cfg.Haiyuapi.Logging.EnableLevelSeparation, + LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig), + } + + for level, levelCfg := range cfg.Haiyuapi.Logging.LevelConfigs { + loggingConfig.LevelConfigs[level] = external_logger.ExternalServiceLevelFileConfig{ + MaxSize: levelCfg.MaxSize, + MaxBackups: levelCfg.MaxBackups, + MaxAge: levelCfg.MaxAge, + Compress: levelCfg.Compress, + } + } + + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + timeout := cfg.Haiyuapi.Timeout + if timeout <= 0 { + timeout = defaultRequestTimeout + } + + return NewHaiyuapiService( + cfg.Haiyuapi.BaseURL, + cfg.Haiyuapi.AccessID, + cfg.Haiyuapi.SecretKey, + timeout, + logger, + ), nil +} + +// NewHaiyuapiServiceWithLogging 使用自定义日志配置创建海宇API服务 +func NewHaiyuapiServiceWithLogging(baseURL, accessID, secretKey string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*HaiyuapiService, error) { + loggingConfig.ServiceName = "haiyuapi" + + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + return NewHaiyuapiService(baseURL, accessID, secretKey, timeout, logger), nil +} + +// NewHaiyuapiServiceSimple 创建无日志的海宇API服务 +func NewHaiyuapiServiceSimple(baseURL, accessID, secretKey string, timeout time.Duration) *HaiyuapiService { + return NewHaiyuapiService(baseURL, accessID, secretKey, timeout, nil) +} diff --git a/internal/infrastructure/external/haiyuapi/haiyuapi_service.go b/internal/infrastructure/external/haiyuapi/haiyuapi_service.go new file mode 100644 index 0000000..ec2e1bf --- /dev/null +++ b/internal/infrastructure/external/haiyuapi/haiyuapi_service.go @@ -0,0 +1,178 @@ +package haiyuapi + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "tyapi-server/internal/shared/external_logger" +) + +const defaultRequestTimeout = 60 * time.Second + +// serviceConfig 海宇API服务运行时配置(Access Key 为 16 进制 AES-128 密钥) +type serviceConfig struct { + BaseURL string + AccessID string + SecretKey string + Timeout time.Duration +} + +// HaiyuapiService 海宇API上游服务客户端 +type HaiyuapiService struct { + config serviceConfig + logger *external_logger.ExternalServiceLogger +} + +// NewHaiyuapiService 创建海宇API服务实例 +func NewHaiyuapiService(baseURL, accessID, secretKey string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *HaiyuapiService { + if timeout <= 0 { + timeout = defaultRequestTimeout + } + return &HaiyuapiService{ + config: serviceConfig{ + BaseURL: strings.TrimRight(baseURL, "/"), + AccessID: accessID, + SecretKey: secretKey, + Timeout: timeout, + }, + logger: logger, + } +} + +// CallAPI 调用海宇API:apiPath 如 /api/v1/FLXGHB4F,自动拼接 base_url 与 ?t=13位毫秒时间戳,返回解密后的明文 JSON +func (s *HaiyuapiService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) ([]byte, error) { + startTime := time.Now() + hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", time.Now().UnixNano(), s.config.SecretKey))) + requestID := fmt.Sprintf("haiyuapi_%x", hash[:8]) + + var transactionID string + if id, ok := ctx.Value("transaction_id").(string); ok { + transactionID = id + } + + path := apiPath + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + reqURL := s.config.BaseURL + path + "?t=" + timestamp + + if s.logger != nil { + s.logger.LogRequest(requestID, transactionID, apiPath, reqURL) + } + + encryptedData, err := EncryptParams(params, s.config.SecretKey) + if 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 + } + + bodyBytes, err := json.Marshal(RequestPayload{Data: encryptedData}) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, params) + } + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewBuffer(bodyBytes)) + 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(HeaderContentType, ContentTypeJSON) + req.Header.Set(HeaderAccessID, s.config.AccessID) + + resp, err := (&http.Client{Timeout: s.config.Timeout}).Do(req) + if err != nil { + err = wrapHTTPError(err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, params) + } + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, err, params) + } + return nil, err + } + + duration := time.Since(startTime) + + 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, params) + } + return nil, err + } + + var apiResp APIResponse + if err := json.Unmarshal(respBody, &apiResp); 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 + } + + if apiResp.Code != CodeSuccess { + apiErr := NewHaiyuapiAPIError(apiResp.Code, apiResp.Message) + err = errors.Join(GetErrByCode(apiResp.Code), apiErr) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiPath, apiErr, params) + } + return nil, err + } + + plainResp, err := DecryptData(apiResp.Data, s.config.SecretKey) + if 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 + } + + if s.logger != nil { + s.logger.LogResponse(requestID, transactionID, apiPath, resp.StatusCode, duration) + } + + return plainResp, nil +} + +func wrapHTTPError(err error) error { + if err == context.DeadlineExceeded { + return errors.Join(ErrDatasource, err) + } + if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { + return errors.Join(ErrDatasource, err) + } + switch err.Error() { + case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled": + return errors.Join(ErrDatasource, err) + default: + return errors.Join(ErrSystem, err) + } +} diff --git a/internal/infrastructure/external/haiyuapi/haiyuapi_types.go b/internal/infrastructure/external/haiyuapi/haiyuapi_types.go new file mode 100644 index 0000000..3167d60 --- /dev/null +++ b/internal/infrastructure/external/haiyuapi/haiyuapi_types.go @@ -0,0 +1,21 @@ +package haiyuapi + +// HTTP 请求头 +const ( + HeaderAccessID = "Access-Id" + HeaderContentType = "Content-Type" + ContentTypeJSON = "application/json" +) + +// APIResponse 海宇API公共响应(data 为 AES 加密后的 Base64 字符串) +type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + TransactionID string `json:"transaction_id"` + Data string `json:"data"` +} + +// RequestPayload 海宇API请求体(业务参数加密后置于 data) +type RequestPayload struct { + Data string `json:"data"` +}