458 lines
15 KiB
Go
458 lines
15 KiB
Go
package WxPay
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"errors"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gitea.ddegame.cn/open/servicebase/pkg/common"
|
||
"gitea.ddegame.cn/open/servicebase/pkg/htools"
|
||
"gitea.ddegame.cn/open/servicebase/pkg/log"
|
||
"gitea.ddegame.cn/open/servicebase/pkg/partner/wxpay_utility"
|
||
|
||
"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
|
||
}
|