Skip to content

Commit

Permalink
Feat/gift balance (#4390)
Browse files Browse the repository at this point in the history
* init

* optimize && add query

* optimize && add query

* go mod tidy

* rename activity field

* fix get activity discount

* fix update anno

* save bonus amount to db
  • Loading branch information
bxy4543 committed Dec 7, 2023
1 parent 17fe9c2 commit 5312580
Show file tree
Hide file tree
Showing 19 changed files with 781 additions and 105 deletions.
3 changes: 3 additions & 0 deletions controllers/account/api/v1/account_types.go
Expand Up @@ -36,6 +36,7 @@ const (
Recharge
TransferIn
TransferOut
ActivityGiving
)

const QueryAllType Type = -1
Expand Down Expand Up @@ -92,6 +93,8 @@ type AccountStatus struct {
EncryptBalance *string `json:"encryptBalance,omitempty"`
// Recharge amount
Balance int64 `json:"balance,omitempty"`
// ActivityBonus: for demonstration purposes only and does not participate in calculation
ActivityBonus int64 `json:"activityBonus,omitempty"`
//Deduction amount
DeductionBalance int64 `json:"deductionBalance,omitempty"`
// EncryptDeductionBalance is to encrypt DeductionBalance
Expand Down
1 change: 1 addition & 0 deletions controllers/account/api/v1/billinginfoquery_types.go
Expand Up @@ -27,6 +27,7 @@ const (
QueryTypeNamespacesHistory = "NamespacesHistory"
QueryTypeProperties = "Properties"
QueryTypeAppType = "AppType"
QueryTypeRecharge = "Recharge"
)

// BillingInfoQuerySpec defines the desired state of BillingInfoQuery
Expand Down
Expand Up @@ -52,6 +52,11 @@ spec:
status:
description: AccountStatus defines the observed state of Account
properties:
activityBonus:
description: 'ActivityBonus: for demonstration purposes only and does
not participate in calculation'
format: int64
type: integer
balance:
description: Recharge amount
format: int64
Expand Down
130 changes: 104 additions & 26 deletions controllers/account/controllers/account_controller.go
Expand Up @@ -18,13 +18,18 @@ package controllers

import (
"context"
"encoding/json"
"fmt"
"math"
"os"
"strconv"
"strings"
"time"

"k8s.io/client-go/rest"

"k8s.io/client-go/kubernetes"

"github.com/go-logr/logr"
gonanoid "github.com/matoous/go-nanoid/v2"

Expand All @@ -33,15 +38,14 @@ import (
"github.com/labring/sealos/controllers/pkg/database"
"github.com/labring/sealos/controllers/pkg/pay"
"github.com/labring/sealos/controllers/pkg/resources"
pkgtypes "github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/controllers/pkg/utils/env"
"github.com/labring/sealos/controllers/pkg/utils/retry"
userv1 "github.com/labring/sealos/controllers/user/api/v1"

corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
cretry "k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
Expand Down Expand Up @@ -73,6 +77,9 @@ type AccountReconciler struct {
AccountSystemNamespace string
DBClient database.Account
MongoDBURI string
Activities pkgtypes.Activities
RechargeStep []int64
RechargeRatio []float64
}

//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -143,12 +150,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
now := time.Now().UTC()
//1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount
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 {
r.Logger.Error(err, "get recharge-gift ConfigMap failed")
}
gift, err := giveGift(payAmount, configMap)
updateAnno, gift, err := r.getAmountWithRates(payAmount, account)
if err != nil {
r.Logger.Error(err, "get gift error")
}
Expand All @@ -157,12 +159,18 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, fmt.Errorf("recharge encrypt balance failed: %v", err)
}
if err := SyncAccountStatus(ctx, r.Client, account); err != nil {
return ctrl.Result{}, fmt.Errorf("update account failed: %v", err)
return ctrl.Result{}, fmt.Errorf("update account status failed: %v", err)
}
payment.Status.Status = pay.PaymentSuccess
if err := r.Status().Update(ctx, payment); err != nil {
return ctrl.Result{}, fmt.Errorf("update payment failed: %v", err)
}
if len(updateAnno) > 0 {
account.Annotations = updateAnno
if err := r.Update(ctx, account); err != nil {
return ctrl.Result{}, fmt.Errorf("update account failed: %v", err)
}
}

id, err := gonanoid.New(12)
if err != nil {
Expand Down Expand Up @@ -436,6 +444,62 @@ func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controll
Complete(r)
}

func RawParseRechargeConfig() (activities pkgtypes.Activities, discountsteps []int64, discountratios []float64, returnErr error) {
// local test
//config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
//if err != nil {
// fmt.Printf("Error building kubeconfig: %v\n", err)
// os.Exit(1)
//}
config, err := rest.InClusterConfig()
if err != nil {
returnErr = fmt.Errorf("get in cluster config failed: %v", err)
return
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
returnErr = fmt.Errorf("get clientset failed: %v", err)
return
}
configMap, err := clientset.CoreV1().ConfigMaps(SEALOS).Get(context.TODO(), RECHARGEGIFT, metav1.GetOptions{})
if err != nil {
returnErr = fmt.Errorf("get configmap failed: %v", err)
return
}
if returnErr = parseConfigList(configMap.Data["steps"], &discountsteps, "steps"); returnErr != nil {
return
}

if returnErr = parseConfigList(configMap.Data["ratios"], &discountratios, "ratios"); returnErr != nil {
return
}

if activityStr := configMap.Data["activities"]; activityStr != "" {
returnErr = json.Unmarshal([]byte(activityStr), &activities)
}
return
}

func parseConfigList(s string, list interface{}, configName string) error {
for _, v := range strings.Split(s, ",") {
switch list := list.(type) {
case *[]int64:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("%s format error: %v", configName, err)
}
*list = append(*list, i)
case *[]float64:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return fmt.Errorf("%s format error: %v", configName, err)
}
*list = append(*list, f)
}
}
return nil
}

func GetUserOwner(user *userv1.User) string {
own := user.Annotations[userv1.UserAnnotationOwnerKey]
if own == "" {
Expand Down Expand Up @@ -467,28 +531,42 @@ func (p *NamespaceFilterPredicate) Generic(e event.GenericEvent) bool {

const BaseUnit = 1_000_000

func giveGift(amount int64, configMap *corev1.ConfigMap) (int64, error) {
if configMap.Data == nil {
return amount, fmt.Errorf("configMap's data is nil")
func (r *AccountReconciler) getAmountWithRates(amount int64, account *accountv1.Account) (anno map[string]string, amt int64, err error) {
userActivities, err := pkgtypes.ParseUserActivities(account.Annotations)
if err != nil {
return nil, 0, fmt.Errorf("parse user activities failed: %w", err)
}
stepsStr := strings.Split(configMap.Data["steps"], ",")
ratiosStr := strings.Split(configMap.Data["ratios"], ",")

var ratio float64

for i, stepStr := range stepsStr {
step, err := strconv.ParseInt(stepStr, 10, 64)
if err != nil {
return amount, fmt.Errorf("steps format error :%s", err)
}
if amount >= step*BaseUnit {
ratio, err = strconv.ParseFloat(ratiosStr[i], 32)
if err != nil {
return amount, fmt.Errorf("ratios format error :%s", err)
rechargeDiscount := pkgtypes.RechargeDiscount{
DiscountSteps: r.RechargeStep,
DiscountRates: r.RechargeRatio,
}
if len(userActivities) > 0 {
if activityType, phase, _ := pkgtypes.GetUserActivityDiscount(r.Activities, &userActivities); phase != nil {
if len(phase.RechargeDiscount.DiscountSteps) > 0 {
rechargeDiscount.DiscountSteps = phase.RechargeDiscount.DiscountSteps
rechargeDiscount.DiscountRates = phase.RechargeDiscount.DiscountRates
}
rechargeDiscount.SpecialDiscount = phase.RechargeDiscount.SpecialDiscount
rechargeDiscount = phase.RechargeDiscount
currentPhase := userActivities[activityType].Phases[userActivities[activityType].CurrentPhase]
anno = pkgtypes.SetUserPhaseRechargeTimes(account.Annotations, activityType, currentPhase.Name, currentPhase.RechargeNums+1)
}
}
return anno, getAmountWithDiscount(amount, rechargeDiscount), 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
}
var r float64
for i, s := range discount.DiscountSteps {
if amount >= s*BaseUnit {
r = discount.DiscountRates[i]
} else {
break
}
}
return int64(math.Ceil(float64(amount)*ratio/100)) + amount, nil
return int64(math.Ceil(float64(amount)*r)) + amount
}
73 changes: 47 additions & 26 deletions controllers/account/controllers/account_controller_test.go
Expand Up @@ -19,44 +19,65 @@ package controllers
import (
"testing"

corev1 "k8s.io/api/core/v1"
"github.com/labring/sealos/controllers/pkg/types"
)

func Test_giveGift(t *testing.T) {
//func Test_giveGift(t *testing.T) {
// type args struct {
// amount int64
// }
// const BaseUnit = 1_000_000
// tests := []struct {
// name string
// args args
// want int64
// }{
// // [1-298] -> 0%, [299-598] -> 10%, [599-1998] -> 15%, [1999-4998] -> 20%, [4999-19998] -> 25%, [19999+] -> 30%
// {name: "0% less than 299", args: args{amount: 100 * BaseUnit}, want: 0 + 100*BaseUnit},
// {name: "10% between 299 and 599", args: args{amount: 299 * BaseUnit}, want: 29_900_000 + 299*BaseUnit},
// {name: "10% between 299 and 599", args: args{amount: 300 * BaseUnit}, want: 30_000_000 + 300*BaseUnit},
// {name: "15% between 599 and 1999", args: args{amount: 599 * BaseUnit}, want: 89_850_000 + 599*BaseUnit},
// {name: "15% between 599 and 1999", args: args{amount: 600 * BaseUnit}, want: 90_000_000 + 600*BaseUnit},
// {name: "20% between 1999 and 4999", args: args{amount: 1999 * BaseUnit}, want: 399_800_000 + 1999*BaseUnit},
// {name: "20% between 1999 and 4999", args: args{amount: 2000 * BaseUnit}, want: 400_000_000 + 2000*BaseUnit},
// {name: "25% between 4999 and 19999", args: args{amount: 4999 * BaseUnit}, want: 1249_750_000 + 4999*BaseUnit},
// {name: "25% between 4999 and 19999", args: args{amount: 5000 * BaseUnit}, want: 1250_000_000 + 5000*BaseUnit},
// {name: "30% more than 19999", args: args{amount: 19999 * BaseUnit}, want: 5999_700_000 + 19999*BaseUnit},
// {name: "30% more than 19999", args: args{amount: 20000 * BaseUnit}, want: 6000_000_000 + 20000*BaseUnit},
// {name: "30% more than 19999", args: args{amount: 99999 * BaseUnit}, want: 29999_700_000 + 99999*BaseUnit},
// {"0% less than 299", args{amount: 1 * BaseUnit}, 1 * BaseUnit},
// }
//
// configMap := &corev1.ConfigMap{}
// configMap.Data = make(map[string]string)
// configMap.Data["steps"] = "299,599,1999,4999,19999"
// configMap.Data["ratios"] = "10.0,15.0,20.0,25.0,30.0"
//
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if got, _ := giveGift(tt.args.amount, configMap); got != tt.want {
// t.Errorf("giveGift() = %v, want %v", got, tt.want)
// }
// })
// }
//}

func Test_getAmountWithDiscount(t *testing.T) {
type args struct {
amount int64
amount int64
discount types.RechargeDiscount
}
const BaseUnit = 1_000_000
tests := []struct {
name string
args args
want int64
}{
// [1-298] -> 0%, [299-598] -> 10%, [599-1998] -> 15%, [1999-4998] -> 20%, [4999-19998] -> 25%, [19999+] -> 30%
{name: "0% less than 299", args: args{amount: 100 * BaseUnit}, want: 0 + 100*BaseUnit},
{name: "10% between 299 and 599", args: args{amount: 299 * BaseUnit}, want: 29_900_000 + 299*BaseUnit},
{name: "10% between 299 and 599", args: args{amount: 300 * BaseUnit}, want: 30_000_000 + 300*BaseUnit},
{name: "15% between 599 and 1999", args: args{amount: 599 * BaseUnit}, want: 89_850_000 + 599*BaseUnit},
{name: "15% between 599 and 1999", args: args{amount: 600 * BaseUnit}, want: 90_000_000 + 600*BaseUnit},
{name: "20% between 1999 and 4999", args: args{amount: 1999 * BaseUnit}, want: 399_800_000 + 1999*BaseUnit},
{name: "20% between 1999 and 4999", args: args{amount: 2000 * BaseUnit}, want: 400_000_000 + 2000*BaseUnit},
{name: "25% between 4999 and 19999", args: args{amount: 4999 * BaseUnit}, want: 1249_750_000 + 4999*BaseUnit},
{name: "25% between 4999 and 19999", args: args{amount: 5000 * BaseUnit}, want: 1250_000_000 + 5000*BaseUnit},
{name: "30% more than 19999", args: args{amount: 19999 * BaseUnit}, want: 5999_700_000 + 19999*BaseUnit},
{name: "30% more than 19999", args: args{amount: 20000 * BaseUnit}, want: 6000_000_000 + 20000*BaseUnit},
{name: "30% more than 19999", args: args{amount: 99999 * BaseUnit}, want: 29999_700_000 + 99999*BaseUnit},
{"0% less than 299", args{amount: 1 * BaseUnit}, 1 * BaseUnit},
// TODO: Add test cases.
}

configMap := &corev1.ConfigMap{}
configMap.Data = make(map[string]string)
configMap.Data["steps"] = "299,599,1999,4999,19999"
configMap.Data["ratios"] = "10.0,15.0,20.0,25.0,30.0"

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := giveGift(tt.args.amount, configMap); got != tt.want {
t.Errorf("giveGift() = %v, want %v", got, tt.want)
if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want {
t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want)
}
})
}
Expand Down

0 comments on commit 5312580

Please sign in to comment.