mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
feat: develop payment interface (#3610)
* feat: develop Payment interface
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -30,6 +30,9 @@ import (
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
type WechatPayment struct {
|
||||
}
|
||||
|
||||
// ENV keys
|
||||
const (
|
||||
WechatPrivateKey = "WechatPrivateKey"
|
||||
|
||||
Reference in New Issue
Block a user