first commit

This commit is contained in:
Yangtao
2025-11-18 17:48:20 +08:00
commit 6e56cab848
196 changed files with 65809 additions and 0 deletions

View 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和网页支付提交用户端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]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
}