2025-12-31 15:42:05 +08:00
|
|
|
|
package jiguang
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"crypto/md5"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"strconv"
|
2025-12-31 17:46:03 +08:00
|
|
|
|
"strings"
|
2025-12-31 15:42:05 +08:00
|
|
|
|
"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"`
|
2025-12-31 15:42:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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路径
|
2025-12-31 15:42:05 +08:00
|
|
|
|
// 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) {
|
2025-12-31 15:42:05 +08:00
|
|
|
|
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, "/")
|
|
|
|
|
|
|
2025-12-31 15:42:05 +08:00
|
|
|
|
// 记录请求日志
|
|
|
|
|
|
if j.logger != nil {
|
2026-01-20 18:32:16 +08:00
|
|
|
|
j.logger.LogRequest(requestID, transactionID, apiCode, requestURL)
|
2025-12-31 15:42:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将请求参数转换为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))
|
2025-12-31 15:42:05 +08:00
|
|
|
|
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)
|
2025-12-31 15:42:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-03-05 18:44:17 +08:00
|
|
|
|
j.logger.LogInfo(fmt.Sprintf("极光响应: %+v", jiguangResp))
|
|
|
|
|
|
// 记录响应日志(不记录具体响应数据)
|
2025-12-31 15:42:05 +08:00
|
|
|
|
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)
|
2025-12-31 15:42:05 +08:00
|
|
|
|
} else {
|
2026-03-05 18:44:17 +08:00
|
|
|
|
j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, duration)
|
2025-12-31 15:42:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查业务状态码
|
2026-01-06 18:11:37 +08:00
|
|
|
|
if jiguangResp.Code != 0 && jiguangResp.Code != 200 {
|
2025-12-31 15:42:05 +08:00
|
|
|
|
// 创建极光错误
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 记录错误日志(查无记录的情况不记录错误日志)
|
2025-12-31 15:42:05 +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() {
|
2025-12-31 15:42:05 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|