package user import ( "context" "time" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" "gorm.io/gorm/clause" ) type familyJoinResult struct { FamilyId int64 OwnerUserId int64 } type familyBindingHelper struct { ctx context.Context svcCtx *svc.ServiceContext } func newFamilyBindingHelper(ctx context.Context, svcCtx *svc.ServiceContext) *familyBindingHelper { return &familyBindingHelper{ ctx: ctx, svcCtx: svcCtx, } } func (h *familyBindingHelper) getUserEmailMethod(userId int64) (*user.AuthMethods, error) { method, err := h.svcCtx.UserModel.FindUserAuthMethodByUserId(h.ctx, "email", userId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user email auth method failed") } 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") } var ownerFamily user.UserFamily err := h.svcCtx.DB.WithContext(h.ctx). Unscoped(). Model(&user.UserFamily{}). 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{}). Where("user_id = ?", memberUserId). First(&memberRecord).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member family relation failed") } if err == nil { memberExists = true } if memberExists { if err = validateMemberJoinConflict(ownerFamily.Id, &memberRecord); err != nil { return err } } if ownerFamily.Id == 0 { return nil } var activeCount int64 if err = h.svcCtx.DB.WithContext(h.ctx). Model(&user.UserFamilyMember{}). Where("family_id = ? AND status = ?", ownerFamily.Id, user.FamilyMemberActive). Count(&activeCount).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count family members failed") } if activeCount >= ownerFamily.MaxMembers { return errors.Wrapf(xerr.NewErrCode(xerr.FamilyMemberLimitExceeded), "family member limit exceeded") } return nil } func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) { if ownerUserId == memberUserId { return nil, errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already bound to this family") } result := &familyJoinResult{ OwnerUserId: ownerUserId, } removedSubscribes := make([]user.Subscribe, 0) err := h.svcCtx.DB.WithContext(h.ctx).Transaction(func(tx *gorm.DB) error { ownerFamily, err := h.getOrCreateOwnerFamily(tx, ownerUserId) if err != nil { return err } result.FamilyId = ownerFamily.Id if err = h.ensureOwnerMember(tx, ownerFamily.Id, ownerUserId); err != nil { return err } var memberRecord user.UserFamilyMember err = tx.Unscoped().Model(&user.UserFamilyMember{}).Where("user_id = ?", memberUserId).First(&memberRecord).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member family relation failed") } memberExists := err == nil if memberExists { if err = validateMemberJoinConflict(ownerFamily.Id, &memberRecord); err != nil { return err } } var activeCount int64 if err = tx.Model(&user.UserFamilyMember{}). Where("family_id = ? AND status = ?", ownerFamily.Id, user.FamilyMemberActive). Count(&activeCount).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count family members failed") } if activeCount >= ownerFamily.MaxMembers { return errors.Wrapf(xerr.NewErrCode(xerr.FamilyMemberLimitExceeded), "family member limit exceeded") } now := time.Now() if !memberExists { memberRecord = user.UserFamilyMember{ FamilyId: ownerFamily.Id, UserId: memberUserId, Role: user.FamilyRoleMember, Status: user.FamilyMemberActive, JoinSource: source, JoinedAt: now, } if err = tx.Create(&memberRecord).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create family member failed") } } else { 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 memberRecord.DeletedAt = gorm.DeletedAt{} if err = tx.Unscoped().Save(&memberRecord).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update family member failed") } } removedSubscribes, err = transferMemberSubscribesToOwner(tx, memberUserId, ownerUserId) if err != nil { return err } return nil }) if err != nil { return nil, err } if err = h.clearRemovedMemberSubscribeCache(removedSubscribes); err != nil { return nil, err } return result, nil } func transferMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) { var subscribes []user.Subscribe if err := tx.Model(&user.Subscribe{}). Where("user_id = ?", memberUserId). Find(&subscribes).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member subscribe list failed") } if len(subscribes) == 0 { return nil, nil } if err := tx.Model(&user.Subscribe{}). Where("user_id = ?", memberUserId). Update("user_id", ownerUserId).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "transfer member subscribes to owner failed") } return subscribes, nil } func (h *familyBindingHelper) clearRemovedMemberSubscribeCache(removedSubscribes []user.Subscribe) error { if len(removedSubscribes) == 0 { return nil } subscribeModels, subscribeIDSet := buildRemovedSubscribeCacheMeta(removedSubscribes) if err := h.svcCtx.UserModel.ClearSubscribeCache(h.ctx, subscribeModels...); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear member subscribe cache failed") } for subscribeID := range subscribeIDSet { if err := h.svcCtx.SubscribeModel.ClearCache(h.ctx, subscribeID); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear subscribe cache failed") } } if err := h.svcCtx.NodeModel.ClearServerAllCache(h.ctx); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear node cache failed") } return nil } func buildRemovedSubscribeCacheMeta(removedSubscribes []user.Subscribe) ([]*user.Subscribe, map[int64]struct{}) { subscribeModels := make([]*user.Subscribe, 0, len(removedSubscribes)) subscribeIDSet := make(map[int64]struct{}, len(removedSubscribes)) for i := range removedSubscribes { subscribeModels = append(subscribeModels, &removedSubscribes[i]) if removedSubscribes[i].SubscribeId > 0 { subscribeIDSet[removedSubscribes[i].SubscribeId] = struct{}{} } } return subscribeModels, subscribeIDSet } func (h *familyBindingHelper) getOrCreateOwnerFamily(tx *gorm.DB, ownerUserId int64) (*user.UserFamily, error) { var ownerFamily user.UserFamily err := tx.Unscoped().Clauses(clause.Locking{Strength: "UPDATE"}). Model(&user.UserFamily{}). Where("owner_user_id = ?", ownerUserId). First(&ownerFamily).Error if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query owner family failed") } ownerFamily = user.UserFamily{ OwnerUserId: ownerUserId, MaxMembers: user.DefaultFamilyMaxSize, Status: user.FamilyStatusActive, } if err = tx.Create(&ownerFamily).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create owner family failed") } } else if ownerFamily.Status != user.FamilyStatusActive || ownerFamily.DeletedAt.Valid { ownerFamily.Status = user.FamilyStatusActive ownerFamily.DeletedAt = gorm.DeletedAt{} if err = tx.Unscoped().Save(&ownerFamily).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "activate owner family failed") } } return &ownerFamily, nil } func (h *familyBindingHelper) ensureOwnerMember(tx *gorm.DB, familyId, ownerUserId int64) error { var ownerMember user.UserFamilyMember err := tx.Unscoped().Model(&user.UserFamilyMember{}).Where("user_id = ?", ownerUserId).First(&ownerMember).Error if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query owner family member failed") } ownerMember = user.UserFamilyMember{ FamilyId: familyId, UserId: ownerUserId, Role: user.FamilyRoleOwner, Status: user.FamilyMemberActive, JoinSource: "owner_init", JoinedAt: time.Now(), } if err = tx.Create(&ownerMember).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create owner family member failed") } return nil } if ownerMember.FamilyId != familyId { return errors.Wrapf(xerr.NewErrCode(xerr.FamilyCrossBindForbidden), "owner already belongs to another family") } if ownerMember.Status != user.FamilyMemberActive || ownerMember.Role != user.FamilyRoleOwner || ownerMember.DeletedAt.Valid { ownerMember.Status = user.FamilyMemberActive ownerMember.Role = user.FamilyRoleOwner ownerMember.LeftAt = nil ownerMember.DeletedAt = gorm.DeletedAt{} if err = tx.Unscoped().Save(&ownerMember).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update owner family member failed") } } return nil }