mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-12-27 02:26:00 +08:00
This commit is contained in:
197
server/internal/library/payment/alipay/handle.go
Normal file
197
server/internal/library/payment/alipay/handle.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package alipay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/alipay"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/input/payin"
|
||||
)
|
||||
|
||||
func New(config *model.PayConfig) *aliPay {
|
||||
return &aliPay{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type aliPay struct {
|
||||
config *model.PayConfig
|
||||
}
|
||||
|
||||
// Refund 订单退款
|
||||
func (h *aliPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("refund_amount", in.RefundMoney).
|
||||
Set("out_request_no", in.RefundSn).
|
||||
Set("refund_reason", in.Remark)
|
||||
|
||||
refund, err := client.TradeRefund(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if refund.Response.FundChange != "Y" {
|
||||
err = gerror.New("支付宝本次退款未发生资金变化!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Notify 异步通知
|
||||
func (h *aliPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
|
||||
notifyReq, err := alipay.ParseNotifyToBodyMap(ghttp.RequestFromCtx(ctx).Request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 支付宝异步通知验签(公钥证书模式)
|
||||
ok, err := alipay.VerifySignWithCert(h.config.AliPayCertPublicKeyRSA2, notifyReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = gerror.New("支付宝验签不通过!")
|
||||
return
|
||||
}
|
||||
|
||||
var notify *NotifyRequest
|
||||
if err = gconv.Scan(notifyReq, ¬ify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if notify == nil {
|
||||
err = gerror.New("解析订单参数失败!")
|
||||
return
|
||||
}
|
||||
|
||||
if notify.TradeStatus != "TRADE_SUCCESS" {
|
||||
err = gerror.New("非交易支付成功状态,无需处理!")
|
||||
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
if notify.OutTradeNo == "" {
|
||||
err = gerror.New("订单中没有找到商户单号!")
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.NotifyModel)
|
||||
res.TransactionId = notify.TradeNo
|
||||
res.OutTradeNo = notify.OutTradeNo
|
||||
res.PayAt = notify.GmtPayment
|
||||
res.ActualAmount = gconv.Float64(notify.ReceiptAmount)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateOrder 创建订单
|
||||
func (h *aliPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置回调地址
|
||||
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
|
||||
|
||||
switch in.Pay.TradeType {
|
||||
case consts.TradeTypeAliScan, consts.TradeTypeAliWeb:
|
||||
return h.scan(ctx, in)
|
||||
case consts.TradeTypeAliWap:
|
||||
return h.wap(ctx, in)
|
||||
default:
|
||||
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetClient(config *model.PayConfig) (client *alipay.Client, err error) {
|
||||
client, err = alipay.NewClient(config.AliPayAppId, gfile.GetContents(config.AliPayPrivateKey), true)
|
||||
if err != nil {
|
||||
err = gerror.Newf("创建支付宝客户端失败:%+v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 打开Debug开关,输出日志,默认关闭
|
||||
if config.Debug {
|
||||
client.DebugSwitch = gopay.DebugOn
|
||||
}
|
||||
|
||||
client.SetLocation(alipay.LocationShanghai) // 设置时区,不设置或出错均为默认服务器时间
|
||||
|
||||
// 证书路径
|
||||
err = client.SetCertSnByPath(config.AliPayAppCertPublicKey, config.AliPayRootCert, config.AliPayCertPublicKeyRSA2)
|
||||
return
|
||||
}
|
||||
|
||||
// scan 扫码支付
|
||||
func (h *aliPay) scan(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置回调地址
|
||||
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("total_amount", in.Pay.PayAmount)
|
||||
|
||||
payUrl, err := client.TradePagePay(ctx, bm)
|
||||
if err != nil {
|
||||
if bizErr, ok := alipay.IsBizError(err); ok {
|
||||
return nil, bizErr
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.PayURL = payUrl
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
return
|
||||
}
|
||||
|
||||
func (h *aliPay) wap(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置回调地址
|
||||
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("total_amount", in.Pay.PayAmount).
|
||||
Set("product_code", "QUICK_WAP_WAY")
|
||||
|
||||
// 手机网站支付请求
|
||||
payUrl, err := client.TradeWapPay(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.PayURL = payUrl
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
return
|
||||
}
|
||||
61
server/internal/library/payment/alipay/model.go
Normal file
61
server/internal/library/payment/alipay/model.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package alipay
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// NotifyRequest 支付宝异步通知参数
|
||||
// 文档:https://opendocs.alipay.com/open/203/105286
|
||||
type NotifyRequest struct {
|
||||
NotifyTime string `json:"notify_time,omitempty"`
|
||||
NotifyType string `json:"notify_type,omitempty"`
|
||||
NotifyId string `json:"notify_id,omitempty"`
|
||||
AppId string `json:"app_id,omitempty"`
|
||||
Charset string `json:"charset,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
SignType string `json:"sign_type,omitempty"`
|
||||
Sign string `json:"sign,omitempty"`
|
||||
AuthAppId string `json:"auth_app_id,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
OutTradeNo string `json:"out_trade_no,omitempty"`
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
BuyerId string `json:"buyer_id,omitempty"`
|
||||
BuyerLogonId string `json:"buyer_logon_id,omitempty"`
|
||||
SellerId string `json:"seller_id,omitempty"`
|
||||
SellerEmail string `json:"seller_email,omitempty"`
|
||||
TradeStatus string `json:"trade_status,omitempty"`
|
||||
TotalAmount string `json:"total_amount,omitempty"`
|
||||
ReceiptAmount string `json:"receipt_amount,omitempty"`
|
||||
InvoiceAmount string `json:"invoice_amount,omitempty"`
|
||||
BuyerPayAmount string `json:"buyer_pay_amount,omitempty"`
|
||||
PointAmount string `json:"point_amount,omitempty"`
|
||||
RefundFee string `json:"refund_fee,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
GmtCreate string `json:"gmt_create,omitempty"`
|
||||
GmtPayment *gtime.Time `json:"gmt_payment,omitempty"`
|
||||
GmtRefund string `json:"gmt_refund,omitempty"`
|
||||
GmtClose string `json:"gmt_close,omitempty"`
|
||||
FundBillList []*FundBillListInfo `json:"fund_bill_list,omitempty"`
|
||||
PassbackParams string `json:"passback_params,omitempty"`
|
||||
VoucherDetailList []*VoucherDetail `json:"voucher_detail_list,omitempty"`
|
||||
Method string `json:"method,omitempty"` // 电脑网站支付 支付宝请求 return_url 同步返回参数
|
||||
Timestamp string `json:"timestamp,omitempty"` // 电脑网站支付 支付宝请求 return_url 同步返回参数
|
||||
}
|
||||
|
||||
type FundBillListInfo struct {
|
||||
Amount string `json:"amount,omitempty"`
|
||||
FundChannel string `json:"fundChannel,omitempty"` // 异步通知里是 fundChannel
|
||||
}
|
||||
|
||||
type VoucherDetail struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Amount string `json:"amount,omitempty"`
|
||||
MerchantContribute string `json:"merchant_contribute,omitempty"`
|
||||
OtherContribute string `json:"other_contribute,omitempty"`
|
||||
Memo string `json:"memo,omitempty"`
|
||||
TemplateId string `json:"template_id,omitempty"`
|
||||
PurchaseBuyerContribute string `json:"purchase_buyer_contribute,omitempty"`
|
||||
PurchaseMerchantContribute string `json:"purchase_merchant_contribute,omitempty"`
|
||||
PurchaseAntContribute string `json:"purchase_ant_contribute,omitempty"`
|
||||
}
|
||||
13
server/internal/library/payment/config.go
Normal file
13
server/internal/library/payment/config.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package payment
|
||||
|
||||
import "hotgo/internal/model"
|
||||
|
||||
var config *model.PayConfig
|
||||
|
||||
func SetConfig(c *model.PayConfig) {
|
||||
config = c
|
||||
}
|
||||
|
||||
func GetConfig() *model.PayConfig {
|
||||
return config
|
||||
}
|
||||
44
server/internal/library/payment/notifycall.go
Normal file
44
server/internal/library/payment/notifycall.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/model/input/payin"
|
||||
"hotgo/utility/simple"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 异步回调
|
||||
|
||||
type NotifyCallFunc func(ctx context.Context, pay payin.NotifyCallFuncInp) (err error)
|
||||
|
||||
var (
|
||||
notifyCall = make(map[string]NotifyCallFunc)
|
||||
ncLock sync.Mutex
|
||||
)
|
||||
|
||||
// RegisterNotifyCall 注册支付成功回调方法
|
||||
func RegisterNotifyCall(group string, f NotifyCallFunc) {
|
||||
ncLock.Lock()
|
||||
defer ncLock.Unlock()
|
||||
if _, ok := notifyCall[group]; ok {
|
||||
panic("notifyCall repeat registration, group:" + group)
|
||||
}
|
||||
notifyCall[group] = f
|
||||
}
|
||||
|
||||
// NotifyCall 执行订单分组的异步回调
|
||||
func NotifyCall(ctx context.Context, in payin.NotifyCallFuncInp) {
|
||||
f, ok := notifyCall[in.Pay.OrderGroup]
|
||||
if ok {
|
||||
ctx = contexts.Detach(ctx)
|
||||
simple.SafeGo(ctx, func(ctx context.Context) {
|
||||
if err := f(ctx, in); err != nil {
|
||||
g.Log().Warningf(ctx, "payment.NotifyCall in:%+v exec err:%+v", gjson.New(in.Pay).String(), err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
121
server/internal/library/payment/payment.go
Normal file
121
server/internal/library/payment/payment.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/payment/alipay"
|
||||
"hotgo/internal/library/payment/qqpay"
|
||||
"hotgo/internal/library/payment/wxpay"
|
||||
"hotgo/internal/model/input/payin"
|
||||
"hotgo/utility/validate"
|
||||
)
|
||||
|
||||
// PayClient 支付客户端
|
||||
type PayClient interface {
|
||||
// CreateOrder 创建订单
|
||||
CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error)
|
||||
// Notify 异步通知
|
||||
Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error)
|
||||
// Refund 订单退款
|
||||
Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error)
|
||||
}
|
||||
|
||||
func New(name ...string) PayClient {
|
||||
var (
|
||||
payType = consts.PayTypeWxPay
|
||||
client PayClient
|
||||
)
|
||||
|
||||
if len(name) > 0 && name[0] != "" {
|
||||
payType = name[0]
|
||||
}
|
||||
|
||||
switch payType {
|
||||
case consts.PayTypeAliPay:
|
||||
client = alipay.New(config)
|
||||
case consts.PayTypeWxPay:
|
||||
client = wxpay.New(config)
|
||||
case consts.PayTypeQQPay:
|
||||
client = qqpay.New(config)
|
||||
default:
|
||||
panic(fmt.Sprintf("暂不支持的支付方式:%v", payType))
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// GenOrderSn 生成业务订单号
|
||||
func GenOrderSn() string {
|
||||
orderSn := fmt.Sprintf("HG@%v%v", gtime.Now().Format("YmdHis"), grand.S(4))
|
||||
count, err := g.Model("pay_log").Where("order_sn", orderSn).Count()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("payment.GenOrderSn err:%+v", err))
|
||||
}
|
||||
if count > 0 {
|
||||
return GenOrderSn()
|
||||
}
|
||||
return orderSn
|
||||
}
|
||||
|
||||
// GenOutTradeNo 生成商户订单号
|
||||
func GenOutTradeNo() string {
|
||||
outTradeNo := fmt.Sprintf("%v%v", gtime.Now().Format("YmdHis"), grand.N(10000000, 99999999))
|
||||
count, err := g.Model("pay_log").Where("out_trade_no", outTradeNo).Count()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("payment.GenOutTradeNo err:%+v", err))
|
||||
}
|
||||
if count > 0 {
|
||||
return GenOutTradeNo()
|
||||
}
|
||||
return outTradeNo
|
||||
}
|
||||
|
||||
// GenRefundSn 生成退款订单号
|
||||
func GenRefundSn() string {
|
||||
outTradeNo := fmt.Sprintf("%v%v", gtime.Now().Format("YmdHis"), grand.N(10000, 99999))
|
||||
count, err := g.Model("pay_refund").Where("refund_trade_no", outTradeNo).Count()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("payment.GenRefundSn err:%+v", err))
|
||||
}
|
||||
if count > 0 {
|
||||
return GenRefundSn()
|
||||
}
|
||||
return outTradeNo
|
||||
}
|
||||
|
||||
// AutoTradeType 根据userAgent自动识别交易方式,在实际支付场景中你可以手动调整识别规则
|
||||
func AutoTradeType(payType, userAgent string) (tradeType string) {
|
||||
isMobile := validate.IsMobileVisit(userAgent)
|
||||
switch payType {
|
||||
case consts.PayTypeAliPay:
|
||||
if isMobile {
|
||||
return consts.TradeTypeAliWap
|
||||
}
|
||||
return consts.TradeTypeAliWeb
|
||||
case consts.PayTypeWxPay:
|
||||
if isMobile {
|
||||
if validate.IsWxBrowserVisit(userAgent) {
|
||||
return consts.TradeTypeWxMP
|
||||
}
|
||||
|
||||
if validate.IsWxMiniProgramVisit(userAgent) {
|
||||
return consts.TradeTypeWxMini
|
||||
}
|
||||
|
||||
return consts.TradeTypeWxH5
|
||||
}
|
||||
return consts.TradeTypeWxScan
|
||||
case consts.PayTypeQQPay:
|
||||
if isMobile {
|
||||
return consts.TradeTypeQQWap
|
||||
}
|
||||
return consts.TradeTypeQQWeb
|
||||
default:
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
134
server/internal/library/payment/qqpay/handle.go
Normal file
134
server/internal/library/payment/qqpay/handle.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package qqpay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/qq"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/input/payin"
|
||||
)
|
||||
|
||||
func New(config *model.PayConfig) *qqPay {
|
||||
return &qqPay{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type qqPay struct {
|
||||
config *model.PayConfig
|
||||
}
|
||||
|
||||
// Refund 订单退款
|
||||
func (h *qqPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
|
||||
err = gerror.New("暂不支持QQ支付申请退款,如有疑问请联系管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// Notify 异步通知
|
||||
func (h *qqPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
|
||||
notifyReq, err := qq.ParseNotifyToBodyMap(ghttp.RequestFromCtx(ctx).Request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 验签操作
|
||||
ok, err := qq.VerifySign(h.config.QQPayApiKey, qq.SignType_MD5, notifyReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = gerror.New("QQ支付验签不通过!")
|
||||
return
|
||||
}
|
||||
|
||||
var notify *NotifyRequest
|
||||
if err = gconv.Scan(notifyReq, ¬ify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if notify == nil {
|
||||
err = gerror.New("解析订单参数失败!")
|
||||
return
|
||||
}
|
||||
|
||||
if notify.TradeState != "SUCCESS" {
|
||||
err = gerror.New("非交易支付成功状态,无需处理!")
|
||||
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
if notify.OutTradeNo == "" {
|
||||
err = gerror.New("订单中没有找到商户单号!")
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.NotifyModel)
|
||||
res.TransactionId = notify.TransactionId
|
||||
res.OutTradeNo = notify.OutTradeNo
|
||||
res.PayAt = gtime.New(notify.TimeEnd)
|
||||
res.ActualAmount = gconv.Float64(notify.CouponFee) / 100 // 用户本次交易中,实际支付的金额 转为元,和系统内保持一至
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateOrder 创建订单
|
||||
func (h *qqPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client := GetClient(h.config)
|
||||
|
||||
switch in.Pay.TradeType {
|
||||
case consts.TradeTypeQQWeb, consts.TradeTypeQQWap:
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.
|
||||
Set("mch_id", h.config.QQPayMchId).
|
||||
Set("body", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("notify_url", in.Pay.NotifyUrl).
|
||||
Set("nonce_str", grand.Letters(32)).
|
||||
Set("spbill_create_ip", in.Pay.CreateIp).
|
||||
Set("trade_type", "NATIVE"). // MICROPAY、APP、JSAPI、NATIVE
|
||||
Set("total_fee", int64(in.Pay.PayAmount*100))
|
||||
|
||||
qqRsp, err := client.UnifiedOrder(ctx, bm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if qqRsp.ReturnCode != "SUCCESS" {
|
||||
err = gerror.New(qqRsp.ReturnMsg)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if qqRsp.ResultCode != "SUCCESS" {
|
||||
err = gerror.New(qqRsp.ErrCodeDes)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.PayURL = qqRsp.CodeUrl
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
|
||||
default:
|
||||
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetClient(config *model.PayConfig) (client *qq.Client) {
|
||||
client = qq.NewClient(config.QQPayMchId, config.QQPayApiKey)
|
||||
|
||||
// 打开Debug开关,输出日志,默认关闭
|
||||
if config.Debug {
|
||||
client.DebugSwitch = gopay.DebugOn
|
||||
}
|
||||
return
|
||||
}
|
||||
23
server/internal/library/payment/qqpay/model.go
Normal file
23
server/internal/library/payment/qqpay/model.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package qqpay
|
||||
|
||||
// NotifyRequest QQ支付异步通知参数
|
||||
// 文档:https://mp.qpay.tenpay.cn/buss/wiki/38/1204
|
||||
type NotifyRequest struct {
|
||||
Appid string `xml:"appid,omitempty" json:"appid,omitempty"`
|
||||
MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"`
|
||||
NonceStr string `xml:"nonce_str,omitempty" json:"nonce_str,omitempty"`
|
||||
Sign string `xml:"sign,omitempty" json:"sign,omitempty"`
|
||||
DeviceInfo string `xml:"device_info,omitempty" json:"device_info,omitempty"`
|
||||
TradeType string `xml:"trade_type,omitempty" json:"trade_type,omitempty"`
|
||||
TradeState string `xml:"trade_state,omitempty" json:"trade_state,omitempty"`
|
||||
BankType string `xml:"bank_type,omitempty" json:"bank_type,omitempty"`
|
||||
FeeType string `xml:"fee_type,omitempty" json:"fee_type,omitempty"`
|
||||
TotalFee string `xml:"total_fee,omitempty" json:"total_fee,omitempty"`
|
||||
CashFee string `xml:"cash_fee,omitempty" json:"cash_fee,omitempty"`
|
||||
CouponFee string `xml:"coupon_fee,omitempty" json:"coupon_fee,omitempty"`
|
||||
TransactionId string `xml:"transaction_id,omitempty" json:"transaction_id,omitempty"`
|
||||
OutTradeNo string `xml:"out_trade_no,omitempty" json:"out_trade_no,omitempty"`
|
||||
Attach string `xml:"attach,omitempty" json:"attach,omitempty"`
|
||||
TimeEnd string `xml:"time_end,omitempty" json:"time_end,omitempty"`
|
||||
Openid string `xml:"openid,omitempty" json:"openid,omitempty"`
|
||||
}
|
||||
311
server/internal/library/payment/wxpay/handle.go
Normal file
311
server/internal/library/payment/wxpay/handle.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package wxpay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/pkg/xpem"
|
||||
"github.com/go-pay/gopay/wechat/v3"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"hotgo/internal/consts"
|
||||
weOpen "hotgo/internal/library/wechat"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/input/payin"
|
||||
"time"
|
||||
)
|
||||
|
||||
func New(config *model.PayConfig) *wxPay {
|
||||
return &wxPay{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type wxPay struct {
|
||||
config *model.PayConfig
|
||||
}
|
||||
|
||||
// Refund 订单退款
|
||||
func (h *wxPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("out_refund_no", in.RefundSn).
|
||||
Set("reason", in.Remark).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", int64(in.Pay.PayAmount*100)).
|
||||
Set("currency", "CNY").
|
||||
Set("refund", int64(in.RefundMoney*100))
|
||||
})
|
||||
|
||||
refund, err := client.V3Refund(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if refund.Error != "" {
|
||||
err = gerror.Newf("微信支付发起退款失败,原因:%v", refund.Response.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if refund.Response.Status != "SUCCESS" && refund.Response.Status != "PROCESSING" {
|
||||
err = gerror.Newf("微信支付发起退款失败,状态码:%v", refund.Response.Status)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Notify 异步通知
|
||||
func (h *wxPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
|
||||
notifyReq, err := wechat.V3ParseNotify(ghttp.RequestFromCtx(ctx).Request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取微信平台证书
|
||||
certMap, err := getPublicKeyMap(client)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证异步通知的签名
|
||||
if err = notifyReq.VerifySignByPKMap(certMap); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
notify, err := notifyReq.DecryptCipherText(h.config.WxPayAPIv3Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if notify.TradeState != "SUCCESS" {
|
||||
err = gerror.New("非交易支付成功状态,无需处理!")
|
||||
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
if notify.OutTradeNo == "" {
|
||||
err = gerror.New("订单中没有找到商户单号!")
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.NotifyModel)
|
||||
res.TransactionId = notify.TransactionId
|
||||
res.OutTradeNo = notify.OutTradeNo
|
||||
res.PayAt = gtime.New(notify.SuccessTime)
|
||||
res.ActualAmount = float64(notify.Amount.PayerTotal / 100) // 转为元,和系统内保持一至
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateOrder 创建订单
|
||||
func (h *wxPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
switch in.Pay.TradeType {
|
||||
case consts.TradeTypeWxScan:
|
||||
return h.scan(ctx, in)
|
||||
case consts.TradeTypeWxMP, consts.TradeTypeWxMini:
|
||||
return h.jsapi(ctx, in)
|
||||
case consts.TradeTypeWxH5:
|
||||
return h.h5(ctx, in)
|
||||
default:
|
||||
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetClient(config *model.PayConfig) (client *wechat.ClientV3, err error) {
|
||||
client, err = wechat.NewClientV3(config.WxPayMchId, config.WxPaySerialNo, config.WxPayAPIv3Key, config.WxPayPrivateKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, err = client.GetAndSelectNewestCertALL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serialNo, snCertMap, err := client.GetAndSelectNewestCert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
snPkMap := make(map[string]*rsa.PublicKey)
|
||||
for sn, cert := range snCertMap {
|
||||
pubKey, err := xpem.DecodePublicKey([]byte(cert))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snPkMap[sn] = pubKey
|
||||
}
|
||||
|
||||
client.SnCertMap = snPkMap
|
||||
client.WxSerialNo = serialNo
|
||||
|
||||
// 打开Debug开关,输出日志,默认关闭
|
||||
if config.Debug {
|
||||
client.DebugSwitch = gopay.DebugOn
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getPublicKeyMap(client *wechat.ClientV3) (wxPublicKeyMap map[string]*rsa.PublicKey, err error) {
|
||||
serialNo, snCertMap, err := client.GetAndSelectNewestCert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
snPkMap := make(map[string]*rsa.PublicKey)
|
||||
for sn, cert := range snCertMap {
|
||||
pubKey, err := xpem.DecodePublicKey([]byte(cert))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snPkMap[sn] = pubKey
|
||||
}
|
||||
client.SnCertMap = snPkMap
|
||||
client.WxSerialNo = serialNo
|
||||
|
||||
wxPublicKeyMap = client.WxPublicKeyMap()
|
||||
return
|
||||
}
|
||||
|
||||
// scan 创建扫码支付订单
|
||||
func (h *wxPay) scan(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", h.config.WxPayAppId).
|
||||
Set("mchid", h.config.WxPayMchId).
|
||||
Set("description", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
|
||||
Set("notify_url", in.Pay.NotifyUrl).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", int64(in.Pay.PayAmount*100)).
|
||||
Set("currency", "CNY")
|
||||
})
|
||||
|
||||
wxRsp, err := client.V3TransactionNative(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if wxRsp.Code != 0 {
|
||||
err = gerror.New(wxRsp.Error)
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.PayURL = wxRsp.Response.CodeUrl
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
return
|
||||
}
|
||||
|
||||
// h5 创建H5支付订单
|
||||
func (h *wxPay) h5(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化参数Map
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", h.config.WxPayAppId).
|
||||
Set("mchid", h.config.WxPayMchId).
|
||||
Set("description", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
|
||||
Set("notify_url", in.Pay.NotifyUrl).
|
||||
SetBodyMap("amount", func(b gopay.BodyMap) {
|
||||
b.Set("total", int64(in.Pay.PayAmount*100)).
|
||||
Set("currency", "CNY")
|
||||
}).
|
||||
SetBodyMap("scene_info", func(b gopay.BodyMap) {
|
||||
b.Set("payer_client_ip", in.Pay.CreateIp).
|
||||
SetBodyMap("h5_info", func(b gopay.BodyMap) {
|
||||
b.Set("type", "Wap")
|
||||
})
|
||||
})
|
||||
|
||||
// 请求支付下单,成功后得到结果
|
||||
wxRsp, err := client.V3TransactionH5(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if wxRsp.Code != 0 {
|
||||
err = gerror.New(wxRsp.Error)
|
||||
return
|
||||
}
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.PayURL = wxRsp.Response.H5Url
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
return
|
||||
}
|
||||
|
||||
// jsapi 创建jsapi支付订单
|
||||
func (h *wxPay) jsapi(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
|
||||
jsApi := new(payin.JSAPI)
|
||||
jsApi.Config, err = weOpen.GetJsConfig(ctx, in.Pay.ReturnUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := GetClient(h.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", h.config.WxPayAppId).
|
||||
Set("mchid", h.config.WxPayMchId).
|
||||
Set("description", in.Pay.Subject).
|
||||
Set("out_trade_no", in.Pay.OutTradeNo).
|
||||
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
|
||||
Set("notify_url", in.Pay.NotifyUrl).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", int64(in.Pay.PayAmount*100)).
|
||||
Set("currency", "CNY")
|
||||
}).
|
||||
SetBodyMap("payer", func(bm gopay.BodyMap) {
|
||||
bm.Set("openid", in.Pay.Openid)
|
||||
})
|
||||
|
||||
wxRsp, err := client.V3TransactionJsapi(ctx, bm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if wxRsp.Code != 0 {
|
||||
err = gerror.New(wxRsp.Error)
|
||||
return
|
||||
}
|
||||
|
||||
js, err := client.PaySignOfJSAPI(h.config.WxPayAppId, wxRsp.Response.PrepayId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
jsApi.Params = js
|
||||
|
||||
res = new(payin.CreateOrderModel)
|
||||
res.TradeType = in.Pay.TradeType
|
||||
res.OutTradeNo = in.Pay.OutTradeNo
|
||||
res.JsApi = jsApi
|
||||
return
|
||||
}
|
||||
1
server/internal/library/payment/wxpay/model.go
Normal file
1
server/internal/library/payment/wxpay/model.go
Normal file
@@ -0,0 +1 @@
|
||||
package wxpay
|
||||
Reference in New Issue
Block a user