From c740ae56393d435cf4777597b146744be0a003dc Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Sun, 9 Nov 2025 16:08:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20muzi=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=B9=B6=E6=8E=A5=E5=85=A5=20ivyz3p9m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml | 28 ++ .../product_application_service_impl.go | 15 +- internal/config/config.go | 46 ++- internal/container/container.go | 21 +- internal/domains/api/dto/api_request_dto.go | 5 + .../api/services/api_request_service.go | 19 +- .../api/services/form_config_service.go | 1 + .../api/services/processors/dependencies.go | 13 +- .../processors/ivyz/ivyz3p9m_processor.go | 52 +++ .../external/muzi/muzi_errors.go | 25 ++ .../external/muzi/muzi_factory.go | 61 +++ .../external/muzi/muzi_service.go | 389 ++++++++++++++++++ .../external/xingwei/xingwei_service.go | 77 ++-- .../shared/external_logger/external_logger.go | 27 +- 14 files changed, 695 insertions(+), 84 deletions(-) create mode 100644 internal/domains/api/services/processors/ivyz/ivyz3p9m_processor.go create mode 100644 internal/infrastructure/external/muzi/muzi_errors.go create mode 100644 internal/infrastructure/external/muzi/muzi_factory.go create mode 100644 internal/infrastructure/external/muzi/muzi_service.go diff --git a/config.yaml b/config.yaml index 1460c26..4bd9f34 100644 --- a/config.yaml +++ b/config.yaml @@ -411,6 +411,34 @@ zhicha: max_age: 30 compress: true +# =========================================== +# 🌐 木子数据配置 +# =========================================== +muzi: + url: "https://carv.m0101.com/magic/carv/pubin/service/academic" + app_id: "713014138179585" + app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d" + timeout: 60s + + logging: + enabled: true + log_dir: "logs/external_services" + service_name: "muzi" + use_daily: true + enable_level_separation: true + + level_configs: + info: + max_size: 50 + max_backups: 3 + max_age: 7 + compress: true + error: + max_size: 100 + max_backups: 5 + max_age: 30 + compress: true + # =========================================== # 🎯 行为数据配置 # =========================================== diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 63c3b23..0e0c422 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -622,7 +622,7 @@ func (s *ProductApplicationServiceImpl) mergePackageItemsDocumentation(ctx conte // 合并文档内容 mergedDoc := &responses.DocumentationResponse{ ProductID: packageItems[0].ProductID, // 使用第一个子产品的ID作为标识 - RequestMethod: "POST", // 默认方法 + RequestMethod: "POST", // 默认方法 Version: "1.0", } @@ -755,7 +755,7 @@ func (s *ProductApplicationServiceImpl) buildCombPackageResponseExample(subProdu } builder.WriteString(" {\n") builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", spDoc.item.ProductCode)) - + // 第一个示例显示成功,其他可能显示部分失败 if i == 0 { builder.WriteString(" \"success\": true,\n") @@ -804,7 +804,7 @@ func (s *ProductApplicationServiceImpl) buildCombPackageResponseExample(subProdu if len(subProductDocs) > 0 { builder.WriteString("## 各子产品详细响应示例\n\n") builder.WriteString("以下为各个子产品的详细响应示例,组合包响应中的 `data` 字段即为对应子产品的完整响应数据。\n\n") - + for i, spDoc := range subProductDocs { if i > 0 { builder.WriteString("\n---\n\n") @@ -823,10 +823,6 @@ func (s *ProductApplicationServiceImpl) buildCombPackageResponseExample(subProdu return builder.String() } - - - - // paramField 参数字段信息 type paramField struct { Name string @@ -1017,6 +1013,7 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} { "FLXG8B4D": &dto.FLXG8B4DReq{}, "IVYZ81NC": &dto.IVYZ81NCReq{}, "IVYZ7F3A": &dto.IVYZ7F3AReq{}, + "IVYZ3P9M": &dto.IVYZ3P9MReq{}, "IVYZ3A7F": &dto.IVYZ3A7FReq{}, "IVYZ9D2E": &dto.IVYZ9D2EReq{}, "DWBG7F3A": &dto.DWBG7F3AReq{}, @@ -1127,7 +1124,7 @@ func (s *ProductApplicationServiceImpl) generateFieldDescription(jsonTag string, descMap := map[string]string{ "mobile_no": "手机号码(11位)", "id_card": "身份证号码(18位)", - "name": "姓名", + "name": "姓名", "man_name": "男方姓名", "woman_name": "女方姓名", "man_id_card": "男方身份证号码", @@ -1183,4 +1180,4 @@ func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType strin default: return "string" } -} \ No newline at end of file +} diff --git a/internal/config/config.go b/internal/config/config.go index 22f6e53..ba63e76 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,7 @@ type Config struct { Wallet WalletConfig `mapstructure:"wallet"` WestDex WestDexConfig `mapstructure:"westdex"` Zhicha ZhichaConfig `mapstructure:"zhicha"` + Muzi MuziConfig `mapstructure:"muzi"` AliPay AliPayConfig `mapstructure:"alipay"` Yushan YushanConfig `mapstructure:"yushan"` TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` @@ -141,9 +142,9 @@ type DailyRateLimitConfig struct { EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理 MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数 // 路径排除配置 - ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径 + ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径 // 域名排除配置 - ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名 + ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名 } // MonitoringConfig 监控配置 @@ -324,9 +325,9 @@ type WalletConfig struct { // BalanceAlertConfig 余额预警配置 type BalanceAlertConfig struct { - DefaultEnabled bool `mapstructure:"default_enabled"` // 默认启用余额预警 - DefaultThreshold float64 `mapstructure:"default_threshold"` // 默认预警阈值 - AlertCooldownHours int `mapstructure:"alert_cooldown_hours"` // 预警冷却时间(小时) + DefaultEnabled bool `mapstructure:"default_enabled"` // 默认启用余额预警 + DefaultThreshold float64 `mapstructure:"default_threshold"` // 默认预警阈值 + AlertCooldownHours int `mapstructure:"alert_cooldown_hours"` // 预警冷却时间(小时) } // AliPayRechargeBonusRule 支付宝充值赠送规则 @@ -391,6 +392,33 @@ type ZhichaLevelFileConfig struct { Compress bool `mapstructure:"compress"` } +// MuziConfig 木子数据配置 +type MuziConfig struct { + URL string `mapstructure:"url"` + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` + Timeout time.Duration `mapstructure:"timeout"` + + Logging MuziLoggingConfig `mapstructure:"logging"` +} + +// MuziLoggingConfig 木子数据日志配置 +type MuziLoggingConfig struct { + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + LevelConfigs map[string]MuziLevelFileConfig `mapstructure:"level_configs"` +} + +// MuziLevelFileConfig 木子数据日志级别配置 +type MuziLevelFileConfig struct { + MaxSize int `mapstructure:"max_size"` + MaxBackups int `mapstructure:"max_backups"` + MaxAge int `mapstructure:"max_age"` + Compress bool `mapstructure:"compress"` +} + // AliPayConfig 支付宝配置 type AliPayConfig struct { AppID string `mapstructure:"app_id"` @@ -451,10 +479,10 @@ type XingweiConfig struct { // XingweiLoggingConfig 行为数据日志配置 type XingweiLoggingConfig struct { - Enabled bool `mapstructure:"enabled"` - LogDir string `mapstructure:"log_dir"` - UseDaily bool `mapstructure:"use_daily"` - EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` LevelConfigs map[string]XingweiLevelFileConfig `mapstructure:"level_configs"` } diff --git a/internal/container/container.go b/internal/container/container.go index 03e6cf2..d5b7a39 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -37,6 +37,7 @@ import ( infra_events "tyapi-server/internal/infrastructure/events" "tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/email" + "tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/ocr" "tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/storage" @@ -339,6 +340,10 @@ func NewContainer() *Container { func(cfg *config.Config) (*westdex.WestDexService, error) { return westdex.NewWestDexServiceWithConfig(cfg) }, + // MuziService - 木子数据服务 + func(cfg *config.Config) (*muzi.MuziService, error) { + return muzi.NewMuziServiceWithConfig(cfg) + }, // ZhichaService - 智查金控服务 func(cfg *config.Config) (*zhicha.ZhichaService, error) { return zhicha.NewZhichaServiceWithConfig(cfg) @@ -401,9 +406,9 @@ func NewContainer() *Container { BlockedCountries: cfg.DailyRateLimit.BlockedCountries, EnableProxyCheck: cfg.DailyRateLimit.EnableProxyCheck, // 排除路径配置 - ExcludePaths: cfg.DailyRateLimit.ExcludePaths, + ExcludePaths: cfg.DailyRateLimit.ExcludePaths, // 排除域名配置 - ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains, + ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains, } return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig) }, @@ -767,8 +772,8 @@ func NewContainer() *Container { }, // AsynqWorker - 任务处理器 func( - cfg *config.Config, - logger *zap.Logger, + cfg *config.Config, + logger *zap.Logger, articleApplicationService article.ArticleApplicationService, apiApplicationService api_app.ApiApplicationService, walletService finance_services.WalletAggregateService, @@ -777,8 +782,8 @@ func NewContainer() *Container { ) *asynq.AsynqWorker { redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port) return asynq.NewAsynqWorker( - redisAddr, - logger, + redisAddr, + logger, articleApplicationService, apiApplicationService, walletService, @@ -1076,12 +1081,12 @@ func RegisterLifecycleHooks( }, OnStop: func(context.Context) error { logger.Info("应用关闭中...") - + // 停止AsynqWorker asynqWorker.Stop() asynqWorker.Shutdown() logger.Info("AsynqWorker已停止") - + return nil }, }) diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index f939647..dcbc040 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -281,6 +281,11 @@ type IVYZ7F3AReq struct { Authorized string `json:"authorized" validate:"required,oneof=0 1"` } +type IVYZ3P9MReq struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` +} + type IVYZ3A7FReq struct { Name string `json:"name" validate:"required,min=1,validName"` IDCard string `json:"id_card" validate:"required,validIDCard"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index a4a2477..7e73f4c 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -18,6 +18,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/muzi" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -36,6 +37,7 @@ var ( type ApiRequestService struct { // 可注入依赖,如第三方服务、模型等 westDexService *westdex.WestDexService + muziService *muzi.MuziService yushanService *yushan.YushanService tianYanChaService *tianyancha.TianYanChaService alicloudService *alicloud.AlicloudService @@ -46,6 +48,7 @@ type ApiRequestService struct { func NewApiRequestService( westDexService *westdex.WestDexService, + muziService *muzi.MuziService, yushanService *yushan.YushanService, tianYanChaService *tianyancha.TianYanChaService, alicloudService *alicloud.AlicloudService, @@ -58,13 +61,14 @@ func NewApiRequestService( combService := comb.NewCombService(productManagementService) // 创建处理器依赖容器 - processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService) + processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService) // 统一注册所有处理器 registerAllProcessors(combService) return &ApiRequestService{ westDexService: westDexService, + muziService: muziService, yushanService: yushanService, tianYanChaService: tianYanChaService, alicloudService: alicloudService, @@ -155,7 +159,7 @@ func registerAllProcessors(combService *comb.CombService) { "YYSY8C2D": yysy.ProcessYYSY8C2DRequest, "YYSY7D3E": yysy.ProcessYYSY7D3ERequest, "YYSY9E4A": yysy.ProcessYYSY9E4ARequest, - + // IVYZ系列处理器 "IVYZ0B03": ivyz.ProcessIVYZ0B03Request, "IVYZ2125": ivyz.ProcessIVYZ2125Request, @@ -172,12 +176,13 @@ func registerAllProcessors(combService *comb.CombService) { "IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest, "IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest, "IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest, + "IVYZ3P9M": ivyz.ProcessIVYZ3P9MRequest, "IVYZ3A7F": ivyz.ProcessIVYZ3A7FRequest, "IVYZ9D2E": ivyz.ProcessIVYZ9D2ERequest, "IVYZ81NC": ivyz.ProcessIVYZ81NCRequest, "IVYZ6G7H": ivyz.ProcessIVYZ6G7HRequest, "IVYZ8I9J": ivyz.ProcessIVYZ8I9JRequest, - + // COMB系列处理器 - 只注册有自定义逻辑的组合包 "COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode @@ -215,17 +220,17 @@ var RequestProcessors map[string]processors.ProcessorFunc func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode string, params []byte, options *commands.ApiCallOptions, callContext *processors.CallContext) ([]byte, error) { // 设置Options和CallContext到依赖容器 deps := a.processorDeps.WithOptions(options).WithCallContext(callContext) - + // 1. 优先查找已注册的自定义处理器 if processor, exists := RequestProcessors[apiCode]; exists { return processor(ctx, params, deps) } - + // 2. 检查是否为组合包(COMB开头),使用通用组合包处理器 if len(apiCode) >= 4 && apiCode[:4] == "COMB" { return a.processGenericCombRequest(ctx, apiCode, params, deps) } - + return nil, fmt.Errorf("%s: 未找到处理器: %s", ErrSystem, apiCode) } @@ -237,7 +242,7 @@ func (a *ApiRequestService) processGenericCombRequest(ctx context.Context, apiCo if err != nil { return nil, err } - + // 直接返回组合结果,无任何自定义处理 return json.Marshal(combinedResult) } diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 32a7220..0f3d474 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -152,6 +152,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "FLXG8B4D": &dto.FLXG8B4DReq{}, "IVYZ81NC": &dto.IVYZ81NCReq{}, "IVYZ7F3A": &dto.IVYZ7F3AReq{}, + "IVYZ3P9M": &dto.IVYZ3P9MReq{}, "IVYZ3A7F": &dto.IVYZ3A7FReq{}, "IVYZ9D2E": &dto.IVYZ9D2EReq{}, "DWBG7F3A": &dto.DWBG7F3AReq{}, diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 3ff3fa6..67c0018 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -4,6 +4,7 @@ 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/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -25,20 +26,22 @@ type CallContext struct { // ProcessorDependencies 处理器依赖容器 type ProcessorDependencies struct { WestDexService *westdex.WestDexService + MuziService *muzi.MuziService YushanService *yushan.YushanService TianYanChaService *tianyancha.TianYanChaService AlicloudService *alicloud.AlicloudService ZhichaService *zhicha.ZhichaService XingweiService *xingwei.XingweiService Validator interfaces.RequestValidator - CombService CombServiceInterface // Changed to interface to break import cycle + CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 - CallContext *CallContext // 添加CallApi调用上下文 + CallContext *CallContext // 添加CallApi调用上下文 } // NewProcessorDependencies 创建处理器依赖容器 func NewProcessorDependencies( westDexService *westdex.WestDexService, + muziService *muzi.MuziService, yushanService *yushan.YushanService, tianYanChaService *tianyancha.TianYanChaService, alicloudService *alicloud.AlicloudService, @@ -49,6 +52,7 @@ func NewProcessorDependencies( ) *ProcessorDependencies { return &ProcessorDependencies{ WestDexService: westDexService, + MuziService: muziService, YushanService: yushanService, TianYanChaService: tianYanChaService, AlicloudService: alicloudService, @@ -74,9 +78,7 @@ func (deps *ProcessorDependencies) WithCallContext(callContext *CallContext) *Pr } // ProcessorFunc 处理器函数类型定义 -type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) - - +type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) // CombinedResult 组合结果 type CombinedResult struct { @@ -91,4 +93,3 @@ type SubProductResult struct { Error string `json:"error,omitempty"` // 错误信息(仅在失败时) SortOrder int `json:"-"` // 排序字段,不输出到JSON } - diff --git a/internal/domains/api/services/processors/ivyz/ivyz3p9m_processor.go b/internal/domains/api/services/processors/ivyz/ivyz3p9m_processor.go new file mode 100644 index 0000000..0ce2909 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyz3p9m_processor.go @@ -0,0 +1,52 @@ +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/muzi" +) + +// ProcessIVYZ3P9MRequest IVYZ3P9M API处理方法 - 学历查询实时版 +func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZ3P9MReq + 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) + } + + encryptedName, err := deps.MuziService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedCertCode, err := deps.MuziService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "realName": encryptedName, + "certCode": encryptedCertCode, + } + + respData, err := deps.MuziService.CallAPI(ctx, "PC0041", reqData) + if err != nil { + switch { + case errors.Is(err, muzi.ErrDatasource): + return nil, errors.Join(processors.ErrDatasource, err) + case errors.Is(err, muzi.ErrSystem): + return nil, errors.Join(processors.ErrSystem, err) + default: + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respData, nil +} diff --git a/internal/infrastructure/external/muzi/muzi_errors.go b/internal/infrastructure/external/muzi/muzi_errors.go new file mode 100644 index 0000000..3b6d21f --- /dev/null +++ b/internal/infrastructure/external/muzi/muzi_errors.go @@ -0,0 +1,25 @@ +package muzi + +import "fmt" + +// MuziError 木子数据业务错误 +type MuziError struct { + Code int + Message string +} + +// Error implements error interface. +func (e *MuziError) Error() string { + return fmt.Sprintf("木子数据返回错误,代码: %d,信息: %s", e.Code, e.Message) +} + +// NewMuziError 根据错误码创建业务错误 +func NewMuziError(code int, message string) *MuziError { + if message == "" { + message = "木子数据返回未知错误" + } + return &MuziError{ + Code: code, + Message: message, + } +} diff --git a/internal/infrastructure/external/muzi/muzi_factory.go b/internal/infrastructure/external/muzi/muzi_factory.go new file mode 100644 index 0000000..7932f51 --- /dev/null +++ b/internal/infrastructure/external/muzi/muzi_factory.go @@ -0,0 +1,61 @@ +package muzi + +import ( + "time" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/external_logger" +) + +// NewMuziServiceWithConfig 使用配置创建木子数据服务 +func NewMuziServiceWithConfig(cfg *config.Config) (*MuziService, error) { + loggingConfig := external_logger.ExternalServiceLoggingConfig{ + Enabled: cfg.Muzi.Logging.Enabled, + LogDir: cfg.Muzi.Logging.LogDir, + ServiceName: "muzi", + UseDaily: cfg.Muzi.Logging.UseDaily, + EnableLevelSeparation: cfg.Muzi.Logging.EnableLevelSeparation, + LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig), + } + + for level, levelCfg := range cfg.Muzi.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 + } + + service := NewMuziService( + cfg.Muzi.URL, + cfg.Muzi.AppID, + cfg.Muzi.AppSecret, + cfg.Muzi.Timeout, + logger, + ) + + return service, nil +} + +// NewMuziServiceWithLogging 使用自定义日志配置创建木子数据服务 +func NewMuziServiceWithLogging(url, appID, appSecret string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*MuziService, error) { + loggingConfig.ServiceName = "muzi" + + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + return NewMuziService(url, appID, appSecret, timeout, logger), nil +} + +// NewMuziServiceSimple 创建无日志的木子数据服务 +func NewMuziServiceSimple(url, appID, appSecret string, timeout time.Duration) *MuziService { + return NewMuziService(url, appID, appSecret, timeout, nil) +} diff --git a/internal/infrastructure/external/muzi/muzi_service.go b/internal/infrastructure/external/muzi/muzi_service.go new file mode 100644 index 0000000..fd6388f --- /dev/null +++ b/internal/infrastructure/external/muzi/muzi_service.go @@ -0,0 +1,389 @@ +package muzi + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "reflect" + "sort" + "strconv" + "time" + + "tyapi-server/internal/shared/external_logger" +) + +const defaultRequestTimeout = 60 * time.Second + +var ( + ErrDatasource = errors.New("数据源异常") + ErrSystem = errors.New("系统异常") +) + +// Muzi状态码常量 +const ( + CodeSuccess = 0 // 成功查询 + CodeSystemError = 500 // 系统异常 + CodeParamMissing = 601 // 参数不全 + CodeInterfaceExpired = 602 // 接口已过期 + CodeVerifyFailed = 603 // 接口校验失败 + CodeIPNotInWhitelist = 604 // IP不在白名单中 + CodeProductNotFound = 701 // 产品编号不存在 + CodeUserNotFound = 702 // 用户名不存在 + CodeUnauthorizedAPI = 703 // 接口未授权 + CodeInsufficientFund = 704 // 商户余额不足 +) + +// MuziResponse 木子数据接口通用响应 +type MuziResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` + Timestamp int64 `json:"timestamp"` + ExecuteTime int64 `json:"executeTime"` +} + +// MuziConfig 木子数据接口配置 +type MuziConfig struct { + URL string + AppID string + AppSecret string + Timeout time.Duration +} + +// MuziService 木子数据接口服务封装 +type MuziService struct { + config MuziConfig + logger *external_logger.ExternalServiceLogger +} + +// NewMuziService 创建木子数据服务实例 +func NewMuziService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *MuziService { + if timeout <= 0 { + timeout = defaultRequestTimeout + } + + return &MuziService{ + config: MuziConfig{ + URL: url, + AppID: appID, + AppSecret: appSecret, + Timeout: timeout, + }, + logger: logger, + } +} + +// generateRequestID 生成请求ID +func (m *MuziService) generateRequestID() string { + timestamp := time.Now().UnixNano() + raw := fmt.Sprintf("%d_%s", timestamp, m.config.AppID) + sum := md5.Sum([]byte(raw)) + return fmt.Sprintf("muzi_%x", sum[:8]) +} + +// CallAPI 调用木子数据接口 +func (m *MuziService) CallAPI(ctx context.Context, prodCode string, params map[string]interface{}) (json.RawMessage, error) { + requestID := m.generateRequestID() + now := time.Now() + timestamp := strconv.FormatInt(now.UnixMilli(), 10) + + flatParams := flattenParams(params) + signParts := collectSignatureValues(params) + signature := m.GenerateSignature(prodCode, timestamp, signParts...) + + // 从上下文获取链路ID + var transactionID string + if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok { + transactionID = ctxTransactionID + } + + requestBody := map[string]interface{}{ + "appId": m.config.AppID, + "prodCode": prodCode, + "timestamp": timestamp, + "signature": signature, + } + for key, value := range flatParams { + requestBody[key] = value + } + + if m.logger != nil { + m.logger.LogRequest(requestID, transactionID, prodCode, m.config.URL, requestBody) + } + + bodyBytes, marshalErr := json.Marshal(requestBody) + if marshalErr != nil { + err := errors.Join(ErrSystem, marshalErr) + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + + req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, m.config.URL, bytes.NewBuffer(bodyBytes)) + if reqErr != nil { + err := errors.Join(ErrSystem, reqErr) + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: m.config.Timeout, + } + + resp, httpErr := client.Do(req) + if httpErr != nil { + err := wrapHTTPError(httpErr) + if errors.Is(err, ErrDatasource) { + err = errors.Join(err, fmt.Errorf("API请求超时: %v", httpErr)) + } + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + defer func(body io.ReadCloser) { + closeErr := body.Close() + if closeErr != nil && m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, errors.Join(ErrSystem, fmt.Errorf("关闭响应体失败: %w", closeErr)), requestBody) + } + }(resp.Body) + + respBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + err := errors.Join(ErrSystem, readErr) + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + + if m.logger != nil { + m.logger.LogResponse(requestID, transactionID, prodCode, resp.StatusCode, respBody, time.Since(now)) + } + + if resp.StatusCode != http.StatusOK { + err := errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode)) + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + + var muziResp MuziResponse + if err := json.Unmarshal(respBody, &muziResp); err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %v", err)) + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, err, requestBody) + } + return nil, err + } + + if muziResp.Code != CodeSuccess { + muziErr := NewMuziError(muziResp.Code, muziResp.Msg) + var resultErr error + + switch muziResp.Code { + case CodeSystemError: + resultErr = errors.Join(ErrDatasource, muziErr) + default: + resultErr = errors.Join(ErrSystem, muziErr) + } + + if m.logger != nil { + m.logger.LogError(requestID, transactionID, prodCode, muziErr, requestBody) + } + return nil, resultErr + } + + return muziResp.Data, nil +} + +func wrapHTTPError(err error) error { + var timeout bool + if err == context.DeadlineExceeded { + timeout = true + } else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { + timeout = true + } else if errStr := err.Error(); errStr == "context deadline exceeded" || + errStr == "timeout" || + errStr == "Client.Timeout exceeded" || + errStr == "net/http: request canceled" { + timeout = true + } + + if timeout { + return errors.Join(ErrDatasource, err) + } + return errors.Join(ErrSystem, err) +} + +func pkcs5Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padtext...) +} + +func flattenParams(params map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + if params == nil { + return result + } + for key, value := range params { + flattenValue(key, value, result) + } + return result +} + +func flattenValue(prefix string, value interface{}, out map[string]interface{}) { + switch val := value.(type) { + case map[string]interface{}: + for k, v := range val { + flattenValue(combinePrefix(prefix, k), v, out) + } + case map[interface{}]interface{}: + for k, v := range val { + keyStr := fmt.Sprint(k) + flattenValue(combinePrefix(prefix, keyStr), v, out) + } + case []interface{}: + for i, item := range val { + nextPrefix := fmt.Sprintf("%s[%d]", prefix, i) + flattenValue(nextPrefix, item, out) + } + case []string: + for i, item := range val { + nextPrefix := fmt.Sprintf("%s[%d]", prefix, i) + flattenValue(nextPrefix, item, out) + } + case []int: + for i, item := range val { + nextPrefix := fmt.Sprintf("%s[%d]", prefix, i) + flattenValue(nextPrefix, item, out) + } + case []float64: + for i, item := range val { + nextPrefix := fmt.Sprintf("%s[%d]", prefix, i) + flattenValue(nextPrefix, item, out) + } + case []bool: + for i, item := range val { + nextPrefix := fmt.Sprintf("%s[%d]", prefix, i) + flattenValue(nextPrefix, item, out) + } + default: + out[prefix] = val + } +} + +func combinePrefix(prefix, key string) string { + if prefix == "" { + return key + } + return prefix + "." + key +} + +// Encrypt 使用 AES/ECB/PKCS5Padding 对单个字符串进行加密并返回 Base64 结果 +func (m *MuziService) Encrypt(value string) (string, error) { + if len(m.config.AppSecret) != 32 { + return "", fmt.Errorf("AppSecret长度必须为32位") + } + + block, err := aes.NewCipher([]byte(m.config.AppSecret)) + if err != nil { + return "", fmt.Errorf("初始化加密器失败: %w", err) + } + + padded := pkcs5Padding([]byte(value), block.BlockSize()) + encrypted := make([]byte, len(padded)) + + for bs, be := 0, block.BlockSize(); bs < len(padded); bs, be = bs+block.BlockSize(), be+block.BlockSize() { + block.Encrypt(encrypted[bs:be], padded[bs:be]) + } + + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +// GenerateSignature 根据协议生成签名,extraValues 会按顺序追加在待签名字符串之后 +func (m *MuziService) GenerateSignature(prodCode, timestamp string, extraValues ...string) string { + signStr := m.config.AppID + prodCode + timestamp + for _, extra := range extraValues { + signStr += extra + } + hash := md5.Sum([]byte(signStr)) + return hex.EncodeToString(hash[:]) +} + +// GenerateTimestamp 生成当前毫秒级时间戳字符串 +func (m *MuziService) GenerateTimestamp() string { + return strconv.FormatInt(time.Now().UnixMilli(), 10) +} + +// FlattenParams 将嵌套参数展平为一维键值对 +func (m *MuziService) FlattenParams(params map[string]interface{}) map[string]interface{} { + return flattenParams(params) +} + +func collectSignatureValues(data interface{}) []string { + var result []string + collectSignatureValuesRecursive(reflect.ValueOf(data), &result) + return result +} + +func collectSignatureValuesRecursive(value reflect.Value, result *[]string) { + if !value.IsValid() { + *result = append(*result, "") + return + } + + switch value.Kind() { + case reflect.Pointer, reflect.Interface: + if value.IsNil() { + *result = append(*result, "") + return + } + collectSignatureValuesRecursive(value.Elem(), result) + case reflect.Map: + keys := value.MapKeys() + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprint(keys[i].Interface()) < fmt.Sprint(keys[j].Interface()) + }) + for _, key := range keys { + collectSignatureValuesRecursive(value.MapIndex(key), result) + } + case reflect.Slice, reflect.Array: + for i := 0; i < value.Len(); i++ { + collectSignatureValuesRecursive(value.Index(i), result) + } + case reflect.Struct: + typeInfo := value.Type() + fieldNames := make([]string, 0, value.NumField()) + fieldIndices := make(map[string]int, value.NumField()) + for i := 0; i < value.NumField(); i++ { + field := typeInfo.Field(i) + if field.PkgPath != "" { + continue + } + fieldNames = append(fieldNames, field.Name) + fieldIndices[field.Name] = i + } + sort.Strings(fieldNames) + for _, name := range fieldNames { + collectSignatureValuesRecursive(value.Field(fieldIndices[name]), result) + } + default: + *result = append(*result, fmt.Sprint(value.Interface())) + } +} diff --git a/internal/infrastructure/external/xingwei/xingwei_service.go b/internal/infrastructure/external/xingwei/xingwei_service.go index 1fa8942..199d1a0 100644 --- a/internal/infrastructure/external/xingwei/xingwei_service.go +++ b/internal/infrastructure/external/xingwei/xingwei_service.go @@ -18,23 +18,23 @@ import ( // 行为数据API状态码常量 const ( - CodeSuccess = 200 // 操作成功 - CodeSystemError = 500 // 系统内部错误 - CodeMerchantError = 3001 // 商家相关报错(商家不存在、商家被禁用、商家余额不足) - CodeAccountExpired = 3002 // 账户已过期 - CodeIPWhitelistMissing = 3003 // 未添加ip白名单 - CodeUnauthorized = 3004 // 未授权调用该接口 - CodeProductIDError = 4001 // 产品id错误 - CodeInterfaceDisabled = 4002 // 接口被停用 - CodeQueryException = 5001 // 接口查询异常,请联系技术人员 - CodeNotFound = 6000 // 未查询到结果 + CodeSuccess = 200 // 操作成功 + CodeSystemError = 500 // 系统内部错误 + CodeMerchantError = 3001 // 商家相关报错(商家不存在、商家被禁用、商家余额不足) + CodeAccountExpired = 3002 // 账户已过期 + CodeIPWhitelistMissing = 3003 // 未添加ip白名单 + CodeUnauthorized = 3004 // 未授权调用该接口 + CodeProductIDError = 4001 // 产品id错误 + CodeInterfaceDisabled = 4002 // 接口被停用 + CodeQueryException = 5001 // 接口查询异常,请联系技术人员 + CodeNotFound = 6000 // 未查询到结果 ) var ( ErrDatasource = errors.New("数据源异常") ErrSystem = errors.New("系统异常") ErrNotFound = errors.New("未查询到结果") - + // 请求ID计数器,确保唯一性 requestIDCounter int64 ) @@ -54,16 +54,16 @@ type XingweiErrorCode struct { // 行为数据错误码映射 var XingweiErrorCodes = map[int]XingweiErrorCode{ - CodeSuccess: {Code: CodeSuccess, Message: "操作成功"}, - CodeSystemError: {Code: CodeSystemError, Message: "系统内部错误"}, - CodeMerchantError: {Code: CodeMerchantError, Message: "商家相关报错(商家不存在、商家被禁用、商家余额不足)"}, - CodeAccountExpired: {Code: CodeAccountExpired, Message: "账户已过期"}, - CodeIPWhitelistMissing: {Code: CodeIPWhitelistMissing, Message: "未添加ip白名单"}, - CodeUnauthorized: {Code: CodeUnauthorized, Message: "未授权调用该接口"}, - CodeProductIDError: {Code: CodeProductIDError, Message: "产品id错误"}, - CodeInterfaceDisabled: {Code: CodeInterfaceDisabled, Message: "接口被停用"}, - CodeQueryException: {Code: CodeQueryException, Message: "接口查询异常,请联系技术人员"}, - CodeNotFound: {Code: CodeNotFound, Message: "未查询到结果"}, + CodeSuccess: {Code: CodeSuccess, Message: "操作成功"}, + CodeSystemError: {Code: CodeSystemError, Message: "系统内部错误"}, + CodeMerchantError: {Code: CodeMerchantError, Message: "商家相关报错(商家不存在、商家被禁用、商家余额不足)"}, + CodeAccountExpired: {Code: CodeAccountExpired, Message: "账户已过期"}, + CodeIPWhitelistMissing: {Code: CodeIPWhitelistMissing, Message: "未添加ip白名单"}, + CodeUnauthorized: {Code: CodeUnauthorized, Message: "未授权调用该接口"}, + CodeProductIDError: {Code: CodeProductIDError, Message: "产品id错误"}, + CodeInterfaceDisabled: {Code: CodeInterfaceDisabled, Message: "接口被停用"}, + CodeQueryException: {Code: CodeQueryException, Message: "接口查询异常,请联系技术人员"}, + CodeNotFound: {Code: CodeNotFound, Message: "未查询到结果"}, } // GetXingweiErrorMessage 根据错误码获取错误消息 @@ -172,14 +172,13 @@ func (x *XingweiService) CallAPI(ctx context.Context, projectID string, params m 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 { @@ -244,7 +243,7 @@ func (x *XingweiService) CallAPI(ctx context.Context, projectID string, params m if xingweiResp.Data == nil { return []byte("{}"), nil } - + // 将data转换为JSON字节 dataBytes, err := json.Marshal(xingweiResp.Data) if err != nil { @@ -254,39 +253,39 @@ func (x *XingweiService) CallAPI(ctx context.Context, projectID string, params m } return nil, err } - + return dataBytes, nil - + case CodeNotFound: // 未查询到结果,返回查空错误 if x.logger != nil { - x.logger.LogError(requestID, transactionID, "xingwei_api", + x.logger.LogError(requestID, transactionID, "xingwei_api", errors.Join(ErrNotFound, fmt.Errorf("未查询到结果")), params) } return nil, errors.Join(ErrNotFound, fmt.Errorf("未查询到结果")) - + case CodeSystemError: // 系统内部错误 errorMsg := GetXingweiErrorMessage(xingweiResp.Code) systemErr := fmt.Errorf("行为数据系统错误[%d]: %s", xingweiResp.Code, errorMsg) - + if x.logger != nil { - x.logger.LogError(requestID, transactionID, "xingwei_api", + x.logger.LogError(requestID, transactionID, "xingwei_api", errors.Join(ErrSystem, systemErr), params) } - + return nil, errors.Join(ErrSystem, systemErr) - + default: // 其他业务错误 errorMsg := GetXingweiErrorMessage(xingweiResp.Code) businessErr := fmt.Errorf("行为数据业务错误[%d]: %s", xingweiResp.Code, errorMsg) - + if x.logger != nil { - x.logger.LogError(requestID, transactionID, "xingwei_api", + x.logger.LogError(requestID, transactionID, "xingwei_api", errors.Join(ErrDatasource, businessErr), params) } - + return nil, errors.Join(ErrDatasource, businessErr) } } diff --git a/internal/shared/external_logger/external_logger.go b/internal/shared/external_logger/external_logger.go index 087145a..07e494a 100644 --- a/internal/shared/external_logger/external_logger.go +++ b/internal/shared/external_logger/external_logger.go @@ -45,9 +45,12 @@ type ExternalServiceLogger struct { // NewExternalServiceLogger 创建外部服务日志器 func NewExternalServiceLogger(config ExternalServiceLoggingConfig) (*ExternalServiceLogger, error) { if !config.Enabled { + nopLogger := zap.NewNop() return &ExternalServiceLogger{ - logger: zap.NewNop(), - serviceName: config.ServiceName, + logger: nopLogger, + serviceName: config.ServiceName, + requestLogger: nopLogger, + responseLogger: nopLogger, }, nil } @@ -186,7 +189,7 @@ func createSeparatedCore(logDir string, config ExternalServiceLoggingConfig) zap infoWriter := createFileWriter(logDir, "info", config.LevelConfigs["info"], config.ServiceName, config.UseDaily) errorWriter := createFileWriter(logDir, "error", config.LevelConfigs["error"], config.ServiceName, config.UseDaily) warnWriter := createFileWriter(logDir, "warn", config.LevelConfigs["warn"], config.ServiceName, config.UseDaily) - + // 新增:请求和响应日志的独立文件输出 requestWriter := createFileWriter(logDir, "request", config.RequestLogConfig, config.ServiceName, config.UseDaily) responseWriter := createFileWriter(logDir, "response", config.ResponseLogConfig, config.ServiceName, config.UseDaily) @@ -293,7 +296,11 @@ func createFileWriter(logDir, level string, config ExternalServiceLevelFileConfi // LogRequest 记录请求日志 func (e *ExternalServiceLogger) LogRequest(requestID, transactionID, apiCode string, url interface{}, params interface{}) { - e.requestLogger.Info(fmt.Sprintf("%s API请求", e.serviceName), + logger := e.requestLogger + if logger == nil { + logger = e.logger + } + logger.Info(fmt.Sprintf("%s API请求", e.serviceName), zap.String("service", e.serviceName), zap.String("request_id", requestID), zap.String("transaction_id", transactionID), @@ -306,7 +313,11 @@ func (e *ExternalServiceLogger) LogRequest(requestID, transactionID, apiCode str // LogResponse 记录响应日志 func (e *ExternalServiceLogger) LogResponse(requestID, transactionID, apiCode string, statusCode int, response []byte, duration time.Duration) { - e.responseLogger.Info(fmt.Sprintf("%s API响应", e.serviceName), + logger := e.responseLogger + if logger == nil { + logger = e.logger + } + logger.Info(fmt.Sprintf("%s API响应", e.serviceName), zap.String("service", e.serviceName), zap.String("request_id", requestID), zap.String("transaction_id", transactionID), @@ -320,7 +331,11 @@ func (e *ExternalServiceLogger) LogResponse(requestID, transactionID, apiCode st // LogResponseWithID 记录包含响应ID的响应日志 func (e *ExternalServiceLogger) LogResponseWithID(requestID, transactionID, apiCode string, statusCode int, response []byte, duration time.Duration, responseID string) { - e.responseLogger.Info(fmt.Sprintf("%s API响应", e.serviceName), + logger := e.responseLogger + if logger == nil { + logger = e.logger + } + logger.Info(fmt.Sprintf("%s API响应", e.serviceName), zap.String("service", e.serviceName), zap.String("request_id", requestID), zap.String("transaction_id", transactionID),