using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using UniVerse.Application.DTOs.Common; using UniVerse.Application.DTOs.Reviews; using UniVerse.Application.Interfaces; using System.Security.Claims; namespace UniVerse.Api.Controllers; /// Отзывы студентов на лекции с LLM-анализом и модерацией. [ApiController] [Route("api/v1/reviews")] [Authorize] [Produces("application/json")] public class ReviewsController : ControllerBase { private readonly IReviewService _reviews; public ReviewsController(IReviewService reviews) => _reviews = reviews; private int CurrentUserId => int.Parse( User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0"); /// Создать отзыв к лекции. /// /// Только Student. После создания отзыв помещается в очередь LLM-анализа /// (статус `Pending`). LLM оценивает содержательность и начисляет монеты /// скрытно от пользователя. /// /// ID лекции, оценка (Like/Neutral/Dislike) и текст отзыва. /// Отзыв создан и поставлен в очередь на LLM-анализ. /// Требуется аутентификация. /// Требуется роль Student. /// Лекция не найдена. /// Студент уже оставил отзыв к этой лекции. [Authorize(Roles = "Student")] [HttpPost] [ProducesResponseType(typeof(ReviewDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task> Create([FromBody] CreateReviewRequest req) => CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req)); /// Получить список всех отзывов. /// Только Admin. Возвращает все отзывы независимо от LLM-статуса. /// Параметры пагинации. /// Список всех отзывов (пагинированный). /// Требуется аутентификация. /// Требуется роль Admin. [Authorize(Roles = "Admin")] [HttpGet] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task List([FromQuery] PaginationRequest pagination) => Ok(await _reviews.GetAllAsync(pagination)); /// Получить отзыв по ID. /// Только Admin или Teacher. /// ID отзыва. /// Данные отзыва (включая LLM-статус и сентимент). /// Требуется аутентификация. /// Требуется роль Admin или Teacher. /// Отзыв не найден. [Authorize(Roles = "Admin,Teacher")] [HttpGet("{id:int}")] [ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Get(int id) => Ok(await _reviews.GetByIdAsync(id)); /// Обновить отзыв. /// /// Разрешено любому авторизованному пользователю, но сервис проверяет владельца. /// Изменение текста сбрасывает LLM-статус в `Pending` (повторный анализ). /// /// ID отзыва. /// Новая оценка и/или текст. /// Обновлённые данные отзыва. /// Требуется аутентификация. /// Отзыв принадлежит другому пользователю. /// Отзыв не найден. [HttpPut("{id:int}")] [ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Update(int id, [FromBody] UpdateReviewRequest req) => Ok(await _reviews.UpdateAsync(id, CurrentUserId, req)); /// Удалить отзыв. /// Владелец может удалить свой отзыв. Admin может удалить любой. /// ID отзыва. /// Отзыв удалён. /// Требуется аутентификация. /// Нет прав на удаление (не владелец и не Admin). /// Отзыв не найден. [HttpDelete("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(int id) { await _reviews.DeleteAsync(id, CurrentUserId, User.IsInRole("Admin")); return NoContent(); } /// Получить список отзывов, ожидающих LLM-анализа. /// Только Admin. Используется для мониторинга очереди обработки. /// Параметры пагинации. /// Список отзывов со статусом Pending (пагинированный). /// Требуется аутентификация. /// Требуется роль Admin. [Authorize(Roles = "Admin")] [HttpGet("pending")] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task Pending([FromQuery] PaginationRequest pagination) => Ok(await _reviews.GetPendingAsync(pagination)); /// Запустить повторный LLM-анализ отзыва. /// /// Только Admin. Сбрасывает статус отзыва на `Pending` и ставит его /// в очередь на повторную обработку фоновым сервисом. /// /// ID отзыва. /// Повторный анализ запланирован. /// Требуется аутентификация. /// Требуется роль Admin. /// Отзыв не найден. [Authorize(Roles = "Admin")] [HttpPost("{id:int}/reanalyze")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Reanalyze(int id) { await _reviews.ReanalyzeAsync(id); return NoContent(); } }