From 40a6a5d8d8d4bf9f3df1eea707329801a88ba2a2 Mon Sep 17 00:00:00 2001 From: ZeroDeng Date: Thu, 6 Jun 2024 01:11:06 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20Alipay=20payment=20(#?= =?UTF-8?q?251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加支付宝当面付(扫码支付)和网页跳转支付功能 --- go.mod | 8 +- go.sum | 8 ++ payment/gateway/alipay/payment.go | 173 +++++++++++++++++++++++++++ payment/gateway/alipay/type.go | 25 ++++ payment/payment.go | 2 + web/src/views/Payment/type/Config.js | 39 +++++- 6 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 payment/gateway/alipay/payment.go create mode 100644 payment/gateway/alipay/type.go diff --git a/go.mod b/go.mod index 1faf6c06..e6ec50ca 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 github.com/aws/smithy-go v1.20.1 + github.com/bwmarrin/snowflake v0.3.0 github.com/gin-contrib/cors v1.7.0 github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.4 @@ -21,7 +22,9 @@ require ( github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.1 + github.com/mitchellh/mapstructure v1.5.0 github.com/shopspring/decimal v1.4.0 + github.com/smartwalle/alipay/v3 v3.2.21 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 github.com/wneessen/go-mail v0.4.1 @@ -35,17 +38,18 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/smartwalle/ncrypto v1.0.4 // indirect + github.com/smartwalle/ngx v1.0.9 // indirect + github.com/smartwalle/nsign v1.0.9 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/go.sum b/go.sum index 50ff550d..4af5dc6f 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,14 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartwalle/alipay/v3 v3.2.21 h1:zQmzYOGU4WCc2f21VzqBqC60iEJ6meVxe1Sl33nJ+vE= +github.com/smartwalle/alipay/v3 v3.2.21/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= +github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= +github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= +github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= +github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/payment/gateway/alipay/payment.go b/payment/gateway/alipay/payment.go new file mode 100644 index 00000000..881c31fe --- /dev/null +++ b/payment/gateway/alipay/payment.go @@ -0,0 +1,173 @@ +package alipay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "github.com/smartwalle/alipay/v3" + "net/http" + "net/url" + sysconfig "one-api/common/config" + "one-api/payment/types" + "strconv" +) + +type Alipay struct{} + +type AlipayConfig struct { + AppID string `json:"app_id"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + PayType PayType `json:"pay_type"` +} + +var client *alipay.Client + +const isProduction bool = true + +func (a *Alipay) Name() string { + return "支付宝当面付" +} + +func (a *Alipay) InitClient(config *AlipayConfig) error { + var err error + client, err = alipay.New(config.AppID, config.PrivateKey, isProduction) + if err != nil { + return err + } + return client.LoadAliPayPublicKey(config.PublicKey) +} + +func (a *Alipay) Pay(config *types.PayConfig, gatewayConfig string) (*types.PayRequest, error) { + alipayConfig, err := getAlipayConfig(gatewayConfig) + if err != nil { + return nil, err + } + + if client == nil { + err := a.InitClient(alipayConfig) + if err != nil { + return nil, err + } + } + + if alipayConfig.PayType != PagePay { + var p = alipay.TradePreCreate{} + p.OutTradeNo = config.TradeNo + p.TotalAmount = strconv.FormatFloat(config.Money, 'f', 2, 64) + p.Subject = sysconfig.SystemName + "-Token充值:" + p.TotalAmount + p.NotifyURL = config.NotifyURL + p.ReturnURL = config.ReturnURL + ctx := context.Background() + alipayRes, err := client.TradePreCreate(ctx, p) + if err != nil { + return nil, fmt.Errorf("alipay trade precreate failed: %s", alipayRes.Msg) + } + if !alipayRes.IsSuccess() { + return nil, fmt.Errorf("alipay trade precreate failed: %s", alipayRes.Msg) + } + if alipayRes.Code != "10000" { + return nil, fmt.Errorf("alipay trade precreate failed: %s", alipayRes.Msg) + } + payRequest := &types.PayRequest{ + Type: 2, + Data: types.PayRequestData{ + URL: alipayRes.QRCode, + Method: http.MethodGet, + }, + } + return payRequest, nil + } else { + var p = alipay.TradePagePay{} + p.OutTradeNo = config.TradeNo + p.TotalAmount = strconv.FormatFloat(config.Money, 'f', 2, 64) + p.Subject = sysconfig.SystemName + "-Token充值:" + p.TotalAmount + p.NotifyURL = config.NotifyURL + p.ReturnURL = config.ReturnURL + alipayRes, err := client.TradePagePay(p) + if err != nil { + return nil, fmt.Errorf("alipay trade precreate failed: %s", err.Error()) + } + payUrl, parms, err := extractURLAndParams(alipayRes.String()) + if err != nil { + return nil, fmt.Errorf("alipay trade precreate failed: %s", err.Error()) + } + payRequest := &types.PayRequest{ + Type: 1, + Data: types.PayRequestData{ + URL: payUrl, + Params: parms, + Method: http.MethodGet, + }, + } + return payRequest, nil + } + +} + +func (a *Alipay) HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error) { + // 获取通知参数 + params := c.Request.URL.Query() + if err := c.Request.ParseForm(); err != nil { + c.Writer.Write([]byte("failure")) + return nil, fmt.Errorf("Alipay params failed: %v", err) + } + for k, v := range c.Request.PostForm { + params[k] = v + } + // 验证通知签名 + if err := client.VerifySign(params); err != nil { + c.Writer.Write([]byte("failure")) + return nil, fmt.Errorf("Alipay Signature verification failed: %v", err) + } + //解析通知内容 + var noti, err = client.DecodeNotification(params) + if err != nil { + c.Writer.Write([]byte("failure")) + return nil, fmt.Errorf("Alipay Error decoding notification: %v", err) + } + + if noti.TradeStatus == alipay.TradeStatusSuccess { + payNotify := &types.PayNotify{ + TradeNo: noti.OutTradeNo, + GatewayNo: noti.TradeNo, + } + alipay.ACKNotification(c.Writer) + return payNotify, nil + } + c.Writer.Write([]byte("failure")) + return nil, fmt.Errorf("trade status not success") +} + +func getAlipayConfig(gatewayConfig string) (*AlipayConfig, error) { + var alipayConfig AlipayConfig + if err := json.Unmarshal([]byte(gatewayConfig), &alipayConfig); err != nil { + return nil, errors.New("config error") + } + + return &alipayConfig, nil +} + +// extractURLAndParams 从给定的原始 URL 中提取网址和参数,并将参数转换为 map[string]string +func extractURLAndParams(rawURL string) (string, map[string]string, error) { + // 解析 URL + parsedURL, err := url.Parse(rawURL) + if err != nil { + return "", nil, err + } + + // 提取网址 + baseURL := fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, parsedURL.Path) + + // 提取参数并转换成 map[string]string + params := parsedURL.Query() + paramMap := make(map[string]string) + for key, values := range params { + // 由于 URL 参数可能有多个值,这里只取第一个值 + paramMap[key] = values[0] + } + + return baseURL, paramMap, nil +} diff --git a/payment/gateway/alipay/type.go b/payment/gateway/alipay/type.go new file mode 100644 index 00000000..0b9514b5 --- /dev/null +++ b/payment/gateway/alipay/type.go @@ -0,0 +1,25 @@ +package alipay + +type PayType string + +var ( + FacePay PayType = "facepay" // 当面付 + PagePay PayType = "pagepay" // 跳转支付 + +) + +type PayArgs struct { + AppID string `json:"app_id"` + OutTradeNo string `json:"out_trade_no"` + NotifyUrl string `json:"notify_url"` + ReturnUrl string `json:"return_url"` + Subject string `json:"subject"` + TotalAmount string `json:"total_amount"` +} + +type PaymentResult struct { + OutTradeNo string `mapstructure:"out_trade_no"` + TradeNo string `mapstructure:"trade_no"` + TotalAmount string `mapstructure:"total_amount"` + TradeStatus string `mapstructure:"trade_status"` +} diff --git a/payment/payment.go b/payment/payment.go index 1f02f9ee..8c1ae272 100644 --- a/payment/payment.go +++ b/payment/payment.go @@ -1,6 +1,7 @@ package payment import ( + "one-api/payment/gateway/alipay" "one-api/payment/gateway/epay" "one-api/payment/types" @@ -17,4 +18,5 @@ var Gateways = make(map[string]PaymentProcessor) func init() { Gateways["epay"] = &epay.Epay{} + Gateways["alipay"] = &alipay.Alipay{} } diff --git a/web/src/views/Payment/type/Config.js b/web/src/views/Payment/type/Config.js index 2e283b25..98a30020 100644 --- a/web/src/views/Payment/type/Config.js +++ b/web/src/views/Payment/type/Config.js @@ -1,5 +1,6 @@ const PaymentType = { - epay: '易支付' + epay: '易支付', + alipay: '支付宝' }; const CurrencyType = { @@ -67,6 +68,42 @@ const PaymentConfig = { } ] } + }, + alipay: { + app_id: { + name: '应用ID', + description: '支付宝应用ID', + type: 'text', + value: '' + }, + private_key: { + name: '应用私钥', + description: '应用私钥,开发者自己生成,详细参考官方文档 https://opendocs.alipay.com/common/02kipl?pathHash=84adb0fd', + type: 'text', + value: '' + }, + public_key: { + name: '支付宝公钥', + description: '支付宝公钥,详细参考官方文档 https://opendocs.alipay.com/common/02kdnc?pathHash=fb0c752a', + type: 'text', + value: '' + }, + pay_type: { + name: '支付类型', + description: '支付类型,需要您再支付宝开发者中心开通相关权限才可以使用对应类型支付方式', + type: 'select', + value: '', + options: [ + { + name: '当面付', + value: 'facepay' + }, + { + name: '跳转支付', + value: 'pagepay' + } + ] + } } };