Files
servicebase/pkg/partner/WxPay/WxPayClient.go
2025-11-19 14:24:13 +08:00

458 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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和网页支付提交用户端ipNative支付填调用微信支付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 // 取值如下JSAPINATIVEAPP等说明详见参数规定
// 公众号支付
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
}