296 lines
8.6 KiB
Go
296 lines
8.6 KiB
Go
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, 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", 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
|
||
}
|
||
|
||
// 记录响应日志
|
||
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 && 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
|
||
}
|
||
|
||
// 定义需要在查无记录时抛出异常的处理器列表
|
||
processorsToThrowError := map[string]bool{
|
||
"QCXG9P1C": true, // QCXG9P1C 处理器在查无记录时需要抛出异常
|
||
"QCXG4D2E": true, // QCXG4D2E 处理器在查无记录时需要抛出异常
|
||
"QCXG5F3A": true, // QCXG5F3A 处理器在查无记录时需要抛出异常
|
||
}
|
||
|
||
// 如果是特定处理器,抛出异常;否则返回空数组
|
||
if processorsToThrowError[processorCode] {
|
||
// 记录错误日志
|
||
if j.logger != nil {
|
||
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID)
|
||
}
|
||
return nil, errors.Join(ErrNotFound, jiguangErr)
|
||
}
|
||
|
||
// 查无记录时,返回空数组,API调用记录为成功
|
||
return []byte("[]"), nil
|
||
}
|
||
|
||
// 记录错误日志(查无记录的情况不记录错误日志)
|
||
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
|
||
}
|