Skip to content

Commit

Permalink
Feat/active task (#5121)
Browse files Browse the repository at this point in the history
* feat active task & first recharge discount
* sort discount step & add lock for payment
bxy4543 authored Oct 8, 2024
1 parent d9b34cd commit 351e761
Showing 15 changed files with 563 additions and 77 deletions.
30 changes: 23 additions & 7 deletions controllers/account/controllers/account_controller.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import (
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
@@ -276,21 +277,36 @@ const BaseUnit = 1_000_000
// return getAmountWithDiscount(amount, *discount), nil
//}

func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int64 {
if discount.SpecialDiscount != nil && discount.SpecialDiscount[amount/BaseUnit] != 0 {
return amount + discount.SpecialDiscount[amount/BaseUnit]*BaseUnit
}
func getAmountWithDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) int64 {
var r float64
for i, s := range discount.DiscountSteps {
if amount >= s*BaseUnit {
r = discount.DiscountRates[i]
for _, step := range sortSteps(discount.DefaultSteps) {
ratio := discount.DefaultSteps[step]
if amount >= step*BaseUnit {
r = ratio
} else {
break
}
}
return int64(math.Ceil(float64(amount) * r / 100))
}

func sortSteps(steps map[int64]float64) (keys []int64) {
for k := range steps {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})
return
}

func getFirstRechargeDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) (bool, int64) {
if discount.FirstRechargeSteps != nil && discount.FirstRechargeSteps[amount/BaseUnit] != 0 {
return true, int64(math.Ceil(float64(amount) * discount.FirstRechargeSteps[amount/BaseUnit] / 100))
}
return false, getAmountWithDiscount(amount, discount)
}

func (r *AccountReconciler) BillingCVM() error {
cvmMap, err := r.CVMDBClient.GetPendingStateInstance(os.Getenv("LOCAL_REGION"))
if err != nil {
85 changes: 63 additions & 22 deletions controllers/account/controllers/account_controller_test.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ package controllers

import (
"context"
"encoding/json"
"os"
"testing"

@@ -26,8 +27,6 @@ import (
"github.com/labring/sealos/controllers/pkg/database"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
"github.com/labring/sealos/controllers/pkg/database/mongo"

"github.com/labring/sealos/controllers/pkg/types"
)

//func Test_giveGift(t *testing.T) {
@@ -70,26 +69,26 @@ import (
// }
//}

func Test_getAmountWithDiscount(t *testing.T) {
type args struct {
amount int64
discount types.RechargeDiscount
}
tests := []struct {
name string
args args
want int64
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want {
t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want)
}
})
}
}
//func Test_getAmountWithDiscount(t *testing.T) {
// type args struct {
// amount int64
// discount types.RechargeDiscount
// }
// tests := []struct {
// name string
// args args
// want int64
// }{
// // TODO: Add test cases.
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want {
// t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want)
// }
// })
// }
//}

func TestAccountReconciler_BillingCVM(t *testing.T) {
dbCtx := context.Background()
@@ -136,3 +135,45 @@ func TestAccountReconciler_BillingCVM(t *testing.T) {
t.Errorf("AccountReconciler.BillingCVM() error = %v", err)
}
}

func TestAccountV2_GetAccountConfig(t *testing.T) {
os.Setenv("LOCAL_REGION", "")
v2Account, err := cockroach.NewCockRoach("", "")
if err != nil {
t.Fatalf("unable to connect to cockroach: %v", err)
}
defer func() {
err := v2Account.Close()
if err != nil {
t.Errorf("unable to disconnect from cockroach: %v", err)
}
}()
err = v2Account.InitTables()
if err != nil {
t.Fatalf("unable to init tables: %v", err)
}

//if err = v2Account.InsertAccountConfig(&types.AccountConfig{
// TaskProcessRegion: "192.160.0.55.nip.io",
// FirstRechargeDiscountSteps: map[int64]float64{
// 8: 100, 32: 100, 128: 100, 256: 100, 512: 100, 1024: 100,
// },
// DefaultDiscountSteps: map[int64]float64{
// //128,256,512,1024,2048,4096; 10,15,20,25,30,35
// 128: 10, 256: 15, 512: 20, 1024: 25, 2048: 30, 4096: 35,
// },
//}); err != nil {
// t.Fatalf("unable to insert account config: %v", err)
//}

aa, err := v2Account.GetAccountConfig()
if err != nil {
t.Fatalf("failed to get account config: %v", err)
}

data, err := json.MarshalIndent(aa, "", " ")
if err != nil {
t.Fatalf("failed to marshal account config: %v", err)
}
t.Logf("success get account config:\n%s", string(data))
}
67 changes: 39 additions & 28 deletions controllers/account/controllers/payment_controller.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@ import (
"sync"
"time"

"github.com/google/uuid"

"sigs.k8s.io/controller-runtime/pkg/manager"

pkgtypes "github.com/labring/sealos/controllers/pkg/types"
@@ -47,6 +49,8 @@ type PaymentReconciler struct {
Logger logr.Logger
reconcileDuration time.Duration
createDuration time.Duration
accountConfig pkgtypes.AccountConfig
userLock map[uuid.UUID]*sync.Mutex
domain string
}

@@ -69,13 +73,14 @@ const (
//+kubebuilder:rbac:groups=account.sealos.io,resources=payments/finalizers,verbs=update

// SetupWithManager sets up the controller with the Manager.
func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) (err error) {
const controllerName = "payment_controller"
r.Logger = ctrl.Log.WithName(controllerName)
r.Logger.V(1).Info("init reconcile controller payment")
r.domain = os.Getenv("DOMAIN")
r.reconcileDuration = defaultReconcileDuration
r.createDuration = defaultCreateDuration
r.userLock = make(map[uuid.UUID]*sync.Mutex)
if duration := os.Getenv(EnvPaymentReconcileDuration); duration != "" {
reconcileDuration, err := time.ParseDuration(duration)
if err == nil {
@@ -88,6 +93,14 @@ func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.createDuration = createDuration
}
}
r.accountConfig, err = r.Account.AccountV2.GetAccountConfig()
if err != nil {
return fmt.Errorf("get account config failed: %w", err)
}
if len(r.accountConfig.DefaultDiscountSteps) == 0 {
return fmt.Errorf("default discount steps is empty")
}
r.Logger.V(1).Info("account config", "config", r.accountConfig)
r.Logger.V(1).Info("reconcile duration", "reconcileDuration", r.reconcileDuration, "createDuration", r.createDuration)
if err := mgr.Add(r); err != nil {
return fmt.Errorf("add payment controller failed: %w", err)
@@ -142,22 +155,6 @@ func (r *PaymentReconciler) reconcilePayments(_ context.Context) (errs []error)
}

func (r *PaymentReconciler) reconcileCreatePayments(ctx context.Context) (errs []error) {
//paymentList := &accountv1.PaymentList{}
//listOpts := &client.ListOptions{
// FieldSelector: fields.OneTermEqualSelector("status.tradeNO", ""),
//}
//// handler old payment
//err := r.Client.List(context.Background(), paymentList, listOpts)
//if err != nil {
// errs = append(errs, fmt.Errorf("watch payment failed: %w", err))
// return
//}
//for _, payment := range paymentList.Items {
// if err := r.reconcileNewPayment(&payment); err != nil {
// errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err))
// }
//}
// watch new payment
watcher, err := r.WatchClient.Watch(context.Background(), &accountv1.PaymentList{}, &client.ListOptions{})
if err != nil {
errs = append(errs, fmt.Errorf("watch payment failed: %w", err))
@@ -211,20 +208,34 @@ func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error {
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}
if r.userLock[user.UID] == nil {
r.userLock[user.UID] = &sync.Mutex{}
}
r.userLock[user.UID].Lock()
defer r.userLock[user.UID].Unlock()
userDiscount, err := r.Account.AccountV2.GetUserRechargeDiscount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
if err != nil {
return fmt.Errorf("get user discount failed: %w", err)
}
//1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount
payAmount := orderAmount * 10000
gift := getAmountWithDiscount(payAmount, r.Account.DefaultDiscount)
isFirstRecharge, gift := getFirstRechargeDiscount(payAmount, userDiscount)
paymentRaw := pkgtypes.PaymentRaw{
UserUID: user.UID,
Amount: payAmount,
Gift: gift,
CreatedAt: payment.CreationTimestamp.Time,
RegionUserOwner: getUsername(payment.Namespace),
Method: payment.Spec.PaymentMethod,
TradeNO: payment.Status.TradeNO,
CodeURL: payment.Status.CodeURL,
}
if isFirstRecharge {
paymentRaw.ActivityType = pkgtypes.ActivityTypeFirstRecharge
}

if err = r.Account.AccountV2.Payment(&pkgtypes.Payment{
PaymentRaw: pkgtypes.PaymentRaw{
UserUID: user.UID,
Amount: payAmount,
Gift: gift,
CreatedAt: payment.CreationTimestamp.Time,
RegionUserOwner: getUsername(payment.Namespace),
Method: payment.Spec.PaymentMethod,
TradeNO: payment.Status.TradeNO,
CodeURL: payment.Status.CodeURL,
},
PaymentRaw: paymentRaw,
}); err != nil {
return fmt.Errorf("payment failed: %w", err)
}
206 changes: 204 additions & 2 deletions controllers/pkg/database/cockroach/accountv2.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
package cockroach

import (
"encoding/json"
"errors"
"fmt"
"log"
@@ -46,6 +47,8 @@ type Cockroach struct {
activities types.Activities
//TODO need init
defaultRechargeDiscount types.RechargeDiscount
accountConfig *types.AccountConfig
tasks map[uuid.UUID]types.Task
}

const (
@@ -120,6 +123,174 @@ func (c *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.User, error) {
return &user, nil
}

func cloneMap(m map[int64]float64) map[int64]float64 {
newMap := make(map[int64]float64, len(m))
for k, v := range m {
newMap[k] = v
}
return newMap
}

func (c *Cockroach) GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) {
if ops.UID == uuid.Nil {
user, err := c.GetUser(ops)
if err != nil {
return types.UserRechargeDiscount{}, fmt.Errorf("failed to get user cr: %v", err)
}
ops.UID = user.UID
}
cfg, err := c.GetAccountConfig()
if err != nil {
return types.UserRechargeDiscount{}, fmt.Errorf("failed to get account config: %v", err)
}
isFirstRecharge, err := c.IsNullRecharge(ops)
if err != nil {
return types.UserRechargeDiscount{}, fmt.Errorf("failed to check is null recharge: %v", err)
}
defaultSteps, firstRechargeSteps := cfg.DefaultDiscountSteps, cloneMap(cfg.FirstRechargeDiscountSteps)
if !isFirstRecharge && firstRechargeSteps != nil {
payments, err := c.getFirstRechargePayments(ops)
if err != nil {
return types.UserRechargeDiscount{}, fmt.Errorf("failed to get first recharge payments: %v", err)
}
if len(payments) == 0 {
firstRechargeSteps = map[int64]float64{}
} else {
for i := range payments {
delete(firstRechargeSteps, payments[i].Amount/BaseUnit)
}
}
}
return types.UserRechargeDiscount{
DefaultSteps: defaultSteps,
FirstRechargeSteps: firstRechargeSteps,
}, nil
}

func (c *Cockroach) GetAccountConfig() (types.AccountConfig, error) {
if c.accountConfig == nil {
config := &types.Configs{}
if err := c.DB.Where(&types.Configs{}).First(config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return types.AccountConfig{}, nil
}
return types.AccountConfig{}, fmt.Errorf("failed to get account config: %v", err)
}
var accountConfig types.AccountConfig
if err := json.Unmarshal([]byte(config.Data), &accountConfig); err != nil {
return types.AccountConfig{}, fmt.Errorf("failed to unmarshal account config: %v", err)
}
c.accountConfig = &accountConfig
}
return *c.accountConfig, nil
}

func (c *Cockroach) InsertAccountConfig(config *types.AccountConfig) error {
data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal account config: %v", err)
}
return c.DB.Model(&types.Configs{}).Create(&types.Configs{Type: types.AccountConfigType, Data: string(data)}).Error
}

func (c *Cockroach) IsNullRecharge(ops *types.UserQueryOpts) (bool, error) {
if ops.UID == uuid.Nil {
user, err := c.GetUser(ops)
if err != nil {
return false, fmt.Errorf("failed to get user: %v", err)
}
ops.UID = user.UID
}
var count int64
if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).
Count(&count).Error; err != nil {
return false, fmt.Errorf("failed to get payment count: %v", err)
}
return count == 0, nil
}

func (c *Cockroach) getFirstRechargePayments(ops *types.UserQueryOpts) ([]types.Payment, error) {
if ops.UID == uuid.Nil {
user, err := c.GetUser(ops)
if err != nil {
return nil, fmt.Errorf("failed to get user: %v", err)
}
ops.UID = user.UID
}
var payments []types.Payment
if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where(`"activityType" = ?`, types.ActivityTypeFirstRecharge).
Find(&payments).Error; err != nil {
return nil, fmt.Errorf("failed to get payment count: %v", err)
}
return payments, nil
}

func (c *Cockroach) ProcessPendingTaskRewards() error {
userTasks, err := c.getPendingRewardUserTask()
if err != nil {
return fmt.Errorf("failed to get pending reward user task: %w", err)
}
tasks, err := c.getTask()
if err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for i := range userTasks {
err = c.DB.Transaction(func(tx *gorm.DB) error {
task := tasks[userTasks[i].TaskID]
if task.Reward == 0 {
fmt.Printf("usertask %v reward is 0, skip\n", userTasks[i])
return nil
}
if err = c.updateBalanceRaw(tx, &types.UserQueryOpts{UID: userTasks[i].UserUID}, task.Reward, false, true, true); err != nil {
return fmt.Errorf("failed to update balance: %w", err)
}
msg := fmt.Sprintf("task %s reward", task.Title)
transaction := types.AccountTransaction{
Balance: task.Reward,
Type: string(task.TaskType) + "_Reward",
UserUID: userTasks[i].UserUID,
ID: uuid.New(),
Message: &msg,
BillingID: userTasks[i].ID,
}
if err = tx.Save(&transaction).Error; err != nil {
return fmt.Errorf("failed to save transaction: %w", err)
}
return c.completeRewardUserTask(tx, &userTasks[i])
})
if err != nil {
return fmt.Errorf("failed to process reward pending user task %v rewards: %w", userTasks[i], err)
}
}
return nil
}

func (c *Cockroach) getTask() (map[uuid.UUID]types.Task, error) {
if len(c.tasks) != 0 {
return c.tasks, nil
}
c.tasks = make(map[uuid.UUID]types.Task)
var tasks []types.Task
if err := c.DB.Model(&types.Task{IsActive: true, IsNewUserTask: true}).Find(&tasks).Error; err != nil {
return nil, fmt.Errorf("failed to get tasks: %v", err)
}
for i := range tasks {
c.tasks[tasks[i].ID] = tasks[i]
}
return c.tasks, nil
}

func (c *Cockroach) getPendingRewardUserTask() ([]types.UserTask, error) {
var userTasks []types.UserTask
return userTasks, c.DB.Where(&types.UserTask{Status: types.TaskStatusCompleted, RewardStatus: types.TaskStatusNotCompleted}).
Find(&userTasks).Error
}

func (c *Cockroach) completeRewardUserTask(tx *gorm.DB, userTask *types.UserTask) error {
userTask.RewardStatus = types.TaskStatusCompleted
return tx.Model(userTask).Update("rewardStatus", types.TaskStatusCompleted).Error
}

func (c *Cockroach) GetUserCr(ops *types.UserQueryOpts) (*types.RegionUserCr, error) {
if ops.UID == uuid.Nil && ops.Owner == "" {
if ops.ID == "" {
@@ -314,6 +485,10 @@ func (c *Cockroach) GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.Oaut
}

func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool) error {
return c.updateBalanceRaw(tx, ops, amount, isDeduction, add, false)
}

func (c *Cockroach) updateBalanceRaw(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool, isActive bool) error {
if ops.UID == uuid.Nil {
user, err := c.GetUserCr(ops)
if err != nil {
@@ -334,6 +509,9 @@ func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount
if err := c.updateWithAccount(isDeduction, add, account, amount); err != nil {
return err
}
if isActive {
account.ActivityBonus = account.ActivityBonus + amount
}
if err := tx.Save(account).Error; err != nil {
return fmt.Errorf("failed to update account balance: %w", err)
}
@@ -359,6 +537,12 @@ func (c *Cockroach) AddBalance(ops *types.UserQueryOpts, amount int64) error {
})
}

func (c *Cockroach) AddRewardBalance(ops *types.UserQueryOpts, amount int64, db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
return c.updateBalance(tx, ops, amount, false, true)
})
}

func (c *Cockroach) ReduceBalance(ops *types.UserQueryOpts, amount int64) error {
return c.DB.Transaction(func(tx *gorm.DB) error {
return c.updateBalance(tx, ops, amount, false, false)
@@ -856,6 +1040,7 @@ func (c *Cockroach) NewAccount(ops *types.UserQueryOpts) (*types.Account, error)
return account, nil
}

// //TODO: remove this method
func (c *Cockroach) GetUserAccountRechargeDiscount(ops *types.UserQueryOpts) (*types.RechargeDiscount, error) {
userID := ops.UID
if userID == uuid.Nil {
@@ -958,7 +1143,7 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64,
return fmt.Errorf("insufficient balance in sender account, sender is %v, transfer amount %d, the transferable amount is: %d", sender, amount, sender.Balance-sender.DeductionBalance-MinBalance-sender.ActivityBonus)
}
} else {
amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance
amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance - sender.ActivityBonus
if amount <= 0 {
return ErrInsufficientBalance
}
@@ -987,7 +1172,24 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64,
}

func (c *Cockroach) InitTables() error {
return CreateTableIfNotExist(c.DB, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{})
err := CreateTableIfNotExist(c.DB, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{}, types.Configs{})
if err != nil {
return fmt.Errorf("failed to create table: %v", err)
}

// TODO: remove this after migration
if !c.DB.Migrator().HasColumn(&types.Payment{}, `activityType`) {
//if err := c.DB.Migrator().AddColumn(&types.Payment{PaymentRaw: types.PaymentRaw{}}, `PaymentRaw."activityType"`); err != nil {
// return fmt.Errorf("failed to add column activityType: %v", err)
//}
fmt.Println("add column activityType")
tableName := types.Payment{}.TableName()
err := c.DB.Exec(`ALTER TABLE "?" ADD COLUMN "activityType" TEXT;`, gorm.Expr(tableName)).Error
if err != nil {
return fmt.Errorf("failed to add column activityType: %v", err)
}
}
return nil
}

func NewCockRoach(globalURI, localURI string) (*Cockroach, error) {
4 changes: 4 additions & 0 deletions controllers/pkg/database/interface.go
Original file line number Diff line number Diff line change
@@ -95,10 +95,14 @@ type AccountV2 interface {
GetUserCr(user *types.UserQueryOpts) (*types.RegionUserCr, error)
GetUser(ops *types.UserQueryOpts) (*types.User, error)
GetAccount(user *types.UserQueryOpts) (*types.Account, error)
GetAccountConfig() (types.AccountConfig, error)
InsertAccountConfig(config *types.AccountConfig) error
GetRegions() ([]types.Region, error)
GetLocalRegion() types.Region
GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.OauthProvider, error)
GetWorkspace(namespaces ...string) ([]types.Workspace, error)
GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error)
//TODO will be removed this method
GetUserAccountRechargeDiscount(user *types.UserQueryOpts) (*types.RechargeDiscount, error)
SetAccountCreateLocalRegion(account *types.Account, region string) error
CreateUser(oAuth *types.OauthProvider, regionUserCr *types.RegionUserCr, user *types.User, workspace *types.Workspace, userWorkspace *types.UserWorkspace) error
6 changes: 6 additions & 0 deletions controllers/pkg/types/activity.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,12 @@ import (
"gorm.io/gorm"
)

type UserRechargeDiscount struct {
DefaultSteps map[int64]float64 `json:"defaultSteps,omitempty" bson:"defaultSteps,omitempty"`
FirstRechargeSteps map[int64]float64 `json:"firstRechargeDiscount,omitempty" bson:"firstRechargeDiscount,omitempty"`
}

// TODO the following structures will be deleted
type Activity struct {
gorm.Model
ActivityType string `gorm:"uniqueIndex"`
36 changes: 36 additions & 0 deletions controllers/pkg/types/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2024.
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 types

type Configs struct {
Type ConfigType `json:"type" gorm:"type:varchar(255);not null,primaryKey"`
Data string `json:"data" gorm:"type:jsonb"`
}

type ConfigType string

const AccountConfigType ConfigType = "account"

type AccountConfig struct {
TaskProcessRegion string `json:"taskProcessRegion"`
FirstRechargeDiscountSteps map[int64]float64 `json:"firstRechargeDiscountSteps"`
DefaultDiscountSteps map[int64]float64 `json:"defaultDiscountSteps"`
}

func (c Configs) TableName() string {
return "Configs"
}
15 changes: 11 additions & 4 deletions controllers/pkg/types/global.go
Original file line number Diff line number Diff line change
@@ -237,12 +237,19 @@ type PaymentRaw struct {
Gift int64 `gorm:"type:bigint"`
TradeNO string `gorm:"type:text;unique;not null"`
// CodeURL is the codeURL of wechatpay
CodeURL string `gorm:"type:text"`
InvoicedAt bool `gorm:"type:boolean;default:false"`
Remark string `gorm:"type:text"`
Message string `gorm:"type:text;not null"`
CodeURL string `gorm:"type:text"`
InvoicedAt bool `gorm:"type:boolean;default:false"`
Remark string `gorm:"type:text"`
ActivityType ActivityType `gorm:"type:text;column:activityType"`
Message string `gorm:"type:text;not null"`
}

type ActivityType string

const (
ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE"
)

func (ErrorPaymentCreate) TableName() string {
return "ErrorPaymentCreate"
}
80 changes: 80 additions & 0 deletions controllers/pkg/types/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright 2024.
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 types

import (
"time"

"github.com/google/uuid"
)

// Task represents the Task model in Go with GORM annotations.
type Task struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"`
Title string `gorm:"column:title;type:text;not null" json:"title"`
Description string `gorm:"column:description;type:text;not null" json:"description"`
Reward int64 `gorm:"column:reward;type:bigint;not null" json:"reward"`
Order int `gorm:"column:order;type:integer;not null" json:"order"`
IsActive bool `gorm:"column:isActive;type:boolean;default:true;not null" json:"isActive"`
IsNewUserTask bool `gorm:"column:isNewUserTask;type:boolean;default:false;not null" json:"isNewUserTask"`
TaskType TaskType `gorm:"column:taskType;type:TaskType;not null" json:"taskType"`
CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"`
}

// UserTask represents the UserTask model in Go with GORM annotations.
type UserTask struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"`
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null" json:"userUid"`
TaskID uuid.UUID `gorm:"column:taskId;type:uuid;not null" json:"taskId"`
Status TaskStatus `gorm:"column:status;type:TaskStatus;not null" json:"status"`
RewardStatus TaskStatus `gorm:"column:rewardStatus;type:TaskStatus;not null" json:"rewardStatus"`
CompletedAt time.Time `gorm:"column:completedAt;type:timestamp(3);not null" json:"completedAt"`
CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"`

//User User `gorm:"foreignKey:UserUid;references:UID" json:"user"`
//Task Task `gorm:"foreignKey:TaskId;references:ID" json:"task"`
}

// TableName specifies the table name for GORM
func (Task) TableName() string {
return "Task"
}

// TableName specifies the table name for GORM
func (UserTask) TableName() string {
return "UserTask"
}

// TaskType represents the TaskType enum in Go.
type TaskType string

//const (
// TaskTypeLaunchpad TaskType = "LAUNCHPAD"
// TaskTypeCostcenter TaskType = "COSTCENTER"
// TaskTypeDatabase TaskType = "DATABASE"
// TaskTypeDesktop TaskType = "DESKTOP"
//)

// TaskStatus represents the TaskStatus enum in Go.
type TaskStatus string

const (
TaskStatusNotCompleted TaskStatus = "NOT_COMPLETED"
TaskStatusCompleted TaskStatus = "COMPLETED"
)
33 changes: 31 additions & 2 deletions service/account/api/api.go
Original file line number Diff line number Diff line change
@@ -726,8 +726,9 @@ func ParseAuthTokenUser(c *gin.Context) (auth *helper.Auth, err error) {
return nil, fmt.Errorf("invalid user: %v", user)
}
auth = &helper.Auth{
Owner: user.UserCrName,
UserID: user.UserID,
Owner: user.UserCrName,
UserID: user.UserID,
UserUID: user.UserUID,
}
// if the user is not in the local region, get the user cr name from db
if dao.DBClient.GetLocalRegion().UID.String() != user.RegionUID {
@@ -977,6 +978,34 @@ func UserUsage(c *gin.Context) {
})
}

// GetRechargeDiscount
// @Summary Get recharge discount
// @Description Get recharge discount
// @Tags RechargeDiscount
// @Accept json
// @Produce json
// @Param request body helper.GetRechargeDiscountReq true "Get recharge discount request"
// @Success 200 {object} map[string]interface{} "successfully get recharge discount"
// @Failure 400 {object} map[string]interface{} "failed to parse get recharge discount request"
// @Failure 401 {object} map[string]interface{} "authenticate error"
// @Failure 500 {object} map[string]interface{} "failed to get recharge discount"
// @Router /account/v1alpha1/recharge/discount [post]
func GetRechargeDiscount(c *gin.Context) {
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
discount, err := dao.DBClient.GetRechargeDiscount(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get recharge discount : %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"discount": discount,
})
}

// GetUserRealNameInfo
// @Summary Get user real name information
// @Description Retrieve the real name information for a user
18 changes: 18 additions & 0 deletions service/account/dao/interface.go
Original file line number Diff line number Diff line change
@@ -57,6 +57,8 @@ type Interface interface {
GetRegions() ([]types.Region, error)
GetLocalRegion() types.Region
UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, error)
GetRechargeDiscount(req helper.AuthReq) (helper.RechargeDiscountResp, error)
ProcessPendingTaskRewards() error
GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error)
}

@@ -1446,6 +1448,22 @@ func (m *Account) UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, erro
return giftCode, nil
}

func (m *Account) GetRechargeDiscount(req helper.AuthReq) (helper.RechargeDiscountResp, error) {
userQuery := &types.UserQueryOpts{UID: req.GetAuth().UserUID}
userDiscount, err := m.ck.GetUserRechargeDiscount(userQuery)
if err != nil {
return helper.RechargeDiscountResp{}, fmt.Errorf("failed to get user recharge discount: %v", err)
}
return helper.RechargeDiscountResp{
DefaultSteps: userDiscount.DefaultSteps,
FirstRechargeSteps: userDiscount.FirstRechargeSteps,
}, nil
}

func (m *Account) ProcessPendingTaskRewards() error {
return m.ck.ProcessPendingTaskRewards()
}

func (m *Account) GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error) {
// get user info
userRealNameInfo, err := m.ck.GetUserRealNameInfoByUserID(req.UserID)
1 change: 1 addition & 0 deletions service/account/helper/common.go
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ const (
GetInvoicePayment = "/invoice/get-payment"
UseGiftCode = "/gift-code/use"
UserUsage = "/user-usage"
GetRechargeDiscount = "/recharge-discount"
GetUserRealNameInfo = "/real-name-info"
)

16 changes: 9 additions & 7 deletions service/account/helper/jwt.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import (
"strings"
"time"

"github.com/google/uuid"

"github.com/gin-gonic/gin"

"github.com/golang-jwt/jwt"
@@ -21,13 +23,13 @@ type UserClaims struct {
}

type JwtUser struct {
UserUID string `json:"userUid,omitempty"`
UserCrUID string `json:"userCrUid,omitempty"`
UserCrName string `json:"userCrName,omitempty"`
RegionUID string `json:"regionUid,omitempty"`
UserID string `json:"userId,omitempty"`
WorkspaceID string `json:"workspaceId,omitempty"`
WorkspaceUID string `json:"workspaceUid,omitempty"`
UserUID uuid.UUID `json:"userUid,omitempty"`
UserCrUID string `json:"userCrUid,omitempty"`
UserCrName string `json:"userCrName,omitempty"`
RegionUID string `json:"regionUid,omitempty"`
UserID string `json:"userId,omitempty"`
WorkspaceID string `json:"workspaceId,omitempty"`
WorkspaceUID string `json:"workspaceUid,omitempty"`
}

func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
16 changes: 12 additions & 4 deletions service/account/helper/request.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import (
"fmt"
"time"

"github.com/google/uuid"

"github.com/labring/sealos/service/account/common"

"github.com/dustin/go-humanize"
@@ -292,6 +294,11 @@ func (a *AuthBase) SetAuth(auth *Auth) {
a.Auth = auth
}

type RechargeDiscountResp struct {
DefaultSteps map[int64]float64 `json:"defaultSteps,omitempty" bson:"defaultSteps,omitempty"`
FirstRechargeSteps map[int64]float64 `json:"firstRechargeDiscount,omitempty" bson:"firstRechargeDiscount,omitempty"`
}

type NamespaceBillingHistoryResp struct {
Data NamespaceBillingHistoryRespData `json:"data,omitempty" bson:"data,omitempty"`
Message string `json:"message,omitempty" bson:"message" example:"successfully retrieved namespace list"`
@@ -320,10 +327,11 @@ type TimeRange struct {
}

type Auth struct {
Owner string `json:"owner" bson:"owner" example:"admin"`
UserID string `json:"userID" bson:"userID" example:"admin"`
KubeConfig string `json:"kubeConfig" bson:"kubeConfig"`
Token string `json:"token" bson:"token" example:"token"`
Owner string `json:"owner" bson:"owner" example:"admin"`
UserUID uuid.UUID `json:"userUID" bson:"userUID" example:"user-123"`
UserID string `json:"userID" bson:"userID" example:"admin"`
KubeConfig string `json:"kubeConfig" bson:"kubeConfig"`
Token string `json:"token" bson:"token" example:"token"`
}

func ParseNamespaceBillingHistoryReq(c *gin.Context) (*NamespaceBillingHistoryReq, error) {
27 changes: 26 additions & 1 deletion service/account/router/router.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"os"
"os/signal"
"syscall"
"time"

"github.com/labring/sealos/controllers/pkg/utils/env"

@@ -30,8 +31,9 @@ func RegisterPayRouter() {
if err := dao.InitDB(); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
ctx := context.Background()
defer func() {
if err := dao.DBClient.Disconnect(context.Background()); err != nil {
if err := dao.DBClient.Disconnect(ctx); err != nil {
log.Fatalf("Error disconnecting database: %v", err)
}
}()
@@ -63,6 +65,7 @@ func RegisterPayRouter() {
POST(helper.GetInvoicePayment, api.GetInvoicePayment).
POST(helper.UseGiftCode, api.UseGiftCode).
POST(helper.UserUsage, api.UserUsage).
POST(helper.GetRechargeDiscount, api.GetRechargeDiscount).
POST(helper.GetUserRealNameInfo, api.GetUserRealNameInfo)
docs.SwaggerInfo.Host = env.GetEnvWithDefault("SWAGGER_HOST", "localhost:2333")
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
@@ -80,9 +83,31 @@ func RegisterPayRouter() {
}
}()

// process task
if os.Getenv("REWARD_PROCESSING") == "true" {
fmt.Println("Start reward processing timer")
go startRewardProcessingTimer(ctx)
}

// Wait for interrupt signal.
<-interrupt

// Terminate procedure.
os.Exit(0)
}

func startRewardProcessingTimer(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := dao.DBClient.ProcessPendingTaskRewards(); err != nil {
log.Printf("Error processing pending task rewards: %v", err)
}
case <-ctx.Done():
log.Println("Reward processing timer stopped")
return
}
}
}

0 comments on commit 351e761

Please sign in to comment.