微信小程序支付,golang 服务端实现示例分析

golang 示例代码:https://blog.csdn.net/mostone/article/details/92785658

具体代码见以上链接,下面主要分析小程序支付实现流程及数据处理。

小程序支付,涉及三个对象:

a:微信小程序

b:商户系统

c:微信后台

流程:

1、商户系统调用《统一下单api》,从微信后台得到预付编号(prepay_id)

2、微信小程序从商户系统取得prepay_id等参数,调用 wx.requestPayment(),转到用户付款操作

3、微信后台向商户系统发送支付结果通知

具体:

这里,有两处要与微信支付后台交互,一个是《统一下单》,另一个是《支付结果通知》,前者是主动请求,后者是微信支付后台在用户完成支付后,请求统一下单时提交的 notify_url 值,主动以 HTTP POST 方式发送数据到商户系统的 uri。

与微信支付后台的 request, response,数据格式都为 xml,字符集为 utf-8,因此发送时,需设置http request header 以下两个值:

    Header.Set("Accept", "application/xml")

    Header.Set("Content-Type", "application/xml;charset=utf-8")

关于数据内容,为了保证通讯的安全性,必须要对数据进行验证。

微信支付采用的方法是,将所有交互数据合并为一个以 key1=value1&key2=value2...的字符串(要按key的字面符号进行升序排列),再加上一个只保存在商户系统和微信支付后台的 api_key,最后也就是:key1=value1&key2=value2...&key=api_key。

然后对这个字符串,进行指定的 hash 运算,生成此文本的特征码。hash 算法有 MD5,HMAC-SHA256,默认是 MD5,如果使用SHA256,要确认相关 API 是否支持(似乎有个别 api 尚不支持 SHA256 hash 算法)

这个 hash 字串,称为 signature,在 xml 数据中,键名是 "sign",hash 算法的键名是 "sign_type"。与数字证书类比的话,这个 signature 可以打比方说是 “公钥”,而在商户支付系统里申请的 api_key 可以比方成 “私钥”(这个比喻并不准确)。这个 signature 是在网上进行 http post 发送来发送去的,每次都会根据不同的发送参数,重新进行计算生成,虽然是用了 SSL 对 http 通讯进行了再次加密,但也不能保证完全安全,因此,有这个 api_key 私钥的存在,进一步加强了安全性。由此可见,这个 api_key 很重要,绝对不能泄露,绝对不能在任何 http 通讯中被传输使用,不然就有可能被不良的第三方伪造支付结果通知,结果收到支付通知,而实际上却没有发生支付动作,从而被骗取交易。

需要注意,在生成 signature 时,不能遗漏任何一个非空值的字段,不然最终 hash 的源文本不同,生成的结果自然不一致。比方说,你发送了10个字段给服务器,但只按9个字段来生成 sign,但微信支付后台收到你的 10个字段以及你生成的 sign 后,会用这10个字段来生成 sign,再进行验证,这时结果当然是不一致的。反之,微信支付后台发送给你的也一样方式,由商户系统验证。这里,除了 sign 这个字段,其它所有参与 http post 的字段,都要加入到 hash 计算中来,包括 sign_type(如果采用默认 MD5时,不发送或留空除外)。

 

关于 golang xml 的解析与生成,有现成的 xml.Marshal 和 xml.Unmarshal 可用。默认时,xml 根节点的 node 名称是 struct 的 type 名称,而微信支付后台要求的根节点名称为 xml,因此要在 struct 中添加 xml.Name, 如:

type UnifyOrderRequest struct {
	XMLName         xml.Name `xml:"xml"`
	AppID           string   `xml:"appid"`
...
}

另外,在收到支付结果通知时,如果消费者使用了 代金券,会产生N条自增长的字段,如 coupon_id_0, coupon_id_1。这时,golang 的 xml.Unmarshal 似乎不能应对这种情况,只好以手工补丁方式,追加这部分内容的识别。当前的方式是使用 regexp 来提取 "<coupon_id_...><![CDATA[xxxxx]]></coupon_id_...>",再用字符串替换方式提取 xxxxx 这部分的最终结果值。代码:

// PaymentNoticeCallbackRequest data struct of transaction result which from the Wechat payment system after payment completed
type PaymentNoticeCallbackRequest struct {
	AppID                string `xml:"appid"`
	Attach               string `xml:"attach"`
	BankType             string `xml:"bank_type"`
	CashFee              int    `xml:"cash_fee"`
	CashFeeType          string `xml:"cash_fee_type"`
	CouponCount          int    `xml:"coupon_count"`
	CouponFee            string `xml:"coupon_fee"`

	CouponFees           string `xml:"coupon_fee_0"`
	CouponIDs            string `xml:"coupon_id_0"`
	CouponTypes          string `xml:"coupon_type_0"`
...
}

// get coupon message
if req.CouponCount > 0 {
	ss := string(s)
	req.CouponIDs = getCouponValues(ss, "coupon_id", req.CouponCount)
	req.CouponTypes = getCouponValues(ss, "coupon_type", req.CouponCount)
	req.CouponFees = getCouponValues(ss, "coupon_fee", req.CouponCount)
}

// getCouponValues find coupon values then return as "v0"
func getCouponValues(s, key string, l int) string {
	re := regexp.MustCompile(fmt.Sprintf(`<%s_(\d+)>(.*)</%s_\d+>`, key, key))
	as := re.FindAllStringSubmatch(s, -1)
	if len(as) != l {
		panic("Length not match")
	}

	val := make([]string, l, l)
	for _, a := range as {
		i, _ := strconv.Atoi(a[1])
		// use string replace because maybe one day
		// the wechat payment system change to number type without tag "<![DATA[]]>"
		val[i] = strings.Replace(strings.Replace(a[2], "<![CDATA[", "", -1), "]]>", "", -1)
	}

	var res string
	for i, v := range val {
		if i == 0 {
			res += v
		} else {
			res += fmt.Sprintf("&%s_%d=%s", key, i, v)
		}
	}

	return res
}

这样一来,就可以在生成 signature 时,包含了微信支付后台 http post 过来的全部字段。

最后提一下 CalculateSignature() 函数,我在这里用的是 interface{} 参数,相比其它人用 map[string]string,key 的名称全部在 struct 中,通过 xml tag 来定义,以避免调用时,由于手误给 map[string]string 指定错误的 key。如此一来,就需要告诉 CalculateSignature() 哪些 field 是不要参与 hash 计算的,在最后的参数 excludeKeys 数组进行指定。

如果要单独在其它场合迁移使用 CalculateSignature(),注意要多加一个参数 apiKey,示例中使用了全局的 apiKey。

wxpay 是一个使用Go语言编写的微信支付商户平台SDK。举个栗子以查询企业付款API为栗:package main import (     "log"     "github.com/go-with/wxpay" ) const (     appId  = "" // 微信公众平台应用ID     mchId  = "" // 微信支付商户平台商户号     apiKey = "" // 微信支付商户平台API密钥     // 微信支付商户平台证书路径     certFile   = "cert/apiclient_cert.pem"     keyFile    = "cert/apiclient_key.pem"     rootcaFile = "cert/rootca.pem" ) func main() {     c := wxpay.NewClient(appId, mchId, apiKey)     // 附着商户证书     err := c.WithCert(certFile, keyFile, rootcaFile)     if err != nil {         log.Fatal(err)     }     params := make(wxpay.Params)     // 查询企业付款接口请求参数     params.SetString("appid", c.AppId)     params.SetString("mch_id", c.MchId)     params.SetString("nonce_str", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS")  // 随机字符串     params.SetString("partner_trade_no", "10000098201411111234567890") // 商户订单号     params.SetString("sign", c.Sign(params))                           // 签名     // 查询企业付款接口请求URL     url := "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo"     // 发送查询企业付款请求     ret, err := c.Post(url, params, true)     if err != nil {         log.Fatal(err)     }     log.Print(ret) } 标签:wxpay
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值