package appeal import ( "errors" "time" "api_yal/internal/models" "api_yal/internal/repository" ) // AppealService интерфейс сервиса обращений type AppealService interface { // Create создает новое обращение Create(req *CreateAppealRequest, authorID *uint, ipAddress, userAgent string) (*models.Appeal, error) // GetByID возвращает обращение по ID GetByID(id uint) (*models.Appeal, error) // Update обновляет обращение Update(id uint, req *UpdateAppealRequest, userID uint, isAdmin bool) (*models.Appeal, error) // Delete удаляет обращение (мягкое удаление) Delete(id uint, userID uint, isAdmin bool) error // List возвращает список обращений List(offset, limit int, filters map[string]interface{}) ([]models.Appeal, int64, error) // UpdateStatus обновляет статус обращения UpdateStatus(id uint, req *UpdateStatusRequest, userID uint) error // AssignTo назначает ответственного AssignTo(id uint, req *AssignRequest, userID uint) error // Resolve решает обращение Resolve(id uint, req *ResolveRequest, userID uint) error // GetHistory возвращает историю изменений GetHistory(appealID uint, offset, limit int) ([]models.AppealHistory, int64, error) // GetStatistics возвращает статистику по обращениям GetStatistics() (*AppealStatisticsResponse, error) // GetMyAppeals возвращает обращения текущего пользователя GetMyAppeals(userID uint, offset, limit int) ([]models.Appeal, int64, error) // GetAppealsByAuthor возвращает обращения автора GetAppealsByAuthor(authorID uint, offset, limit int) ([]models.Appeal, int64, error) } type appealServiceImpl struct { appealRepo repository.AppealRepository } // NewAppealService создает новый экземпляр сервиса обращений func NewAppealService(appealRepo repository.AppealRepository) AppealService { return &appealServiceImpl{ appealRepo: appealRepo, } } // Create создает новое обращение func (s *appealServiceImpl) Create(req *CreateAppealRequest, authorID *uint, ipAddress, userAgent string) (*models.Appeal, error) { // Валидация if req.Title == "" { return nil, errors.New("title is required") } if req.Message == "" { return nil, errors.New("message is required") } // Конвертация типов appealType := models.AppealType(req.Type) if !isValidAppealType(appealType) { return nil, errors.New("invalid appeal type") } priority := models.AppealPriority(req.Priority) if req.Priority == "" { priority = models.AppealPriorityMedium } if !isValidAppealPriority(priority) { return nil, errors.New("invalid appeal priority") } appeal := &models.Appeal{ Type: appealType, Status: models.AppealStatusNew, Priority: priority, Title: req.Title, Message: req.Message, AuthorID: authorID, ObjectID: req.ObjectID, FeedbackID: req.FeedbackID, CommentID: req.CommentID, ContactName: req.ContactName, ContactEmail: req.ContactEmail, ContactPhone: req.ContactPhone, Attachments: req.Attachments, Category: req.Category, Labels: req.Labels, CustomData: req.CustomData, IPAddress: ipAddress, UserAgent: userAgent, } if err := s.appealRepo.Create(appeal); err != nil { return nil, err } // Создаем запись в истории history := &models.AppealHistory{ AppealID: appeal.ID, UserID: authorID, OldStatus: "", NewStatus: models.AppealStatusNew, Comment: "Обращение создано", } _ = s.appealRepo.CreateHistory(history) return appeal, nil } // GetByID возвращает обращение по ID func (s *appealServiceImpl) GetByID(id uint) (*models.Appeal, error) { return s.appealRepo.GetByID(id) } // Update обновляет обращение func (s *appealServiceImpl) Update(id uint, req *UpdateAppealRequest, userID uint, isAdmin bool) (*models.Appeal, error) { appeal, err := s.appealRepo.GetByID(id) if err != nil { return nil, err } // Проверка прав: только автор или админ может редактировать if !isAdmin && (appeal.AuthorID == nil || *appeal.AuthorID != userID) { return nil, errors.New("permission denied") } // Только новые обращения можно редактировать if !isAdmin && appeal.Status != models.AppealStatusNew { return nil, errors.New("only new appeals can be edited") } if req.Title != nil { appeal.Title = *req.Title } if req.Message != nil { appeal.Message = *req.Message } if req.Priority != nil { priority := models.AppealPriority(*req.Priority) if isValidAppealPriority(priority) { appeal.Priority = priority } } if req.Category != nil { appeal.Category = *req.Category } if req.Labels != nil { appeal.Labels = req.Labels } if req.Attachments != nil { appeal.Attachments = req.Attachments } if req.CustomData != nil { appeal.CustomData = req.CustomData } if err := s.appealRepo.Update(appeal); err != nil { return nil, err } return appeal, nil } // Delete удаляет обращение func (s *appealServiceImpl) Delete(id uint, userID uint, isAdmin bool) error { appeal, err := s.appealRepo.GetByID(id) if err != nil { return err } // Проверка прав: только автор или админ может удалить if !isAdmin && (appeal.AuthorID == nil || *appeal.AuthorID != userID) { return errors.New("permission denied") } return s.appealRepo.Delete(id) } // List возвращает список обращений func (s *appealServiceImpl) List(offset, limit int, filters map[string]interface{}) ([]models.Appeal, int64, error) { if offset < 0 { offset = 0 } if limit <= 0 || limit > 100 { limit = 20 } var appeals []models.Appeal var total int64 var err error // Применяем фильтры if status, ok := filters["status"].(string); ok { appeals, err = s.appealRepo.ListByStatus(models.AppealStatus(status), offset, limit) if err == nil { total, _ = s.appealRepo.Count() } } else if appealType, ok := filters["type"].(string); ok { appeals, err = s.appealRepo.ListByType(models.AppealType(appealType), offset, limit) if err == nil { total, _ = s.appealRepo.Count() } } else if priority, ok := filters["priority"].(string); ok { appeals, err = s.appealRepo.ListByPriority(models.AppealPriority(priority), offset, limit) if err == nil { total, _ = s.appealRepo.Count() } } else if query, ok := filters["search"].(string); ok && query != "" { appeals, err = s.appealRepo.Search(query, offset, limit) if err == nil { total = int64(len(appeals)) } } else { appeals, err = s.appealRepo.List(offset, limit) if err == nil { total, _ = s.appealRepo.Count() } } if err != nil { return nil, 0, err } return appeals, total, nil } // UpdateStatus обновляет статус обращения func (s *appealServiceImpl) UpdateStatus(id uint, req *UpdateStatusRequest, userID uint) error { appeal, err := s.appealRepo.GetByID(id) if err != nil { return err } oldStatus := appeal.Status newStatus := models.AppealStatus(req.Status) if !isValidAppealStatus(newStatus) { return errors.New("invalid appeal status") } if err := s.appealRepo.UpdateStatus(id, newStatus); err != nil { return err } // Создаем запись в истории history := &models.AppealHistory{ AppealID: id, UserID: &userID, OldStatus: oldStatus, NewStatus: newStatus, Comment: req.Comment, } if err := s.appealRepo.CreateHistory(history); err != nil { return err } return nil } // AssignTo назначает ответственного func (s *appealServiceImpl) AssignTo(id uint, req *AssignRequest, userID uint) error { appeal, err := s.appealRepo.GetByID(id) if err != nil { return err } if err := s.appealRepo.AssignTo(id, req.AssignedToID); err != nil { return err } // Создаем запись в истории comment := "Назначен ответственный" if req.AssignedToID == nil { comment = "Ответственный снят" } history := &models.AppealHistory{ AppealID: id, UserID: &userID, OldStatus: appeal.Status, NewStatus: appeal.Status, Comment: comment, } _ = s.appealRepo.CreateHistory(history) return nil } // Resolve решает обращение func (s *appealServiceImpl) Resolve(id uint, req *ResolveRequest, userID uint) error { appeal, err := s.appealRepo.GetByID(id) if err != nil { return err } now := time.Now() appeal.Status = models.AppealStatusResolved appeal.ResolvedAt = &now appeal.ResolvedBy = &userID appeal.Resolution = req.Resolution if err := s.appealRepo.Update(appeal); err != nil { return err } // Создаем запись в истории history := &models.AppealHistory{ AppealID: id, UserID: &userID, OldStatus: appeal.Status, NewStatus: models.AppealStatusResolved, Comment: "Обращение решено: " + req.Resolution, } if err := s.appealRepo.CreateHistory(history); err != nil { return err } return nil } // GetHistory возвращает историю изменений func (s *appealServiceImpl) GetHistory(appealID uint, offset, limit int) ([]models.AppealHistory, int64, error) { if offset < 0 { offset = 0 } if limit <= 0 || limit > 100 { limit = 20 } histories, err := s.appealRepo.ListHistory(appealID, offset, limit) if err != nil { return nil, 0, err } // Для подсчета общего количества нужно реализовать отдельный метод total := int64(len(histories)) return histories, total, nil } // GetStatistics возвращает статистику по обращениям func (s *appealServiceImpl) GetStatistics() (*AppealStatisticsResponse, error) { total, err := s.appealRepo.Count() if err != nil { return nil, err } // Здесь нужно добавить агрегационные запросы для статистики // Для простоты возвращаем базовую структуру stats := &AppealStatisticsResponse{ Total: total, ByStatus: make(map[string]int64), ByType: make(map[string]int64), ByPriority: make(map[string]int64), AvgResolveTime: 0, } // TODO: Реализовать подсчет статистики через raw SQL запросы return stats, nil } // GetMyAppeals возвращает обращения текущего пользователя func (s *appealServiceImpl) GetMyAppeals(userID uint, offset, limit int) ([]models.Appeal, int64, error) { // Получаем все обращения и фильтруем по автору allAppeals, err := s.appealRepo.List(0, 1000) if err != nil { return nil, 0, err } var myAppeals []models.Appeal for _, appeal := range allAppeals { if appeal.AuthorID != nil && *appeal.AuthorID == userID { myAppeals = append(myAppeals, appeal) } } total := int64(len(myAppeals)) start := offset end := offset + limit if start > len(myAppeals) { return []models.Appeal{}, total, nil } if end > len(myAppeals) { end = len(myAppeals) } return myAppeals[start:end], total, nil } // GetAppealsByAuthor возвращает обращения автора func (s *appealServiceImpl) GetAppealsByAuthor(authorID uint, offset, limit int) ([]models.Appeal, int64, error) { allAppeals, err := s.appealRepo.List(0, 1000) if err != nil { return nil, 0, err } var authorAppeals []models.Appeal for _, appeal := range allAppeals { if appeal.AuthorID != nil && *appeal.AuthorID == authorID { authorAppeals = append(authorAppeals, appeal) } } total := int64(len(authorAppeals)) start := offset end := offset + limit if start > len(authorAppeals) { return []models.Appeal{}, total, nil } if end > len(authorAppeals) { end = len(authorAppeals) } return authorAppeals[start:end], total, nil } // Вспомогательные функции валидации func isValidAppealType(t models.AppealType) bool { switch t { case models.AppealTypeComplaint, models.AppealTypeSuggestion, models.AppealTypeWish, models.AppealTypeQuestion, models.AppealTypeOther: return true } return false } func isValidAppealStatus(s models.AppealStatus) bool { switch s { case models.AppealStatusNew, models.AppealStatusInProgress, models.AppealStatusResolved, models.AppealStatusRejected, models.AppealStatusClosed: return true } return false } func isValidAppealPriority(p models.AppealPriority) bool { switch p { case models.AppealPriorityLow, models.AppealPriorityMedium, models.AppealPriorityHigh, models.AppealPriorityCritical: return true } return false }