Files
tyapi-server/internal/infrastructure/external/zhicha/zhicha_service.go
liangzai a91bde0c67 fix ivyz7c9d
fix external_logger
2025-08-26 16:03:46 +08:00

305 lines
8.0 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 zhicha
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"tyapi-server/internal/shared/external_logger"
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
)
type ZhichaResp struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Success bool `json:"success"`
}
type ZhichaConfig struct {
URL string
AppID string
AppSecret string
EncryptKey string
}
type ZhichaService struct {
config ZhichaConfig
logger *external_logger.ExternalServiceLogger
}
// NewZhichaService 是一个构造函数,用于初始化 ZhichaService
func NewZhichaService(url, appID, appSecret, encryptKey string, logger *external_logger.ExternalServiceLogger) *ZhichaService {
return &ZhichaService{
config: ZhichaConfig{
URL: url,
AppID: appID,
AppSecret: appSecret,
EncryptKey: encryptKey,
},
logger: logger,
}
}
// generateRequestID 生成请求ID
func (z *ZhichaService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, z.config.AppID)))
return fmt.Sprintf("zhicha_%x", hash[:8])
}
// generateSign 生成签名
func (z *ZhichaService) generateSign(timestamp int64) string {
// 第一步对app_secret进行MD5加密
encryptedSecret := fmt.Sprintf("%x", md5.Sum([]byte(z.config.AppSecret)))
// 第二步将加密后的密钥和时间戳拼接再次MD5加密
signStr := encryptedSecret + strconv.FormatInt(timestamp, 10)
sign := fmt.Sprintf("%x", md5.Sum([]byte(signStr)))
return sign
}
// CallAPI 调用智查金控的 API
func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[string]interface{}) (data interface{}, err error) {
startTime := time.Now()
requestID := z.generateRequestID()
timestamp := time.Now().Unix()
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 记录请求日志
if z.logger != nil {
z.logger.LogRequest(requestID, transactionID, proID, z.config.URL, params)
}
jsonData, marshalErr := json.Marshal(params)
if marshalErr != nil {
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
// 创建HTTP POST请求
req, err := http.NewRequestWithContext(ctx, "POST", z.config.URL, bytes.NewBuffer(jsonData))
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("appId", z.config.AppID)
req.Header.Set("proId", proID)
req.Header.Set("timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("sign", z.generateSign(timestamp))
// 创建HTTP客户端
client := &http.Client{
Timeout: 20 * time.Second,
}
// 发送请求
response, err := client.Do(req)
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
defer response.Body.Close()
// 读取响应
respBody, err := io.ReadAll(response.Body)
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
// 记录响应日志
if z.logger != nil {
duration := time.Since(startTime)
z.logger.LogResponse(requestID, transactionID, proID, response.StatusCode, respBody, duration)
}
// 检查HTTP状态码
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: HTTP状态码 %d", ErrDatasource, response.StatusCode)
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
// 解析响应
var zhichaResp ZhichaResp
if err := json.Unmarshal(respBody, &zhichaResp); err != nil {
err = fmt.Errorf("%w: 响应解析失败: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, err, params)
}
return nil, err
}
// 检查业务状态码
if zhichaResp.Code != "200" && zhichaResp.Code != "201" {
// 创建智查金控错误用于日志记录
zhichaErr := NewZhichaErrorFromCode(zhichaResp.Code)
if zhichaErr.Code == "未知错误" {
zhichaErr.Message = zhichaResp.Message
}
// 记录智查金控的详细错误信息到日志
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, zhichaErr, params)
}
// 对外统一返回数据源异常错误
return nil, ErrDatasource
}
// 返回data字段
return zhichaResp.Data, nil
}
// Encrypt 使用配置的加密密钥对数据进行AES-128-CBC加密
func (z *ZhichaService) Encrypt(data string) (string, error) {
if z.config.EncryptKey == "" {
return "", fmt.Errorf("加密密钥未配置")
}
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(z.config.EncryptKey)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < 16 { // AES-128, 16 bytes
return "", fmt.Errorf("密钥长度不足需要至少16字节")
}
// 从密钥前16个字符生成IV
iv := []byte(z.config.EncryptKey[:16])
// 创建AES加密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES加密器失败: %w", err)
}
// 对数据进行PKCS7填充
paddedData := z.pkcs7Padding([]byte(data), aes.BlockSize)
// 创建CBC模式加密器
mode := cipher.NewCBCEncrypter(block, iv)
// 加密
ciphertext := make([]byte, len(paddedData))
mode.CryptBlocks(ciphertext, paddedData)
// 返回Base64编码结果
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 使用配置的加密密钥对数据进行AES-128-CBC解密
func (z *ZhichaService) Decrypt(encryptedData string) (string, error) {
if z.config.EncryptKey == "" {
return "", fmt.Errorf("加密密钥未配置")
}
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(z.config.EncryptKey)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < 16 { // AES-128, 16 bytes
return "", fmt.Errorf("密钥长度不足需要至少16字节")
}
// 从密钥前16个字符生成IV
iv := []byte(z.config.EncryptKey[:16])
// 解码Base64数据
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return "", fmt.Errorf("Base64解码失败: %w", err)
}
// 检查数据长度是否为AES块大小的倍数
if len(decodedData) == 0 || len(decodedData)%aes.BlockSize != 0 {
return "", fmt.Errorf("加密数据长度无效,必须是%d字节的倍数", aes.BlockSize)
}
// 创建AES解密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES解密器失败: %w", err)
}
// 创建CBC模式解密器
mode := cipher.NewCBCDecrypter(block, iv)
// 解密
plaintext := make([]byte, len(decodedData))
mode.CryptBlocks(plaintext, decodedData)
// 移除PKCS7填充
unpadded, err := z.pkcs7Unpadding(plaintext)
if err != nil {
return "", fmt.Errorf("移除填充失败: %w", err)
}
return string(unpadded), nil
}
// pkcs7Padding 使用PKCS7填充数据
func (z *ZhichaService) pkcs7Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
// pkcs7Unpadding 移除PKCS7填充
func (z *ZhichaService) pkcs7Unpadding(src []byte) ([]byte, error) {
length := len(src)
if length == 0 {
return nil, fmt.Errorf("数据为空")
}
unpadding := int(src[length-1])
if unpadding > length {
return nil, fmt.Errorf("填充长度无效")
}
return src[:length-unpadding], nil
}