feat: add Alipay payment (#251)

增加支付宝当面付(扫码支付)和网页跳转支付功能
This commit is contained in:
ZeroDeng 2024-06-06 01:11:06 +08:00 committed by GitHub
parent e2bfec77a4
commit 40a6a5d8d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 252 additions and 3 deletions

8
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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{}
}

View File

@ -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'
}
]
}
}
};