f
This commit is contained in:
316
internal/infrastructure/external/jiguang/jiguang_service.go
vendored
Normal file
316
internal/infrastructure/external/jiguang/jiguang_service.go
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// JiguangResponse 极光API响应结构(兼容两套字段命名)
|
||||
//
|
||||
// 格式一:ordernum、message、result(定位/查询类接口常见)
|
||||
// 格式二:order_id、msg、data(文档中的 code/msg/order_id/data)
|
||||
type JiguangResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Message string `json:"message"`
|
||||
OrderID string `json:"order_id"`
|
||||
OrderNum string `json:"ordernum"`
|
||||
Data interface{} `json:"data"`
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
// normalize 将异名字段合并到 OrderID、Msg,便于后续统一分支使用
|
||||
func (r *JiguangResponse) normalize() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if r.OrderID == "" && r.OrderNum != "" {
|
||||
r.OrderID = r.OrderNum
|
||||
}
|
||||
if r.Msg == "" && r.Message != "" {
|
||||
r.Msg = r.Message
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
jiguangResp.normalize()
|
||||
|
||||
// 记录响应日志(不记录具体响应数据)
|
||||
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) {
|
||||
if jiguangResp.Msg != "" {
|
||||
jiguangErr.Message = jiguangResp.Msg
|
||||
} else if jiguangResp.Message != "" {
|
||||
jiguangErr.Message = jiguangResp.Message
|
||||
}
|
||||
}
|
||||
// 根据错误类型返回不同的错误
|
||||
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 或 result
|
||||
payload := jiguangResp.Data
|
||||
if payload == nil {
|
||||
payload = jiguangResp.Result
|
||||
}
|
||||
if payload == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
dataBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("业务数据序列化失败: %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
|
||||
}
|
||||
Reference in New Issue
Block a user