318075d686
modified: internal/domain/appeal/dto.go new file: internal/domain/appeal/handler.go modified: internal/domain/appeal/router.go modified: internal/domain/appeal/service.go modified: internal/models/appeal.go modified: internal/router/router.go fix bag with no embeded the Base into appeal
452 lines
13 KiB
Go
452 lines
13 KiB
Go
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
|
|
} |