feat(订单): 实现推荐奖励系统支持佣金和赠送天数两种模式
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 9m13s
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 9m13s
重构推荐奖励处理逻辑,新增支持根据配置选择佣金奖励或赠送天数奖励 修改Discount相关字段类型为float64以支持小数折扣 添加GiftDays配置项控制赠送天数 新增FindActiveSubscribe方法查询用户有效订阅
This commit is contained in:
parent
7da63ade5c
commit
bfbc675e1a
@ -187,6 +187,7 @@ type (
|
|||||||
ForcedInvite bool `json:"forced_invite"`
|
ForcedInvite bool `json:"forced_invite"`
|
||||||
ReferralPercentage int64 `json:"referral_percentage"`
|
ReferralPercentage int64 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||||
|
GiftDays int64 `json:"gift_days"`
|
||||||
}
|
}
|
||||||
TelegramConfig {
|
TelegramConfig {
|
||||||
TelegramBotToken string `json:"telegram_bot_token"`
|
TelegramBotToken string `json:"telegram_bot_token"`
|
||||||
@ -206,8 +207,8 @@ type (
|
|||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
}
|
}
|
||||||
SubscribeDiscount {
|
SubscribeDiscount {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount int64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
Subscribe {
|
Subscribe {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
|||||||
@ -203,6 +203,7 @@ type InviteConfig struct {
|
|||||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||||
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
|
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
|
||||||
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
||||||
|
GiftDays int64 `yaml:"GiftDays" default:"3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
|
|||||||
@ -861,10 +861,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverV2GroupRouter := router.Group("/v2/server")
|
serverGroupRouterV2 := router.Group("/v2/server")
|
||||||
|
|
||||||
{
|
{
|
||||||
// Get Server Protocol Config
|
// Get Server Protocol Config
|
||||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
serverGroupRouterV2.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package order
|
|||||||
import "github.com/perfect-panel/server/internal/types"
|
import "github.com/perfect-panel/server/internal/types"
|
||||||
|
|
||||||
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
||||||
var finalDiscount int64 = 100
|
var finalDiscount float64 = 1.0
|
||||||
|
|
||||||
for _, discount := range discounts {
|
for _, discount := range discounts {
|
||||||
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
||||||
@ -11,5 +11,5 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return float64(finalDiscount) / float64(100)
|
return finalDiscount
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 {
|
||||||
var finalDiscount int64 = 100
|
var finalDiscount float64 = 1.0
|
||||||
|
|
||||||
for _, discount := range discounts {
|
for _, discount := range discounts {
|
||||||
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
if inputMonths >= discount.Quantity && discount.Discount < finalDiscount {
|
||||||
finalDiscount = discount.Discount
|
finalDiscount = discount.Discount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return float64(finalDiscount) / float64(100)
|
return finalDiscount
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 {
|
||||||
|
|||||||
@ -71,8 +71,8 @@ func (s *Subscribe) BeforeUpdate(tx *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Discount struct {
|
type Discount struct {
|
||||||
Months int64 `json:"months"`
|
Months int64 `json:"months"`
|
||||||
Discount int64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
|||||||
@ -77,6 +77,7 @@ type customUserLogicModel interface {
|
|||||||
QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error)
|
QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error)
|
||||||
FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error)
|
FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error)
|
||||||
FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error)
|
FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error)
|
||||||
|
FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error)
|
||||||
FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error)
|
FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error)
|
||||||
UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error
|
UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error
|
||||||
QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error)
|
QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error)
|
||||||
|
|||||||
@ -198,3 +198,24 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx
|
|||||||
func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error {
|
func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error {
|
||||||
return m.ClearSubscribeCacheByModels(ctx, data...)
|
return m.ClearSubscribeCacheByModels(ctx, data...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindActiveSubscribe finds the user's active subscription
|
||||||
|
func (m *defaultUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
|
||||||
|
var data Subscribe
|
||||||
|
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
now := time.Now()
|
||||||
|
return conn.Model(&Subscribe{}).
|
||||||
|
Where("user_id = ? AND status = ? AND expire_time > ?", userId, 1, now).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
First(&data).Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1170,6 +1170,7 @@ type InviteConfig struct {
|
|||||||
ForcedInvite bool `json:"forced_invite"`
|
ForcedInvite bool `json:"forced_invite"`
|
||||||
ReferralPercentage int64 `json:"referral_percentage"`
|
ReferralPercentage int64 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||||
|
GiftDays int64 `json:"gift_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KickOfflineRequest struct {
|
type KickOfflineRequest struct {
|
||||||
@ -2065,8 +2066,8 @@ type SubscribeConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeDiscount struct {
|
type SubscribeDiscount struct {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount int64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeGroup struct {
|
type SubscribeGroup struct {
|
||||||
|
|||||||
@ -179,8 +179,8 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle commission in separate goroutine to avoid blocking
|
// Handle referral reward in separate goroutine to avoid blocking
|
||||||
go l.handleCommission(context.Background(), userInfo, orderInfo)
|
go l.handleReferralReward(context.Background(), userInfo, orderInfo)
|
||||||
|
|
||||||
// Clear cache
|
// Clear cache
|
||||||
l.clearServerCache(ctx, sub)
|
l.clearServerCache(ctx, sub)
|
||||||
@ -349,10 +349,12 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
|
|||||||
return userSub, nil
|
return userSub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCommission processes referral commission for the referrer if applicable.
|
// handleReferralReward processes referral rewards for the referrer if applicable.
|
||||||
// This runs asynchronously to avoid blocking the main order processing flow.
|
// This runs asynchronously to avoid blocking the main order processing flow.
|
||||||
func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
|
// If referral percentage > 0: commission reward
|
||||||
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
|
// If referral percentage = 0: gift days to both parties
|
||||||
|
func (l *ActivateOrderLogic) handleReferralReward(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
|
||||||
|
if !l.shouldProcessReferralReward(userInfo, orderInfo.IsNew) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,13 +374,25 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
|||||||
referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage)
|
referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is commission reward or gift days reward
|
||||||
|
if referralPercentage > 0 {
|
||||||
|
// Commission reward mode
|
||||||
|
l.processCommissionReward(ctx, referer, orderInfo, referralPercentage)
|
||||||
|
} else {
|
||||||
|
// Gift days reward mode
|
||||||
|
l.processGiftDaysReward(ctx, referer, userInfo, orderInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processCommissionReward handles commission-based rewards
|
||||||
|
func (l *ActivateOrderLogic) processCommissionReward(ctx context.Context, referer *user.User, orderInfo *order.Order, percentage uint8) {
|
||||||
// Order commission calculation: (Order Amount - Order Fee) * Referral Percentage
|
// Order commission calculation: (Order Amount - Order Fee) * Referral Percentage
|
||||||
amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage)
|
amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, percentage)
|
||||||
|
|
||||||
// Use transaction for commission updates
|
// Use transaction for commission updates
|
||||||
err = l.svc.DB.Transaction(func(tx *gorm.DB) error {
|
err := l.svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
referer.Commission += amount
|
referer.Commission += amount
|
||||||
if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil {
|
if err := l.svc.UserModel.Update(ctx, referer, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,9 +434,73 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldProcessCommission determines if commission should be processed based on
|
// processGiftDaysReward handles gift days rewards for both parties
|
||||||
|
func (l *ActivateOrderLogic) processGiftDaysReward(ctx context.Context, referer *user.User, referee *user.User, orderInfo *order.Order) {
|
||||||
|
giftDays := l.svc.Config.Invite.GiftDays
|
||||||
|
if giftDays <= 0 {
|
||||||
|
giftDays = 3 // Default to 3 days
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the subscription info to determine the unit time
|
||||||
|
sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("Get subscribe info failed for gift days",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("subscribe_id", orderInfo.SubscribeId),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant gift days to both referer and referee
|
||||||
|
l.grantGiftDays(ctx, referer, giftDays, sub.UnitTime, "referer")
|
||||||
|
l.grantGiftDays(ctx, referee, giftDays, sub.UnitTime, "referee")
|
||||||
|
}
|
||||||
|
|
||||||
|
// grantGiftDays grants gift days to a user by extending their subscription
|
||||||
|
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, user *user.User, days int64, unitTime string, role string) {
|
||||||
|
// Find user's active subscription
|
||||||
|
userSub, err := l.svc.UserModel.FindActiveSubscribe(ctx, user.Id)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("Find user active subscription failed for gift days",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("user_id", user.Id),
|
||||||
|
logger.Field("role", role),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSub == nil {
|
||||||
|
logger.WithContext(ctx).Info("User has no active subscription for gift days",
|
||||||
|
logger.Field("user_id", user.Id),
|
||||||
|
logger.Field("role", role),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend subscription by gift days
|
||||||
|
userSub.ExpireTime = tool.AddTime("day", days, userSub.ExpireTime)
|
||||||
|
|
||||||
|
err = l.svc.UserModel.UpdateSubscribe(ctx, userSub)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("Update user subscription for gift days failed",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("user_id", user.Id),
|
||||||
|
logger.Field("role", role),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WithContext(ctx).Info("Gift days granted successfully",
|
||||||
|
logger.Field("user_id", user.Id),
|
||||||
|
logger.Field("role", role),
|
||||||
|
logger.Field("days", days),
|
||||||
|
logger.Field("new_expire_time", userSub.ExpireTime),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldProcessReferralReward determines if referral reward should be processed based on
|
||||||
// referrer existence, commission settings, and order type
|
// referrer existence, commission settings, and order type
|
||||||
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
|
func (l *ActivateOrderLogic) shouldProcessReferralReward(userInfo *user.User, isFirstPurchase bool) bool {
|
||||||
if userInfo == nil || userInfo.RefererId == 0 {
|
if userInfo == nil || userInfo.RefererId == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -504,8 +582,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
// Clear cache
|
// Clear cache
|
||||||
l.clearServerCache(ctx, sub)
|
l.clearServerCache(ctx, sub)
|
||||||
|
|
||||||
// Handle commission
|
// Handle referral reward
|
||||||
go l.handleCommission(context.Background(), userInfo, orderInfo)
|
go l.handleReferralReward(context.Background(), userInfo, orderInfo)
|
||||||
|
|
||||||
// Send notifications
|
// Send notifications
|
||||||
l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify)
|
l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify)
|
||||||
|
|||||||
@ -5,17 +5,26 @@ set -e
|
|||||||
|
|
||||||
cd /Users/Apple/vpn/ppanel-server
|
cd /Users/Apple/vpn/ppanel-server
|
||||||
# 固定版本号为latest
|
# 固定版本号为latest
|
||||||
VERSION=dev
|
VERSION=v1.0
|
||||||
|
|
||||||
# 构建镜像
|
# 构建镜像
|
||||||
echo "Building image with version: $VERSION"
|
echo "Building image with version: $VERSION"
|
||||||
docker build -f Dockerfile --platform linux/amd64 --build-arg TARGETARCH=amd64 -t registry.kxsw.us/ppanel/ario-server:$VERSION .
|
docker build -f Dockerfile --platform linux/amd64 --build-arg TARGETARCH=amd64 -t registry.kxsw.us/ppanel/ario-server:$VERSION .
|
||||||
docker tag registry.kxsw.us/ppanel/ario-server:$VERSION registry.kxsw.us/ppanel/ario-server:latest
|
docker tag registry.kxsw.us/ppanel/ario-server:$VERSION registry.kxsw.us/ppanel/ario-server:$VERSION
|
||||||
|
|
||||||
# 推送镜像
|
# 推送镜像
|
||||||
echo "Pushing image to registry.kxsw.us"
|
echo "Pushing image to registry.kxsw.us"
|
||||||
docker push registry.kxsw.us/ppanel/ario-server:$VERSION
|
docker push registry.kxsw.us/ppanel/ario-server:$VERSION
|
||||||
docker push registry.kxsw.us/ppanel/ario-server:latest
|
docker push registry.kxsw.us/ppanel/ario-server:$VERSION
|
||||||
|
|
||||||
echo "Build and push completed successfully!"
|
echo "Build and push completed successfully!"
|
||||||
# docker-compose exec certbot certbot certonly --webroot --webroot-path=/etc/letsencrypt -d api-dev.kxsw.us
|
# docker-compose exec certbot certbot certonly --webroot --webroot-path=/etc/letsencrypt -d api-dev.kxsw.us
|
||||||
|
|
||||||
|
docker run -d -p 3001:3000 \
|
||||||
|
-e NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US \
|
||||||
|
-e NEXT_PUBLIC_SITE_URL=https://4d3vsw8.88xgaen.hifast.biz \
|
||||||
|
-e NEXT_PUBLIC_API_URL=https://api.hifast.biz \
|
||||||
|
-e NEXT_PUBLIC_DEFAULT_USER_EMAIL=admin@ppanel.dev \
|
||||||
|
-e NEXT_PUBLIC_DEFAULT_USER_PASSWORD=password \
|
||||||
|
--name ppanel-admin-web \
|
||||||
|
ppanel/ppanel-admin-web:latest
|
||||||
Loading…
x
Reference in New Issue
Block a user