Files
tyapi-server/internal/infrastructure/external/jiguang/jiguang_service.go

297 lines
8.7 KiB
Go
Raw Normal View History

package jiguang
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
2025-12-31 17:46:03 +08:00
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("查询为空")
)
// JiguangResponse 极光API响应结构
type JiguangResponse struct {
2026-01-06 16:37:31 +08:00
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
2025-12-31 17:46:03 +08:00
// apiCode: API服务编码如 marriage-single-v2用于请求头
// apiPath: API路径如 marriage/single-v2用于URL路径
// params: 请求参数会作为JSON body发送
2025-12-31 17:46:03 +08:00
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
}
2025-12-31 17:46:03 +08:00
// 构建完整的请求URL使用apiPath作为路径
requestURL := strings.TrimSuffix(j.config.URL, "/") + "/" + strings.TrimPrefix(apiPath, "/")
// 记录请求日志
if j.logger != nil {
2026-01-20 18:32:16 +08:00
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请求
2025-12-31 17:46:03 +08:00
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 {
2026-03-05 18:44:17 +08:00
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
2026-03-05 18:44:17 +08:00
j.logger.LogInfo(fmt.Sprintf("极光响应: %+v", jiguangResp))
// 记录响应日志(不记录具体响应数据)
if j.logger != nil {
if jiguangResp.OrderID != "" {
2026-03-05 18:44:17 +08:00
j.logger.LogResponseWithID(requestID, transactionID, apiCode, httpResp.StatusCode, duration, jiguangResp.OrderID)
} else {
2026-03-05 18:44:17 +08:00
j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, duration)
}
}
// 检查业务状态码
2026-01-06 18:11:37 +08:00
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
}
2026-01-14 12:57:53 +08:00
// 根据错误类型返回不同的错误
if jiguangErr.IsNoRecord() {
2026-01-19 14:28:21 +08:00
// 从context中获取apiCode判断是否需要抛出异常
var processorCode string
if ctxProcessorCode, ok := ctx.Value("api_code").(string); ok {
processorCode = ctxProcessorCode
}
2026-01-20 11:29:58 +08:00
// 定义不需要抛出异常的处理器列表(默认情况下查无记录时抛出异常)
processorsNotToThrowError := map[string]bool{
// 在这个列表中的处理器,查无记录时返回空数组,不抛出异常
// 示例:如果需要添加某个处理器,取消下面的注释
// "QCXG9P1C": true,
2026-01-19 14:28:21 +08:00
}
2026-01-20 11:29:58 +08:00
// 如果是不需要抛出异常的处理器,返回空数组;否则(默认)抛出异常
if processorsNotToThrowError[processorCode] {
// 查无记录时返回空数组API调用记录为成功
return []byte("[]"), nil
2026-01-19 14:28:21 +08:00
}
2026-01-20 11:29:58 +08:00
// 默认情况下,查无记录时抛出异常
// 记录错误日志
if j.logger != nil {
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID)
}
return nil, errors.Join(ErrNotFound, jiguangErr)
2026-01-14 12:57:53 +08:00
}
// 记录错误日志(查无记录的情况不记录错误日志)
if j.logger != nil {
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID)
}
2026-01-14 12:57:53 +08:00
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
}