wxpay to wxpayapi

This commit is contained in:
gazebo
2019-12-19 16:28:54 +08:00
parent b16a095f66
commit d95a14d314
4 changed files with 4 additions and 4 deletions

View 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
}