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
580 lines
19 KiB
Go
580 lines
19 KiB
Go
package appeal
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"api_yal/internal/logger"
|
|
"api_yal/internal/middleware"
|
|
"api_yal/internal/models"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Handler HTTP обработчик для обращений
|
|
type Handler struct {
|
|
service AppealService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewHandler создает новый экземпляр обработчика
|
|
func NewHandler(service AppealService) *Handler {
|
|
return &Handler{
|
|
service: service,
|
|
logger: logger.Get(),
|
|
}
|
|
}
|
|
|
|
// CreateAppeal создает новое обращение
|
|
// @Summary Создать обращение
|
|
// @Description Создает новое обращение (жалобу, предложение и т.д.)
|
|
// @Tags appeals
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body CreateAppealRequest true "Данные обращения"
|
|
// @Success 201 {object} AppealResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 401 {object} map[string]string
|
|
// @Router /appeals [post]
|
|
func (h *Handler) CreateAppeal(w http.ResponseWriter, r *http.Request) {
|
|
var req CreateAppealRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.logger.Error("Failed to decode request", zap.Error(err))
|
|
respondWithError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
// Получаем ID автора из контекста (если авторизован)
|
|
authorID, isAuth := middleware.GetUserID(r.Context())
|
|
var authorIDPtr *uint
|
|
if isAuth {
|
|
authorIDPtr = &authorID
|
|
}
|
|
|
|
// Получаем IP и User-Agent
|
|
ipAddress := r.RemoteAddr
|
|
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
ipAddress = strings.Split(forwarded, ",")[0]
|
|
}
|
|
userAgent := r.Header.Get("User-Agent")
|
|
|
|
appeal, err := h.service.Create(&req, authorIDPtr, ipAddress, userAgent)
|
|
if err != nil {
|
|
h.logger.Error("Failed to create appeal", zap.Error(err))
|
|
respondWithError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusCreated, NewAppealResponse(appeal))
|
|
}
|
|
|
|
// GetAppeal возвращает обращение по ID
|
|
// @Summary Получить обращение
|
|
// @Description Возвращает обращение по ID
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Success 200 {object} AppealResponse
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /appeals/{id} [get]
|
|
func (h *Handler) GetAppeal(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
appeal, err := h.service.GetByID(id)
|
|
if err != nil {
|
|
h.logger.Error("Failed to get appeal", zap.Error(err), zap.Uint("id", id))
|
|
respondWithError(w, http.StatusNotFound, "Appeal not found")
|
|
return
|
|
}
|
|
|
|
// Проверка прав доступа
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
if !canAccessAppeal(appeal, userID, userRole) {
|
|
respondWithError(w, http.StatusForbidden, "Access denied")
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, NewAppealResponse(appeal))
|
|
}
|
|
|
|
// UpdateAppeal обновляет обращение
|
|
// @Summary Обновить обращение
|
|
// @Description Обновляет существующее обращение
|
|
// @Tags appeals
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Param request body UpdateAppealRequest true "Данные для обновления"
|
|
// @Success 200 {object} AppealResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 403 {object} map[string]string
|
|
// @Router /appeals/{id} [put]
|
|
func (h *Handler) UpdateAppeal(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateAppealRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
isAdmin := userRole == "admin"
|
|
|
|
appeal, err := h.service.Update(id, &req, userID, isAdmin)
|
|
if err != nil {
|
|
h.logger.Error("Failed to update appeal", zap.Error(err), zap.Uint("id", id))
|
|
if err.Error() == "permission denied" {
|
|
respondWithError(w, http.StatusForbidden, err.Error())
|
|
return
|
|
}
|
|
respondWithError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, NewAppealResponse(appeal))
|
|
}
|
|
|
|
// DeleteAppeal удаляет обращение
|
|
// @Summary Удалить обращение
|
|
// @Description Мягкое удаление обращения
|
|
// @Tags appeals
|
|
// @Param id path int true "ID обращения"
|
|
// @Success 204 "No Content"
|
|
// @Failure 403 {object} map[string]string
|
|
// @Router /appeals/{id} [delete]
|
|
func (h *Handler) DeleteAppeal(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
isAdmin := userRole == "admin"
|
|
|
|
if err := h.service.Delete(id, userID, isAdmin); err != nil {
|
|
h.logger.Error("Failed to delete appeal", zap.Error(err), zap.Uint("id", id))
|
|
if err.Error() == "permission denied" {
|
|
respondWithError(w, http.StatusForbidden, err.Error())
|
|
return
|
|
}
|
|
respondWithError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ListAppeals возвращает список обращений
|
|
// @Summary Список обращений
|
|
// @Description Возвращает список обращений с пагинацией и фильтрацией
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Param offset query int false "Смещение" default(0)
|
|
// @Param limit query int false "Лимит" default(20)
|
|
// @Param status query string false "Статус" Enums(new, in_progress, resolved, rejected, closed)
|
|
// @Param type query string false "Тип" Enums(complaint, suggestion, wish, question, other)
|
|
// @Param priority query string false "Приоритет" Enums(low, medium, high, critical)
|
|
// @Param search query string false "Поиск по заголовку/сообщению"
|
|
// @Success 200 {object} ListAppealsResponse
|
|
// @Router /appeals [get]
|
|
func (h *Handler) ListAppeals(w http.ResponseWriter, r *http.Request) {
|
|
offset, limit := parsePagination(r)
|
|
|
|
// Собираем фильтры
|
|
filters := make(map[string]interface{})
|
|
if status := r.URL.Query().Get("status"); status != "" {
|
|
filters["status"] = status
|
|
}
|
|
if appealType := r.URL.Query().Get("type"); appealType != "" {
|
|
filters["type"] = appealType
|
|
}
|
|
if priority := r.URL.Query().Get("priority"); priority != "" {
|
|
filters["priority"] = priority
|
|
}
|
|
if search := r.URL.Query().Get("search"); search != "" {
|
|
filters["search"] = search
|
|
}
|
|
|
|
appeals, total, err := h.service.List(offset, limit, filters)
|
|
if err != nil {
|
|
h.logger.Error("Failed to list appeals", zap.Error(err))
|
|
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals")
|
|
return
|
|
}
|
|
|
|
data := make([]AppealResponse, len(appeals))
|
|
for i, appeal := range appeals {
|
|
data[i] = NewAppealResponse(&appeal)
|
|
}
|
|
|
|
totalPages := int(total) / limit
|
|
if int(total)%limit > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, ListAppealsResponse{
|
|
Data: data,
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
TotalPages: totalPages,
|
|
})
|
|
}
|
|
|
|
// UpdateAppealStatus обновляет статус обращения
|
|
// @Summary Обновить статус
|
|
// @Description Обновляет статус обращения (требует прав администратора или модератора)
|
|
// @Tags appeals
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Param request body UpdateStatusRequest true "Новый статус"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 400 {object} map[string]string
|
|
// @Router /appeals/{id}/status [patch]
|
|
func (h *Handler) UpdateAppealStatus(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateStatusRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
|
|
// Только администраторы и модераторы могут менять статус
|
|
if userRole != "admin" && userRole != "moderator" {
|
|
respondWithError(w, http.StatusForbidden, "Permission denied")
|
|
return
|
|
}
|
|
|
|
if err := h.service.UpdateStatus(id, &req, userID); err != nil {
|
|
h.logger.Error("Failed to update status", zap.Error(err), zap.Uint("id", id))
|
|
respondWithError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Status updated successfully"})
|
|
}
|
|
|
|
// AssignAppeal назначает ответственного
|
|
// @Summary Назначить ответственного
|
|
// @Description Назначает ответственного за обработку обращения
|
|
// @Tags appeals
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Param request body AssignRequest true "ID ответственного"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /appeals/{id}/assign [post]
|
|
func (h *Handler) AssignAppeal(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
var req AssignRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
|
|
if userRole != "admin" && userRole != "moderator" {
|
|
respondWithError(w, http.StatusForbidden, "Permission denied")
|
|
return
|
|
}
|
|
|
|
if err := h.service.AssignTo(id, &req, userID); err != nil {
|
|
h.logger.Error("Failed to assign appeal", zap.Error(err), zap.Uint("id", id))
|
|
respondWithError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Assigned successfully"})
|
|
}
|
|
|
|
// ResolveAppeal решает обращение
|
|
// @Summary Решить обращение
|
|
// @Description Отмечает обращение как решенное с указанием решения
|
|
// @Tags appeals
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Param request body ResolveRequest true "Решение"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /appeals/{id}/resolve [post]
|
|
func (h *Handler) ResolveAppeal(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
var req ResolveRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
userID, _ := middleware.GetUserID(r.Context())
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
|
|
if userRole != "admin" && userRole != "moderator" {
|
|
respondWithError(w, http.StatusForbidden, "Permission denied")
|
|
return
|
|
}
|
|
|
|
if err := h.service.Resolve(id, &req, userID); err != nil {
|
|
h.logger.Error("Failed to resolve appeal", zap.Error(err), zap.Uint("id", id))
|
|
respondWithError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Appeal resolved successfully"})
|
|
}
|
|
|
|
// GetAppealHistory возвращает историю изменений обращения
|
|
// @Summary История изменений
|
|
// @Description Возвращает историю изменений статуса обращения
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Param id path int true "ID обращения"
|
|
// @Param offset query int false "Смещение" default(0)
|
|
// @Param limit query int false "Лимит" default(20)
|
|
// @Success 200 {object} ListHistoryResponse
|
|
// @Router /appeals/{id}/history [get]
|
|
func (h *Handler) GetAppealHistory(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseIDParam(r, "id")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid appeal ID")
|
|
return
|
|
}
|
|
|
|
offset, limit := parsePagination(r)
|
|
|
|
histories, total, err := h.service.GetHistory(id, offset, limit)
|
|
if err != nil {
|
|
h.logger.Error("Failed to get history", zap.Error(err), zap.Uint("id", id))
|
|
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve history")
|
|
return
|
|
}
|
|
|
|
data := make([]AppealHistoryResponse, len(histories))
|
|
for i, history := range histories {
|
|
data[i] = NewAppealHistoryResponse(&history)
|
|
}
|
|
|
|
totalPages := int(total) / limit
|
|
if int(total)%limit > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, ListHistoryResponse{
|
|
Data: data,
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
TotalPages: totalPages,
|
|
})
|
|
}
|
|
|
|
// GetAppealStatistics возвращает статистику по обращениям
|
|
// @Summary Статистика обращений
|
|
// @Description Возвращает статистику по обращениям (требует прав администратора)
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Success 200 {object} AppealStatisticsResponse
|
|
// @Router /appeals/statistics [get]
|
|
func (h *Handler) GetAppealStatistics(w http.ResponseWriter, r *http.Request) {
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
if userRole != "admin" {
|
|
respondWithError(w, http.StatusForbidden, "Admin access required")
|
|
return
|
|
}
|
|
|
|
stats, err := h.service.GetStatistics()
|
|
if err != nil {
|
|
h.logger.Error("Failed to get statistics", zap.Error(err))
|
|
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve statistics")
|
|
return
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// GetMyAppeals возвращает обращения текущего пользователя
|
|
// @Summary Мои обращения
|
|
// @Description Возвращает обращения текущего авторизованного пользователя
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Param offset query int false "Смещение" default(0)
|
|
// @Param limit query int false "Лимит" default(20)
|
|
// @Success 200 {object} ListAppealsResponse
|
|
// @Router /appeals/me [get]
|
|
func (h *Handler) GetMyAppeals(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := middleware.GetUserID(r.Context())
|
|
if !ok {
|
|
respondWithError(w, http.StatusUnauthorized, "User not authenticated")
|
|
return
|
|
}
|
|
|
|
offset, limit := parsePagination(r)
|
|
|
|
appeals, total, err := h.service.GetMyAppeals(userID, offset, limit)
|
|
if err != nil {
|
|
h.logger.Error("Failed to get my appeals", zap.Error(err), zap.Uint("user_id", userID))
|
|
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals")
|
|
return
|
|
}
|
|
|
|
data := make([]AppealResponse, len(appeals))
|
|
for i, appeal := range appeals {
|
|
data[i] = NewAppealResponse(&appeal)
|
|
}
|
|
|
|
totalPages := int(total) / limit
|
|
if int(total)%limit > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, ListAppealsResponse{
|
|
Data: data,
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
TotalPages: totalPages,
|
|
})
|
|
}
|
|
|
|
// GetAppealsByAuthor возвращает обращения пользователя по ID
|
|
// @Summary Обращения пользователя
|
|
// @Description Возвращает обращения указанного пользователя (админ доступ)
|
|
// @Tags appeals
|
|
// @Produce json
|
|
// @Param userID path int true "ID пользователя"
|
|
// @Param offset query int false "Смещение" default(0)
|
|
// @Param limit query int false "Лимит" default(20)
|
|
// @Success 200 {object} ListAppealsResponse
|
|
// @Router /appeals/user/{userID} [get]
|
|
func (h *Handler) GetAppealsByAuthor(w http.ResponseWriter, r *http.Request) {
|
|
userRole, _ := middleware.GetUserRole(r.Context())
|
|
if userRole != "admin" {
|
|
respondWithError(w, http.StatusForbidden, "Admin access required")
|
|
return
|
|
}
|
|
|
|
authorID, err := parseIDParam(r, "userID")
|
|
if err != nil {
|
|
respondWithError(w, http.StatusBadRequest, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
offset, limit := parsePagination(r)
|
|
|
|
appeals, total, err := h.service.GetAppealsByAuthor(authorID, offset, limit)
|
|
if err != nil {
|
|
h.logger.Error("Failed to get appeals by author", zap.Error(err), zap.Uint("author_id", authorID))
|
|
respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals")
|
|
return
|
|
}
|
|
|
|
data := make([]AppealResponse, len(appeals))
|
|
for i, appeal := range appeals {
|
|
data[i] = NewAppealResponse(&appeal)
|
|
}
|
|
|
|
totalPages := int(total) / limit
|
|
if int(total)%limit > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
respondWithJSON(w, http.StatusOK, ListAppealsResponse{
|
|
Data: data,
|
|
Total: total,
|
|
Offset: offset,
|
|
Limit: limit,
|
|
TotalPages: totalPages,
|
|
})
|
|
}
|
|
|
|
// Вспомогательные функции
|
|
|
|
func parseIDParam(r *http.Request, param string) (uint, error) {
|
|
idStr := chi.URLParam(r, param)
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint(id), nil
|
|
}
|
|
|
|
func parsePagination(r *http.Request) (offset, limit int) {
|
|
offsetStr := r.URL.Query().Get("offset")
|
|
limitStr := r.URL.Query().Get("limit")
|
|
|
|
offset, _ = strconv.Atoi(offsetStr)
|
|
limit, _ = strconv.Atoi(limitStr)
|
|
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 20
|
|
}
|
|
return
|
|
}
|
|
|
|
func respondWithJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
|
logger.Get().Error("Failed to encode JSON response", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func respondWithError(w http.ResponseWriter, status int, message string) {
|
|
respondWithJSON(w, status, map[string]string{"error": message})
|
|
}
|
|
|
|
func canAccessAppeal(appeal *models.Appeal, userID uint, userRole string) bool {
|
|
// Администратор может видеть все
|
|
if userRole == "admin" {
|
|
return true
|
|
}
|
|
// Модератор может видеть все
|
|
if userRole == "moderator" {
|
|
return true
|
|
}
|
|
// Обычный пользователь может видеть только свои обращения
|
|
if appeal.AuthorID != nil && *appeal.AuthorID == userID {
|
|
return true
|
|
}
|
|
// Анонимные обращения могут видеть только админы и модераторы
|
|
return false
|
|
} |