feat: develop payment interface (#3610)

* feat: develop Payment interface
This commit is contained in:
xy
2023-08-01 17:30:04 +08:00
committed by GitHub
parent 9e8b703e1e
commit 0b2dd6da78
9 changed files with 223 additions and 18 deletions
@@ -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
}
@@ -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
+1
View File
@@ -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=
+29
View File
@@ -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"
)
+36
View File
@@ -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)
}
}
+54
View File
@@ -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
}
+8
View File
@@ -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{
+49
View File
@@ -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
}
+3
View File
@@ -30,6 +30,9 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)
type WechatPayment struct {
}
// ENV keys
const (
WechatPrivateKey = "WechatPrivateKey"