feat: add userinfo bind-email trial use status
Build docker and publish / build (20.15.1) (push) Successful in 9m2s

This commit is contained in:
2026-05-18 03:02:35 -07:00
parent 7c6efe9dfe
commit a1184ef5ed
4 changed files with 123 additions and 1 deletions
+1 -1
View File
@@ -27,6 +27,7 @@ type (
EnableLoginNotify bool `json:"enable_login_notify"` EnableLoginNotify bool `json:"enable_login_notify"`
EnableSubscribeNotify bool `json:"enable_subscribe_notify"` EnableSubscribeNotify bool `json:"enable_subscribe_notify"`
EnableTradeNotify bool `json:"enable_trade_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"` AuthMethods []UserAuthMethod `json:"auth_methods"`
UserDevices []UserDevice `json:"user_devices"` UserDevices []UserDevice `json:"user_devices"`
Rules []string `json:"rules"` Rules []string `json:"rules"`
@@ -1004,4 +1005,3 @@ type (
ConfigSnapshot map[string]interface{} `json:"config_snapshot,omitempty"` ConfigSnapshot map[string]interface{} `json:"config_snapshot,omitempty"`
} }
) )
@@ -6,7 +6,9 @@ import (
"sort" "sort"
"strings" "strings"
authlogic "github.com/perfect-panel/server/internal/logic/auth"
logicCommon "github.com/perfect-panel/server/internal/logic/common" 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/constant"
"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"
@@ -44,6 +46,7 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
} }
tool.DeepCopy(resp, u) tool.DeepCopy(resp, u)
resp.UseStatus = true
// 用家庭范围查设备,而不是只看当前用户自己的 UserDevices // 用家庭范围查设备,而不是只看当前用户自己的 UserDevices
scopeHelper := newFamilyScopeHelper(l.ctx, l.svcCtx) scopeHelper := newFamilyScopeHelper(l.ctx, l.svcCtx)
@@ -65,6 +68,14 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
} }
resp.UserDevices = userDevices 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 为空时自动生成 // refer_code 为空时自动生成
if resp.ReferCode == "" { if resp.ReferCode == "" {
resp.ReferCode = uuidx.UserInviteCode(u.Id) resp.ReferCode = uuidx.UserInviteCode(u.Id)
@@ -108,6 +119,49 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
return resp, nil 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 { func (l *QueryUserInfoLogic) fillFamilyContext(resp *types.User, userId int64) *user.AuthMethods {
type familyRelation struct { type familyRelation struct {
FamilyId int64 FamilyId int64
@@ -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)
}
})
}
}
+1
View File
@@ -3278,6 +3278,7 @@ type User struct {
EnableLoginNotify bool `json:"enable_login_notify"` EnableLoginNotify bool `json:"enable_login_notify"`
EnableSubscribeNotify bool `json:"enable_subscribe_notify"` EnableSubscribeNotify bool `json:"enable_subscribe_notify"`
EnableTradeNotify bool `json:"enable_trade_notify"` EnableTradeNotify bool `json:"enable_trade_notify"`
UseStatus bool `json:"use_status"`
AuthMethods []UserAuthMethod `json:"auth_methods"` AuthMethods []UserAuthMethod `json:"auth_methods"`
UserDevices []UserDevice `json:"user_devices"` UserDevices []UserDevice `json:"user_devices"`
Rules []string `json:"rules"` Rules []string `json:"rules"`