feat: add practice session workflow endpoints

This commit is contained in:
xiong
2026-07-26 15:25:33 +08:00
parent db319ef5a2
commit e505384ecf
7 changed files with 1090 additions and 1 deletions

View File

@@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Tiku.Application.Learning;
using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
@@ -19,6 +21,89 @@ public sealed class LearningLimitQueryDto
}
}
public sealed class PracticeSessionQueryDto
{
public Guid? PracticeSessionId { get; set; }
public Guid? BlueprintId { get; set; }
[StringLength(50)]
public string? Mode { get; set; }
[StringLength(50)]
public string? Status { get; set; }
[Range(1, 200)]
public int? Limit { get; set; }
public PracticeSessionFilter ToFilter(Guid? routePracticeSessionId = null)
{
return new PracticeSessionFilter(
routePracticeSessionId ?? PracticeSessionId,
BlueprintId,
Mode,
Status,
Limit);
}
}
public sealed class CreatePracticeSessionDto
{
[StringLength(50)]
public string? Mode { get; set; }
[StringLength(50)]
public string? TargetType { get; set; }
public Guid? TargetId { get; set; }
public Guid? BlueprintId { get; set; }
public Guid? CollectionId { get; set; }
public Guid? EntryId { get; set; }
public Guid? ContentNodeId { get; set; }
[Range(1, 500)]
public int? QuestionLimit { get; set; }
[Range(1, 1440)]
public int? DurationMinutes { get; set; }
[Range(typeof(decimal), "0", "99999")]
public decimal? TotalScore { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public PracticeSessionCommand ToCommand()
{
return new PracticeSessionCommand(
Mode,
TargetType,
TargetId,
BlueprintId,
CollectionId,
EntryId,
ContentNodeId,
QuestionLimit,
DurationMinutes,
TotalScore,
Metadata);
}
}
public sealed class SubmitPracticeSessionDto
{
[Required]
public Guid PracticeSessionId { get; set; }
public PracticeSessionFilter ToFilter()
{
return new PracticeSessionFilter(PracticeSessionId);
}
}
public sealed class SubmitAnswerDto
{
[Required]

View File

@@ -15,6 +15,88 @@ public sealed class LearningController(
ICurrentUser currentUser,
ICurrentTenant currentTenant) : ControllerBase
{
[HttpPost("practice-sessions")]
[EndpointSummary("创建练习会话")]
[ProducesResponseType<PracticeSessionItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PracticeSessionItem>> CreatePracticeSession(
CreatePracticeSessionDto request,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.CreatePracticeSessionAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("practice-sessions/detail")]
[EndpointSummary("获取练习会话详情")]
[ProducesResponseType<PracticeSessionDetailItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PracticeSessionDetailItem>> GetPracticeSessionDetail(
[FromQuery] PracticeSessionQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetPracticeSessionDetailAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPost("practice-sessions/submit")]
[EndpointSummary("提交练习会话并生成报告")]
[ProducesResponseType<PracticeSessionReportItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PracticeSessionReportItem>> SubmitPracticeSession(
SubmitPracticeSessionDto request,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.SubmitPracticeSessionAsync(
ResolveActor(),
request.ToFilter(),
cancellationToken));
}
[HttpGet("practice-sessions/report")]
[EndpointSummary("获取练习会话报告")]
[ProducesResponseType<PracticeSessionReportItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PracticeSessionReportItem>> GetPracticeSessionReport(
[FromQuery] PracticeSessionQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetPracticeSessionReportAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpGet("practice-reports")]
[EndpointSummary("查询练习报告列表")]
[ProducesResponseType<LearningList<PracticeSessionReportItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<LearningList<PracticeSessionReportItem>>> GetPracticeReports(
[FromQuery] PracticeSessionQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetPracticeReportsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpGet("practice-sessions/history")]
[EndpointSummary("查询练习历史")]
[ProducesResponseType<LearningList<PracticeHistoryItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<LearningList<PracticeHistoryItem>>> GetPracticeHistory(
[FromQuery] PracticeSessionQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await learningActivityService.GetPracticeHistoryAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPost("answers")]
[EndpointSummary("提交题目答案")]
[ProducesResponseType<AnswerRecordItem>(StatusCodes.Status200OK)]

View File

@@ -93,7 +93,7 @@ public sealed class ExceptionHandlingMiddleware(
await WriteProblemAsync(
context,
learningValidationException.Message,
StatusCodes.Status400BadRequest,
LearningValidationStatusCode(learningValidationException.Code),
learningValidationException.Code);
return;
}
@@ -200,4 +200,13 @@ public sealed class ExceptionHandlingMiddleware(
_ => StatusCodes.Status400BadRequest
};
}
private static int LearningValidationStatusCode(string code)
{
return code switch
{
"no_practice_questions" or "practice_session_empty" => StatusCodes.Status409Conflict,
_ => StatusCodes.Status400BadRequest
};
}
}