wxpay to wxpayapi
This commit is contained in:
199
platformapi/wxpayapi/callback.go
Normal file
199
platformapi/wxpayapi/callback.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package wxpayapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.rosy.net.cn/baseapi/utils"
|
||||
"github.com/clbanning/mxj"
|
||||
"github.com/nanjishidu/gomini/gocrypto"
|
||||
)
|
||||
|
||||
type CData string
|
||||
|
||||
func (c CData) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return e.EncodeElement(struct {
|
||||
string `xml:",cdata"`
|
||||
}{string(c)}, start)
|
||||
}
|
||||
|
||||
const (
|
||||
MsgTypeUnkown = 0 // 未知
|
||||
MsgTypePay = 1 // 支付结果回调
|
||||
MsgTypeRefund = 2 // 退款结果回调
|
||||
)
|
||||
|
||||
type CallbackResponse struct {
|
||||
XMLName xml.Name `json:"-" xml:"xml"`
|
||||
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg CData `json:"return_msg" xml:"return_msg"`
|
||||
}
|
||||
|
||||
type BaseResultMsg struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
}
|
||||
|
||||
type PayResultMsg struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
ResultCode string `json:"result_code" xml:"result_code"`
|
||||
ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"`
|
||||
|
||||
BankType string `json:"bank_type"`
|
||||
CashFee string `json:"cash_fee"`
|
||||
FeeType string `json:"fee_type"`
|
||||
IsSubscribe string `json:"is_subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TimeEnd string `json:"time_end"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
TradeType string `json:"trade_type"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
}
|
||||
|
||||
type RefundReqInfo struct {
|
||||
OutRefundNo string `json:"out_refund_no"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
RefundAccount string `json:"refund_account"`
|
||||
RefundFee string `json:"refund_fee" xml:"refund_fee"`
|
||||
RefundID string `json:"refund_id"`
|
||||
RefundRecvAccout string `json:"refund_recv_accout"`
|
||||
RefundRequestSource string `json:"refund_request_source"`
|
||||
RefundStatus string `json:"refund_status"`
|
||||
SettlementRefundFee string `json:"settlement_refund_fee"`
|
||||
SettlementTotalFee string `json:"settlement_total_fee"`
|
||||
SuccessTime string `json:"success_time"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
}
|
||||
|
||||
type RefundResultMsg struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
ResultCode string `json:"result_code" xml:"result_code"`
|
||||
ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"`
|
||||
|
||||
ReqInfoStr string `json:"req_info,omitempty"`
|
||||
ReqInfoObj *RefundReqInfo `json:"req_info_obj,omitempty"`
|
||||
}
|
||||
|
||||
type CallbackMsg struct {
|
||||
MsgType int
|
||||
MapData map[string]interface{}
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
var (
|
||||
SuccessResponse = &CallbackResponse{
|
||||
ReturnCode: ResponseCodeSuccess,
|
||||
ReturnMsg: "OK",
|
||||
}
|
||||
)
|
||||
|
||||
func Err2CallbackResponse(err error, data string) *CallbackResponse {
|
||||
if err == nil {
|
||||
return SuccessResponse
|
||||
}
|
||||
returnMsg := err.Error()
|
||||
if data != "" {
|
||||
returnMsg = fmt.Sprintf("%s,%s", returnMsg, data)
|
||||
}
|
||||
return &CallbackResponse{
|
||||
ReturnCode: ResponseCodeFail,
|
||||
ReturnMsg: CData(returnMsg),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) decodeReqInfo(msg string) (decryptedMsg string, err error) {
|
||||
binMsg, err := base64.StdEncoding.DecodeString(msg)
|
||||
if err == nil {
|
||||
// aesKey := []byte(fmt.Sprintf("%x", md5.Sum([]byte(a.appKey))))
|
||||
// binResult, err2 := utils.AESCBCDecpryt(binMsg, aesKey, aesKey[:16])
|
||||
gocrypto.SetAesKey(strings.ToLower(gocrypto.Md5(a.appKey)))
|
||||
binResult, err2 := gocrypto.AesECBDecrypt(binMsg)
|
||||
if err = err2; err == nil {
|
||||
decryptedMsg = string(binResult)
|
||||
}
|
||||
}
|
||||
return decryptedMsg, err
|
||||
}
|
||||
|
||||
func (a *API) GetCallbackMsg(request *http.Request) (msg *CallbackMsg, callbackResponse *CallbackResponse) {
|
||||
data, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, Err2CallbackResponse(err, "")
|
||||
}
|
||||
return a.getCallbackMsg(string(data))
|
||||
}
|
||||
|
||||
func (a *API) getCallbackMsg(msgBody string) (msg *CallbackMsg, callbackResponse *CallbackResponse) {
|
||||
mapData, _, err := a.checkResultAsMap(msgBody)
|
||||
if err != nil {
|
||||
return nil, Err2CallbackResponse(err, "")
|
||||
}
|
||||
|
||||
msg = &CallbackMsg{
|
||||
MapData: mapData,
|
||||
}
|
||||
returnCode := utils.Interface2String(mapData["return_code"])
|
||||
if returnCode != ResponseCodeSuccess {
|
||||
msg.Data = &BaseResultMsg{
|
||||
ReturnCode: returnCode,
|
||||
ReturnMsg: utils.Interface2String(mapData["return_msg"]),
|
||||
}
|
||||
} else {
|
||||
reqInfo := utils.Interface2String(mapData["req_info"])
|
||||
if reqInfo == "" {
|
||||
sign := utils.Interface2String(mapData[sigKey])
|
||||
desiredSign := a.signParam(mapData)
|
||||
if desiredSign != sign {
|
||||
return nil, Err2CallbackResponse(fmt.Errorf("desiredSign:%s <> sign:%s", desiredSign, sign), "")
|
||||
}
|
||||
}
|
||||
if reqInfo != "" {
|
||||
msg.MsgType = MsgTypeRefund
|
||||
var refundResult *RefundResultMsg
|
||||
if err = utils.Map2StructByJson(mapData, &refundResult, false); err == nil {
|
||||
if reqInfo, err = a.decodeReqInfo(reqInfo); err == nil {
|
||||
mv, err2 := mxj.NewMapXml([]byte(reqInfo))
|
||||
if err = err2; err == nil {
|
||||
reqInfoMap := mv["root"].(map[string]interface{})
|
||||
if err = utils.Map2StructByJson(reqInfoMap, &refundResult.ReqInfoObj, false); err == nil {
|
||||
msg.Data = refundResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if transactionID := utils.Interface2String(mapData["transaction_id"]); transactionID != "" {
|
||||
msg.MsgType = MsgTypePay
|
||||
var payResult *PayResultMsg
|
||||
if err = utils.Map2StructByJson(mapData, &payResult, false); err == nil {
|
||||
msg.Data = payResult
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
callbackResponse = Err2CallbackResponse(err, "")
|
||||
}
|
||||
return msg, callbackResponse
|
||||
}
|
||||
35
platformapi/wxpayapi/callback_test.go
Normal file
35
platformapi/wxpayapi/callback_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package wxpayapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.rosy.net.cn/baseapi/utils"
|
||||
)
|
||||
|
||||
func TestPayCallback(t *testing.T) {
|
||||
bodyMsg := strings.Replace(`
|
||||
<xml><appid><![CDATA[wx4b5930c13f8b1170]]></appid>\n<bank_type><![CDATA[CMB_CREDIT]]></bank_type>\n<cash_fee><![CDATA[1]]></cash_fee>\n<fee_type><![CDATA[CNY]]></fee_type>\n<is_subscribe><![CDATA[N]]></is_subscribe>\n<mch_id><![CDATA[1390686702]]></mch_id>\n<nonce_str><![CDATA[8E0DDB300B7511EA908C186590E02977]]></nonce_str>\n<openid><![CDATA[ojWb10N52xdnLuInkn06bkn9pUhk]]></openid>\n<out_trade_no><![CDATA[8E0DD6260B7511EA908C186590E02977]]></out_trade_no>\n<result_code><![CDATA[SUCCESS]]></result_code>\n<return_code><![CDATA[SUCCESS]]></return_code>\n<sign><![CDATA[5281BDD91AF551FF7CDF1A0F6D702A30]]></sign>\n<time_end><![CDATA[20191120171206]]></time_end>\n<total_fee>1</total_fee>\n<trade_type><![CDATA[NATIVE]]></trade_type>\n<transaction_id><![CDATA[4200000455201911201954456843]]></transaction_id>\n</xml>
|
||||
|
||||
|
||||
|
||||
`, "\\n", "\n", -1)
|
||||
result, err := api.getCallbackMsg(bodyMsg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(result, false))
|
||||
}
|
||||
|
||||
func TestPayRefundCallback(t *testing.T) {
|
||||
bodyMsg := strings.Replace(`
|
||||
<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx4b5930c13f8b1170]]></appid><mch_id><![CDATA[1390686702]]></mch_id><nonce_str><![CDATA[092e154ce3baac44dd1c9091b50639ec]]></nonce_str><req_info><![CDATA[5bRx2mV2fwR09J0CxiLBfJkeIFYRgW6m8qKkz853XZEf5nxXLzevP7j2eF+Gno1v/800tU4ZRAW1RsJjNUckKdtHBxvaVMxD+oMzDRf1YqRSfiLS9s6km1aMAXwqlJbX3leMWw9QNmngeNBA2cSiZe02pY8Gbj+R8b2YEU2QStq+1+iZya14fvvc9wxTtXBO3KeZ9beJ0jf/mI7IEBXC1KG5QX42Yuyo8gXFQwF/64wr/2gsg+A8KVwEJF07lVEMPYhRdXz8C5Qr40nVM1Pd8ulHEoO+kCELW5GWjT2hs8Io4e6OLtZ5kG8Zp7E5u7iY5XmpTIsgtypn9AuW2voCBZ0VAeYVmcsR6qVBJFKVVkcDA0Kmb0II3cp9otOztGgfATuqDAfncYEoC8NqUdopwBVesC7fEbOEdfPfi6GvJdXTN4mAjDVtRudvttLn3+wTt5X7BqgeuZrAfcbT/gr3pbUFi2Cgv7ubHWtcOGFyhIw/qnA7burUMZ06WdT6Q0CZKeWPJTgT0UbB47T15Zbu9VFeMS0cZlOxn2L1cU/XgY/XDRmjB7gDayasrHQUaotj7F77KSX5Ijf+YbTmVyiGDwb1vocLlQMZax6GD9ECcytFty1jdW9GYJVz56Xnbmq9K7LUgzNZURlWZPY+aoPv8rG+3LaRAKDLBzQYKN2+lyNziXBP60xRicdMHmdTnYHPb8Bcj5c0Byy9ufw9+hMjAXzC9D9qMeDxvt1qHjGd3b8bA6cgpbQtDg6vI3arruiR+WVfn9/ZN26WrG/iLxST927wXmHRwJSitCBty0X/x+/lealMiyC3Kmf0C5ba+gIFxOe1Wx7OP0RQmkL5WPvttMf7ogU9RZamVfjgs6aPw3l4bxTmMHJkSvePUpdWpOU4ZXVH+aVgcX3KZWrn7ltf2Tvdbfi8IVFjzbvwn+4smC6u4cMQpMMpRLeuIqwwwg4J+rqOkyV3JdiTGInu2NErhFshQl868hoaSe4lJ2aaU/z9miDtBQkS0K3bQIWIUv4+kLyEgzQ6ld3qjEXEVme/DiLygEJLstIXvNMY43MdUYRF1BpOzpVB/ed28bQrshx++ROPrjthlX4bwksi5Qtpvg==]]></req_info></xml>
|
||||
|
||||
|
||||
`, "\\n", "\n", -1)
|
||||
result, err := api.getCallbackMsg(bodyMsg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(result, false))
|
||||
}
|
||||
376
platformapi/wxpayapi/wxpayapi.go
Normal file
376
platformapi/wxpayapi/wxpayapi.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package wxpayapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rosy.net.cn/baseapi"
|
||||
"git.rosy.net.cn/baseapi/platformapi"
|
||||
"git.rosy.net.cn/baseapi/utils"
|
||||
"github.com/clbanning/mxj"
|
||||
)
|
||||
|
||||
const (
|
||||
prodURL = "https://api.mch.weixin.qq.com"
|
||||
sandboxURL = "https://api.mch.weixin.qq.com/sandboxnew"
|
||||
)
|
||||
|
||||
const (
|
||||
ResponseCodeSuccess = "SUCCESS"
|
||||
ResponseCodeFail = "FAIL"
|
||||
|
||||
sigKey = "sign"
|
||||
sigTypeKey = "sign_type"
|
||||
sigType = "MD5"
|
||||
)
|
||||
|
||||
const (
|
||||
FeeTypeCNY = "CNY"
|
||||
)
|
||||
|
||||
const (
|
||||
TradeTypeJSAPI = "JSAPI" // JSAPI支付(或小程序支付)
|
||||
TradeTypeNative = "NATIVE"
|
||||
TradeTypeAPP = "APP"
|
||||
TradeTypeMicroPay = "MICROPAY"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
appID string
|
||||
appKey string
|
||||
mchID string
|
||||
client *http.Client
|
||||
config *platformapi.APIConfig
|
||||
}
|
||||
|
||||
type RequestBase struct {
|
||||
XMLName xml.Name `json:"-" xml:"xml"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
SignType string `json:"sign_type,omitempty" xml:"sign_type,omitempty"`
|
||||
}
|
||||
|
||||
type IRequestBase interface {
|
||||
SetAppID(appID string)
|
||||
SetMchID(mchID string)
|
||||
SetNonceStr(nonceStr string)
|
||||
SetSign(sign string)
|
||||
SetSignType(signType string)
|
||||
}
|
||||
|
||||
func (r *RequestBase) SetAppID(appID string) {
|
||||
r.AppID = appID
|
||||
}
|
||||
|
||||
func (r *RequestBase) SetMchID(mchID string) {
|
||||
r.MchID = mchID
|
||||
}
|
||||
|
||||
func (r *RequestBase) SetNonceStr(nonceStr string) {
|
||||
r.NonceStr = nonceStr
|
||||
}
|
||||
|
||||
func (r *RequestBase) SetSign(sign string) {
|
||||
r.Sign = sign
|
||||
}
|
||||
|
||||
func (r *RequestBase) SetSignType(signType string) {
|
||||
r.SignType = signType
|
||||
}
|
||||
|
||||
type OrderQueryParam struct {
|
||||
RequestBase
|
||||
TransactionID string `json:"transaction_id" xml:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"`
|
||||
}
|
||||
|
||||
type OrderInfo struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
ResultCode string `json:"result_code" xml:"result_code"`
|
||||
ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"`
|
||||
|
||||
Attach string `json:"attach"`
|
||||
BankType string `json:"bank_type"`
|
||||
CashFee string `json:"cash_fee"`
|
||||
CashFeeType string `json:"cash_fee_type"`
|
||||
FeeType string `json:"fee_type"`
|
||||
IsSubscribe string `json:"is_subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TimeEnd string `json:"time_end"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
TradeState string `json:"trade_state"`
|
||||
TradeStateDesc string `json:"trade_state_desc"`
|
||||
TradeType string `json:"trade_type"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
}
|
||||
|
||||
type CreateOrderSceneInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AreaCode string `json:"area_code"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type CreateOrderParam struct {
|
||||
RequestBase
|
||||
Body string `json:"body" xml:"body"`
|
||||
NotifyURL string `json:"notify_url" xml:"notify_url"`
|
||||
OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"`
|
||||
SpbillCreateIP string `json:"spbill_create_ip" xml:"spbill_create_ip"`
|
||||
TradeType string `json:"trade_type" xml:"trade_type"`
|
||||
TotalFee int `json:"total_fee" xml:"total_fee"`
|
||||
|
||||
Detail CData `json:"detail.omitempty" xml:"detail,omitempty"`
|
||||
Attach string `json:"attach,omitempty" xml:"attach,omitempty"`
|
||||
FeeType string `json:"fee_type,omitempty" xml:"fee_type,omitempty"`
|
||||
TimeStart string `json:"time_start,omitempty" xml:"time_start,omitempty"`
|
||||
TimeExpire string `json:"time_expire,omitempty" xml:"time_expire,omitempty"`
|
||||
GoodsTag string `json:"goods_tag,omitempty" xml:"goods_tag,omitempty"`
|
||||
ProductID string `json:"product_id,omitempty" xml:"product_id,omitempty"`
|
||||
LimitPay string `json:"limit_pay,omitempty" xml:"limit_pay,omitempty"`
|
||||
OpenID string `json:"openid,omitempty" xml:"openid,omitempty"`
|
||||
Receipt string `json:"receipt,omitempty" xml:"receipt,omitempty"`
|
||||
SceneInfo string `json:"scene_info,omitempty" xml:"scene_info,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOrderResult struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
ResultCode string `json:"result_code" xml:"result_code"`
|
||||
ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"`
|
||||
|
||||
TradeType string `json:"trade_type"`
|
||||
PrepayID string `json:"prepay_id"`
|
||||
CodeURL string `json:"code_url"`
|
||||
}
|
||||
|
||||
type PayRefundParam struct {
|
||||
RequestBase
|
||||
|
||||
TransactionID string `json:"transaction_id,omitempty" xml:"transaction_id,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no,omitempty" xml:"out_trade_no,omitempty"`
|
||||
OutRefundNo string `json:"out_refund_no" xml:"out_refund_no"`
|
||||
TotalFee int `json:"total_fee" xml:"total_fee"`
|
||||
RefundFee int `json:"refund_fee" xml:"refund_fee"`
|
||||
|
||||
RefundFeeType string `json:"refund_fee_type,omitempty" xml:"refund_fee_type,omitempty"`
|
||||
RefundDesc CData `json:"refund_desc,omitempty" xml:"refund_desc,omitempty"`
|
||||
NotifyURL string `json:"notify_url,omitempty" xml:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
type PayRefundResult struct {
|
||||
ReturnCode string `json:"return_code" xml:"return_code"`
|
||||
ReturnMsg string `json:"return_msg" xml:"return_msg"`
|
||||
|
||||
AppID string `json:"appid" xml:"appid"`
|
||||
DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"`
|
||||
MchID string `json:"mch_id" xml:"mch_id"`
|
||||
NonceStr string `json:"nonce_str" xml:"nonce_str"`
|
||||
Sign string `json:"sign" xml:"sign"`
|
||||
ResultCode string `json:"result_code" xml:"result_code"`
|
||||
ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"`
|
||||
|
||||
CashFee string `json:"cash_fee"`
|
||||
CashRefundFee string `json:"cash_refund_fee"`
|
||||
CouponRefundCount string `json:"coupon_refund_count"`
|
||||
CouponRefundFee string `json:"coupon_refund_fee"`
|
||||
OutRefundNo string `json:"out_refund_no"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
RefundChannel string `json:"refund_channel"`
|
||||
RefundFee string `json:"refund_fee"`
|
||||
RefundID string `json:"refund_id"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
}
|
||||
|
||||
func New(appID, appKey, mchID string, config ...*platformapi.APIConfig) *API {
|
||||
curConfig := platformapi.DefAPIConfig
|
||||
if len(config) > 0 {
|
||||
curConfig = *config[0]
|
||||
}
|
||||
return &API{
|
||||
appID: appID,
|
||||
appKey: appKey,
|
||||
mchID: mchID,
|
||||
client: &http.Client{Timeout: curConfig.ClientTimeout},
|
||||
config: &curConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func NewWithCertificate(appID, appKey, mchID string, certPEMBlock, keyPEMBlock interface{}, config ...*platformapi.APIConfig) (a *API) {
|
||||
var certs tls.Certificate
|
||||
var err error
|
||||
if binCertPEMBlock, ok := certPEMBlock.([]byte); ok {
|
||||
certs, err = tls.X509KeyPair(binCertPEMBlock, keyPEMBlock.([]byte))
|
||||
} else {
|
||||
certs, err = tls.LoadX509KeyPair(certPEMBlock.(string), keyPEMBlock.(string))
|
||||
}
|
||||
if err == nil {
|
||||
a = New(appID, appKey, mchID, config...)
|
||||
a.client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{certs},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
baseapi.SugarLogger.Warnf("NewWithCertificate failed with err:%v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *API) GetAppID() string {
|
||||
return a.appID
|
||||
}
|
||||
|
||||
func (a *API) GetMchID() string {
|
||||
return a.mchID
|
||||
}
|
||||
|
||||
func (a *API) signParam(params map[string]interface{}) (sig string) {
|
||||
var valueList []string
|
||||
for k, v := range params {
|
||||
if k != sigKey {
|
||||
if str := fmt.Sprint(v); str != "" {
|
||||
valueList = append(valueList, fmt.Sprintf("%s=%s", k, str))
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.StringSlice(valueList))
|
||||
valueList = append(valueList, fmt.Sprintf("key=%s", a.appKey))
|
||||
sig = strings.Join(valueList, "&")
|
||||
sig = fmt.Sprintf("%X", md5.Sum([]byte(sig)))
|
||||
return sig
|
||||
}
|
||||
|
||||
func mustMarshalXML(obj interface{}) []byte {
|
||||
byteArr, err := xml.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("err when Marshal obj:%v with error:%v", obj, err))
|
||||
}
|
||||
return byteArr
|
||||
}
|
||||
|
||||
func (a *API) AccessAPI(action string, requestParam IRequestBase) (retVal map[string]interface{}, err error) {
|
||||
requestParam.SetAppID(a.appID)
|
||||
requestParam.SetMchID(a.mchID)
|
||||
requestParam.SetNonceStr(utils.GetUUID())
|
||||
requestParam.SetSignType(sigType)
|
||||
signStr := a.signParam(utils.Struct2FlatMap(requestParam))
|
||||
requestParam.SetSign(signStr)
|
||||
|
||||
fullURL := utils.GenerateGetURL(prodURL, action, nil)
|
||||
|
||||
var responseStr string
|
||||
err = platformapi.AccessPlatformAPIWithRetry(a.client,
|
||||
func() *http.Request {
|
||||
request, _ := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(mustMarshalXML(requestParam)))
|
||||
return request
|
||||
},
|
||||
a.config,
|
||||
func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) {
|
||||
responseStr = bodyStr
|
||||
if jsonResult1 == nil {
|
||||
return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil")
|
||||
}
|
||||
retVal, errLevel, err = a.checkResultAsMap(jsonResult1[platformapi.KeyData].(string))
|
||||
return errLevel, err
|
||||
})
|
||||
if err == nil {
|
||||
if utils.Interface2String(retVal["result_code"]) != ResponseCodeSuccess {
|
||||
err = utils.NewErrorCode(utils.Interface2String(retVal["err_code_des"]), utils.Interface2String(retVal["err_code"]))
|
||||
if err != nil {
|
||||
baseapi.SugarLogger.Debugf("wxpay AccessAPI %s failed:%s, err:%v", signStr, strings.ReplaceAll(responseStr, "\\n", " "), err)
|
||||
}
|
||||
retVal = nil
|
||||
}
|
||||
}
|
||||
return retVal, err
|
||||
}
|
||||
|
||||
func (a *API) checkResultAsMap(xmlStr string) (result map[string]interface{}, errLevel string, err error) {
|
||||
mv, err := mxj.NewMapXml([]byte(xmlStr))
|
||||
if err != nil {
|
||||
errLevel = platformapi.ErrLevelGeneralFail
|
||||
} else {
|
||||
result = mv["xml"].(map[string]interface{})
|
||||
returnCode := utils.Interface2String(result["return_code"])
|
||||
if returnCode != ResponseCodeSuccess {
|
||||
errLevel = platformapi.ErrLevelGeneralFail
|
||||
err = utils.NewErrorCode(utils.Interface2String(result["return_msg"]), returnCode)
|
||||
result = nil
|
||||
}
|
||||
}
|
||||
return result, errLevel, err
|
||||
}
|
||||
|
||||
func SceneInfoList2String(sceneList []*CreateOrderSceneInfo) (str string) {
|
||||
return string(utils.MustMarshal(sceneList))
|
||||
}
|
||||
|
||||
func Time2PayTime(t time.Time) (str string) {
|
||||
return t.Format("20060102150405")
|
||||
}
|
||||
|
||||
func PayTime2Time(str string) (t time.Time) {
|
||||
t, _ = time.ParseInLocation("20060102150405", str, time.Local)
|
||||
return t
|
||||
}
|
||||
|
||||
func (a *API) OrderQuery(transactionID, outTradeNo string) (orderInfo *OrderInfo, err error) {
|
||||
param := &OrderQueryParam{
|
||||
TransactionID: transactionID,
|
||||
OutTradeNo: outTradeNo,
|
||||
}
|
||||
retVal, err := a.AccessAPI("pay/orderquery", param)
|
||||
if err == nil {
|
||||
err = utils.Map2StructByJson(retVal, &orderInfo, false)
|
||||
}
|
||||
return orderInfo, err
|
||||
}
|
||||
|
||||
func (a *API) CreateUnifiedOrder(param *CreateOrderParam) (createOrderResult *CreateOrderResult, err error) {
|
||||
retVal, err := a.AccessAPI("pay/unifiedorder", param)
|
||||
if err == nil {
|
||||
err = utils.Map2StructByJson(retVal, &createOrderResult, false)
|
||||
}
|
||||
return createOrderResult, err
|
||||
}
|
||||
|
||||
func (a *API) PayRefund(param *PayRefundParam) (refundResult *PayRefundResult, err error) {
|
||||
if a.client.Transport == nil {
|
||||
return nil, fmt.Errorf("没有配置证书,不能退款")
|
||||
}
|
||||
retVal, err := a.AccessAPI("secapi/pay/refund", param)
|
||||
if err == nil {
|
||||
err = utils.Map2StructByJson(retVal, &refundResult, false)
|
||||
}
|
||||
return refundResult, err
|
||||
}
|
||||
94
platformapi/wxpayapi/wxpayapi_test.go
Normal file
94
platformapi/wxpayapi/wxpayapi_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package wxpayapi
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.rosy.net.cn/baseapi"
|
||||
"git.rosy.net.cn/baseapi/utils"
|
||||
"github.com/clbanning/mxj"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
api *API
|
||||
sugarLogger *zap.SugaredLogger
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
sugarLogger = logger.Sugar()
|
||||
baseapi.Init(sugarLogger)
|
||||
certPEMBlock, _ := ioutil.ReadFile("1390686702_20190115_cert/apiclient_cert.pem")
|
||||
keyPEMBlock, _ := ioutil.ReadFile("1390686702_20190115_cert/apiclient_key.pem")
|
||||
api = NewWithCertificate("wx4b5930c13f8b1170", "XKJPOIHJ233adf01KJIXlIeQDSDKFJAD", "1390686702", certPEMBlock, keyPEMBlock)
|
||||
// api = New("wx4b5930c13f8b1170", "XKJPOIHJ233adf01KJIXlIeQDSDKFJAD", "1390686702")
|
||||
}
|
||||
|
||||
func TestOrderQuery(t *testing.T) {
|
||||
result, err := api.OrderQuery("4200000411201910186159598703", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(result, false))
|
||||
}
|
||||
|
||||
func TestCreateUnifiedOrder(t *testing.T) {
|
||||
orderNo := "367609100BA711EAAA20186590E02977" // utils.GetUUID()
|
||||
// t.Log(orderNo)
|
||||
result, err := api.CreateUnifiedOrder(&CreateOrderParam{
|
||||
Body: "这里一个测试商品",
|
||||
NotifyURL: "http://callback.test.jxc4.com/wxpay/msg/",
|
||||
OutTradeNo: orderNo,
|
||||
SpbillCreateIP: "114.114.114.114",
|
||||
TradeType: TradeTypeNative,
|
||||
TotalFee: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(result, false))
|
||||
}
|
||||
|
||||
func TestPayRefund(t *testing.T) {
|
||||
result, err := api.PayRefund(&PayRefundParam{
|
||||
TransactionID: "",
|
||||
OutTradeNo: "8E0DD6260B7511EA908C186590E02977",
|
||||
NotifyURL: "http://callback.test.jxc4.com/wxpay/msg/",
|
||||
OutRefundNo: utils.GetUUID(),
|
||||
TotalFee: 1,
|
||||
RefundFee: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(result, false))
|
||||
}
|
||||
|
||||
func TestXml2Json(t *testing.T) {
|
||||
xmlStr := strings.Replace(`
|
||||
<root>
|
||||
<out_refund_no><![CDATA[131811191610442717309]]></out_refund_no>
|
||||
<out_trade_no><![CDATA[71106718111915575302817]]></out_trade_no>
|
||||
<refund_account><![CDATA[REFUND_SOURCE_RECHARGE_FUNDS]]></refund_account>
|
||||
<refund_fee><![CDATA[3960]]></refund_fee>
|
||||
<refund_id><![CDATA[50000408942018111907145868882]]></refund_id>
|
||||
<refund_recv_accout><![CDATA[支付用户零钱]]></refund_recv_accout>
|
||||
<refund_request_source><![CDATA[API]]></refund_request_source>
|
||||
<refund_status><![CDATA[SUCCESS]]></refund_status>
|
||||
<settlement_refund_fee><![CDATA[3960]]></settlement_refund_fee>
|
||||
<settlement_total_fee><![CDATA[3960]]></settlement_total_fee>
|
||||
<success_time><![CDATA[2018-11-19 16:24:13]]></success_time>
|
||||
<total_fee><![CDATA[3960]]></total_fee>
|
||||
<transaction_id><![CDATA[4200000215201811190261405420]]></transaction_id>
|
||||
</root>
|
||||
|
||||
`, "\\n", "\n", -1)
|
||||
mv, err := mxj.NewMapXml([]byte(xmlStr))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(utils.Format4Output(mv, false))
|
||||
}
|
||||
Reference in New Issue
Block a user