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
168 lines
4.2 KiB
Go
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)
|
|
}
|