341 lines
9.3 KiB
Go
341 lines
9.3 KiB
Go
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"
|
||
AppIdShuiLaiTF = "1682472761"
|
||
)
|
||
|
||
var allWxpayClients = map[string]*WxpayClient{}
|
||
|
||
func GetDefaultWxpayClient() *WxpayClient {
|
||
return allWxpayClients[AppIdShuiLaiTF]
|
||
}
|
||
|
||
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)
|
||
logger.Info("RefundOne bm: %v, resp: %v, code: %v, err: %v", util.ToJson(bm), util.ToJson(resp), resp.Code, resp.Error)
|
||
if err != nil {
|
||
return
|
||
}
|
||
return
|
||
}
|