添加文件: ymt_v3/client.go
This commit is contained in:
parent
39a8373a17
commit
769b9b4ddc
|
|
@ -0,0 +1,249 @@
|
||||||
|
package ymt_v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 客户端配置
|
||||||
|
type Config struct {
|
||||||
|
AppID string // 应用ID
|
||||||
|
PrivateKey string // 应用私钥(PEM格式),用于请求签名
|
||||||
|
PublicKey string // 平台公钥(PEM格式),用于响应验签
|
||||||
|
Key string // 业务参数加密密钥
|
||||||
|
EncryptType string // 加密类型:aes 或 sm4
|
||||||
|
BaseURL string // 接口地址
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client SDK客户端
|
||||||
|
type Client struct {
|
||||||
|
config *Config
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建新的SDK客户端
|
||||||
|
func NewClient(config *Config) *Client {
|
||||||
|
if config.HTTPClient == nil {
|
||||||
|
config.HTTPClient = http.DefaultClient
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
config: config,
|
||||||
|
client: config.HTTPClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest 发送请求并处理响应
|
||||||
|
func (c *Client) doRequest(ctx context.Context, path string, bizParams interface{}, result interface{}) error {
|
||||||
|
// 1. 加密业务参数
|
||||||
|
ciphertext, err := EncryptBizParams(bizParams, []byte(c.config.Key), c.config.EncryptType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加密业务参数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成时间戳
|
||||||
|
timestamp := GenerateTimestamp()
|
||||||
|
|
||||||
|
// 3. 拼接签名字符串:app_id + timestamp + ciphertext
|
||||||
|
signStr := c.config.AppID + timestamp + ciphertext
|
||||||
|
|
||||||
|
// 4. 使用应用私钥签名
|
||||||
|
sign, err := SignWithRSA(signStr, c.config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("签名失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 构建请求体
|
||||||
|
reqBody := EncryptedRequest{
|
||||||
|
Ciphertext: ciphertext,
|
||||||
|
}
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化请求体失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 创建HTTP请求
|
||||||
|
url := strings.TrimRight(c.config.BaseURL, "/") + path
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 设置Header
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Appid", c.config.AppID)
|
||||||
|
req.Header.Set("Timestamp", timestamp)
|
||||||
|
req.Header.Set("Sign", sign)
|
||||||
|
|
||||||
|
// 8. 发送请求
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("发送请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 解析响应
|
||||||
|
var encryptedResp EncryptedResponse
|
||||||
|
if err := json.Unmarshal(respBody, &encryptedResp); err != nil {
|
||||||
|
return fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 检查错误码
|
||||||
|
if encryptedResp.Code != 200 {
|
||||||
|
return &APIError{
|
||||||
|
Code: encryptedResp.Code,
|
||||||
|
Message: encryptedResp.Message,
|
||||||
|
Reason: encryptedResp.Reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. 解密响应数据
|
||||||
|
if encryptedResp.Data == nil || encryptedResp.Data.Ciphertext == "" {
|
||||||
|
return fmt.Errorf("响应数据为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := DecryptBizParams(encryptedResp.Data.Ciphertext, []byte(c.config.Key), c.config.EncryptType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解密响应数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 解析解密后的数据到结果结构体
|
||||||
|
if err := json.Unmarshal(plaintext, result); err != nil {
|
||||||
|
return fmt.Errorf("解析解密数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 方法
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Order 获取券码
|
||||||
|
func (c *Client) Order(ctx context.Context, req *OrderRequest) (*OrderResponse, error) {
|
||||||
|
result := &OrderResponse{}
|
||||||
|
if err := c.doRequest(ctx, "/openapi/v1/key/order", req, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query 券码查询
|
||||||
|
func (c *Client) Query(ctx context.Context, req *QueryRequest) (*OrderResponse, error) {
|
||||||
|
result := &OrderResponse{}
|
||||||
|
if err := c.doRequest(ctx, "/openapi/v1/key/query", req, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard 券码作废
|
||||||
|
func (c *Client) Discard(ctx context.Context, req *DiscardRequest) (*DiscardResponse, error) {
|
||||||
|
result := &DiscardResponse{}
|
||||||
|
if err := c.doRequest(ctx, "/openapi/v1/key/discard", req, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchOrder 批量发卡
|
||||||
|
func (c *Client) BatchOrder(ctx context.Context, req *BatchOrderRequest) (*BatchOrderResponse, error) {
|
||||||
|
result := &BatchOrderResponse{}
|
||||||
|
if err := c.doRequest(ctx, "/openapi/v1/key/batch_order", req, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchQuery 批量查询
|
||||||
|
func (c *Client) BatchQuery(ctx context.Context, req *BatchQueryRequest) (*BatchQueryResponse, error) {
|
||||||
|
result := &BatchQueryResponse{}
|
||||||
|
if err := c.doRequest(ctx, "/openapi/v1/key/batch_query", req, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 回调验签
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// VerifyCallback 验证回调通知的签名
|
||||||
|
// 参数:
|
||||||
|
// - appID: 应用ID
|
||||||
|
// - timestamp: 回调请求中的时间戳
|
||||||
|
// - sign: 回调请求中的签名
|
||||||
|
// - data: 回调请求中的业务数据(明文JSON字符串)
|
||||||
|
// - publicKeyPEM: 平台公钥(PEM格式)
|
||||||
|
// - key: 业务参数加密密钥
|
||||||
|
// - encryptType: 加密类型:aes 或 sm4
|
||||||
|
func VerifyCallback(appID, timestamp, sign, data, publicKeyPEM string, key []byte, encryptType string) error {
|
||||||
|
// 1. 使用key将data加密得到ciphertext
|
||||||
|
ciphertext, err := EncryptBizParamsFromJSON(data, key, encryptType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加密回调数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 拼接签名字符串:app_id + timestamp + ciphertext
|
||||||
|
signStr := appID + timestamp + ciphertext
|
||||||
|
|
||||||
|
// 3. 使用平台公钥验签
|
||||||
|
return VerifyWithRSA(signStr, sign, publicKeyPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptBizParamsFromJSON 将JSON字符串按规则加密
|
||||||
|
func EncryptBizParamsFromJSON(jsonStr string, key []byte, encryptType string) (string, error) {
|
||||||
|
// 解析JSON
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||||
|
return "", fmt.Errorf("解析JSON失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉零值
|
||||||
|
cleaned := make(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
if !isZeroValue(v) {
|
||||||
|
cleaned[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按key排序
|
||||||
|
keys := make([]string, 0, len(cleaned))
|
||||||
|
for k := range cleaned {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
// 构建有序map
|
||||||
|
orderedMap := make(map[string]interface{})
|
||||||
|
for _, k := range keys {
|
||||||
|
orderedMap[k] = cleaned[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转JSON
|
||||||
|
plaintext, err := json.Marshal(orderedMap)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("JSON序列化失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
switch encryptType {
|
||||||
|
case "aes":
|
||||||
|
return AESECBEncrypt(plaintext, key)
|
||||||
|
case "sm4":
|
||||||
|
return SM4CBCEncrypt(plaintext, key)
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("不支持的加密类型: %s", encryptType)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue