diff --git a/internal/config/config.go b/internal/config/config.go index 505f6a9..20cab68 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -38,6 +38,7 @@ type Config struct { TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` Alicloud AlicloudConfig `mapstructure:"alicloud"` Xingwei XingweiConfig `mapstructure:"xingwei"` + Jiguang JiguangConfig `mapstructure:"jiguang"` } // ServerConfig HTTP服务器配置 @@ -520,6 +521,35 @@ type XingweiLevelFileConfig struct { Compress bool `mapstructure:"compress"` } +// JiguangConfig 极光配置 +type JiguangConfig struct { + URL string `mapstructure:"url"` + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` + SignMethod string `mapstructure:"sign_method"` // md5 或 hmac,默认 hmac + Timeout time.Duration `mapstructure:"timeout"` + + // 极光日志配置 + Logging JiguangLoggingConfig `mapstructure:"logging"` +} + +// JiguangLoggingConfig 极光日志配置 +type JiguangLoggingConfig struct { + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + LevelConfigs map[string]JiguangLevelFileConfig `mapstructure:"level_configs"` +} + +// JiguangLevelFileConfig 极光级别文件配置 +type JiguangLevelFileConfig 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 71aee98..7dc9fad 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -43,6 +43,7 @@ import ( "tyapi-server/internal/infrastructure/external/ocr" "tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/storage" + "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -366,6 +367,10 @@ func NewContainer() *Container { func(cfg *config.Config) (*xingwei.XingweiService, error) { return xingwei.NewXingweiServiceWithConfig(cfg) }, + // JiguangService - 极光服务 + func(cfg *config.Config) (*jiguang.JiguangService, error) { + return jiguang.NewJiguangServiceWithConfig(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 b9595f4..06f3634 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -723,6 +723,11 @@ type IVYZ6M8PReq struct { Name string `json:"name" validate:"required,min=1,validName"` } +type IVYZ9H2MReq struct { + IDNo string `json:"id_no" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` +} + type YYSY9E4AReq struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` } diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 0dfdddc..9b6b44c 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/jiguang" "tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" @@ -54,6 +55,7 @@ func NewApiRequestService( alicloudService *alicloud.AlicloudService, zhichaService *zhicha.ZhichaService, xingweiService *xingwei.XingweiService, + jiguangService *jiguang.JiguangService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, ) *ApiRequestService { @@ -61,7 +63,7 @@ func NewApiRequestService( combService := comb.NewCombService(productManagementService) // 创建处理器依赖容器 - processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService) + processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, validator, combService) // 统一注册所有处理器 registerAllProcessors(combService) @@ -206,6 +208,7 @@ func registerAllProcessors(combService *comb.CombService) { "IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历) "IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型 "IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书 + "IVYZ9H2M": ivyz.ProcessIVYZ9H2MRequest, //极光个人婚姻查询(V2版) "IVYZZQT3": ivyz.ProcessIVYZZQT3Request, //人脸比对V3 "IVYZBPQ2": ivyz.ProcessIVYZBPQ2Request, //人脸比对V2 "IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index a60b8b4..2100677 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -198,6 +198,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历) "IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型 "IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书 + "IVYZ9H2M": &dto.IVYZ9H2MReq{}, //极光个人婚姻查询(V2版) "QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证 "QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询 "IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3 diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 67c0018..f9160c5 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -5,6 +5,7 @@ import ( "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/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei" @@ -32,6 +33,7 @@ type ProcessorDependencies struct { AlicloudService *alicloud.AlicloudService ZhichaService *zhicha.ZhichaService XingweiService *xingwei.XingweiService + JiguangService *jiguang.JiguangService Validator interfaces.RequestValidator CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 @@ -47,6 +49,7 @@ func NewProcessorDependencies( alicloudService *alicloud.AlicloudService, zhichaService *zhicha.ZhichaService, xingweiService *xingwei.XingweiService, + jiguangService *jiguang.JiguangService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface ) *ProcessorDependencies { @@ -58,6 +61,7 @@ func NewProcessorDependencies( AlicloudService: alicloudService, ZhichaService: zhichaService, XingweiService: xingweiService, + JiguangService: jiguangService, Validator: validator, CombService: combService, Options: nil, // 初始化为nil,在调用时设置 diff --git a/internal/domains/api/services/processors/ivyz/ivyz9h2m_processor.go b/internal/domains/api/services/processors/ivyz/ivyz9h2m_processor.go new file mode 100644 index 0000000..dfc7638 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyz9h2m_processor.go @@ -0,0 +1,45 @@ +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/jiguang" +) + +// ProcessIVYZ9H2MRequest IVYZ9H2M API处理方法 - 极光个人婚姻查询(V2版) +func ProcessIVYZ9H2MRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZ9H2MReq + 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) + } + + // 构建请求参数 + reqData := map[string]interface{}{ + "idNo": paramsDto.IDNo, + "name": paramsDto.Name, + } + + // 调用极光API,apiCode为 marriage-single-v2 + respBytes, err := deps.JiguangService.CallAPI(ctx, "marriage-single-v2", reqData) + if err != nil { + // 根据错误类型返回相应的错误 + if errors.Is(err, jiguang.ErrNotFound) { + return nil, errors.Join(processors.ErrNotFound, err) + } else if errors.Is(err, jiguang.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 极光服务已经返回了 data 字段的 JSON,直接返回即可 + return respBytes, nil +} diff --git a/internal/infrastructure/external/jiguang/crypto.go b/internal/infrastructure/external/jiguang/crypto.go new file mode 100644 index 0000000..3d897a7 --- /dev/null +++ b/internal/infrastructure/external/jiguang/crypto.go @@ -0,0 +1,47 @@ +package jiguang + +import ( + "crypto/hmac" + "crypto/md5" + "encoding/hex" + "fmt" +) + +// SignMethod 签名方法类型 +type SignMethod string + +const ( + SignMethodMD5 SignMethod = "md5" + SignMethodHMACMD5 SignMethod = "hmac" +) + +// GenerateSign 生成签名 +// 根据 signMethod 参数选择使用 MD5 或 HMAC-MD5 算法 +// MD5: md5(timestamp + "&appSecret=" + appSecret),然后转大写十六进制 +// HMAC-MD5: hmac_md5(timestamp, appSecret),然后转大写十六进制 +func GenerateSign(timestamp string, appSecret string, signMethod SignMethod) (string, error) { + var hashBytes []byte + + switch signMethod { + case SignMethodMD5: + // MD5算法:在待签名字符串后面加上 &appSecret=xxx 再进行计算 + signStr := timestamp + "&appSecret=" + appSecret + hash := md5.Sum([]byte(signStr)) + hashBytes = hash[:] + case SignMethodHMACMD5: + // HMAC-MD5算法:使用 appSecret 初始化摘要算法再进行计算 + mac := hmac.New(md5.New, []byte(appSecret)) + mac.Write([]byte(timestamp)) + hashBytes = mac.Sum(nil) + default: + return "", fmt.Errorf("不支持的签名方法: %s", signMethod) + } + + // 将二进制转化为大写的十六进制(正确签名应该为32大写字符串) + return hex.EncodeToString(hashBytes), nil +} + +// GenerateSignWithDefault 使用默认的 HMAC-MD5 方法生成签名 +func GenerateSignWithDefault(timestamp string, appSecret string) (string, error) { + return GenerateSign(timestamp, appSecret, SignMethodHMACMD5) +} diff --git a/internal/infrastructure/external/jiguang/jiguang_errors.go b/internal/infrastructure/external/jiguang/jiguang_errors.go new file mode 100644 index 0000000..e7b45f2 --- /dev/null +++ b/internal/infrastructure/external/jiguang/jiguang_errors.go @@ -0,0 +1,149 @@ +package jiguang + +import ( + "fmt" +) + +// JiguangError 极光服务错误 +type JiguangError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Error 实现error接口 +func (e *JiguangError) Error() string { + return fmt.Sprintf("极光错误 [%d]: %s", e.Code, e.Message) +} + +// IsSuccess 检查是否成功 +func (e *JiguangError) IsSuccess() bool { + return e.Code == 0 +} + +// IsQueryFailed 检查是否查询失败 +func (e *JiguangError) IsQueryFailed() bool { + return e.Code == 922 +} + +// IsNoRecord 检查是否查无记录 +func (e *JiguangError) IsNoRecord() bool { + return e.Code == 921 +} + +// IsParamError 检查是否是参数相关错误 +func (e *JiguangError) IsParamError() bool { + return e.Code == 400 || e.Code == 906 || e.Code == 914 || e.Code == 918 +} + +// IsAuthError 检查是否是认证相关错误 +func (e *JiguangError) IsAuthError() bool { + return e.Code == 902 || e.Code == 903 || e.Code == 904 || e.Code == 905 +} + +// IsSystemError 检查是否是系统错误 +func (e *JiguangError) IsSystemError() bool { + return e.Code == 405 || e.Code == 911 || e.Code == 912 || e.Code == 915 || e.Code == 916 || e.Code == 917 || e.Code == 919 || e.Code == 923 +} + +// 预定义错误常量 +var ( + // 成功状态 + ErrSuccess = &JiguangError{Code: 0, Message: "请求成功"} + + // 参数错误 + ErrParamInvalid = &JiguangError{Code: 400, Message: "请求参数不正确"} + ErrMethodInvalid = &JiguangError{Code: 405, Message: "请求方法不正确"} + ErrParamFormInvalid = &JiguangError{Code: 906, Message: "请求参数形式不正确"} + ErrBodyIncomplete = &JiguangError{Code: 914, Message: "Body 请求参数不完整"} + ErrBodyNotSupported = &JiguangError{Code: 918, Message: "Body 请求参数不支持"} + + // 认证错误 + ErrAppIDInvalid = &JiguangError{Code: 902, Message: "错误的 appId/账户已删除"} + ErrTimestampInvalid = &JiguangError{Code: 903, Message: "错误的时间戳/时间误差大于 10 分钟"} + ErrSignMethodInvalid = &JiguangError{Code: 904, Message: "无法识别的签名方法"} + ErrSignInvalid = &JiguangError{Code: 905, Message: "签名不合法"} + + // 系统错误 + ErrAccountStatusError = &JiguangError{Code: 911, Message: "账户状态异常"} + ErrInterfaceDisabled = &JiguangError{Code: 912, Message: "接口状态不可用"} + ErrAPICallError = &JiguangError{Code: 915, Message: "API 接口调用有误"} + ErrInternalError = &JiguangError{Code: 916, Message: "内部接口调用错误,请联系相关人员"} + ErrTimeout = &JiguangError{Code: 917, Message: "请求超时"} + ErrBusinessDisabled = &JiguangError{Code: 919, Message: "业务状态不可用"} + ErrInterfaceException = &JiguangError{Code: 923, Message: "接口异常"} + + // 业务错误 + ErrNoRecord = &JiguangError{Code: 921, Message: "查无记录"} + ErrQueryFailed = &JiguangError{Code: 922, Message: "查询失败"} +) + +// NewJiguangError 创建新的极光错误 +func NewJiguangError(code int, message string) *JiguangError { + return &JiguangError{ + Code: code, + Message: message, + } +} + +// NewJiguangErrorFromCode 根据状态码创建错误 +func NewJiguangErrorFromCode(code int) *JiguangError { + switch code { + case 0: + return ErrSuccess + case 400: + return ErrParamInvalid + case 405: + return ErrMethodInvalid + case 902: + return ErrAppIDInvalid + case 903: + return ErrTimestampInvalid + case 904: + return ErrSignMethodInvalid + case 905: + return ErrSignInvalid + case 906: + return ErrParamFormInvalid + case 911: + return ErrAccountStatusError + case 912: + return ErrInterfaceDisabled + case 914: + return ErrBodyIncomplete + case 915: + return ErrAPICallError + case 916: + return ErrInternalError + case 917: + return ErrTimeout + case 918: + return ErrBodyNotSupported + case 919: + return ErrBusinessDisabled + case 921: + return ErrNoRecord + case 922: + return ErrQueryFailed + case 923: + return ErrInterfaceException + default: + return &JiguangError{ + Code: code, + Message: fmt.Sprintf("未知错误码: %d", code), + } + } +} + +// IsJiguangError 检查是否是极光错误 +func IsJiguangError(err error) bool { + _, ok := err.(*JiguangError) + return ok +} + +// GetJiguangError 获取极光错误 +func GetJiguangError(err error) *JiguangError { + if jiguangErr, ok := err.(*JiguangError); ok { + return jiguangErr + } + return nil +} diff --git a/internal/infrastructure/external/jiguang/jiguang_factory.go b/internal/infrastructure/external/jiguang/jiguang_factory.go new file mode 100644 index 0000000..fadfef5 --- /dev/null +++ b/internal/infrastructure/external/jiguang/jiguang_factory.go @@ -0,0 +1,85 @@ +package jiguang + +import ( + "time" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/external_logger" +) + +// NewJiguangServiceWithConfig 使用配置创建极光服务 +func NewJiguangServiceWithConfig(cfg *config.Config) (*JiguangService, error) { + // 将配置类型转换为通用外部服务日志配置 + loggingConfig := external_logger.ExternalServiceLoggingConfig{ + Enabled: cfg.Jiguang.Logging.Enabled, + LogDir: cfg.Jiguang.Logging.LogDir, + ServiceName: "jiguang", + UseDaily: cfg.Jiguang.Logging.UseDaily, + EnableLevelSeparation: cfg.Jiguang.Logging.EnableLevelSeparation, + LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig), + } + + // 转换级别配置 + for key, value := range cfg.Jiguang.Logging.LevelConfigs { + loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{ + MaxSize: value.MaxSize, + MaxBackups: value.MaxBackups, + MaxAge: value.MaxAge, + Compress: value.Compress, + } + } + + // 创建通用外部服务日志器 + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + // 解析签名方法 + var signMethod SignMethod + if cfg.Jiguang.SignMethod == "md5" { + signMethod = SignMethodMD5 + } else { + signMethod = SignMethodHMACMD5 // 默认使用 HMAC-MD5 + } + + // 解析超时时间 + timeout := 60 * time.Second + if cfg.Jiguang.Timeout > 0 { + timeout = cfg.Jiguang.Timeout + } + + // 创建极光服务 + service := NewJiguangService( + cfg.Jiguang.URL, + cfg.Jiguang.AppID, + cfg.Jiguang.AppSecret, + signMethod, + timeout, + logger, + ) + + return service, nil +} + +// NewJiguangServiceWithLogging 使用自定义日志配置创建极光服务 +func NewJiguangServiceWithLogging(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*JiguangService, error) { + // 设置服务名称 + loggingConfig.ServiceName = "jiguang" + + // 创建通用外部服务日志器 + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + // 创建极光服务 + service := NewJiguangService(url, appID, appSecret, signMethod, timeout, logger) + + return service, nil +} + +// NewJiguangServiceSimple 创建简单的极光服务(无日志) +func NewJiguangServiceSimple(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration) *JiguangService { + return NewJiguangService(url, appID, appSecret, signMethod, timeout, nil) +} diff --git a/internal/infrastructure/external/jiguang/jiguang_service.go b/internal/infrastructure/external/jiguang/jiguang_service.go new file mode 100644 index 0000000..1547ffe --- /dev/null +++ b/internal/infrastructure/external/jiguang/jiguang_service.go @@ -0,0 +1,265 @@ +package jiguang + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "tyapi-server/internal/shared/external_logger" +) + +var ( + ErrDatasource = errors.New("数据源异常") + ErrSystem = errors.New("系统异常") + ErrNotFound = errors.New("查询为空") +) + +// JiguangResponse 极光API响应结构 +type JiguangResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + OrderID string `json:"order_id"` + Data map[string]interface{} `json:"data"` +} + +// JiguangConfig 极光服务配置 +type JiguangConfig struct { + URL string + AppID string + AppSecret string + SignMethod SignMethod // 签名方法:md5 或 hmac + Timeout time.Duration +} + +// JiguangService 极光服务 +type JiguangService struct { + config JiguangConfig + logger *external_logger.ExternalServiceLogger +} + +// NewJiguangService 创建一个新的极光服务实例 +func NewJiguangService(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *JiguangService { + // 如果没有指定签名方法,默认使用 HMAC-MD5 + if signMethod == "" { + signMethod = SignMethodHMACMD5 + } + + // 如果没有指定超时时间,默认使用 60 秒 + if timeout == 0 { + timeout = 60 * time.Second + } + + return &JiguangService{ + config: JiguangConfig{ + URL: url, + AppID: appID, + AppSecret: appSecret, + SignMethod: signMethod, + Timeout: timeout, + }, + logger: logger, + } +} + +// generateRequestID 生成请求ID +func (j *JiguangService) generateRequestID() string { + timestamp := time.Now().UnixNano() + hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, j.config.AppID))) + return fmt.Sprintf("jiguang_%x", hash[:8]) +} + +// CallAPI 调用极光API +// apiCode: API服务编码(如 marriage-single-v2) +// params: 请求参数(会作为JSON body发送) +func (j *JiguangService) CallAPI(ctx context.Context, apiCode string, params map[string]interface{}) (resp []byte, err error) { + startTime := time.Now() + requestID := j.generateRequestID() + + // 生成时间戳(毫秒) + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + + // 从ctx中获取transactionId + var transactionID string + if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok { + transactionID = ctxTransactionID + } + + // 生成签名 + sign, signErr := GenerateSign(timestamp, j.config.AppSecret, j.config.SignMethod) + if signErr != nil { + err = errors.Join(ErrSystem, fmt.Errorf("生成签名失败: %w", signErr)) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 记录请求日志 + if j.logger != nil { + j.logger.LogRequest(requestID, transactionID, apiCode, j.config.URL, params) + } + + // 将请求参数转换为JSON + jsonData, marshalErr := json.Marshal(params) + if marshalErr != nil { + err = errors.Join(ErrSystem, marshalErr) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 创建HTTP POST请求 + req, newRequestErr := http.NewRequestWithContext(ctx, "POST", j.config.URL, bytes.NewBuffer(jsonData)) + if newRequestErr != nil { + err = errors.Join(ErrSystem, newRequestErr) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + req.Header.Set("appId", j.config.AppID) + req.Header.Set("apiCode", apiCode) + req.Header.Set("timestamp", timestamp) + req.Header.Set("signMethod", string(j.config.SignMethod)) + req.Header.Set("sign", sign) + + // 创建HTTP客户端 + client := &http.Client{ + Timeout: j.config.Timeout, + } + + // 发送请求 + httpResp, clientDoErr := client.Do(req) + if clientDoErr != nil { + // 检查是否是超时错误 + isTimeout := false + if ctx.Err() == context.DeadlineExceeded { + 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" { + isTimeout = true + } + + if isTimeout { + err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", clientDoErr)) + } else { + err = errors.Join(ErrSystem, clientDoErr) + } + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + defer func(Body io.ReadCloser) { + closeErr := Body.Close() + if closeErr != nil { + // 记录关闭错误 + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, errors.Join(ErrSystem, fmt.Errorf("关闭响应体失败: %w", closeErr)), params) + } + } + }(httpResp.Body) + + // 计算请求耗时 + duration := time.Since(startTime) + + // 读取响应体 + bodyBytes, readErr := io.ReadAll(httpResp.Body) + if readErr != nil { + err = errors.Join(ErrSystem, readErr) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 检查HTTP状态码 + if httpResp.StatusCode != http.StatusOK { + err = errors.Join(ErrSystem, fmt.Errorf("极光请求失败,状态码: %d", httpResp.StatusCode)) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 解析响应结构 + var jiguangResp JiguangResponse + if err := json.Unmarshal(bodyBytes, &jiguangResp); err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err)) + if j.logger != nil { + j.logger.LogError(requestID, transactionID, apiCode, err, params) + } + return nil, err + } + + // 记录响应日志 + if j.logger != nil { + if jiguangResp.OrderID != "" { + j.logger.LogResponseWithID(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration, jiguangResp.OrderID) + } else { + j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration) + } + } + + // 检查业务状态码 + if jiguangResp.Code != 0 { + // 创建极光错误 + jiguangErr := NewJiguangErrorFromCode(jiguangResp.Code) + if jiguangErr.Message == fmt.Sprintf("未知错误码: %d", jiguangResp.Code) && jiguangResp.Msg != "" { + jiguangErr.Message = jiguangResp.Msg + } + + // 记录错误日志 + if j.logger != nil { + j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID) + } + + // 根据错误类型返回不同的错误 + if jiguangErr.IsNoRecord() { + return nil, errors.Join(ErrNotFound, jiguangErr) + } else if jiguangErr.IsQueryFailed() { + return nil, errors.Join(ErrDatasource, jiguangErr) + } else if jiguangErr.IsSystemError() { + return nil, errors.Join(ErrSystem, jiguangErr) + } else { + return nil, errors.Join(ErrDatasource, jiguangErr) + } + } + + // 成功响应,返回data字段 + if jiguangResp.Data == nil { + return []byte("{}"), nil + } + + // 将data转换为JSON字节 + dataBytes, err := json.Marshal(jiguangResp.Data) + if err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("data字段序列化失败: %w", err)) + if j.logger != nil { + j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, err, params, jiguangResp.OrderID) + } + return nil, err + } + + return dataBytes, nil +} + +// GetConfig 获取配置信息 +func (j *JiguangService) GetConfig() JiguangConfig { + return j.config +} diff --git a/worker b/worker deleted file mode 100644 index 08200dc..0000000 Binary files a/worker and /dev/null differ