package wxpaycli import ( "context" "crypto" cryptorand "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/sha512" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "net/http" "os" "service/library/idgenerator" "time" "github.com/go-pay/gopay" wxpayv2 "github.com/go-pay/gopay/wechat" wxpayv3 "github.com/go-pay/gopay/wechat/v3" "service/bizcommon/util" "service/library/configcenter" "service/library/logger" ) const ( DefaultOrderTimeoutSeconds = 900 // 默认订单超时时间,单位: s ) const ( AppIdXinYiDaoLe = "1665016206" AppIdTieFenZone = "1675813721" AppIdTieFanZone = "1679775327" ) var allWxpayClients = map[string]*WxpayClient{} func GetDefaultWxpayClient() *WxpayClient { return allWxpayClients[AppIdTieFanZone] } type WxpayClient struct { clientV3 *wxpayv3.ClientV3 MchId string `json:"mch_id"` AppSecret string `json:"app_secret"` AppId string `json:"app_id"` NotifyUrl string `json:"notify_url"` PrivateKeyPath string `json:"private_key_path"` } func InitMulti(cfgList ...*configcenter.WxpayClientConfig) (err error) { for _, cfg := range cfgList { var cli *WxpayClient cli, err = NewWxpayClient(cfg) if err != nil { return } if cli == nil { err = errors.New("NewAlipayClient fail") return } allWxpayClients[cli.MchId] = cli } return } func NewWxpayClient(cfg *configcenter.WxpayClientConfig) (ret *WxpayClient, err error) { // private key bs, err := os.ReadFile(cfg.PrivateKeyPath) if err != nil { logger.Error("real PrivateKeyPath fail, cfg: %v, err: %v", util.ToJson(cfg), err) return } privateKey := string(bs) wxpayCliV3, err := wxpayv3.NewClientV3(cfg.MchId, cfg.SerialNo, cfg.ApiV3Key, privateKey) if err != nil { logger.Error("NewClientV3 fail, cfg: %v, err: %v", util.ToJson(cfg), err) return } ret = &WxpayClient{ clientV3: wxpayCliV3, MchId: cfg.MchId, AppSecret: cfg.AppSecret, AppId: cfg.AppId, NotifyUrl: cfg.NotifyUrl, PrivateKeyPath: cfg.PrivateKeyPath, } return } // 验签 func (c *WxpayClient) ParseNotify(req *http.Request) (notify *wxpayv3.V3DecryptResult, err error) { notifyReq, err := wxpayv3.V3ParseNotify(req) if err != nil { logger.Error("V3ParseNotify fail, notifyReq: %v, err: %v", util.ToJson(notifyReq), err) return } if notifyReq == nil { logger.Error("V3ParseNotify nil, err: %v", err) return } notifyTmp, err := notifyReq.DecryptCipherText(string(c.clientV3.ApiV3Key)) if err != nil { logger.Error("DecryptCipherText fail, notifyTmp: %v, err: %v", util.ToJson(notifyTmp), err) return } if notifyTmp == nil { logger.Error("DecryptCipherText nil, err: %v", err) return } logger.Info("Wxpay ParseNotify, %v", util.ToJson(notifyTmp)) notify = notifyTmp return } // 微信支付 native支付 type NativePayParam struct { Description string OutTradeNo string // 商家订单id,我们自己的订单id TotalAmount int64 // 金额,单位:分 TimeOutSeconds int // 订单有效时间,单位:秒 } func (c *WxpayClient) NativePay(ctx context.Context, param *NativePayParam) (wxpayNativeParamStr string, err error) { if param.TimeOutSeconds <= 0 { param.TimeOutSeconds = DefaultOrderTimeoutSeconds } bm := gopay.BodyMap{ "appid": c.AppId, "description": param.Description, "out_trade_no": param.OutTradeNo, "time_expire": time.Now().Add(time.Second * time.Duration(param.TimeOutSeconds)).Format(time.RFC3339), "notify_url": c.NotifyUrl, "amount": gopay.BodyMap{ "total": param.TotalAmount, "currency": "CNY", }, } resp, err := c.clientV3.V3TransactionNative(ctx, bm) if err != nil { return } if resp.Code != wxpayv3.Success { logger.Info("wxpayv3 NativePay fail, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } wxpayNativeParamStr = resp.Response.CodeUrl logger.Info("wxpayv3 NativePay success, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } // 通过authcode获取openid func (c *WxpayClient) GetOpenIdByAuthCode(ctx context.Context, authCode string) (openid string, err error) { at, err := wxpayv2.GetOauth2AccessToken(ctx, c.AppId, c.AppSecret, authCode) if err != nil { return } if len(at.Openid) <= 0 { err = errors.New(fmt.Sprintf("fail, %s", util.ToJson(at))) return } openid = at.Openid return } // 微信支付 jsapi支付 type JsapiPayParam struct { Description string OutTradeNo string // 商家订单id,我们自己的订单id TotalAmount int64 // 金额,单位:分 TimeOutSeconds int // 订单有效时间,单位:秒 OpenId string } type JsapiPayResp struct { PrepayId string `json:"-"` AppId string `json:"appId"` TimeStamp string `json:"timeStamp"` NonceStr string `json:"nonceStr"` Package string `json:"package"` SignType string `json:"signType"` PaySign string `json:"paySign"` } func (c *WxpayClient) JsapiPay(ctx context.Context, param *JsapiPayParam) (wxpayJsapiResp JsapiPayResp, err error) { if param.TimeOutSeconds <= 0 { param.TimeOutSeconds = DefaultOrderTimeoutSeconds } bm := gopay.BodyMap{ "appid": c.AppId, "description": param.Description, "out_trade_no": param.OutTradeNo, "time_expire": time.Now().Add(time.Second * time.Duration(param.TimeOutSeconds)).Format(time.RFC3339), "notify_url": c.NotifyUrl, "amount": gopay.BodyMap{ "total": param.TotalAmount, "currency": "CNY", }, "payer": gopay.BodyMap{ "openid": param.OpenId, }, } resp, err := c.clientV3.V3TransactionJsapi(ctx, bm) if err != nil { return } if resp.Code != wxpayv3.Success { logger.Info("wxpayv3 NativePay fail, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } r := JsapiPayResp{ PrepayId: resp.Response.PrepayId, AppId: c.AppId, TimeStamp: fmt.Sprintf("%d", time.Now().Unix()), NonceStr: util.RandomString(32), Package: "prepay_id=" + resp.Response.PrepayId, SignType: "RSA", PaySign: resp.SignInfo.SignBody, } var ( cipherText = fmt.Sprintf("%s\n%s\n%s\n%s\n", r.AppId, r.TimeStamp, r.NonceStr, r.Package) keyBytes, _ = os.ReadFile(c.PrivateKeyPath) ) paySignBytes, err := rsaSign(keyBytes, crypto.SHA256, []byte(cipherText)) if err != nil { return } r.PaySign = base64.StdEncoding.EncodeToString(paySignBytes) wxpayJsapiResp = r logger.Info("wxpayv3 JsapiPay success, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(wxpayJsapiResp)) return } func rsaSign(prvkey []byte, hash crypto.Hash, data []byte) ([]byte, error) { block, _ := pem.Decode(prvkey) if block == nil { return nil, errors.New("decode private key error") } privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } // MD5 and SHA1 are not supported as they are not secure. var hashed []byte switch hash { case crypto.SHA224: h := sha256.Sum224(data) hashed = h[:] case crypto.SHA256: h := sha256.Sum256(data) hashed = h[:] case crypto.SHA384: h := sha512.Sum384(data) hashed = h[:] case crypto.SHA512: h := sha512.Sum512(data) hashed = h[:] } return rsa.SignPKCS1v15(cryptorand.Reader, privateKey.(*rsa.PrivateKey), hash, hashed) } // 微信支付 h5支付 type H5PayParam struct { Description string OutTradeNo string // 商家订单id,我们自己的订单id TotalAmount int64 // 金额,单位:分 TimeOutSeconds int // 订单有效时间,单位:秒 Cip string // 客户端ip } func (c *WxpayClient) H5Pay(ctx context.Context, param *H5PayParam) (wxpayH5ParamStr string, err error) { if param.TimeOutSeconds <= 0 { param.TimeOutSeconds = DefaultOrderTimeoutSeconds } bm := gopay.BodyMap{ "appid": c.AppId, "description": param.Description, "out_trade_no": param.OutTradeNo, "time_expire": time.Now().Add(time.Second * time.Duration(param.TimeOutSeconds)).Format(time.RFC3339), "notify_url": c.NotifyUrl, "amount": gopay.BodyMap{ "total": param.TotalAmount, "currency": "CNY", }, "scene_info": gopay.BodyMap{ "payer_client_ip": param.Cip, "h5_info": gopay.BodyMap{ "type": "Wap", }, }, } resp, err := c.clientV3.V3TransactionH5(ctx, bm) if err != nil { return } if resp.Code != wxpayv3.Success { logger.Info("wxpayv3 H5Pay fail, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } wxpayH5ParamStr = resp.Response.H5Url logger.Info("wxpayv3 H5 success, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } // 退款 type RefundOneParam struct { OutTradeNo string // 商家订单id,我们自己的订单id RefundAmount int64 // 退款金额,单位:分 RefundReason string // 退款理由 } func (c *WxpayClient) RefundOne(ctx context.Context, param *RefundOneParam) (resp *wxpayv3.RefundRsp, err error) { bm := gopay.BodyMap{ "out_trade_no": param.OutTradeNo, "out_refund_no": idgenerator.GenWxpayRefundId(), "reason": param.RefundReason, "amount": gopay.BodyMap{ "refund": param.RefundAmount, "total": param.RefundAmount, "currency": "CNY", }, } resp, err = c.clientV3.V3Refund(ctx, bm) if err != nil { return } return }