diff --git a/initialize/migrate/database/02137_user_auth_methods_deleted_at.down.sql b/initialize/migrate/database/02137_user_auth_methods_deleted_at.down.sql new file mode 100644 index 0000000..fb79f23 --- /dev/null +++ b/initialize/migrate/database/02137_user_auth_methods_deleted_at.down.sql @@ -0,0 +1,4 @@ +DROP INDEX `idx_user_deleted_at` ON `user_auth_methods`; + +ALTER TABLE `user_auth_methods` + DROP COLUMN `deleted_at`; diff --git a/initialize/migrate/database/02137_user_auth_methods_deleted_at.up.sql b/initialize/migrate/database/02137_user_auth_methods_deleted_at.up.sql new file mode 100644 index 0000000..aeb90fe --- /dev/null +++ b/initialize/migrate/database/02137_user_auth_methods_deleted_at.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user_auth_methods` + ADD COLUMN `deleted_at` DATETIME(3) DEFAULT NULL COMMENT 'Deletion Time'; + +CREATE INDEX `idx_user_deleted_at` ON `user_auth_methods` (`user_id`, `deleted_at`); diff --git a/internal/logic/public/user/familyBindingHelper.go b/internal/logic/public/user/familyBindingHelper.go index 9f66b20..0674d39 100644 --- a/internal/logic/public/user/familyBindingHelper.go +++ b/internal/logic/public/user/familyBindingHelper.go @@ -40,6 +40,25 @@ func (h *familyBindingHelper) getUserEmailMethod(userId int64) (*user.AuthMethod return method, nil } +func validateMemberJoinConflict(ownerFamilyId int64, memberRecord *user.UserFamilyMember) error { + if memberRecord == nil { + return nil + } + + if ownerFamilyId != 0 && memberRecord.FamilyId == ownerFamilyId { + if memberRecord.Status == user.FamilyMemberActive { + return errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already in this family") + } + return nil + } + + if memberRecord.Status == user.FamilyMemberActive { + return errors.Wrapf(xerr.NewErrCode(xerr.FamilyCrossBindForbidden), "user already belongs to another family") + } + + return nil +} + func (h *familyBindingHelper) validateJoinFamily(ownerUserId, memberUserId int64) error { if ownerUserId == memberUserId { return errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already bound to this family") @@ -49,13 +68,14 @@ func (h *familyBindingHelper) validateJoinFamily(ownerUserId, memberUserId int64 err := h.svcCtx.DB.WithContext(h.ctx). Unscoped(). Model(&user.UserFamily{}). - Where("owner_user_id = ? AND status = ?", ownerUserId, user.FamilyStatusActive). + Where("owner_user_id = ?", ownerUserId). First(&ownerFamily).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query owner family failed") } var memberRecord user.UserFamilyMember + memberExists := false err = h.svcCtx.DB.WithContext(h.ctx). Unscoped(). Model(&user.UserFamilyMember{}). @@ -65,10 +85,12 @@ func (h *familyBindingHelper) validateJoinFamily(ownerUserId, memberUserId int64 return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member family relation failed") } if err == nil { - if ownerFamily.Id != 0 && memberRecord.FamilyId == ownerFamily.Id { - return errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already in this family") + memberExists = true + } + if memberExists { + if err = validateMemberJoinConflict(ownerFamily.Id, &memberRecord); err != nil { + return err } - return errors.Wrapf(xerr.NewErrCode(xerr.FamilyCrossBindForbidden), "user already belongs to another family") } if ownerFamily.Id == 0 { @@ -116,12 +138,8 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source memberExists := err == nil if memberExists { - if memberRecord.FamilyId == ownerFamily.Id { - if memberRecord.Status == user.FamilyMemberActive { - return errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already in this family") - } - } else { - return errors.Wrapf(xerr.NewErrCode(xerr.FamilyCrossBindForbidden), "user already belongs to another family") + if err = validateMemberJoinConflict(ownerFamily.Id, &memberRecord); err != nil { + return err } } @@ -151,12 +169,16 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source return nil } + if memberRecord.FamilyId != ownerFamily.Id { + memberRecord.FamilyId = ownerFamily.Id + } memberRecord.Status = user.FamilyMemberActive memberRecord.Role = user.FamilyRoleMember memberRecord.JoinSource = source memberRecord.JoinedAt = now memberRecord.LeftAt = nil - if err = tx.Save(&memberRecord).Error; err != nil { + memberRecord.DeletedAt = gorm.DeletedAt{} + if err = tx.Unscoped().Save(&memberRecord).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update family member failed") } return nil diff --git a/internal/logic/public/user/familyBindingHelper_test.go b/internal/logic/public/user/familyBindingHelper_test.go new file mode 100644 index 0000000..c6ef4b3 --- /dev/null +++ b/internal/logic/public/user/familyBindingHelper_test.go @@ -0,0 +1,107 @@ +package user + +import ( + stderrors "errors" + "testing" + + modelUser "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" + pkgerrors "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func extractFamilyJoinCode(err error) uint32 { + if err == nil { + return 0 + } + + var codeErr *xerr.CodeError + if stderrors.As(pkgerrors.Cause(err), &codeErr) { + return codeErr.GetErrCode() + } + return 0 +} + +func TestValidateMemberJoinConflict(t *testing.T) { + ownerFamilyID := int64(11) + + testCases := []struct { + name string + ownerFamily int64 + memberRecord *modelUser.UserFamilyMember + wantCode uint32 + }{ + { + name: "no member record", + ownerFamily: ownerFamilyID, + wantCode: 0, + }, + { + name: "same family active member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID, + Status: modelUser.FamilyMemberActive, + }, + wantCode: xerr.FamilyAlreadyBound, + }, + { + name: "same family left member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID, + Status: modelUser.FamilyMemberLeft, + }, + wantCode: 0, + }, + { + name: "same family removed member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID, + Status: modelUser.FamilyMemberRemoved, + }, + wantCode: 0, + }, + { + name: "cross family active member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID + 1, + Status: modelUser.FamilyMemberActive, + }, + wantCode: xerr.FamilyCrossBindForbidden, + }, + { + name: "cross family left member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID + 1, + Status: modelUser.FamilyMemberLeft, + }, + wantCode: 0, + }, + { + name: "cross family removed member", + ownerFamily: ownerFamilyID, + memberRecord: &modelUser.UserFamilyMember{ + FamilyId: ownerFamilyID + 1, + Status: modelUser.FamilyMemberRemoved, + }, + wantCode: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := validateMemberJoinConflict(testCase.ownerFamily, testCase.memberRecord) + if testCase.wantCode == 0 { + require.NoError(t, err) + return + } + + require.Error(t, err) + require.Equal(t, testCase.wantCode, extractFamilyJoinCode(err)) + }) + } +} diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index 8709834..cc475bf 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -36,12 +36,12 @@ func init() { RegisterIPLimit: "Too many registrations", EmailBindError: "Email already bound", UserBindInviteCodeExist: "Invite code already bound", - FamilyMemberLimitExceeded: "Family member limit exceeded", - FamilyAlreadyBound: "Family already bound", - FamilyCrossBindForbidden: "Cross-family binding is forbidden", - FamilyNotExist: "Family does not exist", - FamilyStatusInvalid: "Family status is invalid", - FamilyOwnerOperationForbidden: "Owner operation is forbidden", + FamilyMemberLimitExceeded: "家庭成员数量已达上限", + FamilyAlreadyBound: "已绑定家庭组", + FamilyCrossBindForbidden: "禁止跨家庭组绑定", + FamilyNotExist: "家庭组不存在", + FamilyStatusInvalid: "家庭组状态无效", + FamilyOwnerOperationForbidden: "家庭组所有者不允许此操作", // Node error NodeExist: "Node already exists", diff --git a/pkg/xerr/family_err_test.go b/pkg/xerr/family_err_test.go index ba2481c..35842e0 100644 --- a/pkg/xerr/family_err_test.go +++ b/pkg/xerr/family_err_test.go @@ -7,9 +7,9 @@ import ( ) func TestFamilyErrorCodeMessages(t *testing.T) { - require.Equal(t, "Family member limit exceeded", MapErrMsg(FamilyMemberLimitExceeded)) - require.Equal(t, "Family already bound", MapErrMsg(FamilyAlreadyBound)) - require.Equal(t, "Cross-family binding is forbidden", MapErrMsg(FamilyCrossBindForbidden)) + require.Equal(t, "家庭成员数量已达上限", MapErrMsg(FamilyMemberLimitExceeded)) + require.Equal(t, "已绑定家庭组", MapErrMsg(FamilyAlreadyBound)) + require.Equal(t, "禁止跨家庭组绑定", MapErrMsg(FamilyCrossBindForbidden)) } func TestFamilyErrorCodeIsRegistered(t *testing.T) {