feat: add payment (#132) (#250)

This commit is contained in:
Buer
2024-06-05 13:30:12 +08:00
committed by GitHub
parent e4665a16c8
commit 03d847fe68
42 changed files with 2890 additions and 20 deletions

View File

@@ -0,0 +1,80 @@
package epay
import (
"crypto/md5"
"encoding/hex"
"sort"
"strings"
"github.com/mitchellh/mapstructure"
)
type Client struct {
PayDomain string `json:"pay_domain"`
PartnerID string `json:"partner_id"`
Key string `json:"key"`
}
// FormPay 表单支付
func (c *Client) FormPay(args *PayArgs) (string, map[string]string, error) {
formPayArgs := map[string]string{
"type": string(args.Type),
"pid": c.PartnerID,
"out_trade_no": args.OutTradeNo,
"notify_url": args.NotifyUrl,
"return_url": args.ReturnUrl,
"name": args.Name,
"money": args.Money,
// "device": string(args.Device),
}
formPayArgs["sign"] = c.Sign(formPayArgs)
formPayArgs["sign_type"] = FormArgsSignType
domain := strings.TrimSuffix(c.PayDomain, "/")
return domain + FormSubmitUrl, formPayArgs, nil
}
func (c *Client) Verify(params map[string]string) (*PaymentResult, bool) {
sign := params["sign"]
tradeStatus := params["trade_status"]
if sign == "" || tradeStatus != TradeStatusSuccess {
return nil, false
}
if sign != c.Sign(params) {
return nil, false
}
var paymentResult PaymentResult
mapstructure.Decode(params, &paymentResult)
return &paymentResult, true
}
// Sign 签名
func (c *Client) Sign(args map[string]string) string {
keys := make([]string, 0, len(args))
for k := range args {
if k != "sign" && k != "sign_type" && args[k] != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
signStrs := make([]string, len(keys))
for i, k := range keys {
signStrs[i] = k + "=" + args[k]
}
signStr := strings.Join(signStrs, "&") + c.Key
h := md5.New()
h.Write([]byte(signStr))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -0,0 +1,91 @@
package epay
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"one-api/payment/types"
"strconv"
"github.com/gin-gonic/gin"
)
type Epay struct{}
type EpayConfig struct {
PayType PayType `json:"pay_type"`
Client
}
func (e *Epay) Name() string {
return "易支付"
}
func (e *Epay) Pay(config *types.PayConfig, gatewayConfig string) (*types.PayRequest, error) {
epayConfig, err := getEpayConfig(gatewayConfig)
if err != nil {
return nil, err
}
payArgs := &PayArgs{
Type: epayConfig.PayType,
OutTradeNo: config.TradeNo,
NotifyUrl: config.NotifyURL,
ReturnUrl: config.ReturnURL,
Name: config.TradeNo,
Money: strconv.FormatFloat(config.Money, 'f', 2, 64),
}
formPayURL, formPayArgs, err := epayConfig.FormPay(payArgs)
if err != nil {
return nil, err
}
payRequest := &types.PayRequest{
Type: 1,
Data: types.PayRequestData{
URL: formPayURL,
Params: &formPayArgs,
Method: http.MethodPost,
},
}
return payRequest, nil
}
func (e *Epay) HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error) {
queryMap := make(map[string]string)
if err := c.ShouldBindQuery(&queryMap); err != nil {
c.Writer.Write([]byte("fail"))
return nil, err
}
epayConfig, err := getEpayConfig(gatewayConfig)
if err != nil {
c.Writer.Write([]byte("fail"))
return nil, fmt.Errorf("tradeNo: %s, PaymentNo: %s, err: %v", queryMap["out_trade_no"], queryMap["trade_no"], err)
}
paymentResult, success := epayConfig.Verify(queryMap)
if paymentResult != nil && success {
c.Writer.Write([]byte("success"))
payNotify := &types.PayNotify{
TradeNo: paymentResult.OutTradeNo,
GatewayNo: paymentResult.TradeNo,
}
return payNotify, nil
}
c.Writer.Write([]byte("fail"))
return nil, fmt.Errorf("tradeNo: %s, PaymentNo: %s, Verify Sign failed", queryMap["out_trade_no"], queryMap["trade_no"])
}
func getEpayConfig(gatewayConfig string) (*EpayConfig, error) {
var epayConfig EpayConfig
if err := json.Unmarshal([]byte(gatewayConfig), &epayConfig); err != nil {
return nil, errors.New("config error")
}
return &epayConfig, nil
}

View File

@@ -0,0 +1,45 @@
package epay
type PayType string
var (
EpayPay PayType = "" // 网关
Alipay PayType = "alipay" // 支付宝
Wechat PayType = "wxpay" // 微信
QQ PayType = "qqpay" // QQ
Bank PayType = "bank" // 银行
JD PayType = "jdpay" // 京东
PayPal PayType = "paypal" // PayPal
USDT PayType = "usdt" // USDT
)
// type DeviceType string
// var (
// PC DeviceType = "pc" // PC
// Mobile DeviceType = "mobile" // 移动端
// )
const (
FormArgsSignType = "MD5"
FormSubmitUrl = "/submit.php"
TradeStatusSuccess = "TRADE_SUCCESS"
)
type PayArgs struct {
Type PayType `json:"type,omitempty"`
OutTradeNo string `json:"out_trade_no"`
NotifyUrl string `json:"notify_url"`
ReturnUrl string `json:"return_url"`
Name string `json:"name"`
Money string `json:"money"`
}
type PaymentResult struct {
Type PayType `mapstructure:"type"`
TradeNo string `mapstructure:"trade_no"`
OutTradeNo string `mapstructure:"out_trade_no"`
Name string `mapstructure:"name"`
Money string `mapstructure:"money"`
TradeStatus string `mapstructure:"trade_status"`
}

20
payment/payment.go Normal file
View File

@@ -0,0 +1,20 @@
package payment
import (
"one-api/payment/gateway/epay"
"one-api/payment/types"
"github.com/gin-gonic/gin"
)
type PaymentProcessor interface {
Name() string
Pay(config *types.PayConfig, gatewayConfig string) (*types.PayRequest, error)
HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error)
}
var Gateways = make(map[string]PaymentProcessor)
func init() {
Gateways["epay"] = &epay.Epay{}
}

81
payment/service.go Normal file
View File

@@ -0,0 +1,81 @@
package payment
import (
"errors"
"fmt"
"one-api/common/config"
"one-api/common/logger"
"one-api/model"
"one-api/payment/types"
"strings"
"github.com/gin-gonic/gin"
)
type PaymentService struct {
Payment *model.Payment
gateway PaymentProcessor
}
type PayMoney struct {
Amount float64
Currency model.CurrencyType
}
func NewPaymentService(uuid string) (*PaymentService, error) {
payment, err := model.GetPaymentByUUID(uuid)
if err != nil {
return nil, errors.New("payment not found")
}
gateway, ok := Gateways[payment.Type]
if !ok {
return nil, errors.New("payment gateway not found")
}
return &PaymentService{
Payment: payment,
gateway: gateway,
}, nil
}
func (s *PaymentService) Pay(tradeNo string, amount float64) (*types.PayRequest, error) {
config := &types.PayConfig{
Money: amount,
TradeNo: tradeNo,
NotifyURL: s.getNotifyURL(),
ReturnURL: s.getReturnURL(),
}
payRequest, err := s.gateway.Pay(config, s.Payment.Config)
if err != nil {
return nil, err
}
return payRequest, nil
}
func (s *PaymentService) HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error) {
payNotify, err := s.gateway.HandleCallback(c, gatewayConfig)
if err != nil {
logger.SysError(fmt.Sprintf("%s payment callback error: %v", s.gateway.Name(), err))
}
return payNotify, err
}
func (s *PaymentService) getNotifyURL() string {
notifyDomain := s.Payment.NotifyDomain
if notifyDomain == "" {
notifyDomain = config.ServerAddress
}
notifyDomain = strings.TrimSuffix(notifyDomain, "/")
return fmt.Sprintf("%s/api/payment/notify/%s", notifyDomain, s.Payment.UUID)
}
func (s *PaymentService) getReturnURL() string {
serverAdd := strings.TrimSuffix(config.ServerAddress, "/")
return fmt.Sprintf("%s/panel/log", serverAdd)
}

27
payment/types/types.go Normal file
View File

@@ -0,0 +1,27 @@
package types
// 支付网关的通用配置
type PayConfig struct {
NotifyURL string `json:"notify_url"`
ReturnURL string `json:"return_url"`
TradeNo string `json:"trade_no"`
Money float64 `json:"money"`
}
// 请求支付时的数据结构
type PayRequest struct {
Type int `json:"type"` // 支付类型 1 url 2 qrcode
Data PayRequestData `json:"data"`
}
type PayRequestData struct {
URL string `json:"url"`
Method string `json:"method,omitempty"`
Params any `json:"params,omitempty"`
}
// 支付回调时的数据结构
type PayNotify struct {
TradeNo string `json:"trade_no"`
GatewayNo string `json:"gateway_no"`
}