添加文件: ymt_v3/client.go

This commit is contained in:
renzhiyuan 2026-07-23 17:07:11 +08:00
parent 39a8373a17
commit 769b9b4ddc
1 changed files with 249 additions and 0 deletions

249
ymt_v3/client.go Normal file
View File

@ -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)
}
}