From 3594097d4739067e17e44c8dc421c0d01061220e Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Wed, 4 Mar 2026 20:03:03 -0800 Subject: [PATCH] =?UTF-8?q?=E5=90=84=E7=A7=8D=E9=85=8D=E7=BD=AE=E9=A1=B9?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=8C=E4=BC=98=E5=8C=96=E5=88=B0=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E7=AE=A1=E7=90=86=E7=AB=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logic/public/user/queryUserInfoLogic.go | 113 +++++++++++++++++- .../public/user/queryUserInfoLogic_test.go | 105 ++++++++++++++++ 2 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 internal/logic/public/user/queryUserInfoLogic_test.go diff --git a/internal/logic/public/user/queryUserInfoLogic.go b/internal/logic/public/user/queryUserInfoLogic.go index 4205a7e..86224b5 100644 --- a/internal/logic/public/user/queryUserInfoLogic.go +++ b/internal/logic/public/user/queryUserInfoLogic.go @@ -3,7 +3,9 @@ package user import ( "context" "encoding/json" + "fmt" "sort" + "strings" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/kutt" @@ -16,6 +18,7 @@ import ( "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/phone" "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" ) type QueryUserInfoLogic struct { @@ -41,6 +44,7 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } tool.DeepCopy(resp, u) + ownerEmailMethod := l.fillFamilyContext(resp, u.Id) var userMethods []types.UserAuthMethod for _, method := range resp.AuthMethods { @@ -56,11 +60,9 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { } userMethods = append(userMethods, item) } + userMethods = appendFamilyOwnerEmailIfNeeded(userMethods, resp.FamilyJoined, ownerEmailMethod) - // 按照指定顺序排序:email第一位,mobile第二位,其他按原顺序 - sort.Slice(userMethods, func(i, j int) bool { - return getAuthTypePriority(userMethods[i].AuthType) < getAuthTypePriority(userMethods[j].AuthType) - }) + sortUserAuthMethodsByPriority(userMethods) resp.AuthMethods = userMethods @@ -75,6 +77,109 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { return resp, nil } +func (l *QueryUserInfoLogic) fillFamilyContext(resp *types.User, userId int64) *user.AuthMethods { + type familyRelation struct { + FamilyId int64 + Role uint8 + FamilyStatus uint8 + OwnerUserId int64 + MaxMembers int64 + } + + var relation familyRelation + relationErr := l.svcCtx.DB.WithContext(l.ctx). + Table("user_family_member"). + Select("user_family_member.family_id, user_family_member.role, user_family.status as family_status, user_family.owner_user_id, user_family.max_members"). + Joins("JOIN user_family ON user_family.id = user_family_member.family_id AND user_family.deleted_at IS NULL"). + Where("user_family_member.user_id = ? AND user_family_member.deleted_at IS NULL AND user_family_member.status = ?", userId, user.FamilyMemberActive). + First(&relation).Error + if relationErr != nil { + if !errors.Is(relationErr, gorm.ErrRecordNotFound) { + l.Errorw("query family relation failed", logger.Field("user_id", userId), logger.Field("error", relationErr.Error())) + } + return nil + } + + resp.FamilyJoined = true + resp.FamilyId = relation.FamilyId + resp.FamilyRole = relation.Role + resp.FamilyRoleName = getFamilyRoleName(relation.Role) + resp.FamilyOwnerUserId = relation.OwnerUserId + resp.FamilyStatus = getFamilyStatusName(relation.FamilyStatus) + resp.FamilyMaxMembers = relation.MaxMembers + + var activeMemberCount int64 + countErr := l.svcCtx.DB.WithContext(l.ctx). + Table("user_family_member"). + Where("family_id = ? AND status = ? AND deleted_at IS NULL", relation.FamilyId, user.FamilyMemberActive). + Count(&activeMemberCount).Error + if countErr != nil { + l.Errorw("count family members failed", logger.Field("family_id", relation.FamilyId), logger.Field("error", countErr.Error())) + } else { + resp.FamilyMemberCount = activeMemberCount + } + + ownerEmailMethod, ownerEmailErr := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", relation.OwnerUserId) + if ownerEmailErr != nil { + if !errors.Is(ownerEmailErr, gorm.ErrRecordNotFound) { + l.Errorw("query family owner email failed", logger.Field("owner_user_id", relation.OwnerUserId), logger.Field("error", ownerEmailErr.Error())) + } + return nil + } + return ownerEmailMethod +} + +func appendFamilyOwnerEmailIfNeeded(methods []types.UserAuthMethod, familyJoined bool, ownerEmailMethod *user.AuthMethods) []types.UserAuthMethod { + if !familyJoined || ownerEmailMethod == nil { + return methods + } + ownerEmail := strings.TrimSpace(ownerEmailMethod.AuthIdentifier) + if ownerEmail == "" { + return methods + } + if hasEmailAuthMethod(methods) { + return methods + } + return append(methods, types.UserAuthMethod{ + AuthType: "email", + AuthIdentifier: ownerEmail, + Verified: ownerEmailMethod.Verified, + }) +} + +func hasEmailAuthMethod(methods []types.UserAuthMethod) bool { + for _, method := range methods { + if strings.EqualFold(strings.TrimSpace(method.AuthType), "email") && strings.TrimSpace(method.AuthIdentifier) != "" { + return true + } + } + return false +} + +func sortUserAuthMethodsByPriority(methods []types.UserAuthMethod) { + sort.SliceStable(methods, func(i, j int) bool { + return getAuthTypePriority(methods[i].AuthType) < getAuthTypePriority(methods[j].AuthType) + }) +} + +func getFamilyRoleName(role uint8) string { + switch role { + case user.FamilyRoleOwner: + return "owner" + case user.FamilyRoleMember: + return "member" + default: + return fmt.Sprintf("role_%d", role) + } +} + +func getFamilyStatusName(status uint8) string { + if status == user.FamilyStatusActive { + return "active" + } + return "disabled" +} + // customData 用于解析 SiteConfig.CustomData JSON 字段 // 包含从自定义数据中提取所需的配置项 type customData struct { diff --git a/internal/logic/public/user/queryUserInfoLogic_test.go b/internal/logic/public/user/queryUserInfoLogic_test.go new file mode 100644 index 0000000..8013780 --- /dev/null +++ b/internal/logic/public/user/queryUserInfoLogic_test.go @@ -0,0 +1,105 @@ +package user + +import ( + "testing" + + modelUser "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/types" + "github.com/stretchr/testify/require" +) + +func TestAppendFamilyOwnerEmailIfNeeded(t *testing.T) { + testCases := []struct { + name string + methods []types.UserAuthMethod + familyJoined bool + ownerEmailMethod *modelUser.AuthMethods + wantMethodCount int + wantEmailCount int + wantFirstAuthType string + wantFirstAuthValue string + }{ + { + name: "inject owner email when member has no email", + methods: []types.UserAuthMethod{ + {AuthType: "device", AuthIdentifier: "dev-1", Verified: true}, + }, + familyJoined: true, + ownerEmailMethod: &modelUser.AuthMethods{AuthType: "email", AuthIdentifier: "owner@example.com", Verified: true}, + wantMethodCount: 2, + wantEmailCount: 1, + wantFirstAuthType: "email", + wantFirstAuthValue: "owner@example.com", + }, + { + name: "do not inject when member already has email", + methods: []types.UserAuthMethod{ + {AuthType: "email", AuthIdentifier: "member@example.com", Verified: true}, + {AuthType: "device", AuthIdentifier: "dev-1", Verified: true}, + }, + familyJoined: true, + ownerEmailMethod: &modelUser.AuthMethods{AuthType: "email", AuthIdentifier: "owner@example.com", Verified: true}, + wantMethodCount: 2, + wantEmailCount: 1, + wantFirstAuthType: "email", + wantFirstAuthValue: "member@example.com", + }, + { + name: "do not inject when owner has no email", + methods: []types.UserAuthMethod{ + {AuthType: "device", AuthIdentifier: "dev-1", Verified: true}, + }, + familyJoined: true, + ownerEmailMethod: &modelUser.AuthMethods{AuthType: "email", AuthIdentifier: "", Verified: true}, + wantMethodCount: 1, + wantEmailCount: 0, + wantFirstAuthType: "device", + }, + { + name: "do not inject for non active family relationship", + methods: []types.UserAuthMethod{ + {AuthType: "device", AuthIdentifier: "dev-1", Verified: true}, + }, + familyJoined: false, + ownerEmailMethod: &modelUser.AuthMethods{AuthType: "email", AuthIdentifier: "owner@example.com", Verified: true}, + wantMethodCount: 1, + wantEmailCount: 0, + wantFirstAuthType: "device", + }, + { + name: "sort keeps injected email at first position", + methods: []types.UserAuthMethod{ + {AuthType: "mobile", AuthIdentifier: "+1234567890", Verified: true}, + {AuthType: "device", AuthIdentifier: "dev-1", Verified: true}, + }, + familyJoined: true, + ownerEmailMethod: &modelUser.AuthMethods{AuthType: "email", AuthIdentifier: "owner@example.com", Verified: true}, + wantMethodCount: 3, + wantEmailCount: 1, + wantFirstAuthType: "email", + wantFirstAuthValue: "owner@example.com", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + finalMethods := appendFamilyOwnerEmailIfNeeded(testCase.methods, testCase.familyJoined, testCase.ownerEmailMethod) + sortUserAuthMethodsByPriority(finalMethods) + + require.Len(t, finalMethods, testCase.wantMethodCount) + + emailCount := 0 + for _, method := range finalMethods { + if method.AuthType == "email" { + emailCount++ + } + } + require.Equal(t, testCase.wantEmailCount, emailCount) + + require.Equal(t, testCase.wantFirstAuthType, finalMethods[0].AuthType) + if testCase.wantFirstAuthValue != "" { + require.Equal(t, testCase.wantFirstAuthValue, finalMethods[0].AuthIdentifier) + } + }) + } +}