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
This commit is contained in:
@@ -1,4 +1,219 @@
|
|||||||
package appeal
|
package appeal
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"time"
|
||||||
|
"api_yal/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAppealRequest DTO для создания обращения
|
||||||
|
type CreateAppealRequest struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=complaint suggestion wish question other"`
|
||||||
|
Title string `json:"title" validate:"required,min=3,max=255"`
|
||||||
|
Message string `json:"message" validate:"required,min=10"`
|
||||||
|
Priority string `json:"priority" validate:"omitempty,oneof=low medium high critical"`
|
||||||
|
ObjectID *uint `json:"object_id,omitempty"`
|
||||||
|
FeedbackID *uint `json:"feedback_id,omitempty"`
|
||||||
|
CommentID *uint `json:"comment_id,omitempty"`
|
||||||
|
ContactName string `json:"contact_name,omitempty"`
|
||||||
|
ContactEmail string `json:"contact_email,omitempty" validate:"omitempty,email"`
|
||||||
|
ContactPhone string `json:"contact_phone,omitempty"`
|
||||||
|
Attachments []string `json:"attachments,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
CustomData map[string]interface{} `json:"custom_data,omitempty"`
|
||||||
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppealRequest DTO для обновления обращения
|
||||||
|
type UpdateAppealRequest struct {
|
||||||
|
Title *string `json:"title,omitempty" validate:"omitempty,min=3,max=255"`
|
||||||
|
Message *string `json:"message,omitempty" validate:"omitempty,min=10"`
|
||||||
|
Priority *string `json:"priority,omitempty" validate:"omitempty,oneof=low medium high critical"`
|
||||||
|
Category *string `json:"category,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
Attachments []string `json:"attachments,omitempty"`
|
||||||
|
CustomData map[string]interface{} `json:"custom_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatusRequest DTO для обновления статуса
|
||||||
|
type UpdateStatusRequest struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=new in_progress resolved rejected closed"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignRequest DTO для назначения ответственного
|
||||||
|
type AssignRequest struct {
|
||||||
|
AssignedToID *uint `json:"assigned_to_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveRequest DTO для решения обращения
|
||||||
|
type ResolveRequest struct {
|
||||||
|
Resolution string `json:"resolution" validate:"required,min=5"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppealResponse DTO для ответа
|
||||||
|
type AppealResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
AuthorID *uint `json:"author_id,omitempty"`
|
||||||
|
Author *AuthorInfo `json:"author,omitempty"`
|
||||||
|
ObjectID *uint `json:"object_id,omitempty"`
|
||||||
|
Object *ObjectInfo `json:"object,omitempty"`
|
||||||
|
FeedbackID *uint `json:"feedback_id,omitempty"`
|
||||||
|
CommentID *uint `json:"comment_id,omitempty"`
|
||||||
|
ContactName string `json:"contact_name,omitempty"`
|
||||||
|
ContactEmail string `json:"contact_email,omitempty"`
|
||||||
|
ContactPhone string `json:"contact_phone,omitempty"`
|
||||||
|
Attachments []string `json:"attachments,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
AssignedToID *uint `json:"assigned_to_id,omitempty"`
|
||||||
|
AssignedTo *AuthorInfo `json:"assigned_to,omitempty"`
|
||||||
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||||
|
ResolvedBy *uint `json:"resolved_by,omitempty"`
|
||||||
|
Resolution string `json:"resolution,omitempty"`
|
||||||
|
CustomData map[string]interface{} `json:"custom_data,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorInfo информация об авторе
|
||||||
|
type AuthorInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectInfo информация об объекте
|
||||||
|
type ObjectInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAppealsResponse DTO для списка обращений
|
||||||
|
type ListAppealsResponse struct {
|
||||||
|
Data []AppealResponse `json:"data"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppealHistoryResponse DTO для истории
|
||||||
|
type AppealHistoryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
AppealID uint `json:"appeal_id"`
|
||||||
|
UserID *uint `json:"user_id,omitempty"`
|
||||||
|
User *AuthorInfo `json:"user,omitempty"`
|
||||||
|
OldStatus string `json:"old_status"`
|
||||||
|
NewStatus string `json:"new_status"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListHistoryResponse DTO для списка истории
|
||||||
|
type ListHistoryResponse struct {
|
||||||
|
Data []AppealHistoryResponse `json:"data"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppealStatisticsResponse DTO для статистики
|
||||||
|
type AppealStatisticsResponse struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
ByStatus map[string]int64 `json:"by_status"`
|
||||||
|
ByType map[string]int64 `json:"by_type"`
|
||||||
|
ByPriority map[string]int64 `json:"by_priority"`
|
||||||
|
ByCategory map[string]int64 `json:"by_category,omitempty"`
|
||||||
|
AvgResolveTime float64 `json:"avg_resolve_time_hours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppealResponse создает AppealResponse из модели
|
||||||
|
func NewAppealResponse(appeal *models.Appeal) AppealResponse {
|
||||||
|
resp := AppealResponse{
|
||||||
|
ID: appeal.ID,
|
||||||
|
Type: string(appeal.Type),
|
||||||
|
Status: string(appeal.Status),
|
||||||
|
Priority: string(appeal.Priority),
|
||||||
|
Title: appeal.Title,
|
||||||
|
Message: appeal.Message,
|
||||||
|
AuthorID: appeal.AuthorID,
|
||||||
|
ObjectID: appeal.ObjectID,
|
||||||
|
FeedbackID: appeal.FeedbackID,
|
||||||
|
CommentID: appeal.CommentID,
|
||||||
|
ContactName: appeal.ContactName,
|
||||||
|
ContactEmail: appeal.ContactEmail,
|
||||||
|
ContactPhone: appeal.ContactPhone,
|
||||||
|
Attachments: appeal.Attachments,
|
||||||
|
Category: appeal.Category,
|
||||||
|
Labels: appeal.Labels,
|
||||||
|
AssignedToID: appeal.AssignedToID,
|
||||||
|
ResolvedAt: appeal.ResolvedAt,
|
||||||
|
ResolvedBy: appeal.ResolvedBy,
|
||||||
|
Resolution: appeal.Resolution,
|
||||||
|
CustomData: appeal.CustomData,
|
||||||
|
CreatedAt: appeal.CreatedAt,
|
||||||
|
UpdatedAt: appeal.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if appeal.Author != nil {
|
||||||
|
resp.Author = &AuthorInfo{
|
||||||
|
ID: appeal.Author.ID,
|
||||||
|
Email: appeal.Author.Email,
|
||||||
|
FirstName: appeal.Author.FirstName,
|
||||||
|
LastName: appeal.Author.LastName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appeal.Object != nil {
|
||||||
|
resp.Object = &ObjectInfo{
|
||||||
|
ID: appeal.Object.ID,
|
||||||
|
Name: appeal.Object.ShortName,
|
||||||
|
Description: appeal.Object.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appeal.AssignedTo != nil {
|
||||||
|
resp.AssignedTo = &AuthorInfo{
|
||||||
|
ID: appeal.AssignedTo.ID,
|
||||||
|
Email: appeal.AssignedTo.Email,
|
||||||
|
FirstName: appeal.AssignedTo.FirstName,
|
||||||
|
LastName: appeal.AssignedTo.LastName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppealHistoryResponse создает AppealHistoryResponse из модели
|
||||||
|
func NewAppealHistoryResponse(history *models.AppealHistory) AppealHistoryResponse {
|
||||||
|
resp := AppealHistoryResponse{
|
||||||
|
ID: history.ID,
|
||||||
|
AppealID: history.AppealID,
|
||||||
|
UserID: history.UserID,
|
||||||
|
OldStatus: string(history.OldStatus),
|
||||||
|
NewStatus: string(history.NewStatus),
|
||||||
|
Comment: history.Comment,
|
||||||
|
CreatedAt: history.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if history.User != nil {
|
||||||
|
resp.User = &AuthorInfo{
|
||||||
|
ID: history.User.ID,
|
||||||
|
Email: history.User.Email,
|
||||||
|
FirstName: history.User.FirstName,
|
||||||
|
LastName: history.User.LastName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
@@ -0,0 +1,580 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,3 +1,60 @@
|
|||||||
package appeal
|
package appeal
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"api_yal/internal/logger"
|
||||||
|
"api_yal/internal/middleware"
|
||||||
|
"api_yal/internal/repository"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes регистрирует маршруты для работы с обращениями
|
||||||
|
func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) {
|
||||||
|
l := logger.Get()
|
||||||
|
l.Debug("Регистрация маршрутов обращений")
|
||||||
|
|
||||||
|
// Создаем репозиторий и сервис
|
||||||
|
appealRepo := repository.NewAppealRepository(db)
|
||||||
|
appealService := NewAppealService(appealRepo)
|
||||||
|
appealHandler := NewHandler(appealService)
|
||||||
|
|
||||||
|
// Публичные маршруты (не требуют аутентификации)
|
||||||
|
r.Post("/appeals", appealHandler.CreateAppeal)
|
||||||
|
r.Get("/appeals/{id}", appealHandler.GetAppeal)
|
||||||
|
|
||||||
|
// Защищенные маршруты (требуют аутентификации)
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(middleware.AuthMiddleware(jwtSecret))
|
||||||
|
|
||||||
|
// Личные обращения пользователя
|
||||||
|
r.Get("/appeals/me", appealHandler.GetMyAppeals)
|
||||||
|
|
||||||
|
// Обновление и удаление своих обращений
|
||||||
|
r.Put("/appeals/{id}", appealHandler.UpdateAppeal)
|
||||||
|
r.Delete("/appeals/{id}", appealHandler.DeleteAppeal)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Административные маршруты (требуют прав администратора или модератора)
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(middleware.AuthMiddleware(jwtSecret))
|
||||||
|
r.Use(middleware.AdminOnlyMiddleware) // Или создайте ModeratorOnlyMiddleware
|
||||||
|
|
||||||
|
// Полный список обращений (с фильтрацией)
|
||||||
|
r.Get("/appeals", appealHandler.ListAppeals)
|
||||||
|
|
||||||
|
// Управление статусами
|
||||||
|
r.Patch("/appeals/{id}/status", appealHandler.UpdateAppealStatus)
|
||||||
|
r.Post("/appeals/{id}/assign", appealHandler.AssignAppeal)
|
||||||
|
r.Post("/appeals/{id}/resolve", appealHandler.ResolveAppeal)
|
||||||
|
|
||||||
|
// История и статистика
|
||||||
|
r.Get("/appeals/{id}/history", appealHandler.GetAppealHistory)
|
||||||
|
r.Get("/appeals/statistics", appealHandler.GetAppealStatistics)
|
||||||
|
|
||||||
|
// Просмотр обращений конкретного пользователя
|
||||||
|
r.Get("/appeals/user/{userID}", appealHandler.GetAppealsByAuthor)
|
||||||
|
})
|
||||||
|
|
||||||
|
l.Info("Маршруты обращений зарегистрированы")
|
||||||
|
}
|
||||||
@@ -1,17 +1,452 @@
|
|||||||
package appeal
|
package appeal
|
||||||
|
|
||||||
import "api_yal/internal/repository"
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"api_yal/internal/models"
|
||||||
|
"api_yal/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppealService интерфейс сервиса обращений
|
||||||
type AppealService interface {
|
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 {
|
type appealServiceImpl struct {
|
||||||
appelRepository repository.AppealRepository
|
appealRepo repository.AppealRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAppealService создает новый экземпляр сервиса обращений
|
||||||
func NewAppealService(appealRepo repository.AppealRepository) AppealService {
|
func NewAppealService(appealRepo repository.AppealRepository) AppealService {
|
||||||
return &appealServiceImpl{
|
return &appealServiceImpl{
|
||||||
appelRepository: appealRepo,
|
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
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ type Appeal struct {
|
|||||||
|
|
||||||
// AppealHistory записывает историю изменений статуса обращения
|
// AppealHistory записывает историю изменений статуса обращения
|
||||||
type AppealHistory struct {
|
type AppealHistory struct {
|
||||||
Base Base `gorm:"embedded"`
|
Base `gorm:"embedded"`
|
||||||
|
|
||||||
AppealID uint `gorm:"not null;index" json:"appeal_id"`
|
AppealID uint `gorm:"not null;index" json:"appeal_id"`
|
||||||
Appeal Appeal `gorm:"foreignKey:AppealID;references:ID" json:"appeal,omitempty"`
|
Appeal Appeal `gorm:"foreignKey:AppealID;references:ID" json:"appeal,omitempty"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package router
|
|||||||
import (
|
import (
|
||||||
"api_yal/internal/config"
|
"api_yal/internal/config"
|
||||||
"api_yal/internal/domain/account"
|
"api_yal/internal/domain/account"
|
||||||
|
"api_yal/internal/domain/appeal"
|
||||||
"api_yal/internal/domain/auth"
|
"api_yal/internal/domain/auth"
|
||||||
"api_yal/internal/domain/comment"
|
"api_yal/internal/domain/comment"
|
||||||
"api_yal/internal/domain/feetback"
|
"api_yal/internal/domain/feetback"
|
||||||
@@ -60,17 +61,19 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
|||||||
|
|
||||||
// Регистрируем маршурты обьектов
|
// Регистрируем маршурты обьектов
|
||||||
object.RegisterRoutes(r, db, config.JWTSecret)
|
object.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
// Регистрируем маршруты отзывов
|
// Регистрируем маршруты отзывов
|
||||||
feetback.RegisterRoutes(r, db, config.JWTSecret)
|
feetback.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
// Регистрация маршрутов для комментариев
|
// Регистрация маршрутов для комментариев
|
||||||
comment.RegisterRoutes(r, db, config.JWTSecret)
|
comment.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
// Регистрация маршрутов для райтинга
|
// Регистрация маршрутов для райтинга
|
||||||
rating.RegisterRoutes(r, db, config.JWTSecret)
|
rating.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
|
|
||||||
|
// Регистрируем маршруты обращений
|
||||||
|
appeal.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
zapLogger.Info("Настройка маршрутов завершена")
|
zapLogger.Info("Настройка маршрутов завершена")
|
||||||
|
|||||||
Reference in New Issue
Block a user