From a1184ef5ed01944ff03c627a95450049b7e4b0fb Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Mon, 18 May 2026 03:02:35 -0700 Subject: [PATCH] feat: add userinfo bind-email trial use status --- apis/types.api | 2 +- .../logic/public/user/queryUserInfoLogic.go | 54 +++++++++++++++ .../public/user/queryUserInfoLogic_test.go | 67 +++++++++++++++++++ internal/types/types.go | 1 + 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 internal/logic/public/user/queryUserInfoLogic_test.go diff --git a/apis/types.api b/apis/types.api index bf185a4..2f8d6e8 100644 --- a/apis/types.api +++ b/apis/types.api @@ -27,6 +27,7 @@ type ( EnableLoginNotify bool `json:"enable_login_notify"` EnableSubscribeNotify bool `json:"enable_subscribe_notify"` EnableTradeNotify bool `json:"enable_trade_notify"` + UseStatus bool `json:"use_status"` // Whether to show the "bind email to get free trial" prompt AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` Rules []string `json:"rules"` @@ -1004,4 +1005,3 @@ type ( ConfigSnapshot map[string]interface{} `json:"config_snapshot,omitempty"` } ) - diff --git a/internal/logic/public/user/queryUserInfoLogic.go b/internal/logic/public/user/queryUserInfoLogic.go index 5682b42..ee57146 100644 --- a/internal/logic/public/user/queryUserInfoLogic.go +++ b/internal/logic/public/user/queryUserInfoLogic.go @@ -6,7 +6,9 @@ import ( "sort" "strings" + authlogic "github.com/perfect-panel/server/internal/logic/auth" logicCommon "github.com/perfect-panel/server/internal/logic/common" + modelOrder "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/uuidx" "github.com/perfect-panel/server/pkg/xerr" @@ -44,6 +46,7 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } tool.DeepCopy(resp, u) + resp.UseStatus = true // 用家庭范围查设备,而不是只看当前用户自己的 UserDevices scopeHelper := newFamilyScopeHelper(l.ctx, l.svcCtx) @@ -65,6 +68,14 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { } resp.UserDevices = userDevices } + + useStatus, useStatusErr := l.resolveBindEmailTrialUseStatus(u.Id, scopeUserIds) + if useStatusErr != nil { + l.Errorw("resolve bind email trial use status failed", logger.Field("user_id", u.Id), logger.Field("error", useStatusErr.Error())) + } else { + resp.UseStatus = useStatus + } + // refer_code 为空时自动生成 if resp.ReferCode == "" { resp.ReferCode = uuidx.UserInviteCode(u.Id) @@ -108,6 +119,49 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { return resp, nil } +// resolveBindEmailTrialUseStatus determines whether userinfo should show the +// "bind email to get free trial" prompt. `true` means show the prompt. +func (l *QueryUserInfoLogic) resolveBindEmailTrialUseStatus(currentUserId int64, scopeUserIds []int64) (bool, error) { + if len(scopeUserIds) == 0 { + scopeUserIds = []int64{currentUserId} + } + + var hasBoundEmailCount int64 + if err := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.AuthMethods{}). + Where("user_id IN ? AND auth_type = ? AND auth_identifier != ''", scopeUserIds, "email"). + Count(&hasBoundEmailCount).Error; err != nil { + return false, err + } + + var hasPurchaseCount int64 + if err := l.svcCtx.DB.WithContext(l.ctx). + Model(&modelOrder.Order{}). + Where("user_id IN ? AND type IN ? AND status IN ?", scopeUserIds, []int64{1, 2}, []int64{2, 5}). + Count(&hasPurchaseCount).Error; err != nil { + return false, err + } + + hasTrial := false + registerCfg := l.svcCtx.Config.Register + if authlogic.IsTrialConfigReady(registerCfg) && registerCfg.TrialSubscribe > 0 { + var hasTrialCount int64 + if err := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.Subscribe{}). + Where("user_id IN ? AND subscribe_id = ?", scopeUserIds, registerCfg.TrialSubscribe). + Count(&hasTrialCount).Error; err != nil { + return false, err + } + hasTrial = hasTrialCount > 0 + } + + return shouldShowBindEmailTrialPrompt(hasBoundEmailCount > 0, hasPurchaseCount > 0, hasTrial), nil +} + +func shouldShowBindEmailTrialPrompt(hasBoundEmail, hasPurchased, hasTrial bool) bool { + return !hasBoundEmail && !hasPurchased && !hasTrial +} + func (l *QueryUserInfoLogic) fillFamilyContext(resp *types.User, userId int64) *user.AuthMethods { type familyRelation struct { FamilyId int64 diff --git a/internal/logic/public/user/queryUserInfoLogic_test.go b/internal/logic/public/user/queryUserInfoLogic_test.go new file mode 100644 index 0000000..369614a --- /dev/null +++ b/internal/logic/public/user/queryUserInfoLogic_test.go @@ -0,0 +1,67 @@ +package user + +import "testing" + +func TestShouldShowBindEmailTrialPrompt(t *testing.T) { + tests := []struct { + name string + hasBoundEmail bool + hasPurchased bool + hasTrial bool + want bool + }{ + { + name: "new user should see prompt", + want: true, + }, + { + name: "bound email should hide prompt", + hasBoundEmail: true, + want: false, + }, + { + name: "paid purchase should hide prompt", + hasPurchased: true, + want: false, + }, + { + name: "trial claimed should hide prompt", + hasTrial: true, + want: false, + }, + { + name: "bound email and purchase should hide prompt", + hasBoundEmail: true, + hasPurchased: true, + want: false, + }, + { + name: "bound email and trial should hide prompt", + hasBoundEmail: true, + hasTrial: true, + want: false, + }, + { + name: "purchase and trial should hide prompt", + hasPurchased: true, + hasTrial: true, + want: false, + }, + { + name: "all blockers should hide prompt", + hasBoundEmail: true, + hasPurchased: true, + hasTrial: true, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldShowBindEmailTrialPrompt(tt.hasBoundEmail, tt.hasPurchased, tt.hasTrial) + if got != tt.want { + t.Fatalf("shouldShowBindEmailTrialPrompt(%v, %v, %v) = %v, want %v", tt.hasBoundEmail, tt.hasPurchased, tt.hasTrial, got, tt.want) + } + }) + } +} diff --git a/internal/types/types.go b/internal/types/types.go index 663ceeb..aeccc2a 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -3278,6 +3278,7 @@ type User struct { EnableLoginNotify bool `json:"enable_login_notify"` EnableSubscribeNotify bool `json:"enable_subscribe_notify"` EnableTradeNotify bool `json:"enable_trade_notify"` + UseStatus bool `json:"use_status"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` Rules []string `json:"rules"`