From 0b2dd6da78e369cc08b51708d50f94752c3dcae7 Mon Sep 17 00:00:00 2001 From: xy <135087223+geniuxy@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:30:04 +0800 Subject: [PATCH] feat: develop payment interface (#3610) * feat: develop Payment interface --- .../account/controllers/account_controller.go | 44 ++++++++++----- .../account/controllers/payment_controller.go | 17 +++--- go.work.sum | 1 + pkg/pay/common.go | 29 ++++++++++ pkg/pay/interface.go | 36 +++++++++++++ pkg/pay/stripe.go | 54 +++++++++++++++++++ pkg/pay/stripe_payment.go | 8 +++ pkg/pay/wechat.go | 49 +++++++++++++++++ pkg/pay/wechat_payment.go | 3 ++ 9 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 pkg/pay/common.go create mode 100644 pkg/pay/interface.go create mode 100644 pkg/pay/stripe.go create mode 100644 pkg/pay/wechat.go diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index 743d88b56..92d2afd27 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -75,7 +75,7 @@ const ( SEALOS = "sealos" ) -// AccountReconciler reconciles a Account object +// AccountReconciler reconciles an Account object type AccountReconciler struct { client.Client Scheme *runtime.Scheme @@ -132,7 +132,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if payment.Status.TradeNO == "" { return ctrl.Result{Requeue: true, RequeueAfter: time.Millisecond * 300}, nil } - if payment.Status.Status == pay.StatusSuccess { + if payment.Status.Status == pay.PaymentSuccess { return ctrl.Result{}, nil } @@ -141,13 +141,20 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, fmt.Errorf("get account failed: %v", err) } - orderResp, err := pay.QueryOrder(payment.Status.TradeNO) + // get payment handler + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + r.Logger.Error(err, "get payment handler failed: %s", err) + return ctrl.Result{}, err + } + // get payment details(status, amount) + status, orderAmount, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) if err != nil { return ctrl.Result{}, fmt.Errorf("query order failed: %v", err) } - r.Logger.V(1).Info("query order status", "orderResp", orderResp) - switch *orderResp.TradeState { - case pay.StatusSuccess: + r.Logger.V(1).Info("query order details", "orderStatus", status, "orderAmount", orderAmount) + switch status { + case pay.PaymentSuccess: dbCtx := context.Background() dbClient, err := database.NewMongoDB(dbCtx, r.MongoDBURI) if err != nil { @@ -162,7 +169,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct }() now := time.Now().UTC() //1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount - payAmount := *orderResp.Amount.Total * 10000 + payAmount := orderAmount * 10000 // get recharge-gift configmap configMap := &corev1.ConfigMap{} if err := r.Client.Get(ctx, types.NamespacedName{Name: RECHARGEGIFT, Namespace: SEALOS}, configMap); err != nil { @@ -179,7 +186,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.updateAccountStatus(ctx, account); err != nil { return ctrl.Result{}, fmt.Errorf("update account failed: %v", err) } - payment.Status.Status = pay.StatusSuccess + payment.Status.Status = string(pay.PaymentSuccess) if err := r.Status().Update(ctx, payment); err != nil { return ctrl.Result{}, fmt.Errorf("update payment failed: %v", err) } @@ -201,15 +208,15 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct r.Logger.Error(err, "save billings failed", "id", id, "payment", payment) return ctrl.Result{}, nil } - case pay.StatusProcessing, pay.StatusNotPay: + case pay.PaymentProcessing, pay.PaymentNotPaid: return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, nil - case pay.StatusFail: + case pay.PaymentFailed, pay.PaymentExpired: if err := r.Delete(ctx, payment); err != nil { return ctrl.Result{}, fmt.Errorf("delete payment failed: %v", err) } return ctrl.Result{}, nil default: - return ctrl.Result{}, fmt.Errorf("unknown orderResp: %v", orderResp) + return ctrl.Result{}, fmt.Errorf("unknown status: %v", err) } return ctrl.Result{}, nil @@ -362,8 +369,21 @@ func (r *AccountReconciler) DeletePayment(ctx context.Context) error { return err } for _, payment := range payments.Items { + //get payment handler + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + r.Logger.Error(err, "get payment handler failed: %s", err) + return err + } + //expire session if it is necessary + err = payHandler.ExpireSession(payment.Status.TradeNO) + if err != nil { + r.Logger.Error(err, "cancel payment failed: %s", err) + return err + } + //delete payment if it is exist for more than 5 minutes if time.Since(payment.CreationTimestamp.Time) > time.Minute*5 { - err = r.Delete(ctx, &payment) + err := r.Delete(ctx, &payment) if err != nil { return err } diff --git a/controllers/account/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go index cf7b422ee..1a141b360 100644 --- a/controllers/account/controllers/payment_controller.go +++ b/controllers/account/controllers/payment_controller.go @@ -21,6 +21,7 @@ import ( "os" "time" + "github.com/labring/sealos/pkg/pay" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -28,8 +29,6 @@ import ( "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" - "github.com/labring/sealos/pkg/pay" - "github.com/mdp/qrterminal" ctrl "sigs.k8s.io/controller-runtime" @@ -77,11 +76,17 @@ func (r *PaymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{Requeue: true}, err } } - tradeNO := pay.GetRandomString(32) - // change to prices 1000000 = 1¥ and in wechatPay 100 = 1¥,so need to Amount/10000 - codeURL, err := pay.WechatPay(p.Spec.Amount/10000, p.Spec.UserID, tradeNO, "", "") + + // get payment handler + payHandler, err := pay.NewPayHandler(p.Spec.PaymentMethod) if err != nil { - r.Logger.Error(err, "get codeURL failed") + r.Logger.Error(err, "get payment Interface failed: %s", err) + return ctrl.Result{}, err + } + // get tradeNO and codeURL + tradeNO, codeURL, err := payHandler.CreatePayment(p.Spec.Amount, p.Spec.UserID) + if err != nil { + r.Logger.Error(err, "get tradeNO and codeURL failed") return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, err } p.Status.CodeURL = codeURL diff --git a/go.work.sum b/go.work.sum index 8914355e5..9708eb128 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2628,6 +2628,7 @@ gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQb gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/telebot.v3 v3.1.2/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/warnings.v0 v0.1.1 h1:XM28wIgFzaBmeZ5dNHIpWLQpt/9DGKxk+rCg/22nnYE= +gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gorm.io/driver/postgres v1.3.5/go.mod h1:EGCWefLFQSVFrHGy4J8EtiHCWX5Q8t0yz2Jt9aKkGzU= diff --git a/pkg/pay/common.go b/pkg/pay/common.go new file mode 100644 index 000000000..98e0e097f --- /dev/null +++ b/pkg/pay/common.go @@ -0,0 +1,29 @@ +// Copyright © 2023 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pay + +const ( + PaymentSuccess = "success" + PaymentNotPaid = "not_paid" + PaymentProcessing = "processing" + PaymentExpired = "expired" + PaymentFailed = "failed" + PaymentUnknown = "unknown" +) + +const ( + DefaultSuccessURL string = "https://cloud.sealos.io" + DefaultCancelURL string = "https://cloud.sealos.io" +) diff --git a/pkg/pay/interface.go b/pkg/pay/interface.go new file mode 100644 index 000000000..a28dd1abd --- /dev/null +++ b/pkg/pay/interface.go @@ -0,0 +1,36 @@ +// Copyright © 2023 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pay + +import ( + "fmt" +) + +type Interface interface { + CreatePayment(amount int64, user string) (string, string, error) + GetPaymentDetails(sessionID string) (string, int64, error) + ExpireSession(payment string) error +} + +func NewPayHandler(paymentMethod string) (Interface, error) { + switch paymentMethod { + case "stripe": + return &StripePayment{}, nil + case "wechat": + return &WechatPayment{}, nil + default: + return nil, fmt.Errorf("unsupported payment method: %s", paymentMethod) + } +} diff --git a/pkg/pay/stripe.go b/pkg/pay/stripe.go new file mode 100644 index 000000000..6a943a793 --- /dev/null +++ b/pkg/pay/stripe.go @@ -0,0 +1,54 @@ +// Copyright © 2023 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pay + +import ( + "fmt" + + "github.com/stripe/stripe-go/v74" +) + +func (s StripePayment) CreatePayment(amount int64, _ string) (string, string, error) { + session, err := CreateCheckoutSession(amount, CNY, DefaultSuccessURL, DefaultCancelURL) + if err != nil { + return "", "", err + } + return session.ID, "", nil +} + +func (s StripePayment) GetPaymentDetails(sessionID string) (string, int64, error) { + ses, err := GetSession(sessionID) + if err != nil { + return "", 0, err + } + switch ses.Status { + case stripe.CheckoutSessionStatusComplete: + return PaymentSuccess, ses.AmountTotal, nil + case stripe.CheckoutSessionStatusExpired: + return PaymentExpired, 0, nil + case stripe.CheckoutSessionStatusOpen: + return PaymentProcessing, 0, nil + default: + return PaymentUnknown, 0, fmt.Errorf("unknown order status: %s", ses.Status) + } +} + +func (s StripePayment) ExpireSession(sessionID string) error { + _, err := ExpireSession(sessionID) + if err != nil { + return err + } + return nil +} diff --git a/pkg/pay/stripe_payment.go b/pkg/pay/stripe_payment.go index 880d1027e..6261a840c 100644 --- a/pkg/pay/stripe_payment.go +++ b/pkg/pay/stripe_payment.go @@ -16,6 +16,7 @@ package pay import ( "os" + "time" "github.com/stripe/stripe-go/v74" "github.com/stripe/stripe-go/v74/checkout/session" @@ -23,6 +24,9 @@ import ( const StripeAPIKEY = "STRIPE_API_KEY" +type StripePayment struct { +} + func init() { stripe.Key = os.Getenv(StripeAPIKEY) } @@ -33,11 +37,15 @@ const ( CNY = "cny" ) +const sessionExpirationTime = 30 * time.Minute + func CreateCheckoutSession(amount int64, currency, successURL, cancelURL string) (*stripe.CheckoutSession, error) { + expireAt := time.Now().UTC().Add(sessionExpirationTime).Unix() params := &stripe.CheckoutSessionParams{ PaymentMethodTypes: stripe.StringSlice([]string{ "card", }), + ExpiresAt: &expireAt, LineItems: []*stripe.CheckoutSessionLineItemParams{ { PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{ diff --git a/pkg/pay/wechat.go b/pkg/pay/wechat.go new file mode 100644 index 000000000..8a8378be8 --- /dev/null +++ b/pkg/pay/wechat.go @@ -0,0 +1,49 @@ +// Copyright © 2023 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pay + +import "fmt" + +func (w WechatPayment) CreatePayment(amount int64, user string) (string, string, error) { + tradeNO := GetRandomString(32) + codeURL, err := WechatPay(amount, user, tradeNO, "", "") + if err != nil { + return "", "", err + } + return tradeNO, codeURL, nil +} + +func (w WechatPayment) GetPaymentDetails(sessionID string) (string, int64, error) { + orderResp, err := QueryOrder(sessionID) + if err != nil { + return "", 0, err + } + switch *orderResp.TradeState { + case StatusSuccess: + return PaymentSuccess, *orderResp.Amount.Total, nil + case StatusProcessing: + return PaymentProcessing, 0, nil + case StatusNotPay: + return PaymentNotPaid, 0, nil + case StatusFail: + return PaymentFailed, 0, fmt.Errorf("order failed") + default: + return PaymentUnknown, 0, fmt.Errorf("unknown order status: %s", *orderResp.TradeState) + } +} + +func (w WechatPayment) ExpireSession(_ string) error { + return nil +} diff --git a/pkg/pay/wechat_payment.go b/pkg/pay/wechat_payment.go index a6f2bc34b..246d300dc 100644 --- a/pkg/pay/wechat_payment.go +++ b/pkg/pay/wechat_payment.go @@ -30,6 +30,9 @@ import ( "github.com/wechatpay-apiv3/wechatpay-go/utils" ) +type WechatPayment struct { +} + // ENV keys const ( WechatPrivateKey = "WechatPrivateKey"