x
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m38s

This commit is contained in:
shanshanzhong 2026-04-04 09:41:41 -07:00
parent 98d8525fa9
commit e818ac8764
8 changed files with 120 additions and 96 deletions

View File

@ -82,15 +82,16 @@ type SubscribeConfig struct {
} }
type RegisterConfig struct { type RegisterConfig struct {
StopRegister bool `yaml:"StopRegister" default:"false"` StopRegister bool `yaml:"StopRegister" default:"false"`
EnableTrial bool `yaml:"EnableTrial" default:"false"` EnableTrial bool `yaml:"EnableTrial" default:"false"`
TrialSubscribe int64 `yaml:"TrialSubscribe" default:"0"` EnableTrialEmailWhitelist bool `yaml:"EnableTrialEmailWhitelist" default:"true"`
TrialTime int64 `yaml:"TrialTime" default:"0"` TrialSubscribe int64 `yaml:"TrialSubscribe" default:"0"`
TrialTimeUnit string `yaml:"TrialTimeUnit" default:""` TrialTime int64 `yaml:"TrialTime" default:"0"`
TrialTimeUnit string `yaml:"TrialTimeUnit" default:""`
TrialEmailDomainWhitelist string `yaml:"TrialEmailDomainWhitelist" default:""` TrialEmailDomainWhitelist string `yaml:"TrialEmailDomainWhitelist" default:""`
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"` IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"` IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"` EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
DeviceLimit int64 `yaml:"DeviceLimit" default:"2"` DeviceLimit int64 `yaml:"DeviceLimit" default:"2"`
} }
@ -102,12 +103,12 @@ type EmailConfig struct {
EnableNotify bool `yaml:"enable_notify"` EnableNotify bool `yaml:"enable_notify"`
EnableDomainSuffix bool `yaml:"enable_domain_suffix"` EnableDomainSuffix bool `yaml:"enable_domain_suffix"`
DomainSuffixList string `yaml:"domain_suffix_list"` DomainSuffixList string `yaml:"domain_suffix_list"`
VerifyEmailTemplate string `yaml:"verify_email_template"` VerifyEmailTemplate string `yaml:"verify_email_template"`
VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"` VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"`
ExpirationEmailTemplate string `yaml:"expiration_email_template"` ExpirationEmailTemplate string `yaml:"expiration_email_template"`
MaintenanceEmailTemplate string `yaml:"maintenance_email_template"` MaintenanceEmailTemplate string `yaml:"maintenance_email_template"`
TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"` TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"`
DeleteAccountEmailTemplate string `yaml:"delete_account_email_template"` DeleteAccountEmailTemplate string `yaml:"delete_account_email_template"`
} }
type MobileConfig struct { type MobileConfig struct {

View File

@ -126,7 +126,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
return err return err
} }
rc := l.svcCtx.Config.Register rc := l.svcCtx.Config.Register
if rc.EnableTrial && rc.TrialEmailDomainWhitelist != "" && IsEmailDomainWhitelisted(req.Email, rc.TrialEmailDomainWhitelist) { if ShouldGrantTrialForEmail(rc, req.Email) {
if err = l.activeTrial(userInfo.Id); err != nil { if err = l.activeTrial(userInfo.Id); err != nil {
return err return err
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
authlogic "github.com/perfect-panel/server/internal/logic/auth"
"github.com/perfect-panel/server/internal/model/auth" "github.com/perfect-panel/server/internal/model/auth"
"github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/model/user"
@ -395,8 +396,7 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
} }
rc := l.svcCtx.Config.Register rc := l.svcCtx.Config.Register
// Only activate trial if email domain is in whitelist (whitelist cannot be empty) shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email)
shouldActivateTrial := rc.EnableTrial && rc.TrialEmailDomainWhitelist != "" && (email != "" && l.isEmailDomainWhitelisted(email, rc.TrialEmailDomainWhitelist))
if shouldActivateTrial { if shouldActivateTrial {
l.Debugw("activating trial subscription", l.Debugw("activating trial subscription",

View File

@ -1,6 +1,10 @@
package auth package auth
import "strings" import (
"strings"
"github.com/perfect-panel/server/internal/config"
)
// IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist. // IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
// Returns false if the email format is invalid. // Returns false if the email format is invalid.
@ -20,3 +24,16 @@ func IsEmailDomainWhitelisted(email, whitelistCSV string) bool {
} }
return false return false
} }
func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool {
if !register.EnableTrial {
return false
}
if !register.EnableTrialEmailWhitelist {
return true
}
if register.TrialEmailDomainWhitelist == "" {
return false
}
return IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist)
}

View File

@ -148,7 +148,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time) // Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
rc := l.svcCtx.Config.Register rc := l.svcCtx.Config.Register
if rc.EnableTrial && rc.TrialEmailDomainWhitelist != "" && IsEmailDomainWhitelisted(req.Email, rc.TrialEmailDomainWhitelist) { if ShouldGrantTrialForEmail(rc, req.Email) {
trialSubscribe, err = l.activeTrial(userInfo.Id) trialSubscribe, err = l.activeTrial(userInfo.Id)
if err != nil { if err != nil {
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error())) l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))

View File

@ -8,14 +8,12 @@ import (
"time" "time"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/logic/auth"
"github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/jwt" "github.com/perfect-panel/server/pkg/jwt"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/uuidx" "github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/pkg/xerr" "github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -129,8 +127,7 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Grant trial subscription if email domain is whitelisted tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email)
l.tryGrantTrialOnEmailBind(emailUser.Id, req.Email)
return &types.BindEmailWithVerificationResponse{ return &types.BindEmailWithVerificationResponse{
Success: true, Success: true,
Message: "email user created and joined family", Message: "email user created and joined family",
@ -158,8 +155,7 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
return nil, err return nil, err
} }
// Grant trial subscription if email domain is whitelisted tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email)
l.tryGrantTrialOnEmailBind(existingMethod.UserId, req.Email)
return &types.BindEmailWithVerificationResponse{ return &types.BindEmailWithVerificationResponse{
Success: true, Success: true,
@ -207,74 +203,3 @@ func (l *BindEmailWithVerificationLogic) refreshBindSessionToken(userId int64) (
return token, nil return token, nil
} }
// tryGrantTrialOnEmailBind grants trial subscription to the email user (family owner)
// if email domain is in the configured whitelist (or if whitelist is empty, no trial is granted).
func (l *BindEmailWithVerificationLogic) tryGrantTrialOnEmailBind(ownerUserId int64, email string) {
rc := l.svcCtx.Config.Register
if !rc.EnableTrial || rc.TrialEmailDomainWhitelist == "" {
return
}
if !auth.IsEmailDomainWhitelisted(email, rc.TrialEmailDomainWhitelist) {
l.Infow("email domain not in trial whitelist, skip",
logger.Field("email", email),
logger.Field("owner_user_id", ownerUserId),
)
return
}
// Anti-duplicate: check if owner already has trial subscription
var count int64
if err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
Count(&count).Error; err != nil {
l.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
return
}
if count > 0 {
l.Infow("trial already granted, skip",
logger.Field("owner_user_id", ownerUserId),
)
return
}
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, rc.TrialSubscribe)
if err != nil {
l.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
return
}
userSub := &user.Subscribe{
UserId: ownerUserId,
OrderId: 0,
SubscribeId: sub.Id,
StartTime: time.Now(),
ExpireTime: tool.AddTime(rc.TrialTimeUnit, rc.TrialTime, time.Now()),
Traffic: sub.Traffic,
Download: 0,
Upload: 0,
Token: uuidx.NewUUID().String(),
UUID: uuidx.NewUUID().String(),
Status: 1,
}
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil {
l.Errorw("failed to insert trial subscribe",
logger.Field("error", err.Error()),
logger.Field("owner_user_id", ownerUserId),
)
return
}
// InsertSubscribe auto-clears user subscribe cache via execSubscribeMutation.
// Clear server cache so nodes pick up the new subscription.
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorw("ClearServerAllCache error", logger.Field("error", err.Error()))
}
l.Infow("trial granted on email bind",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("subscribe_id", sub.Id),
)
}

View File

@ -0,0 +1,80 @@
package user
import (
"context"
"time"
"github.com/perfect-panel/server/internal/logic/auth"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/uuidx"
)
func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) {
rc := svcCtx.Config.Register
if !auth.ShouldGrantTrialForEmail(rc, email) {
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
log.Infow("email domain not in trial whitelist, skip",
logger.Field("email", email),
logger.Field("owner_user_id", ownerUserId),
)
}
return
}
var count int64
if err := svcCtx.DB.WithContext(ctx).
Model(&user.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
Count(&count).Error; err != nil {
log.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
return
}
if count > 0 {
log.Infow("trial already granted, skip",
logger.Field("owner_user_id", ownerUserId),
)
return
}
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
if err != nil {
log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
return
}
userSub := &user.Subscribe{
UserId: ownerUserId,
OrderId: 0,
SubscribeId: sub.Id,
StartTime: time.Now(),
ExpireTime: tool.AddTime(rc.TrialTimeUnit, rc.TrialTime, time.Now()),
Traffic: sub.Traffic,
Download: 0,
Upload: 0,
Token: uuidx.NewUUID().String(),
UUID: uuidx.NewUUID().String(),
Status: 1,
}
if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil {
log.Errorw("failed to insert trial subscribe",
logger.Field("error", err.Error()),
logger.Field("owner_user_id", ownerUserId),
)
return
}
if svcCtx.NodeModel != nil {
if err = svcCtx.NodeModel.ClearServerAllCache(ctx); err != nil {
log.Errorw("ClearServerAllCache error", logger.Field("error", err.Error()))
}
}
log.Infow("trial granted on email bind",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("subscribe_id", sub.Id),
)
}

View File

@ -77,5 +77,6 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
if err != nil { if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
} }
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email)
return nil return nil
} }