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]any) 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 }