Files
tyapi-server/internal/infrastructure/external/xingwei/xingwei_service.go
2025-10-16 18:35:18 +08:00

280 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package xingwei
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"sync/atomic"
"time"
"tyapi-server/internal/shared/external_logger"
)
// 行为数据API状态码常量
const (
CodeSuccess = 200 // 操作成功
CodeSystemError = 500 // 系统内部错误
CodeMerchantError = 3001 // 商家相关报错(商家不存在、商家被禁用、商家余额不足)
CodeAccountExpired = 3002 // 账户已过期
CodeIPWhitelistMissing = 3003 // 未添加ip白名单
CodeUnauthorized = 3004 // 未授权调用该接口
CodeProductIDError = 4001 // 产品id错误
CodeInterfaceDisabled = 4002 // 接口被停用
CodeQueryException = 5001 // 接口查询异常,请联系技术人员
CodeNotFound = 6000 // 未查询到结果
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("未查询到结果")
// 请求ID计数器确保唯一性
requestIDCounter int64
)
// XingweiResponse 行为数据API响应结构
type XingweiResponse struct {
Msg string `json:"msg"`
Code int `json:"code"`
Data interface{} `json:"data"`
}
// XingweiErrorCode 行为数据错误码定义
type XingweiErrorCode struct {
Code int
Message string
}
// 行为数据错误码映射
var XingweiErrorCodes = map[int]XingweiErrorCode{
CodeSuccess: {Code: CodeSuccess, Message: "操作成功"},
CodeSystemError: {Code: CodeSystemError, Message: "系统内部错误"},
CodeMerchantError: {Code: CodeMerchantError, Message: "商家相关报错(商家不存在、商家被禁用、商家余额不足)"},
CodeAccountExpired: {Code: CodeAccountExpired, Message: "账户已过期"},
CodeIPWhitelistMissing: {Code: CodeIPWhitelistMissing, Message: "未添加ip白名单"},
CodeUnauthorized: {Code: CodeUnauthorized, Message: "未授权调用该接口"},
CodeProductIDError: {Code: CodeProductIDError, Message: "产品id错误"},
CodeInterfaceDisabled: {Code: CodeInterfaceDisabled, Message: "接口被停用"},
CodeQueryException: {Code: CodeQueryException, Message: "接口查询异常,请联系技术人员"},
CodeNotFound: {Code: CodeNotFound, Message: "未查询到结果"},
}
// GetXingweiErrorMessage 根据错误码获取错误消息
func GetXingweiErrorMessage(code int) string {
if errorCode, exists := XingweiErrorCodes[code]; exists {
return errorCode.Message
}
return fmt.Sprintf("未知错误码: %d", code)
}
type XingweiConfig struct {
URL string
ApiID string
ApiKey string
}
type XingweiService struct {
config XingweiConfig
logger *external_logger.ExternalServiceLogger
}
// NewXingweiService 是一个构造函数,用于初始化 XingweiService
func NewXingweiService(url, apiID, apiKey string, logger *external_logger.ExternalServiceLogger) *XingweiService {
return &XingweiService{
config: XingweiConfig{
URL: url,
ApiID: apiID,
ApiKey: apiKey,
},
logger: logger,
}
}
// generateRequestID 生成请求ID
func (x *XingweiService) generateRequestID() string {
timestamp := time.Now().UnixNano()
// 使用原子计数器确保唯一性
counter := atomic.AddInt64(&requestIDCounter, 1)
hash := md5.Sum([]byte(fmt.Sprintf("%d_%d_%s", timestamp, counter, x.config.ApiID)))
return fmt.Sprintf("xingwei_%x", hash[:8])
}
// createSign 创建签名使用MD5算法将apiId、timestamp、apiKey字符串拼接生成sign
// 参考Java示例DigestUtils.md5Hex(apiId + timestamp + apiKey)
func (x *XingweiService) createSign(timestamp int64) string {
signStr := x.config.ApiID + strconv.FormatInt(timestamp, 10) + x.config.ApiKey
hash := md5.Sum([]byte(signStr))
return fmt.Sprintf("%x", hash)
}
// CallAPI 调用行为数据的 API
func (x *XingweiService) CallAPI(ctx context.Context, projectID string, params map[string]interface{}) (resp []byte, err error) {
startTime := time.Now()
requestID := x.generateRequestID()
timestamp := time.Now().UnixMilli()
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 记录请求日志
if x.logger != nil {
x.logger.LogRequest(requestID, transactionID, "xingwei_api", x.config.URL, params)
}
// 将请求参数转换为JSON
jsonData, marshalErr := json.Marshal(params)
if marshalErr != nil {
err = errors.Join(ErrSystem, marshalErr)
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", x.config.URL, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
err = errors.Join(ErrSystem, newRequestErr)
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("sign", x.createSign(timestamp))
req.Header.Set("API-ID", x.config.ApiID)
req.Header.Set("project_id", projectID)
// 创建HTTP客户端
client := &http.Client{
Timeout: 20 * time.Second,
}
// 发送请求
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
err = errors.Join(ErrSystem, clientDoErr)
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
// 记录关闭错误
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", 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 x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
// 记录响应日志
if x.logger != nil {
x.logger.LogResponse(requestID, transactionID, "xingwei_api", httpResp.StatusCode, bodyBytes, duration)
}
// 检查HTTP状态码
if httpResp.StatusCode != http.StatusOK {
err = errors.Join(ErrSystem, fmt.Errorf("行为数据请求失败,状态码: %d", httpResp.StatusCode))
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
// 解析响应结构
var xingweiResp XingweiResponse
if err := json.Unmarshal(bodyBytes, &xingweiResp); err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
// 检查业务状态码
switch xingweiResp.Code {
case CodeSuccess:
// 成功响应返回data字段
if xingweiResp.Data == nil {
return []byte("{}"), nil
}
// 将data转换为JSON字节
dataBytes, err := json.Marshal(xingweiResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("data字段序列化失败: %w", err))
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
}
return nil, err
}
return dataBytes, nil
case CodeNotFound:
// 未查询到结果,返回查空错误
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api",
errors.Join(ErrNotFound, fmt.Errorf("未查询到结果")), params)
}
return nil, errors.Join(ErrNotFound, fmt.Errorf("未查询到结果"))
case CodeSystemError:
// 系统内部错误
errorMsg := GetXingweiErrorMessage(xingweiResp.Code)
systemErr := fmt.Errorf("行为数据系统错误[%d]: %s", xingweiResp.Code, errorMsg)
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api",
errors.Join(ErrSystem, systemErr), params)
}
return nil, errors.Join(ErrSystem, systemErr)
default:
// 其他业务错误
errorMsg := GetXingweiErrorMessage(xingweiResp.Code)
businessErr := fmt.Errorf("行为数据业务错误[%d]: %s", xingweiResp.Code, errorMsg)
if x.logger != nil {
x.logger.LogError(requestID, transactionID, "xingwei_api",
errors.Join(ErrDatasource, businessErr), params)
}
return nil, errors.Join(ErrDatasource, businessErr)
}
}
// GetConfig 获取配置信息
func (x *XingweiService) GetConfig() XingweiConfig {
return x.config
}