package wxpayapi import ( "bytes" "crypto/hmac" "crypto/md5" "crypto/sha256" "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" sigTypeMd5 = "MD5" sigTypeSha256 = "HMAC-SHA256" ) const ( FeeTypeCNY = "CNY" ) const ( TradeTypeJSAPI = "JSAPI" // JSAPI支付(或小程序支付) TradeTypeNative = "NATIVE" TradeTypeAPP = "APP" TradeTypeMicroPay = "MICROPAY" OptYes = "Y" OptNo = "N" ) 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 RequestBase2 struct { XMLName xml.Name `json:"-" xml:"xml"` AppID string `json:"mch_appid" xml:"mch_appid"` DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"` MchID string `json:"mchid" xml:"mchid"` 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"` ProfitSharing string `json:"profit_sharing,omitempty" xml:"profit_sharing,omitempty"` } type CloseOrderParam struct { RequestBase OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"` } 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"` ResultMsg string `json:"result_msg" xml:"result_msg"` 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"` ResultMsg string `json:"result_msg" xml:"result_msg"` 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"` } type MultiProfitSharing struct { RequestBase TransactionID string `json:"transaction_id" xml:"transaction_id"` OutOrderNo string `json:"out_order_no" xml:"out_order_no"` Receivers CData `json:"receivers" xml:"receivers"` } type MultiProfitSharingResult struct { ReturnCode string `json:"return_code" xml:"return_code"` ReturnMsg string `json:"return_msg" xml:"return_msg"` 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"` AppID string `json:"appid" xml:"appid"` MchID string `json:"mch_id" xml:"mch_id"` NonceStr string `json:"nonce_str" xml:"nonce_str"` Sign string `json:"sign" xml:"sign"` TransactionID string `json:"transaction_id" xml:"transaction_id"` OutOrderNo string `json:"out_order_no" xml:"out_order_no"` OrderID string `json:"order_id" xml:"order_id"` } type Transfers struct { RequestBase2 PartnerTradeNo string `json:"partner_trade_no" xml:"partner_trade_no"` OpenID string `json:"openid" xml:"openid"` CheckName string `json:"check_name" xml:"check_name"` ReUserName string `json:"re_user_name,omitempty" xml:"re_user_name,omitempty"` Amount int `json:"amount" xml:"amount"` Desc string `json:"desc" xml:"desc"` SpbillCreateIP string `json:"spbill_create_ip" xml:"spbill_create_ip"` } type TransfersResult struct { ReturnCode string `json:"return_code" xml:"return_code"` ReturnMsg string `json:"return_msg" xml:"return_msg"` 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"` AppID string `json:"appid" xml:"appid"` MchID string `json:"mch_id" xml:"mch_id"` NonceStr string `json:"nonce_str" xml:"nonce_str"` DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"` PartnerTradeNo string `json:"partner_trade_no" xml:"partner_trade_no"` PaymentNo string `json:"payment_no" xml:"payment_no"` PaymentTime string `json:"payment_time" xml:"payment_time"` } 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(signType string, 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, "&") var binSig []byte if signType == sigTypeMd5 { binSig2 := md5.Sum([]byte(sig)) binSig = binSig2[:] } else if signType == sigTypeSha256 { mac := hmac.New(sha256.New, []byte(a.appKey)) mac.Write([]byte(sig)) binSig = mac.Sum(nil) } sig = fmt.Sprintf("%X", binSig) // baseapi.SugarLogger.Debug(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()) sigType := sigTypeSha256 // sigType := sigTypeMd5 requestParam.SetSignType(sigType) signStr := a.signParam(sigType, 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) parseXmlStrAsMap(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{}) } return result, errLevel, err } func (a *API) checkResultAsMap(xmlStr string) (result map[string]interface{}, errLevel string, err error) { result, errLevel, err = a.parseXmlStrAsMap(xmlStr) if err == nil { 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) mapData2Err(mapData map[string]interface{}) (err error) { if resultCode := utils.Interface2String(mapData["result_code"]); resultCode != ResponseCodeSuccess { err = utils.NewErrorCode(utils.Interface2String(mapData["err_code_des"]), utils.Interface2String(mapData["err_code"])) } return err } func (a *API) translateResult(mapData map[string]interface{}, dataPtr interface{}) (err error) { if err = a.mapData2Err(mapData); err == nil && dataPtr != nil { err = utils.Map2StructByJson(mapData, dataPtr, false) } return err } 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 = a.translateResult(retVal, &orderInfo) } return orderInfo, err } func (a *API) CreateUnifiedOrder(param *CreateOrderParam) (createOrderResult *CreateOrderResult, err error) { retVal, err := a.AccessAPI("pay/unifiedorder", param) if err == nil { err = a.translateResult(retVal, &createOrderResult) } return createOrderResult, err } // 关闭订单 // https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3 // 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。 // 此函数好像可以重复操作,且关闭一个不存在的订单也不会报错的样 func (a *API) CloseOrder(outTradeNo string) (err error) { retVal, err := a.AccessAPI("pay/closeorder", &CloseOrderParam{ OutTradeNo: outTradeNo, }) if err == nil { err = a.translateResult(retVal, nil) } return 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 = a.translateResult(retVal, &refundResult) } return refundResult, err } //请求多次分账 //https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_6&index=2 func (a *API) MultiProfitSharing(param *MultiProfitSharing) (result *MultiProfitSharingResult, err error) { retVal, err := a.AccessAPI("secapi/pay/multiprofitsharing", param) if err == nil { err = a.translateResult(retVal, &result) } return result, err } //企业付款 //https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 func (a *API) Transfers(param *Transfers) (result *TransfersResult, err error) { retVal, err := a.AccessAPI("mmpaymkttransfers/promotion/transfers", param) if err == nil { err = a.translateResult(retVal, &result) } return result, err }