417 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			417 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package tianyuanapi
 | ||
| 
 | ||
| import (
 | ||
| 	"bytes"
 | ||
| 	"crypto/aes"
 | ||
| 	"crypto/cipher"
 | ||
| 	"crypto/rand"
 | ||
| 	"encoding/base64"
 | ||
| 	"encoding/hex"
 | ||
| 	"encoding/json"
 | ||
| 	"errors"
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"net/http"
 | ||
| 	"time"
 | ||
| )
 | ||
| 
 | ||
| // API调用相关错误类型
 | ||
| var (
 | ||
| 	ErrQueryEmpty      = errors.New("查询为空")
 | ||
| 	ErrSystem          = errors.New("接口异常")
 | ||
| 	ErrDecryptFail     = errors.New("解密失败")
 | ||
| 	ErrRequestParam    = errors.New("请求参数结构不正确")
 | ||
| 	ErrInvalidParam    = errors.New("参数校验不正确")
 | ||
| 	ErrInvalidIP       = errors.New("未经授权的IP")
 | ||
| 	ErrMissingAccessId = errors.New("缺少Access-Id")
 | ||
| 	ErrInvalidAccessId = errors.New("未经授权的AccessId")
 | ||
| 	ErrFrozenAccount   = errors.New("账户已冻结")
 | ||
| 	ErrArrears         = errors.New("账户余额不足,无法请求")
 | ||
| 	ErrProductNotFound = errors.New("产品不存在")
 | ||
| 	ErrProductDisabled = errors.New("产品已停用")
 | ||
| 	ErrNotSubscribed   = errors.New("未订阅此产品")
 | ||
| 	ErrBusiness        = errors.New("业务失败")
 | ||
| )
 | ||
| 
 | ||
| // 错误码映射 - 严格按照用户要求
 | ||
| var ErrorCodeMap = map[error]int{
 | ||
| 	ErrQueryEmpty:      1000,
 | ||
| 	ErrSystem:          1001,
 | ||
| 	ErrDecryptFail:     1002,
 | ||
| 	ErrRequestParam:    1003,
 | ||
| 	ErrInvalidParam:    1003,
 | ||
| 	ErrInvalidIP:       1004,
 | ||
| 	ErrMissingAccessId: 1005,
 | ||
| 	ErrInvalidAccessId: 1006,
 | ||
| 	ErrFrozenAccount:   1007,
 | ||
| 	ErrArrears:         1007,
 | ||
| 	ErrProductNotFound: 1008,
 | ||
| 	ErrProductDisabled: 1008,
 | ||
| 	ErrNotSubscribed:   1008,
 | ||
| 	ErrBusiness:        2001,
 | ||
| }
 | ||
| 
 | ||
| // ApiCallOptions API调用选项
 | ||
| type ApiCallOptions struct {
 | ||
| 	Json bool `json:"json,omitempty"` // 是否返回JSON格式
 | ||
| }
 | ||
| 
 | ||
| // Client 天元API客户端
 | ||
| type Client struct {
 | ||
| 	accessID string
 | ||
| 	key      string
 | ||
| 	baseURL  string
 | ||
| 	timeout  time.Duration
 | ||
| 	client   *http.Client
 | ||
| }
 | ||
| 
 | ||
| // Config 客户端配置
 | ||
| type Config struct {
 | ||
| 	AccessID string        // 访问ID
 | ||
| 	Key      string        // AES密钥(16进制)
 | ||
| 	BaseURL  string        // API基础URL
 | ||
| 	Timeout  time.Duration // 超时时间
 | ||
| }
 | ||
| 
 | ||
| // Request 请求参数
 | ||
| type Request struct {
 | ||
| 	InterfaceName string                 `json:"interfaceName"` // 接口名称
 | ||
| 	Params        map[string]interface{} `json:"params"`        // 请求参数
 | ||
| 	Timeout       int                    `json:"timeout"`       // 超时时间(毫秒)
 | ||
| 	Options       *ApiCallOptions        `json:"options"`       // 调用选项
 | ||
| }
 | ||
| 
 | ||
| // ApiResponse HTTP API响应
 | ||
| type ApiResponse struct {
 | ||
| 	Code          int    `json:"code"`
 | ||
| 	Message       string `json:"message"`
 | ||
| 	TransactionID string `json:"transaction_id"` // 流水号
 | ||
| 	Data          string `json:"data"`           // 加密的数据
 | ||
| }
 | ||
| 
 | ||
| // Response Call方法的响应
 | ||
| type Response struct {
 | ||
| 	Code          int                    `json:"code"`
 | ||
| 	Message       string                 `json:"message"`
 | ||
| 	Success       bool                   `json:"success"`
 | ||
| 	TransactionID string                 `json:"transaction_id"` // 流水号
 | ||
| 	Data          map[string]interface{} `json:"data"`           // 解密后的数据
 | ||
| 	Timeout       int64                  `json:"timeout"`        // 请求耗时(毫秒)
 | ||
| 	Error         string                 `json:"error,omitempty"`
 | ||
| }
 | ||
| 
 | ||
| // NewClient 创建新的客户端实例
 | ||
| func NewClient(config Config) (*Client, error) {
 | ||
| 	// 参数校验
 | ||
| 	if config.AccessID == "" {
 | ||
| 		return nil, fmt.Errorf("accessID不能为空")
 | ||
| 	}
 | ||
| 	if config.Key == "" {
 | ||
| 		return nil, fmt.Errorf("key不能为空")
 | ||
| 	}
 | ||
| 	if config.BaseURL == "" {
 | ||
| 		config.BaseURL = "http://127.0.0.1:8080"
 | ||
| 	}
 | ||
| 	if config.Timeout == 0 {
 | ||
| 		config.Timeout = 60 * time.Second
 | ||
| 	}
 | ||
| 
 | ||
| 	// 验证密钥格式
 | ||
| 	if _, err := hex.DecodeString(config.Key); err != nil {
 | ||
| 		return nil, fmt.Errorf("无效的密钥格式,必须是16进制字符串: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	return &Client{
 | ||
| 		accessID: config.AccessID,
 | ||
| 		key:      config.Key,
 | ||
| 		baseURL:  config.BaseURL,
 | ||
| 		timeout:  config.Timeout,
 | ||
| 		client: &http.Client{
 | ||
| 			Timeout: config.Timeout,
 | ||
| 		},
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // Call 调用API接口
 | ||
| func (c *Client) Call(req Request) (*Response, error) {
 | ||
| 	startTime := time.Now()
 | ||
| 
 | ||
| 	// 参数校验
 | ||
| 	if err := c.validateRequest(req); err != nil {
 | ||
| 		return nil, fmt.Errorf("请求参数校验失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 加密参数
 | ||
| 	jsonData, err := json.Marshal(req.Params)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("参数序列化失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	encryptedData, err := c.encrypt(string(jsonData))
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("数据加密失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 构建请求体
 | ||
| 	requestBody := map[string]interface{}{
 | ||
| 		"data": encryptedData,
 | ||
| 	}
 | ||
| 
 | ||
| 	// 添加选项
 | ||
| 	if req.Options != nil {
 | ||
| 		requestBody["options"] = req.Options
 | ||
| 	} else {
 | ||
| 		// 默认选项
 | ||
| 		defaultOptions := &ApiCallOptions{
 | ||
| 			Json: true,
 | ||
| 		}
 | ||
| 		requestBody["options"] = defaultOptions
 | ||
| 	}
 | ||
| 
 | ||
| 	requestBodyBytes, err := json.Marshal(requestBody)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("请求体序列化失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建HTTP请求
 | ||
| 	url := fmt.Sprintf("%s/api/v1/%s", c.baseURL, req.InterfaceName)
 | ||
| 
 | ||
| 	httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBodyBytes))
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 设置请求头
 | ||
| 	httpReq.Header.Set("Content-Type", "application/json")
 | ||
| 	httpReq.Header.Set("Access-Id", c.accessID)
 | ||
| 	httpReq.Header.Set("User-Agent", "TianyuanAPI-Go-SDK/1.0.0")
 | ||
| 
 | ||
| 	// 发送请求
 | ||
| 	resp, err := c.client.Do(httpReq)
 | ||
| 	if err != nil {
 | ||
| 		endTime := time.Now()
 | ||
| 		requestTime := endTime.Sub(startTime).Milliseconds()
 | ||
| 		return &Response{
 | ||
| 			Success: false,
 | ||
| 			Message: "请求失败",
 | ||
| 			Error:   err.Error(),
 | ||
| 			Timeout: requestTime,
 | ||
| 		}, nil
 | ||
| 	}
 | ||
| 	defer resp.Body.Close()
 | ||
| 
 | ||
| 	// 读取响应
 | ||
| 	body, err := io.ReadAll(resp.Body)
 | ||
| 	if err != nil {
 | ||
| 		endTime := time.Now()
 | ||
| 		requestTime := endTime.Sub(startTime).Milliseconds()
 | ||
| 		return &Response{
 | ||
| 			Success: false,
 | ||
| 			Message: "读取响应失败",
 | ||
| 			Error:   err.Error(),
 | ||
| 			Timeout: requestTime,
 | ||
| 		}, nil
 | ||
| 	}
 | ||
| 
 | ||
| 	// 解析HTTP API响应
 | ||
| 	var apiResp ApiResponse
 | ||
| 	if err := json.Unmarshal(body, &apiResp); err != nil {
 | ||
| 		endTime := time.Now()
 | ||
| 		requestTime := endTime.Sub(startTime).Milliseconds()
 | ||
| 		return &Response{
 | ||
| 			Success: false,
 | ||
| 			Message: "响应解析失败",
 | ||
| 			Error:   err.Error(),
 | ||
| 			Timeout: requestTime,
 | ||
| 		}, nil
 | ||
| 	}
 | ||
| 
 | ||
| 	// 计算请求耗时
 | ||
| 	endTime := time.Now()
 | ||
| 	requestTime := endTime.Sub(startTime).Milliseconds()
 | ||
| 
 | ||
| 	// 构建Call方法的响应
 | ||
| 	response := &Response{
 | ||
| 		Code:          apiResp.Code,
 | ||
| 		Message:       apiResp.Message,
 | ||
| 		Success:       apiResp.Code == 0,
 | ||
| 		TransactionID: apiResp.TransactionID,
 | ||
| 		Timeout:       requestTime,
 | ||
| 	}
 | ||
| 
 | ||
| 	// 如果有加密数据,尝试解密
 | ||
| 	if apiResp.Data != "" {
 | ||
| 		decryptedData, err := c.decrypt(apiResp.Data)
 | ||
| 		if err == nil {
 | ||
| 			var decryptedMap map[string]interface{}
 | ||
| 			if json.Unmarshal([]byte(decryptedData), &decryptedMap) == nil {
 | ||
| 				response.Data = decryptedMap
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 根据响应码返回对应的错误
 | ||
| 	if apiResp.Code != 0 {
 | ||
| 		err := GetErrorByCode(apiResp.Code)
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	return response, nil
 | ||
| }
 | ||
| 
 | ||
| // CallInterface 简化接口调用方法
 | ||
| func (c *Client) CallInterface(interfaceName string, params map[string]interface{}, options ...*ApiCallOptions) (*Response, error) {
 | ||
| 	var opts *ApiCallOptions
 | ||
| 	if len(options) > 0 {
 | ||
| 		opts = options[0]
 | ||
| 	}
 | ||
| 
 | ||
| 	req := Request{
 | ||
| 		InterfaceName: interfaceName,
 | ||
| 		Params:        params,
 | ||
| 		Timeout:       60000,
 | ||
| 		Options:       opts,
 | ||
| 	}
 | ||
| 
 | ||
| 	return c.Call(req)
 | ||
| }
 | ||
| 
 | ||
| // validateRequest 校验请求参数
 | ||
| func (c *Client) validateRequest(req Request) error {
 | ||
| 	if req.InterfaceName == "" {
 | ||
| 		return fmt.Errorf("interfaceName不能为空")
 | ||
| 	}
 | ||
| 	if req.Params == nil {
 | ||
| 		return fmt.Errorf("params不能为空")
 | ||
| 	}
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| // encrypt AES CBC加密
 | ||
| func (c *Client) encrypt(plainText string) (string, error) {
 | ||
| 	keyBytes, err := hex.DecodeString(c.key)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	block, err := aes.NewCipher(keyBytes)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 生成随机IV
 | ||
| 	iv := make([]byte, aes.BlockSize)
 | ||
| 	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 填充数据
 | ||
| 	paddedData := c.pkcs7Pad([]byte(plainText), aes.BlockSize)
 | ||
| 
 | ||
| 	// 加密
 | ||
| 	ciphertext := make([]byte, len(iv)+len(paddedData))
 | ||
| 	copy(ciphertext, iv)
 | ||
| 
 | ||
| 	mode := cipher.NewCBCEncrypter(block, iv)
 | ||
| 	mode.CryptBlocks(ciphertext[len(iv):], paddedData)
 | ||
| 
 | ||
| 	return base64.StdEncoding.EncodeToString(ciphertext), nil
 | ||
| }
 | ||
| 
 | ||
| // decrypt AES CBC解密
 | ||
| func (c *Client) decrypt(encryptedText string) (string, error) {
 | ||
| 	keyBytes, err := hex.DecodeString(c.key)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	ciphertext, err := base64.StdEncoding.DecodeString(encryptedText)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	block, err := aes.NewCipher(keyBytes)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	if len(ciphertext) < aes.BlockSize {
 | ||
| 		return "", fmt.Errorf("密文太短")
 | ||
| 	}
 | ||
| 
 | ||
| 	iv := ciphertext[:aes.BlockSize]
 | ||
| 	ciphertext = ciphertext[aes.BlockSize:]
 | ||
| 
 | ||
| 	if len(ciphertext)%aes.BlockSize != 0 {
 | ||
| 		return "", fmt.Errorf("密文长度不是块大小的倍数")
 | ||
| 	}
 | ||
| 
 | ||
| 	mode := cipher.NewCBCDecrypter(block, iv)
 | ||
| 	mode.CryptBlocks(ciphertext, ciphertext)
 | ||
| 
 | ||
| 	// 去除填充
 | ||
| 	unpaddedData, err := c.pkcs7Unpad(ciphertext)
 | ||
| 	if err != nil {
 | ||
| 		return "", err
 | ||
| 	}
 | ||
| 
 | ||
| 	return string(unpaddedData), nil
 | ||
| }
 | ||
| 
 | ||
| // pkcs7Pad PKCS7填充
 | ||
| func (c *Client) pkcs7Pad(data []byte, blockSize int) []byte {
 | ||
| 	padding := blockSize - len(data)%blockSize
 | ||
| 	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
 | ||
| 	return append(data, padtext...)
 | ||
| }
 | ||
| 
 | ||
| // pkcs7Unpad PKCS7去除填充
 | ||
| func (c *Client) pkcs7Unpad(data []byte) ([]byte, error) {
 | ||
| 	length := len(data)
 | ||
| 	if length == 0 {
 | ||
| 		return nil, fmt.Errorf("数据为空")
 | ||
| 	}
 | ||
| 	unpadding := int(data[length-1])
 | ||
| 	if unpadding > length {
 | ||
| 		return nil, fmt.Errorf("无效的填充")
 | ||
| 	}
 | ||
| 	return data[:length-unpadding], nil
 | ||
| }
 | ||
| 
 | ||
| // GetErrorByCode 根据错误码获取错误
 | ||
| func GetErrorByCode(code int) error {
 | ||
| 	// 对于有多个错误对应同一错误码的情况,返回第一个
 | ||
| 	switch code {
 | ||
| 	case 1000:
 | ||
| 		return ErrQueryEmpty
 | ||
| 	case 1001:
 | ||
| 		return ErrSystem
 | ||
| 	case 1002:
 | ||
| 		return ErrDecryptFail
 | ||
| 	case 1003:
 | ||
| 		return ErrRequestParam
 | ||
| 	case 1004:
 | ||
| 		return ErrInvalidIP
 | ||
| 	case 1005:
 | ||
| 		return ErrMissingAccessId
 | ||
| 	case 1006:
 | ||
| 		return ErrInvalidAccessId
 | ||
| 	case 1007:
 | ||
| 		return ErrFrozenAccount
 | ||
| 	case 1008:
 | ||
| 		return ErrProductNotFound
 | ||
| 	case 2001:
 | ||
| 		return ErrBusiness
 | ||
| 	default:
 | ||
| 		return fmt.Errorf("未知错误码: %d", code)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // GetCodeByError 根据错误获取错误码
 | ||
| func GetCodeByError(err error) int {
 | ||
| 	if code, exists := ErrorCodeMap[err]; exists {
 | ||
| 		return code
 | ||
| 	}
 | ||
| 	return -1
 | ||
| }
 |