Files
tp/main_dc/yalarba/api_yal/internal/domain/appeal/handler.go
T
valitovgaziz 318075d686 On branch main
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
2026-05-21 05:04:34 +05:00

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
}