first commit
This commit is contained in:
456
pkg/partner/WxPay/WxPayClient.go
Normal file
456
pkg/partner/WxPay/WxPayClient.go
Normal file
@ -0,0 +1,456 @@
|
||||
package WxPay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"net/http"
|
||||
"servicebase/pkg/common"
|
||||
"servicebase/pkg/htools"
|
||||
"servicebase/pkg/log"
|
||||
"servicebase/pkg/partner/wxpay_utility"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
|
||||
"github.com/anxpp/beego/logs"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
)
|
||||
|
||||
const ()
|
||||
|
||||
type WxPayClient struct {
|
||||
AppId string
|
||||
MchId string
|
||||
Secret string
|
||||
CertificateSerialNo string
|
||||
MchAPIv3Key string
|
||||
PrivateKeyPath string
|
||||
}
|
||||
|
||||
// 支付对象
|
||||
type WxPayModel struct {
|
||||
DeviceInfo string // 自定义参数,可以为终端设备号
|
||||
Body string // 商品简单描述
|
||||
Detail string // 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明”
|
||||
Attach string // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
|
||||
OutTradeNo string // 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。详见商户订单号
|
||||
TotalFee string // 订单总金额,单位为分,详见支付金额
|
||||
SpbillCreateIp string //APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP
|
||||
NotifyUrl string // 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
|
||||
Openid string //trade_type=JSAPI时(即公众号支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识
|
||||
TradeType string // 交易类型 JSAPI=公众号支付 NATIVE=扫码支付 APP=APP支付 MWEB=H5支付
|
||||
}
|
||||
|
||||
// 统一下单返回结果对象
|
||||
type WxCreatePayOrderResult struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
AppId string `xml:"appid"`
|
||||
MchId string `xml:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
ResultCode string `xml:"result_code"`
|
||||
Sign string `xml:"sign"`
|
||||
PrepayId string `xml:"prepay_id"`
|
||||
TradeType string `xml:"trade_type"`
|
||||
ErrCode string `xml:"err_code"`
|
||||
ErrCodeDes string `xml:"err_code_des"`
|
||||
MwebUrl string `xml:"mweb_url"`
|
||||
CodeUrl string `xml:"code_url"`
|
||||
}
|
||||
|
||||
type RefundModel struct {
|
||||
TransactionId string // wx支付单号
|
||||
OutRefundNo string // 退款单号,DD Game内部退款唯一单号
|
||||
Reason string // 退款原因 1、该退款原因参数的长度不得超过80个字节;2、当订单退款金额小于等于1元且为部分退款时,退款原因将不会在消息中体现。
|
||||
NotifyUrl string // 退款回调url
|
||||
RefundAmount int64 // 退款金额,单位是分
|
||||
PayAmount int64 // 原支付订单的金额
|
||||
}
|
||||
|
||||
func (r *RefundModel) CheckParam() error {
|
||||
if len(r.Reason) > 80 {
|
||||
return errors.New("该退款原因参数的长度不得超过80个字节")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 微信通知对象
|
||||
type WxNotifyDataModel struct {
|
||||
Appid string `xml:"appid" json:"appid"`
|
||||
BankType string `xml:"bank_type" json:"bank_type"`
|
||||
CashFee string `xml:"cash_fee" json:"cash_fee"`
|
||||
CouponCount string `xml:"coupon_count" json:"coupon_count"`
|
||||
CouponFee string `xml:"coupon_fee" json:"coupon_fee"`
|
||||
CouponFee0 string `xml:"coupon_fee_0" json:"coupon_fee_0"`
|
||||
CouponId0 string `xml:"coupon_id_0" json:"coupon_id_0"`
|
||||
CouponFee1 string `xml:"coupon_fee_1" json:"coupon_fee_1"`
|
||||
CouponId1 string `xml:"coupon_id_1" json:"coupon_id_1"`
|
||||
CouponFee2 string `xml:"coupon_fee_2" json:"coupon_fee_2"`
|
||||
CouponId2 string `xml:"coupon_id_2" json:"coupon_id_2"`
|
||||
FeeType string `xml:"fee_type" json:"fee_type"`
|
||||
IsSubscribe string `xml:"is_subscribe" json:"is_subscribe"`
|
||||
MchId string `xml:"mch_id" json:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str" json:"nonce_str"`
|
||||
Openid string `xml:"openid" json:"openid"`
|
||||
OutTradeNo string `xml:"out_trade_no" json:"out_trade_no"`
|
||||
ResultCode string `xml:"result_code" json:"result_code"`
|
||||
ReturnCode string `xml:"return_code" json:"return_code"`
|
||||
Sign string `xml:"sign" json:"sign"`
|
||||
SignType string `xml:"sign_type" json:"sign_type"`
|
||||
TimeEnd string `xml:"time_end" json:"time_end"`
|
||||
TotalFee string `xml:"total_fee" json:"total_fee"`
|
||||
TradeType string `xml:"trade_type" json:"trade_type"`
|
||||
TransactionId string `xml:"transaction_id" json:"transaction_id"`
|
||||
Attach string `xml:"attach" json:"attach"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Algorithm string `json:"algorithm"` // AEAD_AES_256_GCM
|
||||
OriginalType string `json:"original_type"`
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
AssociatedData string `json:"associated_data"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
type WxRefundNotifyDataModel struct {
|
||||
MchID string `json:"mchid"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
RefundID string `json:"refund_id"`
|
||||
OutRefundNo string `json:"out_refund_no"`
|
||||
RefundStatus string `json:"refund_status"`
|
||||
SuccessTime time.Time `json:"success_time"`
|
||||
UserReceivedAccount string `json:"user_received_account"`
|
||||
Amount refunddomestic.Amount `json:"amount"`
|
||||
}
|
||||
|
||||
type RefundAmount struct {
|
||||
Total int `json:"total"`
|
||||
Refund int `json:"refund"`
|
||||
PayerTotal int `json:"payer_total"`
|
||||
PayerRefund int `json:"payer_refund"`
|
||||
}
|
||||
|
||||
func NewWxpayClient(appid, mch_id, secret string) *WxPayClient {
|
||||
return &WxPayClient{
|
||||
AppId: appid,
|
||||
MchId: mch_id,
|
||||
Secret: secret,
|
||||
CertificateSerialNo: viper.GetString("tencent.wxPay.CertificateSerialNo"),
|
||||
MchAPIv3Key: viper.GetString("tencent.wxPay.MchAPIv3Key"),
|
||||
PrivateKeyPath: viper.GetString("tencent.wxPay.PrivateKeyPath"),
|
||||
}
|
||||
}
|
||||
|
||||
// 生成微信支付参数
|
||||
func (client *WxPayClient) generatePayRequest(payModel WxPayModel) map[string]string {
|
||||
|
||||
result := make(map[string]string)
|
||||
result["appid"] = client.AppId //appID
|
||||
result["mch_id"] = client.MchId // 商户号
|
||||
result["device_info"] = payModel.DeviceInfo
|
||||
result["nonce_str"] = htools.GetRandomString(8) // 随机字符串
|
||||
result["sign_type"] = "MD5" // 签名类型,默认为MD5,支持HMAC-SHA256和MD5。
|
||||
result["body"] = payModel.Body
|
||||
result["detail"] = payModel.Detail
|
||||
result["attach"] = payModel.Attach
|
||||
result["out_trade_no"] = payModel.OutTradeNo
|
||||
result["fee_type"] = "CNY" // 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型
|
||||
result["total_fee"] = payModel.TotalFee
|
||||
result["spbill_create_ip"] = payModel.SpbillCreateIp
|
||||
result["notify_url"] = payModel.NotifyUrl
|
||||
result["trade_type"] = payModel.TradeType // 取值如下:JSAPI,NATIVE,APP等,说明详见参数规定
|
||||
|
||||
// 公众号支付
|
||||
if payModel.TradeType == common.WEIXIN_PAY_TRADER_TYPE_GONGZHONGHAO {
|
||||
result["openid"] = payModel.Openid
|
||||
}
|
||||
|
||||
//H5支付
|
||||
if payModel.TradeType == common.WEIXIN_PAY_TRADER_TYPE_H5 {
|
||||
// 场景
|
||||
result["scene_info"] = `{"h5_info":{"type":"Wap","wap_url":"https://www.enen.tech","wap_name":"东东电竞"}}`
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
result["sign"] = client.generateSign(result) // 签名
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
// 生成微信支付签名
|
||||
func (client *WxPayClient) generateSign(parameterMap map[string]string) string {
|
||||
|
||||
// 生成待签名字符串
|
||||
sb := getWaitSignString(parameterMap)
|
||||
|
||||
// 拼上key
|
||||
sb.Append("&key=" + client.Secret)
|
||||
|
||||
result := htools.StringToMD5(sb.ToString())
|
||||
|
||||
result = strings.ToUpper(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// map转待签名字符串 ,按key值asc升序,空值不参与计算
|
||||
func getWaitSignString(m map[string]string) *htools.StringBuilder {
|
||||
keys := make([]string, 0)
|
||||
|
||||
for key, val := range m {
|
||||
if key == "sign" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
// 按key升序
|
||||
sort.Strings(keys)
|
||||
|
||||
// 拼接字符串
|
||||
sb := htools.NewStringBuilder()
|
||||
|
||||
keysCount := len(keys)
|
||||
i := 0
|
||||
for _, value := range keys {
|
||||
k := value
|
||||
v := m[k]
|
||||
sb.Append(k + "=" + v)
|
||||
if i < keysCount-1 {
|
||||
sb.Append("&")
|
||||
}
|
||||
i++
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
func (client *WxPayClient) CheckSign(result map[string]string) error {
|
||||
|
||||
inputSign, ok := result["sign"]
|
||||
|
||||
if !ok {
|
||||
return errors.New("待验证参数中缺少sign")
|
||||
}
|
||||
|
||||
calSign := client.generateSign(result)
|
||||
|
||||
if inputSign != calSign {
|
||||
return errors.New("签名验证失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *WxPayClient) CheckRefundNotifySign(request *http.Request) (*WxRefundNotifyDataModel, error) {
|
||||
|
||||
privateKey, err := utils.LoadPrivateKeyWithPath(client.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器
|
||||
err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, privateKey, client.CertificateSerialNo, client.MchId, client.MchAPIv3Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 2. 获取商户号对应的微信支付平台证书访问器
|
||||
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(client.MchId)
|
||||
// 3. 使用证书访问器初始化 `notify.Handler`
|
||||
handler, err := notify.NewRSANotifyHandler(client.MchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := new(map[string]interface{})
|
||||
notifyReq, err := handler.ParseNotifyRequest(context.Background(), request, &content)
|
||||
// 如果验签未通过,或者解密失败
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rst WxRefundNotifyDataModel
|
||||
log.InfoF("回调通知数据: %s", notifyReq.Resource.Plaintext)
|
||||
err = json.Unmarshal([]byte(notifyReq.Resource.Plaintext), &rst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.InfoF("notifyReq : %+v", notifyReq)
|
||||
return &rst, nil
|
||||
}
|
||||
|
||||
// 微信统一下单
|
||||
func (client *WxPayClient) CreatePayOrder(payModel WxPayModel) (resultMap map[string]string, resultErr error) {
|
||||
|
||||
url := "https://api.mch.weixin.qq.com/pay/unifiedorder"
|
||||
|
||||
payRequestMap := client.generatePayRequest(payModel)
|
||||
|
||||
payXml := htools.MapToXML(payRequestMap)
|
||||
result, err := htools.HttpPost(url, payXml)
|
||||
|
||||
if err != nil {
|
||||
logs.Error("统一下单失败:" + err.Error())
|
||||
resultErr = err
|
||||
return
|
||||
}
|
||||
|
||||
logs.Info("统一下单OK:%s, params: %+v", result, payModel)
|
||||
|
||||
var resultModel WxCreatePayOrderResult
|
||||
|
||||
err2 := xml.Unmarshal([]byte(result), &resultModel)
|
||||
|
||||
if err2 != nil {
|
||||
logs.Error("统一下单解析返回结果失败:" + err2.Error())
|
||||
resultErr = err2
|
||||
return
|
||||
}
|
||||
|
||||
if resultModel.ReturnCode != "SUCCESS" {
|
||||
logs.Error("统一下单返回失败:" + resultModel.ReturnCode + "-" + resultModel.ReturnMsg)
|
||||
resultErr = errors.New("统一下单返回失败:" + resultModel.ReturnCode + "-" + resultModel.ReturnMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resultModel.ResultCode != "SUCCESS" {
|
||||
logs.Error("统一下单交易失败:" + resultModel.ResultCode + "-" + resultModel.ErrCode + resultModel.ErrCodeDes)
|
||||
resultErr = errors.New("统一下单交易失败:" + resultModel.ReturnCode + "-" + resultModel.ReturnMsg)
|
||||
return
|
||||
}
|
||||
|
||||
switch payModel.TradeType {
|
||||
case common.WEIXIN_PAY_TRADER_TYPE_GONGZHONGHAO:
|
||||
// 生成公众号需要的支付对象
|
||||
resultMap = client.GenerateGongZhongHaoPayMap(resultModel)
|
||||
return
|
||||
case common.WEIXIN_PAY_TRADER_TYPE_APP:
|
||||
// 生成app需要的支付对象
|
||||
resultMap = client.GenerateAppPayMap(resultModel)
|
||||
|
||||
return
|
||||
case common.WEIXIN_PAY_TRADER_TYPE_H5:
|
||||
// 生成app需要的支付对象
|
||||
resultMap = client.GenerateWxH5PayMap(resultModel)
|
||||
return
|
||||
case common.WEIXIN_PAY_TRADER_TYPE_NATIVE:
|
||||
// 生成native
|
||||
resultMap = client.GenerateWxNativePayMap(resultModel)
|
||||
return
|
||||
default:
|
||||
resultErr = errors.New("只支持APP和公众号支付")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 生成app的支付对象
|
||||
func (client *WxPayClient) GenerateAppPayMap(createPayOrderResult WxCreatePayOrderResult) map[string]string {
|
||||
|
||||
result := make(map[string]string)
|
||||
result["appid"] = createPayOrderResult.AppId
|
||||
result["partnerid"] = createPayOrderResult.MchId
|
||||
result["prepayid"] = createPayOrderResult.PrepayId
|
||||
result["package"] = "Sign=WXPay"
|
||||
result["noncestr"] = htools.GetRandomString(6)
|
||||
result["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
// 生成签名
|
||||
sign := client.generateSign(result)
|
||||
result["sign"] = sign
|
||||
result["pack"] = "Sign=WXPay" // java因为package是关键字 增加这个字段替代
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成公众号的支付对象
|
||||
func (client *WxPayClient) GenerateGongZhongHaoPayMap(createPayOrderResult WxCreatePayOrderResult) map[string]string {
|
||||
|
||||
result := make(map[string]string)
|
||||
result["appId"] = createPayOrderResult.AppId
|
||||
result["package"] = "prepay_id=" + createPayOrderResult.PrepayId
|
||||
result["nonceStr"] = htools.GetRandomString(6)
|
||||
result["timeStamp"] = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
result["signType"] = "MD5"
|
||||
// 生成签名
|
||||
sign := client.generateSign(result)
|
||||
result["paySign"] = sign
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成H5的支付对象
|
||||
func (client *WxPayClient) GenerateWxH5PayMap(createPayOrderResult WxCreatePayOrderResult) map[string]string {
|
||||
|
||||
result := make(map[string]string)
|
||||
result["appId"] = createPayOrderResult.AppId
|
||||
result["wxH5PayUrl"] = createPayOrderResult.MwebUrl
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成Native的支付对象
|
||||
func (client *WxPayClient) GenerateWxNativePayMap(createPayOrderResult WxCreatePayOrderResult) map[string]string {
|
||||
|
||||
result := make(map[string]string)
|
||||
result["codeUrl"] = createPayOrderResult.CodeUrl
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GenerateWxRefund 生成Native的支付对象
|
||||
func (client *WxPayClient) GenerateWxRefund(model *RefundModel) (*refunddomestic.Refund, error) {
|
||||
err := model.CheckParam()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(client.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力
|
||||
opts := []core.ClientOption{
|
||||
option.WithWechatPayAutoAuthCipher(client.MchId, client.CertificateSerialNo, mchPrivateKey, client.MchAPIv3Key),
|
||||
}
|
||||
newClient, err := core.NewClient(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := refunddomestic.RefundsApiService{
|
||||
Client: newClient,
|
||||
}
|
||||
refund, _, err := svc.Create(context.Background(), refunddomestic.CreateRequest{
|
||||
TransactionId: wxpay_utility.String(model.TransactionId),
|
||||
OutRefundNo: wxpay_utility.String(model.OutRefundNo),
|
||||
Reason: wxpay_utility.String(model.Reason),
|
||||
NotifyUrl: wxpay_utility.String(model.NotifyUrl),
|
||||
FundsAccount: refunddomestic.REQFUNDSACCOUNT_AVAILABLE.Ptr(),
|
||||
Amount: &refunddomestic.AmountReq{
|
||||
Refund: wxpay_utility.Int64(model.RefundAmount),
|
||||
Total: wxpay_utility.Int64(model.PayAmount),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return refund, nil
|
||||
}
|
||||
Reference in New Issue
Block a user