package account import ( "api_yal/internal/logger" "api_yal/internal/models" "api_yal/internal/repository" "crypto/rand" "encoding/hex" "errors" "fmt" "time" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // AccountService интерфейс сервиса аккаунтов type AccountService interface { // Основные операции GetAccountByID(id uint) (*AccountResponse, error) GetAccountByEmail(email string) (*AccountResponse, error) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) DeleteAccount(id uint) error // Управление паролем ChangePassword(userID uint, req ChangePasswordRequest) error // Административные функции ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) VerifyAccount(accountID uint, req VerifyAccountRequest) error UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error // Статистика GetAccountStats(userID uint) (*AccountStats, error) GetAccountProfile(userID uint) (*AccountProfileResponse, error) // Внутренние методы для auth сервиса GetAccountModelByID(id uint) (*models.Account, error) GetAccountModelByEmail(email string) (*models.Account, error) CreateAccount(req CreateAccountRequest) (*models.Account, error) UpdateAccountModel(account *models.Account) error } // accountServiceImpl реализация сервиса аккаунтов type accountServiceImpl struct { accountRepo repository.AccountRepository } // NewService создает новый экземпляр сервиса аккаунтов func NewService(accountRepo repository.AccountRepository) AccountService { return &accountServiceImpl{ accountRepo: accountRepo, } } // GetAccountByID получает аккаунт по ID func (s *accountServiceImpl) GetAccountByID(id uint) (*AccountResponse, error) { l := logger.Get() l.Debug("Получение аккаунта по ID", zap.Uint("id", id)) account, err := s.accountRepo.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } l.Error("Ошибка получения аккаунта", zap.Error(err)) return nil, err } response := ToAccountResponse(account) return &response, nil } // GetAccountByEmail получает аккаунт по email func (s *accountServiceImpl) GetAccountByEmail(email string) (*AccountResponse, error) { l := logger.Get() l.Debug("Получение аккаунта по email", zap.String("email", email)) account, err := s.accountRepo.GetByEmail(email) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } l.Error("Ошибка получения аккаунта", zap.Error(err)) return nil, err } response := ToAccountResponse(account) return &response, nil } // GetAccountWithObjects получает аккаунт с его объектами func (s *accountServiceImpl) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) { l := logger.Get() l.Debug("Получение аккаунта с объектами", zap.Uint("id", id)) account, err := s.accountRepo.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } l.Error("Ошибка получения аккаунта", zap.Error(err)) return nil, err } objects, err := s.accountRepo.GetObjects(id) if err != nil { l.Error("Ошибка получения объектов", zap.Error(err)) return nil, err } account.Objects = objects response := ToAccountWithObjectsResponse(account) return &response, nil } // UpdateAccount обновляет информацию об аккаунте func (s *accountServiceImpl) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) { l := logger.Get() l.Info("Обновление аккаунта", zap.Uint("id", id)) account, err := s.accountRepo.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } return nil, err } // Обновляем поля if req.FullName != "" { account.FullName = req.FullName } if req.FirstName != "" { account.FirstName = req.FirstName } if req.LastName != "" { account.LastName = req.LastName } if req.Phone != "" { account.Phone = req.Phone } if req.City != "" { account.City = req.City } if req.OrganizationForm != "" { account.OrganizationForm = req.OrganizationForm } if req.OrganizationName != "" { account.OrganizationName = req.OrganizationName } if req.OrganizationShort != "" { account.OrganizationShort = req.OrganizationShort } if req.INN != "" { account.INN = req.INN } if req.PersonalINN != "" { account.PersonalINN = req.PersonalINN } if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления аккаунта", zap.Error(err)) return nil, err } response := ToAccountResponse(account) return &response, nil } // DeleteAccount удаляет аккаунт (мягкое удаление) func (s *accountServiceImpl) DeleteAccount(id uint) error { l := logger.Get() l.Info("Удаление аккаунта", zap.Uint("id", id)) if err := s.accountRepo.Delete(id); err != nil { l.Error("Ошибка удаления аккаунта", zap.Error(err)) return err } return nil } // ChangePassword изменяет пароль пользователя func (s *accountServiceImpl) ChangePassword(userID uint, req ChangePasswordRequest) error { l := logger.Get() l.Info("Смена пароля", zap.Uint("userID", userID)) account, err := s.accountRepo.GetByID(userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrAccountNotFound } return err } // Проверяем текущий пароль if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.CurrentPassword)); err != nil { l.Error("Неверный текущий пароль") return ErrInvalidCurrentPassword } // Хешируем новый пароль hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { l.Error("Ошибка хеширования пароля", zap.Error(err)) return err } account.PasswordHash = string(hashedPassword) if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления пароля", zap.Error(err)) return err } l.Info("Пароль успешно изменен", zap.Uint("userID", userID)) return nil } // ForgotPassword запрашивает сброс пароля func (s *accountServiceImpl) ForgotPassword(email string) (string, error) { l := logger.Get() l.Info("Запрос сброса пароля", zap.String("email", email)) account, err := s.accountRepo.GetByEmail(email) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // Для безопасности не сообщаем, что пользователь не найден return "", nil } return "", err } // Генерируем reset token (используем метод из auth сервиса) // В реальном приложении здесь должна быть генерация токена resetToken, err := generateResetToken() if err != nil { l.Error("Ошибка генерации reset token", zap.Error(err)) return "", err } passwordReset := &models.PasswordReset{ AccountID: account.ID, Token: resetToken, ExpiresAt: time.Now().Add(1 * time.Hour), Used: false, } if err := s.accountRepo.CreatePasswordReset(passwordReset); err != nil { l.Error("Ошибка сохранения reset token", zap.Error(err)) return "", err } l.Info("Reset token создан", zap.String("email", email)) return resetToken, nil } // ResetPassword сбрасывает пароль по токену func (s *accountServiceImpl) ResetPassword(token, newPassword string) error { l := logger.Get() l.Info("Сброс пароля по токену") passwordReset, err := s.accountRepo.GetPasswordResetByToken(token) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrResetTokenNotFound } return err } // Проверяем срок действия токена if passwordReset.ExpiresAt.Before(time.Now()) { return ErrResetTokenExpired } // Проверяем, не использован ли токен if passwordReset.Used { return ErrResetTokenAlreadyUsed } // Получаем пользователя account, err := s.accountRepo.GetByID(passwordReset.AccountID) if err != nil { return ErrAccountNotFound } // Хешируем новый пароль hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { l.Error("Ошибка хеширования пароля", zap.Error(err)) return err } // Обновляем пароль account.PasswordHash = string(hashedPassword) if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления пароля", zap.Error(err)) return err } // Помечаем токен как использованный passwordReset.Used = true if err := s.accountRepo.UpdatePasswordReset(passwordReset); err != nil { l.Error("Ошибка обновления статуса токена", zap.Error(err)) } l.Info("Пароль успешно сброшен", zap.Uint("userID", account.ID)) return nil } // ListAccounts возвращает список аккаунтов с пагинацией func (s *accountServiceImpl) ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) { l := logger.Get() l.Debug("Получение списка аккаунтов", zap.Any("request", req)) // Устанавливаем значения по умолчанию if req.Page == 0 { req.Page = 1 } if req.PageSize == 0 { req.PageSize = 20 } offset := (req.Page - 1) * req.PageSize var accounts []models.Account var total int64 var err error // Поиск с фильтрацией if req.Search != "" { accounts, err = s.accountRepo.Search(req.Search, offset, req.PageSize) if err != nil { l.Error("Ошибка поиска аккаунтов", zap.Error(err)) return nil, err } total, err = s.getSearchTotal(req.Search) } else { accounts, err = s.accountRepo.List(offset, req.PageSize) if err != nil { l.Error("Ошибка получения списка аккаунтов", zap.Error(err)) return nil, err } total, err = s.accountRepo.Count() } if err != nil { return nil, err } // Фильтруем по роли и статусу (если нужно) filteredAccounts := s.filterAccounts(accounts, req.Role, req.IsActive) items := make([]AccountResponse, len(filteredAccounts)) for i, acc := range filteredAccounts { items[i] = ToAccountResponse(&acc) } totalPages := int(total) / req.PageSize if int(total)%req.PageSize > 0 { totalPages++ } return &AccountListResponse{ Items: items, Total: total, Page: req.Page, PageSize: req.PageSize, TotalPages: totalPages, }, nil } // VerifyAccount верифицирует аккаунт func (s *accountServiceImpl) VerifyAccount(accountID uint, req VerifyAccountRequest) error { l := logger.Get() l.Info("Верификация аккаунта", zap.Uint("id", accountID)) account, err := s.accountRepo.GetByID(accountID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrAccountNotFound } return err } account.IsVerified = req.IsVerified if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления статуса верификации", zap.Error(err)) return err } l.Info("Аккаунт верифицирован", zap.Uint("id", accountID)) return nil } // UpdateAccountStatus обновляет статус аккаунта (админ) func (s *accountServiceImpl) UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error { l := logger.Get() l.Info("Обновление статуса аккаунта", zap.Uint("id", accountID)) account, err := s.accountRepo.GetByID(accountID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrAccountNotFound } return err } account.IsActive = req.IsActive if req.Role != "" { account.Role = req.Role } if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления статуса аккаунта", zap.Error(err)) return err } l.Info("Статус аккаунта обновлен", zap.Uint("id", accountID)) return nil } // GetAccountStats получает статистику аккаунта func (s *accountServiceImpl) GetAccountStats(userID uint) (*AccountStats, error) { l := logger.Get() l.Debug("Получение статистики аккаунта", zap.Uint("userID", userID)) // Здесь должна быть реальная логика подсчета статистики // В реальном приложении нужно запрашивать данные из соответствующих репозиториев stats := &AccountStats{ ObjectsCount: 0, FeedbacksCount: 0, CommentsCount: 0, RatingsCount: 0, AppealsCount: 0, } // TODO: Получить реальную статистику из базы данных return stats, nil } // GetAccountProfile получает профиль пользователя со статистикой func (s *accountServiceImpl) GetAccountProfile(userID uint) (*AccountProfileResponse, error) { l := logger.Get() l.Debug("Получение профиля пользователя", zap.Uint("userID", userID)) account, err := s.accountRepo.GetByID(userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrAccountNotFound } return nil, err } stats, err := s.GetAccountStats(userID) if err != nil { l.Error("Ошибка получения статистики", zap.Error(err)) return nil, err } return &AccountProfileResponse{ AccountResponse: ToAccountResponse(account), Stats: *stats, }, nil } // GetAccountModelByID получает модель аккаунта по ID (для внутреннего использования) func (s *accountServiceImpl) GetAccountModelByID(id uint) (*models.Account, error) { return s.accountRepo.GetByID(id) } // GetAccountModelByEmail получает модель аккаунта по email (для внутреннего использования) func (s *accountServiceImpl) GetAccountModelByEmail(email string) (*models.Account, error) { return s.accountRepo.GetByEmail(email) } // CreateAccount создает новый аккаунт func (s *accountServiceImpl) CreateAccount(req CreateAccountRequest) (*models.Account, error) { l := logger.Get() l.Info("Создание аккаунта", zap.String("email", req.Email)) // Проверяем, существует ли пользователь existing, _ := s.accountRepo.GetByEmail(req.Email) if existing != nil { return nil, ErrAccountAlreadyExists } // Хешируем пароль hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { l.Error("Ошибка хеширования пароля", zap.Error(err)) return nil, err } // Формируем полное имя, если не указано fullName := req.FullName if fullName == "" { fullName = req.FirstName + " " + req.LastName } account := &models.Account{ Email: req.Email, PasswordHash: string(hashedPassword), FullName: fullName, FirstName: req.FirstName, LastName: req.LastName, Phone: req.Phone, City: req.City, OrganizationForm: req.OrganizationForm, OrganizationName: req.OrganizationName, OrganizationShort: req.OrganizationShort, INN: req.INN, PersonalINN: req.PersonalINN, IsActive: true, IsVerified: false, Role: "user", } if err := s.accountRepo.Create(account); err != nil { l.Error("Ошибка создания аккаунта", zap.Error(err)) return nil, err } l.Info("Аккаунт создан", zap.String("email", req.Email)) return account, nil } // UpdateAccountModel обновляет модель аккаунта func (s *accountServiceImpl) UpdateAccountModel(account *models.Account) error { return s.accountRepo.Update(account) } // Вспомогательные методы func (s *accountServiceImpl) getSearchTotal(query string) (int64, error) { // Здесь должна быть реализация подсчета общего количества результатов поиска // Для простоты возвращаем 0 return 0, nil } func (s *accountServiceImpl) filterAccounts(accounts []models.Account, role string, isActive *bool) []models.Account { var filtered []models.Account for _, acc := range accounts { if role != "" && acc.Role != role { continue } if isActive != nil && acc.IsActive != *isActive { continue } filtered = append(filtered, acc) } return filtered } // Генерация reset токена func generateResetToken() (string, error) { // Генерируем 32 байта (64 символа в hex) — достаточно для безопасности bytes := make([]byte, 32) _, err := rand.Read(bytes) if err != nil { return "", fmt.Errorf("failed to generate token: %w", err) } return hex.EncodeToString(bytes), nil }