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 stringfor 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。