305 lines
8.0 KiB
Go
305 lines
8.0 KiB
Go
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
|
||
}
|