You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

168 lines
4.2 KiB
Go

1 month ago
package wechat
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"net/http"
"reflect"
"sort"
"strings"
"time"
)
// APIClient 微信支付API客户端
type APIClient struct {
config Config
client *http.Client
}
// NewAPIClient 创建微信支付API客户端
func NewAPIClient(config Config) *APIClient {
return &APIClient{
config: config,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// UnifiedOrderRequest 统一下单请求参数
type UnifiedOrderRequest struct {
AppID string `xml:"appid"` // 公众号ID
MchID string `xml:"mch_id"` // 商户号
NonceStr string `xml:"nonce_str"` // 随机字符串
Body string `xml:"body"` // 商品描述
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
TotalFee string `xml:"total_fee"` // 订单金额
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
NotifyURL string `xml:"notify_url"` // 通知地址
TradeType string `xml:"trade_type"` // 交易类型
Sign string `xml:"sign"` // 签名
}
// UnifiedOrderResponse 统一下单响应参数
type UnifiedOrderResponse struct {
ReturnCode string `xml:"return_code"` // 返回状态码
ReturnMsg string `xml:"return_msg"` // 返回信息
ResultCode string `xml:"result_code"` // 业务结果
ErrCode string `xml:"err_code"` // 错误代码
ErrCodeDes string `xml:"err_code_des"` // 错误代码描述
PrepayID string `xml:"prepay_id"` // 预支付交易会话标识
TradeType string `xml:"trade_type"` // 交易类型
CodeURL string `xml:"code_url"` // 二维码链接
Sign string `xml:"sign"` // 签名
}
// UnifiedOrder 统一下单
func (c *APIClient) UnifiedOrder(req *UnifiedOrderRequest) (*UnifiedOrderResponse, error) {
// 设置公共参数
req.AppID = c.config.AppID
req.MchID = c.config.MchID
req.NonceStr = generateNonceStr()
req.SpbillCreateIP = "127.0.0.1" // 客户端IP
req.NotifyURL = c.config.NotifyURL
req.TradeType = "NATIVE" // 生成二维码支付链接
// 生成签名
req.Sign = c.generateSign(req)
// 构建XML请求
xmlData := buildXML(req)
// 发送请求
resp, err := c.client.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", strings.NewReader(xmlData))
if err != nil {
return nil, fmt.Errorf("请求微信支付接口失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var result UnifiedOrderResponse
if err := xml.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
// 验证签名
if !c.verifySign(&result) {
return nil, fmt.Errorf("签名验证失败")
}
return &result, nil
}
// generateSign 生成签名
func (c *APIClient) generateSign(v interface{}) string {
// 将结构体转换为map
params := make(map[string]string)
val := reflect.ValueOf(v).Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("xml")
if tag == "" || tag == "sign" {
continue
}
params[tag] = fmt.Sprintf("%v", val.Field(i).Interface())
}
// 按键排序
var keys []string
for k := range params {
if k != "sign" && params[k] != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
// 拼接字符串
var builder strings.Builder
for i, k := range keys {
if i > 0 {
builder.WriteString("&")
}
builder.WriteString(k)
builder.WriteString("=")
builder.WriteString(params[k])
}
builder.WriteString("&key=")
builder.WriteString(c.config.APIKey)
// MD5加密
h := md5.New()
h.Write([]byte(builder.String()))
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
}
// verifySign 验证签名
func (c *APIClient) verifySign(v interface{}) bool {
// TODO: 实现签名验证逻辑
return true
}
// generateNonceStr 生成随机字符串
func generateNonceStr() string {
const length = 32
bytes := make([]byte, length)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// buildXML 构建XML请求
func buildXML(v interface{}) string {
data, err := xml.Marshal(v)
if err != nil {
return ""
}
return string(data)
}