package jiguang import ( "bytes" "context" "crypto/md5" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "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 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),用于请求头 // apiPath: API路径(如 marriage/single-v2),用于URL路径 // params: 请求参数(会作为JSON body发送) func (j *JiguangService) CallAPI(ctx context.Context, apiCode string, apiPath 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 } // 构建完整的请求URL,使用apiPath作为路径 requestURL := strings.TrimSuffix(j.config.URL, "/") + "/" + strings.TrimPrefix(apiPath, "/") // 记录请求日志 if j.logger != nil { j.logger.LogRequest(requestID, transactionID, apiCode, requestURL) } // 将请求参数转换为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", requestURL, 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 } j.logger.LogInfo(fmt.Sprintf("极光响应: %+v", jiguangResp)) // 记录响应日志(不记录具体响应数据) if j.logger != nil { if jiguangResp.OrderID != "" { j.logger.LogResponseWithID(requestID, transactionID, apiCode, httpResp.StatusCode, duration, jiguangResp.OrderID) } else { j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, duration) } } // 检查业务状态码 if jiguangResp.Code != 0 && jiguangResp.Code != 200 { // 创建极光错误 jiguangErr := NewJiguangErrorFromCode(jiguangResp.Code) if jiguangErr.Message == fmt.Sprintf("未知错误码: %d", jiguangResp.Code) && jiguangResp.Msg != "" { jiguangErr.Message = jiguangResp.Msg } // 根据错误类型返回不同的错误 if jiguangErr.IsNoRecord() { // 从context中获取apiCode,判断是否需要抛出异常 var processorCode string if ctxProcessorCode, ok := ctx.Value("api_code").(string); ok { processorCode = ctxProcessorCode } // 定义不需要抛出异常的处理器列表(默认情况下查无记录时抛出异常) processorsNotToThrowError := map[string]bool{ // 在这个列表中的处理器,查无记录时返回空数组,不抛出异常 // 示例:如果需要添加某个处理器,取消下面的注释 // "QCXG9P1C": true, } // 如果是不需要抛出异常的处理器,返回空数组;否则(默认)抛出异常 if processorsNotToThrowError[processorCode] { // 查无记录时,返回空数组,API调用记录为成功 return []byte("[]"), nil } // 默认情况下,查无记录时抛出异常 // 记录错误日志 if j.logger != nil { j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID) } return nil, errors.Join(ErrNotFound, jiguangErr) } // 记录错误日志(查无记录的情况不记录错误日志) if j.logger != nil { j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID) } 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 }