forked from gongxuegit/tiku-backend.net
feat: add learning activity endpoints
This commit is contained in:
101
Tiku.Api/Contracts/LearningDtos.cs
Normal file
101
Tiku.Api/Contracts/LearningDtos.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tiku.Application.Learning;
|
||||
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
public sealed class LearningLimitQueryDto
|
||||
{
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
public Guid? UnitId { get; set; }
|
||||
|
||||
public LearningLimitFilter ToFilter()
|
||||
{
|
||||
return new LearningLimitFilter(Limit, Status, UnitId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SubmitAnswerDto
|
||||
{
|
||||
[Required]
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
public Guid? PracticeSessionId { get; set; }
|
||||
|
||||
public IReadOnlyCollection<string>? SelectedOptions { get; set; }
|
||||
|
||||
[StringLength(10000)]
|
||||
public string? AnswerText { get; set; }
|
||||
|
||||
public bool? SelfJudgedCorrect { get; set; }
|
||||
|
||||
public SubmitAnswerCommand ToCommand()
|
||||
{
|
||||
return new SubmitAnswerCommand(
|
||||
QuestionId,
|
||||
PracticeSessionId,
|
||||
SelectedOptions,
|
||||
AnswerText,
|
||||
SelfJudgedCorrect);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class QuestionActionDto
|
||||
{
|
||||
[Required]
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
public bool? Favorite { get; set; }
|
||||
|
||||
public QuestionActionCommand ToCommand()
|
||||
{
|
||||
return new QuestionActionCommand(QuestionId, Favorite);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WordProgressDto
|
||||
{
|
||||
[Required]
|
||||
public Guid WordId { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[Range(0, 100)]
|
||||
public int? CorrectDelta { get; set; }
|
||||
|
||||
[Range(0, 100)]
|
||||
public int? WrongDelta { get; set; }
|
||||
|
||||
public DateTimeOffset? NextReviewAt { get; set; }
|
||||
|
||||
public WordProgressCommand ToCommand()
|
||||
{
|
||||
return new WordProgressCommand(
|
||||
WordId,
|
||||
Status,
|
||||
CorrectDelta,
|
||||
WrongDelta,
|
||||
NextReviewAt);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FavoriteWordDto
|
||||
{
|
||||
[Required]
|
||||
public Guid WordId { get; set; }
|
||||
|
||||
public bool? Favorite { get; set; }
|
||||
|
||||
[StringLength(1000)]
|
||||
public string? Note { get; set; }
|
||||
|
||||
public FavoriteWordCommand ToCommand()
|
||||
{
|
||||
return new FavoriteWordCommand(WordId, Favorite, Note);
|
||||
}
|
||||
}
|
||||
154
Tiku.Api/Controllers/LearningController.cs
Normal file
154
Tiku.Api/Controllers/LearningController.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/learning")]
|
||||
public sealed class LearningController(
|
||||
ILearningActivityService learningActivityService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpPost("answers")]
|
||||
[EndpointSummary("提交题目答案")]
|
||||
[ProducesResponseType<AnswerRecordItem>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AnswerRecordItem>> SubmitAnswer(
|
||||
SubmitAnswerDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.SubmitAnswerAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("favorites/questions")]
|
||||
[EndpointSummary("查询收藏题目")]
|
||||
[ProducesResponseType<LearningList<FavoriteQuestionItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<FavoriteQuestionItem>>> GetFavoriteQuestions(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetFavoriteQuestionsAsync(
|
||||
ResolveActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("favorites/questions")]
|
||||
[EndpointSummary("收藏或取消收藏题目")]
|
||||
[ProducesResponseType<LearningActionResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LearningActionResult>> ToggleFavoriteQuestion(
|
||||
QuestionActionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.ToggleFavoriteQuestionAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("wrong-questions")]
|
||||
[EndpointSummary("查询错题列表")]
|
||||
[ProducesResponseType<LearningList<WrongQuestionItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<WrongQuestionItem>>> GetWrongQuestions(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetWrongQuestionsAsync(
|
||||
ResolveActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("wrong-questions/resolve")]
|
||||
[EndpointSummary("将错题标记为已解决")]
|
||||
[ProducesResponseType<LearningActionResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LearningActionResult>> ResolveWrongQuestion(
|
||||
QuestionActionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.ResolveWrongQuestionAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/progress")]
|
||||
[EndpointSummary("查询单词学习进度")]
|
||||
[ProducesResponseType<LearningList<WordProgressItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<WordProgressItem>>> GetWordProgress(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetWordProgressAsync(
|
||||
ResolveActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("vocabulary/progress")]
|
||||
[EndpointSummary("更新单词学习进度")]
|
||||
[ProducesResponseType<WordProgressItem>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<WordProgressItem>> UpdateWordProgress(
|
||||
WordProgressDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.UpdateWordProgressAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/favorites")]
|
||||
[EndpointSummary("查询收藏单词")]
|
||||
[ProducesResponseType<LearningList<FavoriteWordItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<FavoriteWordItem>>> GetFavoriteWords(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetFavoriteWordsAsync(
|
||||
ResolveActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("vocabulary/favorites")]
|
||||
[EndpointSummary("收藏或取消收藏单词")]
|
||||
[ProducesResponseType<LearningActionResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LearningActionResult>> ToggleFavoriteWord(
|
||||
FavoriteWordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.ToggleFavoriteWordAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private LearningActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new LearningAccessDeniedException();
|
||||
}
|
||||
|
||||
return new LearningActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LearningAccessDeniedException()
|
||||
: Exception("Learning actor was not resolved.");
|
||||
@@ -4,6 +4,7 @@ using Tiku.Application.Assets;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Infrastructure.Content;
|
||||
using Tiku.Infrastructure.Learning;
|
||||
using Tiku.Infrastructure.QuestionBanks;
|
||||
|
||||
namespace Tiku.Api.Middleware;
|
||||
@@ -87,6 +88,36 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is LearningValidationException learningValidationException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
learningValidationException.Message,
|
||||
StatusCodes.Status400BadRequest,
|
||||
learningValidationException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is LearningResourceNotFoundException learningResourceNotFoundException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
learningResourceNotFoundException.Message,
|
||||
StatusCodes.Status404NotFound,
|
||||
learningResourceNotFoundException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is LearningAccessDeniedException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
exception.Message,
|
||||
StatusCodes.Status403Forbidden,
|
||||
"learning_access_denied");
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is ObjectStorageException storageException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
|
||||
Reference in New Issue
Block a user