diff --git a/apis/types.api b/apis/types.api index c55a5df..1b0774d 100644 --- a/apis/types.api +++ b/apis/types.api @@ -187,6 +187,7 @@ type ( ForcedInvite bool `json:"forced_invite"` ReferralPercentage int64 `json:"referral_percentage"` OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftDays int64 `json:"gift_days"` } TelegramConfig { TelegramBotToken string `json:"telegram_bot_token"` @@ -206,8 +207,8 @@ type ( CurrencySymbol string `json:"currency_symbol"` } SubscribeDiscount { - Quantity int64 `json:"quantity"` - Discount int64 `json:"discount"` + Quantity int64 `json:"quantity"` + Discount float64 `json:"discount"` } Subscribe { Id int64 `json:"id"` diff --git a/internal/config/config.go b/internal/config/config.go index 59ece74..d24defc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -203,6 +203,7 @@ type InviteConfig struct { ForcedInvite bool `yaml:"ForcedInvite" default:"false"` ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"` OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"` + GiftDays int64 `yaml:"GiftDays" default:"3"` } type Telegram struct { diff --git a/internal/handler/routes.go b/internal/handler/routes.go index d42ac7e..817cf2a 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -861,10 +861,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx)) } - serverV2GroupRouter := router.Group("/v2/server") + serverGroupRouterV2 := router.Group("/v2/server") { // Get Server Protocol Config - serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx)) + serverGroupRouterV2.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx)) } } diff --git a/internal/logic/public/order/getDiscount.go b/internal/logic/public/order/getDiscount.go index 34c16a9..2278005 100644 --- a/internal/logic/public/order/getDiscount.go +++ b/internal/logic/public/order/getDiscount.go @@ -3,7 +3,7 @@ package order import "github.com/perfect-panel/server/internal/types" func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 + var finalDiscount float64 = 1.0 for _, discount := range discounts { 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 } diff --git a/internal/logic/public/portal/tool.go b/internal/logic/public/portal/tool.go index c2d2bbd..0f79310 100644 --- a/internal/logic/public/portal/tool.go +++ b/internal/logic/public/portal/tool.go @@ -7,14 +7,14 @@ import ( ) func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 + var finalDiscount float64 = 1.0 for _, discount := range discounts { if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { finalDiscount = discount.Discount } } - return float64(finalDiscount) / float64(100) + return finalDiscount } func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index a80ea63..6bdf9eb 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -71,8 +71,8 @@ func (s *Subscribe) BeforeUpdate(tx *gorm.DB) error { } type Discount struct { - Months int64 `json:"months"` - Discount int64 `json:"discount"` + Months int64 `json:"months"` + Discount float64 `json:"discount"` } type Group struct { diff --git a/internal/model/user/model.go b/internal/model/user/model.go index ffc5020..d5fa385 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -77,6 +77,7 @@ type customUserLogicModel interface { QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) FindOneSubscribeDetailsById(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) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go index 362fb99..a4345c5 100644 --- a/internal/model/user/subscribe.go +++ b/internal/model/user/subscribe.go @@ -198,3 +198,24 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error { 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 +} diff --git a/internal/types/types.go b/internal/types/types.go index 4e481ff..ec8b4ed 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1170,6 +1170,7 @@ type InviteConfig struct { ForcedInvite bool `json:"forced_invite"` ReferralPercentage int64 `json:"referral_percentage"` OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftDays int64 `json:"gift_days"` } type KickOfflineRequest struct { @@ -2065,8 +2066,8 @@ type SubscribeConfig struct { } type SubscribeDiscount struct { - Quantity int64 `json:"quantity"` - Discount int64 `json:"discount"` + Quantity int64 `json:"quantity"` + Discount float64 `json:"discount"` } type SubscribeGroup struct { diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index f6c8f9c..3bb7cbe 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -179,8 +179,8 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O return err } - // Handle commission in separate goroutine to avoid blocking - go l.handleCommission(context.Background(), userInfo, orderInfo) + // Handle referral reward in separate goroutine to avoid blocking + go l.handleReferralReward(context.Background(), userInfo, orderInfo) // Clear cache l.clearServerCache(ctx, sub) @@ -349,10 +349,12 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn 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. -func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) { - if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) { +// If referral percentage > 0: commission reward +// 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 } @@ -372,13 +374,25 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use 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 - amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage) + amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, percentage) // 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 - if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil { + if err := l.svc.UserModel.Update(ctx, referer, tx); err != nil { 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 -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 { return false } @@ -504,8 +582,8 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order // Clear cache l.clearServerCache(ctx, sub) - // Handle commission - go l.handleCommission(context.Background(), userInfo, orderInfo) + // Handle referral reward + go l.handleReferralReward(context.Background(), userInfo, orderInfo) // Send notifications l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify) diff --git a/script/build_docker.sh b/script/build_docker.sh index 0548b3e..5388586 100755 --- a/script/build_docker.sh +++ b/script/build_docker.sh @@ -5,17 +5,26 @@ set -e cd /Users/Apple/vpn/ppanel-server # 固定版本号为latest -VERSION=dev +VERSION=v1.0 # 构建镜像 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 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" 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!" # 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 \ No newline at end of file diff --git a/server b/server new file mode 100755 index 0000000..6bb6b28 Binary files /dev/null and b/server differ