8ac593d36f
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
168 lines
9.8 KiB
C#
168 lines
9.8 KiB
C#
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;
|
|
|
|
/// <summary>Отзывы студентов на лекции с LLM-анализом и модерацией.</summary>
|
|
[ApiController]
|
|
[Route("api/v1/reviews")]
|
|
[Authorize]
|
|
[Produces("application/json")]
|
|
public class ReviewsController : ControllerBase
|
|
{
|
|
private readonly IReviewService _reviews;
|
|
private readonly IReviewPromptService _reviewPrompts;
|
|
|
|
public ReviewsController(IReviewService reviews, IReviewPromptService reviewPrompts)
|
|
{
|
|
_reviews = reviews;
|
|
_reviewPrompts = reviewPrompts;
|
|
}
|
|
|
|
private int CurrentUserId => int.Parse(
|
|
User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
|
|
|
/// <summary>Создать отзыв к лекции.</summary>
|
|
/// <remarks>
|
|
/// Только Student. После создания отзыв отправляется на LLM-анализ
|
|
/// (статус `Pending`). LLM оценивает содержательность и начисляет монеты
|
|
/// скрытно от пользователя.
|
|
/// </remarks>
|
|
/// <param name="req">ID лекции, оценка (Like/Neutral/Dislike) и текст отзыва.</param>
|
|
/// <response code="201">Отзыв создан и поставлен в очередь на LLM-анализ.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Student.</response>
|
|
/// <response code="404">Лекция не найдена.</response>
|
|
/// <response code="409">Студент уже оставил отзыв к этой лекции.</response>
|
|
[Authorize(Roles = "Student")]
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
public async Task<ActionResult<ReviewDto>> Create([FromBody] CreateReviewRequest req) =>
|
|
CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req));
|
|
|
|
/// <summary>Получить список всех отзывов.</summary>
|
|
/// <remarks>Только Admin. Возвращает все отзывы независимо от LLM-статуса.</remarks>
|
|
/// <param name="filter">Параметры фильтрации и пагинации.</param>
|
|
/// <response code="200">Список всех отзывов (пагинированный).</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Admin.</response>
|
|
[Authorize(Roles = "Admin")]
|
|
[HttpGet]
|
|
[ProducesResponseType(typeof(PagedResult<ReviewDto>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
public async Task<ActionResult> List([FromQuery] ReviewFilterRequest filter) =>
|
|
Ok(await _reviews.GetAllAsync(filter));
|
|
|
|
/// <summary>Получить текущий промпт LLM-анализа отзывов.</summary>
|
|
/// <remarks>Только Admin. Если настройка ещё не сохранена, возвращает базовый промпт.</remarks>
|
|
/// <response code="200">Текущий шаблон промпта.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Admin.</response>
|
|
[Authorize(Roles = "Admin")]
|
|
[HttpGet("llm-prompt")]
|
|
[ProducesResponseType(typeof(ReviewPromptDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
public async Task<ActionResult<ReviewPromptDto>> GetLlmPrompt() =>
|
|
Ok(await _reviewPrompts.GetAsync());
|
|
|
|
/// <summary>Обновить промпт LLM-анализа отзывов.</summary>
|
|
/// <remarks>Только Admin. Промпт применяется к следующим анализам и ручным повторам.</remarks>
|
|
/// <param name="request">Новый шаблон с плейсхолдерами {lectureContext} и {reviewText}.</param>
|
|
/// <response code="200">Сохранённый шаблон промпта.</response>
|
|
/// <response code="400">Промпт пустой или не содержит обязательные плейсхолдеры.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Admin.</response>
|
|
[Authorize(Roles = "Admin")]
|
|
[HttpPut("llm-prompt")]
|
|
[ProducesResponseType(typeof(ReviewPromptDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
public async Task<ActionResult<ReviewPromptDto>> UpdateLlmPrompt([FromBody] UpdateReviewPromptRequest request) =>
|
|
Ok(await _reviewPrompts.UpdateAsync(request));
|
|
|
|
/// <summary>Получить отзыв по ID.</summary>
|
|
/// <remarks>Только Admin или Teacher.</remarks>
|
|
/// <param name="id">ID отзыва.</param>
|
|
/// <response code="200">Данные отзыва (включая LLM-статус и сентимент).</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Admin или Teacher.</response>
|
|
/// <response code="404">Отзыв не найден.</response>
|
|
[Authorize(Roles = "Admin,Teacher")]
|
|
[HttpGet("{id:int}")]
|
|
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<ReviewDto>> Get(int id) => Ok(await _reviews.GetByIdAsync(id));
|
|
|
|
/// <summary>Обновить отзыв.</summary>
|
|
/// <remarks>
|
|
/// Разрешено любому авторизованному пользователю, но сервис проверяет владельца.
|
|
/// Изменение текста сбрасывает LLM-статус в `Pending` (повторный анализ).
|
|
/// </remarks>
|
|
/// <param name="id">ID отзыва.</param>
|
|
/// <param name="req">Новая оценка и/или текст.</param>
|
|
/// <response code="200">Обновлённые данные отзыва.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Отзыв принадлежит другому пользователю.</response>
|
|
/// <response code="404">Отзыв не найден.</response>
|
|
[HttpPut("{id:int}")]
|
|
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<ReviewDto>> Update(int id, [FromBody] UpdateReviewRequest req) =>
|
|
Ok(await _reviews.UpdateAsync(id, CurrentUserId, req));
|
|
|
|
/// <summary>Удалить отзыв.</summary>
|
|
/// <remarks>Владелец может удалить свой отзыв. Admin может удалить любой.</remarks>
|
|
/// <param name="id">ID отзыва.</param>
|
|
/// <response code="204">Отзыв удалён.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Нет прав на удаление (не владелец и не Admin).</response>
|
|
/// <response code="404">Отзыв не найден.</response>
|
|
[HttpDelete("{id:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
await _reviews.DeleteAsync(id, CurrentUserId, User.IsInRole("Admin"));
|
|
return NoContent();
|
|
}
|
|
|
|
/// <summary>Запустить повторный LLM-анализ отзыва.</summary>
|
|
/// <remarks>
|
|
/// Только Admin. Сбрасывает статус отзыва на `Pending` и отправляет его
|
|
/// на повторную обработку.
|
|
/// </remarks>
|
|
/// <param name="id">ID отзыва.</param>
|
|
/// <response code="204">Повторный анализ запланирован.</response>
|
|
/// <response code="401">Требуется аутентификация.</response>
|
|
/// <response code="403">Требуется роль Admin.</response>
|
|
/// <response code="404">Отзыв не найден.</response>
|
|
[Authorize(Roles = "Admin")]
|
|
[HttpPost("{id:int}/reanalyze")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> Reanalyze(int id)
|
|
{
|
|
await _reviews.ReanalyzeAsync(id);
|
|
return NoContent();
|
|
}
|
|
}
|