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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import "api_yal/internal/repository"
|
||||
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 {
|
||||
appelRepository repository.AppealRepository
|
||||
appealRepo repository.AppealRepository
|
||||
}
|
||||
|
||||
// NewAppealService создает новый экземпляр сервиса обращений
|
||||
func NewAppealService(appealRepo repository.AppealRepository) AppealService {
|
||||
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 записывает историю изменений статуса обращения
|
||||
type AppealHistory struct {
|
||||
Base Base `gorm:"embedded"`
|
||||
Base `gorm:"embedded"`
|
||||
|
||||
AppealID uint `gorm:"not null;index" json:"appeal_id"`
|
||||
Appeal Appeal `gorm:"foreignKey:AppealID;references:ID" json:"appeal,omitempty"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"api_yal/internal/config"
|
||||
"api_yal/internal/domain/account"
|
||||
"api_yal/internal/domain/appeal"
|
||||
"api_yal/internal/domain/auth"
|
||||
"api_yal/internal/domain/comment"
|
||||
"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)
|
||||
|
||||
|
||||
// Регистрируем маршруты отзывов
|
||||
feetback.RegisterRoutes(r, db, config.JWTSecret)
|
||||
|
||||
|
||||
// Регистрация маршрутов для комментариев
|
||||
comment.RegisterRoutes(r, db, config.JWTSecret)
|
||||
|
||||
// Регистрация маршрутов для райтинга
|
||||
rating.RegisterRoutes(r, db, config.JWTSecret)
|
||||
|
||||
|
||||
// Регистрируем маршруты обращений
|
||||
appeal.RegisterRoutes(r, db, config.JWTSecret)
|
||||
|
||||
})
|
||||
|
||||
zapLogger.Info("Настройка маршрутов завершена")
|
||||
|
||||
Reference in New Issue
Block a user