package account import ( "errors" "fmt" "strings" "time" "rmser/internal/domain/account" "rmser/pkg/logger" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) type pgRepository struct { db *gorm.DB } func NewRepository(db *gorm.DB) account.Repository { return &pgRepository{db: db} } // GetOrCreateUser находит пользователя или создает нового func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last string) (*account.User, error) { var user account.User err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error if err != nil { if err == gorm.ErrRecordNotFound { newUser := account.User{ TelegramID: telegramID, Username: username, FirstName: first, LastName: last, } if err := r.db.Create(&newUser).Error; err != nil { return nil, err } return &newUser, nil } return nil, err } // Обновляем инфо if user.Username != username || user.FirstName != first || user.LastName != last { user.Username = username user.FirstName = first user.LastName = last r.db.Save(&user) } return &user, nil } func (r *pgRepository) GetUserByTelegramID(telegramID int64) (*account.User, error) { var user account.User err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error if err != nil { return nil, err } return &user, nil } func (r *pgRepository) GetUserByID(id uuid.UUID) (*account.User, error) { var user account.User err := r.db.Where("id = ?", id).First(&user).Error if err != nil { return nil, err } return &user, nil } // ConnectServer - Основная точка входа для добавления сервера func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedPass, name string) (*account.RMSServer, error) { cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/") var server account.RMSServer var created bool err := r.db.Transaction(func(tx *gorm.DB) error { // Сначала ищем среди удаленных серверов err := tx.Unscoped().Where("base_url = ?", cleanURL).First(&server).Error if err != nil && err != gorm.ErrRecordNotFound { return err } if err == gorm.ErrRecordNotFound { // --- СЦЕНАРИЙ 1: НОВЫЙ СЕРВЕР (Приветственный бонус) --- trialDays := 30 welcomeBalance := 10 paidUntil := time.Now().AddDate(0, 0, trialDays) server = account.RMSServer{ BaseURL: cleanURL, Name: name, MaxUsers: 5, Balance: welcomeBalance, PaidUntil: &paidUntil, } if err := tx.Create(&server).Error; err != nil { return err } created = true } else if server.DeletedAt.Valid { // --- СЦЕНАРИЙ 2: ВОССТАНОВЛЕНИЕ УДАЛЕННОГО СЕРВЕРА --- // Восстанавливаем сервер, сохраняя старые значения Balance, InvoiceCount и ID server.Name = name server.DeletedAt = gorm.DeletedAt{} // Сбрасываем deleted_at if err := tx.Save(&server).Error; err != nil { return err } created = true // При восстановлении пользователь становится владельцем } else { // --- СЦЕНАРИЙ 3: СУЩЕСТВУЮЩИЙ АКТИВНЫЙ СЕРВЕР --- var userCount int64 tx.Model(&account.ServerUser{}).Where("server_id = ?", server.ID).Count(&userCount) if userCount >= int64(server.MaxUsers) { var exists int64 tx.Model(&account.ServerUser{}).Where("server_id = ? AND user_id = ?", server.ID, userID).Count(&exists) if exists == 0 { return fmt.Errorf("достигнут лимит пользователей на сервере (%d)", server.MaxUsers) } } } targetRole := account.RoleOperator if created { targetRole = account.RoleOwner } if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil { return err } var existingLink account.ServerUser err = tx.Where("server_id = ? AND user_id = ?", server.ID, userID).First(&existingLink).Error if err == nil { existingLink.Login = login existingLink.EncryptedPassword = encryptedPass existingLink.IsActive = true return tx.Save(&existingLink).Error } userLink := account.ServerUser{ ServerID: server.ID, UserID: userID, Role: targetRole, IsActive: true, Login: login, EncryptedPassword: encryptedPass, } return tx.Create(&userLink).Error }) if err != nil { return nil, err } return &server, nil } func (r *pgRepository) SaveServerSettings(server *account.RMSServer) error { return r.db.Model(server).Updates(map[string]interface{}{ "name": server.Name, "default_store_id": server.DefaultStoreID, "root_group_guid": server.RootGroupGUID, "auto_process": server.AutoProcess, "max_users": server.MaxUsers, "sync_interval": server.SyncInterval, }).Error } // UpdateLastActivity обновляет время последней активности пользователя func (r *pgRepository) UpdateLastActivity(serverID uuid.UUID) error { result := r.db.Model(&account.RMSServer{}). Where("id = ?", serverID). Update("last_activity_at", gorm.Expr("NOW()")) if result.Error != nil { logger.Log.Error("Failed to update last_activity_at", zap.String("server_id", serverID.String()), zap.Error(result.Error)) return result.Error } if result.RowsAffected == 0 { logger.Log.Warn("UpdateLastActivity: server not found", zap.String("server_id", serverID.String())) return fmt.Errorf("сервер не найден") } return nil } // UpdateLastSync обновляет время последней успешной синхронизации func (r *pgRepository) UpdateLastSync(serverID uuid.UUID) error { result := r.db.Model(&account.RMSServer{}). Where("id = ?", serverID). Update("last_sync_at", gorm.Expr("NOW()")) if result.Error != nil { logger.Log.Error("Failed to update last_sync_at", zap.String("server_id", serverID.String()), zap.Error(result.Error)) return result.Error } if result.RowsAffected == 0 { logger.Log.Warn("UpdateLastSync: server not found", zap.String("server_id", serverID.String())) return fmt.Errorf("сервер не найден") } return nil } // GetServersForSync возвращает серверы, готовые для синхронизации func (r *pgRepository) GetServersForSync(idleThreshold time.Duration) ([]account.RMSServer, error) { var servers []account.RMSServer // Конвертируем duration в минуты для SQL idleMinutes := int(idleThreshold.Minutes()) query := ` SELECT * FROM rms_servers WHERE deleted_at IS NULL AND ( -- Случай 1: Настало время периодической синхронизации (EXTRACT(EPOCH FROM (NOW() - COALESCE(last_sync_at, '1970-01-01'::timestamp))) / 60) >= sync_interval OR -- Случай 2: Прошло N мин с последней активности, и активность была ПОЗЖЕ синхронизации ( last_activity_at > last_sync_at AND (EXTRACT(EPOCH FROM (NOW() - last_activity_at)) / 60) >= ? ) ) ` err := r.db.Raw(query, idleMinutes).Scan(&servers).Error if err != nil { logger.Log.Error("Failed to get servers for sync", zap.Int("idle_threshold_minutes", idleMinutes), zap.Error(err)) return nil, err } logger.Log.Info("Servers ready for sync", zap.Int("count", len(servers)), zap.Int("idle_threshold_minutes", idleMinutes)) return servers, nil } func (r *pgRepository) SetActiveServer(userID, serverID uuid.UUID) error { return r.db.Transaction(func(tx *gorm.DB) error { // Проверка доступа var count int64 tx.Model(&account.ServerUser{}).Where("user_id = ? AND server_id = ?", userID, serverID).Count(&count) if count == 0 { return errors.New("доступ к серверу запрещен") } if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil { return err } return tx.Model(&account.ServerUser{}).Where("user_id = ? AND server_id = ?", userID, serverID).Update("is_active", true).Error }) } func (r *pgRepository) GetActiveServer(userID uuid.UUID) (*account.RMSServer, error) { var server account.RMSServer err := r.db.Table("rms_servers"). Select("rms_servers.*"). Joins("JOIN server_users ON server_users.server_id = rms_servers.id"). Where("server_users.user_id = ? AND server_users.is_active = ?", userID, true). First(&server).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &server, nil } // GetActiveConnectionCredentials возвращает креды для подключения. // Логика: // 1. Берем личные креды из server_users (если активен) // 2. Если личных нет (пустой пароль) -> ищем креды Владельца (Owner) этого сервера func (r *pgRepository) GetActiveConnectionCredentials(userID uuid.UUID) (url, login, passHash string, err error) { // 1. Получаем связь текущего юзера с активным сервером type Result struct { ServerID uuid.UUID BaseURL string Login string EncryptedPassword string } var res Result err = r.db.Table("server_users"). Select("server_users.server_id, rms_servers.base_url, server_users.login, server_users.encrypted_password"). Joins("JOIN rms_servers ON rms_servers.id = server_users.server_id"). Where("server_users.user_id = ? AND server_users.is_active = ?", userID, true). Scan(&res).Error if err != nil { return "", "", "", err } if res.ServerID == uuid.Nil { return "", "", "", errors.New("нет активного сервера") } // Если есть личные креды - возвращаем их if res.Login != "" && res.EncryptedPassword != "" { return res.BaseURL, res.Login, res.EncryptedPassword, nil } // 2. Фоллбэк: ищем креды владельца (OWNER) var ownerLink account.ServerUser err = r.db.Where("server_id = ? AND role = ?", res.ServerID, account.RoleOwner). Order("created_at ASC"). // На случай коллизий, берем старейшего First(&ownerLink).Error if err != nil { return "", "", "", fmt.Errorf("у вас нет учетных данных, а владелец сервера не найден: %w", err) } if ownerLink.Login == "" || ownerLink.EncryptedPassword == "" { return "", "", "", errors.New("у владельца сервера отсутствуют учетные данные") } return res.BaseURL, ownerLink.Login, ownerLink.EncryptedPassword, nil } func (r *pgRepository) GetAllAvailableServers(userID uuid.UUID) ([]account.RMSServer, error) { var servers []account.RMSServer err := r.db.Table("rms_servers"). Select("rms_servers.*"). Joins("JOIN server_users ON server_users.server_id = rms_servers.id"). Where("server_users.user_id = ?", userID). Find(&servers).Error return servers, err } func (r *pgRepository) DeleteServer(serverID uuid.UUID) error { // Мягкое удаление сервера и всех связей return r.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("server_id = ?", serverID).Delete(&account.ServerUser{}).Error; err != nil { return err } if err := tx.Delete(&account.RMSServer{}, serverID).Error; err != nil { return err } return nil }) } // --- Управление правами --- func (r *pgRepository) GetUserRole(userID, serverID uuid.UUID) (account.Role, error) { var link account.ServerUser err := r.db.Select("role").Where("user_id = ? AND server_id = ?", userID, serverID).First(&link).Error if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New("access denied") } return "", err } return link.Role, nil } func (r *pgRepository) SetUserRole(serverID, targetUserID uuid.UUID, newRole account.Role) error { return r.db.Model(&account.ServerUser{}). Where("server_id = ? AND user_id = ?", serverID, targetUserID). Update("role", newRole).Error } func (r *pgRepository) GetServerUsers(serverID uuid.UUID) ([]account.ServerUser, error) { var users []account.ServerUser // Preload User для отображения имен err := r.db.Preload("User").Where("server_id = ?", serverID).Find(&users).Error return users, err } func (r *pgRepository) AddUserToServer(serverID, userID uuid.UUID, role account.Role) error { // Проверка лимита перед добавлением var server account.RMSServer if err := r.db.First(&server, serverID).Error; err != nil { return err } return r.db.Transaction(func(tx *gorm.DB) error { // 1. Сначала проверяем, существует ли пользователь на этом сервере var existingLink account.ServerUser err := tx.Where("server_id = ? AND user_id = ?", serverID, userID).First(&existingLink).Error if err == nil { // --- ПОЛЬЗОВАТЕЛЬ УЖЕ ЕСТЬ --- // Защита от понижения прав: // Если текущая роль OWNER или ADMIN, а мы пытаемся поставить OPERATOR (через инвайт), // то игнорируем смену роли, просто делаем активным. if (existingLink.Role == account.RoleOwner || existingLink.Role == account.RoleAdmin) && role == account.RoleOperator { role = existingLink.Role } // Обновляем активность и (возможно) роль return tx.Model(&existingLink).Updates(map[string]interface{}{ "role": role, "is_active": true, }).Error } // --- ПОЛЬЗОВАТЕЛЬ НОВЫЙ --- // Проверяем лимит только для новых var currentCount int64 tx.Model(&account.ServerUser{}).Where("server_id = ?", serverID).Count(¤tCount) if currentCount >= int64(server.MaxUsers) { return fmt.Errorf("лимит пользователей (%d) превышен", server.MaxUsers) } // Сбрасываем активность на других серверах if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil { return err } // Создаем связь link := account.ServerUser{ ServerID: serverID, UserID: userID, Role: role, IsActive: true, } return tx.Create(&link).Error }) } func (r *pgRepository) RemoveUserFromServer(serverID, userID uuid.UUID) error { return r.db.Where("server_id = ? AND user_id = ?", serverID, userID).Delete(&account.ServerUser{}).Error } func (r *pgRepository) IncrementInvoiceCount(serverID uuid.UUID) error { return r.db.Model(&account.RMSServer{}). Where("id = ?", serverID). UpdateColumn("invoice_count", gorm.Expr("invoice_count + ?", 1)).Error } // --- Super Admin Functions --- func (r *pgRepository) GetAllServersSystemWide() ([]account.RMSServer, error) { var servers []account.RMSServer // Загружаем вместе с владельцем для отображения err := r.db.Order("name ASC").Find(&servers).Error return servers, err } func (r *pgRepository) TransferOwnership(serverID, newOwnerID uuid.UUID) error { return r.db.Transaction(func(tx *gorm.DB) error { // 1. Находим текущего владельца var currentOwnerLink account.ServerUser if err := tx.Where("server_id = ? AND role = ?", serverID, account.RoleOwner).First(¤tOwnerLink).Error; err != nil { return fmt.Errorf("current owner not found: %w", err) } // 2. Проверяем, что новый владелец вообще есть на сервере var newOwnerLink account.ServerUser if err := tx.Where("server_id = ? AND user_id = ?", serverID, newOwnerID).First(&newOwnerLink).Error; err != nil { return fmt.Errorf("target user not found on server: %w", err) } // 3. Понижаем старого владельца до ADMIN if err := tx.Model(¤tOwnerLink).Update("role", account.RoleAdmin).Error; err != nil { return err } // 4. Повышаем нового до OWNER if err := tx.Model(&newOwnerLink).Update("role", account.RoleOwner).Error; err != nil { return err } // УДАЛЕНО: обновление server.owner_id, так как этого поля нет в модели return nil }) } func (r *pgRepository) GetConnectionByID(id uuid.UUID) (*account.ServerUser, error) { var link account.ServerUser // Preload нужны, чтобы показать имена в админке err := r.db.Preload("User").Preload("Server").Where("id = ?", id).First(&link).Error if err != nil { return nil, err } return &link, nil } func (r *pgRepository) GetServerByURL(rawURL string) (*account.RMSServer, error) { cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/") var server account.RMSServer err := r.db.Where("base_url = ?", cleanURL).First(&server).Error if err != nil { return nil, err } return &server, nil } func (r *pgRepository) GetServerByID(id uuid.UUID) (*account.RMSServer, error) { var server account.RMSServer err := r.db.First(&server, id).Error if err != nil { return nil, err } return &server, nil } // UpdateBalance начисляет пакет или продлевает подписку func (r *pgRepository) UpdateBalance(serverID uuid.UUID, amountChange int, newPaidUntil *time.Time) error { return r.db.Model(&account.RMSServer{}).Where("id = ?", serverID).Updates(map[string]interface{}{ "balance": gorm.Expr("balance + ?", amountChange), "paid_until": newPaidUntil, }).Error } // DecrementBalance списывает 1 единицу при отправке накладной func (r *pgRepository) DecrementBalance(serverID uuid.UUID) error { return r.db.Model(&account.RMSServer{}). Where("id = ? AND balance > 0", serverID). UpdateColumn("balance", gorm.Expr("balance - ?", 1)).Error } // === Уведомления о черновиках === // GetServerUsersForDraftNotification возвращает Admin/Owner пользователей, // которым нужно отправить уведомление о новом черновике func (r *pgRepository) GetServerUsersForDraftNotification(serverID uuid.UUID, excludeUserID uuid.UUID) ([]account.ServerUser, error) { var users []account.ServerUser err := r.db.Preload("User"). Where("server_id = ?", serverID). Where("role IN ?", []account.Role{account.RoleOwner, account.RoleAdmin}). Where("mute_draft_notifications = ?", false). Where("user_id != ?", excludeUserID). Find(&users).Error return users, err } // SetMuteDraftNotifications включает/выключает уведомления для пользователя func (r *pgRepository) SetMuteDraftNotifications(userID, serverID uuid.UUID, mute bool) error { return r.db.Model(&account.ServerUser{}). Where("user_id = ? AND server_id = ?", userID, serverID). Update("mute_draft_notifications", mute).Error } // === Session Management === // CreateSession создает новую сессию пользователя func (r *pgRepository) CreateSession(session *account.Session) error { return r.db.Create(session).Error } // GetSessionByToken ищет сессию по токену func (r *pgRepository) GetSessionByToken(token string) (*account.Session, error) { var session account.Session if err := r.db.Where("refresh_token = ?", token).First(&session).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } return &session, nil } // UpdateSessionExpiry обновляет время истечения сессии (sliding expiration) func (r *pgRepository) UpdateSessionExpiry(sessionID uuid.UUID, newExpiry time.Time) error { return r.db.Model(&account.Session{}).Where("id = ?", sessionID).Update("expires_at", newExpiry).Error } // DeleteSession удаляет сессию по токену func (r *pgRepository) DeleteSession(token string) error { return r.db.Where("refresh_token = ?", token).Delete(&account.Session{}).Error }