refactor(learning): split learning capability services
This commit is contained in:
@@ -25,6 +25,7 @@ internal static class ApiPresentationExtensions
|
||||
services.AddScoped<TenantAdminActorResolver>();
|
||||
services.AddScoped<DirectContentActorResolver>();
|
||||
services.AddScoped<CommerceAdminActorResolver>();
|
||||
services.AddScoped<LearningActorResolver>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
35
Tiku.Api/Controllers/AnsweringController.cs
Normal file
35
Tiku.Api/Controllers/AnsweringController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class AnsweringController(
|
||||
IAnsweringService service,
|
||||
LearningActorResolver actorResolver) : 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 service.SubmitAnswerAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
19
Tiku.Api/Controllers/LearningActorResolver.cs
Normal file
19
Tiku.Api/Controllers/LearningActorResolver.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
public sealed class LearningActorResolver(
|
||||
ICurrentUser currentUser,
|
||||
ITenantContext currentTenant)
|
||||
{
|
||||
internal LearningActor Resolve()
|
||||
{
|
||||
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.");
|
||||
48
Tiku.Api/Controllers/LearningAnalyticsController.cs
Normal file
48
Tiku.Api/Controllers/LearningAnalyticsController.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class LearningAnalyticsController(
|
||||
ILearningAnalyticsService service,
|
||||
LearningActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("stats")]
|
||||
[EndpointSummary("查询学习统计")]
|
||||
[ProducesResponseType<LearningStatsItem>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningStatsItem>> GetStats(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetStatsAsync(actorResolver.Resolve(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("trend")]
|
||||
[EndpointSummary("查询学习趋势")]
|
||||
[ProducesResponseType<LearningList<LearningTrendItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<LearningTrendItem>>> GetTrend(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetTrendAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("leaderboard")]
|
||||
[EndpointSummary("查询学习排行榜")]
|
||||
[ProducesResponseType<LearningLeaderboardResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningLeaderboardResult>> GetLeaderboard(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await service.GetLeaderboardAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class LearningController(
|
||||
ILearningActivityService learningActivityService,
|
||||
ICurrentUser currentUser,
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("stats")]
|
||||
[EndpointSummary("查询学习统计")]
|
||||
[ProducesResponseType<LearningStatsItem>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningStatsItem>> GetStats(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetStatsAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("trend")]
|
||||
[EndpointSummary("查询学习趋势")]
|
||||
[ProducesResponseType<LearningList<LearningTrendItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<LearningTrendItem>>> GetTrend(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("leaderboard")]
|
||||
[EndpointSummary("查询学习排行榜")]
|
||||
[ProducesResponseType<LearningLeaderboardResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningLeaderboardResult>> GetLeaderboard(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await learningActivityService.GetLeaderboardAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[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.ToCommand(),
|
||||
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)]
|
||||
[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));
|
||||
}
|
||||
|
||||
[HttpGet("wrong-questions/review-plan")]
|
||||
[EndpointSummary("生成错题复习计划")]
|
||||
[ProducesResponseType<WrongQuestionReviewPlan>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WrongQuestionReviewPlan>> GetWrongQuestionReviewPlan(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetWrongQuestionReviewPlanAsync(
|
||||
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));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/review-plan")]
|
||||
[EndpointSummary("生成单词复习计划")]
|
||||
[ProducesResponseType<WordReviewPlan>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WordReviewPlan>> GetWordReviewPlan(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetWordReviewPlanAsync(
|
||||
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));
|
||||
}
|
||||
|
||||
[HttpPost("vocabulary/review")]
|
||||
[EndpointSummary("提交单词复习结果")]
|
||||
[ProducesResponseType<WordProgressItem>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<WordProgressItem>> ReviewWord(
|
||||
WordReviewDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.ReviewWordAsync(
|
||||
ResolveActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/stats")]
|
||||
[EndpointSummary("查询单词学习统计")]
|
||||
[ProducesResponseType<WordStatsItem>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WordStatsItem>> GetWordStats(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetWordStatsAsync(
|
||||
ResolveActor(),
|
||||
query.ToFilter(),
|
||||
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.");
|
||||
59
Tiku.Api/Controllers/PracticeReportController.cs
Normal file
59
Tiku.Api/Controllers/PracticeReportController.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class PracticeReportController(
|
||||
IPracticeReportService service,
|
||||
LearningActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[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 service.GetPracticeSessionReportAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.GetPracticeReportsAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.GetPracticeHistoryAsync(
|
||||
actorResolver.Resolve(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
61
Tiku.Api/Controllers/PracticeSessionController.cs
Normal file
61
Tiku.Api/Controllers/PracticeSessionController.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class PracticeSessionController(
|
||||
IPracticeSessionService service,
|
||||
LearningActorResolver actorResolver) : 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 service.CreatePracticeSessionAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.GetPracticeSessionDetailAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.SubmitPracticeSessionAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
86
Tiku.Api/Controllers/QuestionReviewController.cs
Normal file
86
Tiku.Api/Controllers/QuestionReviewController.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class QuestionReviewController(
|
||||
IQuestionReviewService service,
|
||||
LearningActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("favorites/questions")]
|
||||
[EndpointSummary("查询收藏题目")]
|
||||
[ProducesResponseType<LearningList<FavoriteQuestionItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<FavoriteQuestionItem>>> GetFavoriteQuestions(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetFavoriteQuestionsAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.ToggleFavoriteQuestionAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.GetWrongQuestionsAsync(
|
||||
actorResolver.Resolve(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("wrong-questions/review-plan")]
|
||||
[EndpointSummary("生成错题复习计划")]
|
||||
[ProducesResponseType<WrongQuestionReviewPlan>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WrongQuestionReviewPlan>> GetWrongQuestionReviewPlan(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetWrongQuestionReviewPlanAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.ResolveWrongQuestionAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
113
Tiku.Api/Controllers/WordLearningController.cs
Normal file
113
Tiku.Api/Controllers/WordLearningController.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class WordLearningController(
|
||||
IWordLearningService service,
|
||||
LearningActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("vocabulary/progress")]
|
||||
[EndpointSummary("查询单词学习进度")]
|
||||
[ProducesResponseType<LearningList<WordProgressItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LearningList<WordProgressItem>>> GetWordProgress(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetWordProgressAsync(
|
||||
actorResolver.Resolve(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/review-plan")]
|
||||
[EndpointSummary("生成单词复习计划")]
|
||||
[ProducesResponseType<WordReviewPlan>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WordReviewPlan>> GetWordReviewPlan(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetWordReviewPlanAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.UpdateWordProgressAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("vocabulary/review")]
|
||||
[EndpointSummary("提交单词复习结果")]
|
||||
[ProducesResponseType<WordProgressItem>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<WordProgressItem>> ReviewWord(
|
||||
WordReviewDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.ReviewWordAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary/stats")]
|
||||
[EndpointSummary("查询单词学习统计")]
|
||||
[ProducesResponseType<WordStatsItem>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<WordStatsItem>> GetWordStats(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetWordStatsAsync(
|
||||
actorResolver.Resolve(),
|
||||
query.ToFilter(),
|
||||
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 service.GetFavoriteWordsAsync(
|
||||
actorResolver.Resolve(),
|
||||
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 service.ToggleFavoriteWordAsync(
|
||||
actorResolver.Resolve(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
namespace Tiku.Application.Learning;
|
||||
|
||||
public interface ILearningActivityService
|
||||
{
|
||||
Task<LearningStatsItem> GetStatsAsync(
|
||||
LearningActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<LearningTrendItem>> GetTrendAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningLeaderboardResult> GetLeaderboardAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AnswerRecordItem> SubmitAnswerAsync(
|
||||
LearningActor actor,
|
||||
SubmitAnswerCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<FavoriteQuestionItem>> GetFavoriteQuestionsAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningActionResult> ToggleFavoriteQuestionAsync(
|
||||
LearningActor actor,
|
||||
QuestionActionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<WrongQuestionItem>> GetWrongQuestionsAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WrongQuestionReviewPlan> GetWrongQuestionReviewPlanAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningActionResult> ResolveWrongQuestionAsync(
|
||||
LearningActor actor,
|
||||
QuestionActionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<WordProgressItem>> GetWordProgressAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WordReviewPlan> GetWordReviewPlanAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WordProgressItem> UpdateWordProgressAsync(
|
||||
LearningActor actor,
|
||||
WordProgressCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WordProgressItem> ReviewWordAsync(
|
||||
LearningActor actor,
|
||||
WordReviewCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WordStatsItem> GetWordStatsAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<FavoriteWordItem>> GetFavoriteWordsAsync(
|
||||
LearningActor actor,
|
||||
LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningActionResult> ToggleFavoriteWordAsync(
|
||||
LearningActor actor,
|
||||
FavoriteWordCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PracticeSessionItem> CreatePracticeSessionAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PracticeSessionDetailItem> GetPracticeSessionDetailAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PracticeSessionReportItem> SubmitPracticeSessionAsync(
|
||||
LearningActor actor,
|
||||
SubmitPracticeSessionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PracticeSessionReportItem> GetPracticeSessionReportAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<PracticeSessionReportItem>> GetPracticeReportsAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<LearningList<PracticeHistoryItem>> GetPracticeHistoryAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
68
Tiku.Application/Learning/LearningServices.cs
Normal file
68
Tiku.Application/Learning/LearningServices.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
namespace Tiku.Application.Learning;
|
||||
|
||||
public interface ILearningAnalyticsService
|
||||
{
|
||||
Task<LearningStatsItem> GetStatsAsync(LearningActor actor, CancellationToken cancellationToken = default);
|
||||
Task<LearningList<LearningTrendItem>> GetTrendAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningLeaderboardResult> GetLeaderboardAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IAnsweringService
|
||||
{
|
||||
Task<AnswerRecordItem> SubmitAnswerAsync(LearningActor actor, SubmitAnswerCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IQuestionReviewService
|
||||
{
|
||||
Task<LearningList<FavoriteQuestionItem>> GetFavoriteQuestionsAsync(LearningActor actor,
|
||||
LearningLimitFilter filter, CancellationToken cancellationToken = default);
|
||||
Task<LearningActionResult> ToggleFavoriteQuestionAsync(LearningActor actor, QuestionActionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningList<WrongQuestionItem>> GetWrongQuestionsAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<WrongQuestionReviewPlan> GetWrongQuestionReviewPlanAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningActionResult> ResolveWrongQuestionAsync(LearningActor actor, QuestionActionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IWordLearningService
|
||||
{
|
||||
Task<LearningList<WordProgressItem>> GetWordProgressAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<WordReviewPlan> GetWordReviewPlanAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<WordProgressItem> UpdateWordProgressAsync(LearningActor actor, WordProgressCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<WordProgressItem> ReviewWordAsync(LearningActor actor, WordReviewCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<WordStatsItem> GetWordStatsAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningList<FavoriteWordItem>> GetFavoriteWordsAsync(LearningActor actor, LearningLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningActionResult> ToggleFavoriteWordAsync(LearningActor actor, FavoriteWordCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IPracticeSessionService
|
||||
{
|
||||
Task<PracticeSessionItem> CreatePracticeSessionAsync(LearningActor actor, PracticeSessionCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<PracticeSessionDetailItem> GetPracticeSessionDetailAsync(LearningActor actor, PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<PracticeSessionReportItem> SubmitPracticeSessionAsync(LearningActor actor,
|
||||
SubmitPracticeSessionCommand command, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IPracticeReportService
|
||||
{
|
||||
Task<PracticeSessionReportItem> GetPracticeSessionReportAsync(LearningActor actor, PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<LearningList<PracticeSessionReportItem>> GetPracticeReportsAsync(LearningActor actor,
|
||||
PracticeSessionFilter filter, CancellationToken cancellationToken = default);
|
||||
Task<LearningList<PracticeHistoryItem>> GetPracticeHistoryAsync(LearningActor actor,
|
||||
PracticeSessionFilter filter, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -5,7 +5,8 @@ using ZLinq;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal sealed class LearningAnalyticsService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), ILearningAnalyticsService
|
||||
{
|
||||
public async Task<LearningStatsItem> GetStatsAsync(
|
||||
LearningActor actor,
|
||||
@@ -7,7 +7,8 @@ using Tiku.Domain.Learning;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal sealed class AnsweringService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), IAnsweringService
|
||||
{
|
||||
public async Task<AnswerRecordItem> SubmitAnswerAsync(
|
||||
LearningActor actor,
|
||||
@@ -14,9 +14,9 @@ using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal abstract partial class LearningActivityServiceBase
|
||||
{
|
||||
private async Task<PracticeAssembly> BuildPracticeAssemblyAsync(
|
||||
protected async Task<PracticeAssembly> BuildPracticeAssemblyAsync(
|
||||
Guid tenantId,
|
||||
PracticeSessionCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -63,7 +63,7 @@ public sealed partial class LearningActivityService
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<Guid>> CollectQuestionReferenceIdsAsync(
|
||||
protected async Task<List<Guid>> CollectQuestionReferenceIdsAsync(
|
||||
LearningActor actor,
|
||||
PracticeAssembly assembly,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -137,7 +137,7 @@ public sealed partial class LearningActivityService
|
||||
return referenceIds;
|
||||
}
|
||||
|
||||
private static IQueryable<Question> ApplyLegacyTargetFilter(
|
||||
protected static IQueryable<Question> ApplyLegacyTargetFilter(
|
||||
IQueryable<Question> query,
|
||||
string? targetType,
|
||||
Guid targetId)
|
||||
@@ -154,14 +154,14 @@ public sealed partial class LearningActivityService
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<QuestionSelection>> LoadQuestionSelectionsAsync(
|
||||
protected async Task<IReadOnlyList<QuestionSelection>> LoadQuestionSelectionsAsync(
|
||||
Guid tenantId,
|
||||
IReadOnlyCollection<Guid> questionReferenceIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await tenantExecutionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(
|
||||
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityService),
|
||||
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase),
|
||||
"Lock published question versions for a new practice session", Guid.NewGuid().ToString("N")),
|
||||
async (provider, token) =>
|
||||
{
|
||||
@@ -215,14 +215,14 @@ public sealed partial class LearningActivityService
|
||||
return questionReferenceIds.Select(referenceId => byReference[referenceId]).ToArray();
|
||||
}
|
||||
|
||||
private Task<PracticeSessionQuestionItem[]> LoadSessionQuestionItemsAsync(
|
||||
protected Task<PracticeSessionQuestionItem[]> LoadSessionQuestionItemsAsync(
|
||||
Guid tenantId,
|
||||
Guid practiceSessionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return tenantExecutionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(
|
||||
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityService),
|
||||
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(LearningActivityServiceBase),
|
||||
"Read locked question versions for a tenant practice session", Guid.NewGuid().ToString("N")),
|
||||
async (provider, token) =>
|
||||
{
|
||||
@@ -253,7 +253,7 @@ public sealed partial class LearningActivityService
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PracticeSession> GetPracticeSessionAsync(
|
||||
protected async Task<PracticeSession> GetPracticeSessionAsync(
|
||||
LearningActor actor,
|
||||
Guid? practiceSessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -276,7 +276,7 @@ public sealed partial class LearningActivityService
|
||||
return session;
|
||||
}
|
||||
|
||||
private async Task<PracticeSessionReport> BuildPracticeSessionReportAsync(
|
||||
protected async Task<PracticeSessionReport> BuildPracticeSessionReportAsync(
|
||||
LearningActor actor,
|
||||
PracticeSession session,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -450,7 +450,7 @@ public sealed partial class LearningActivityService
|
||||
return report;
|
||||
}
|
||||
|
||||
private async Task EnsureQuestionExistsAsync(
|
||||
protected async Task EnsureQuestionExistsAsync(
|
||||
Guid tenantId,
|
||||
Guid questionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -465,7 +465,7 @@ public sealed partial class LearningActivityService
|
||||
if (!exists) throw new LearningResourceNotFoundException("question_not_found", "Question was not found.");
|
||||
}
|
||||
|
||||
private async Task EnsureWordExistsAsync(
|
||||
protected async Task EnsureWordExistsAsync(
|
||||
Guid tenantId,
|
||||
Guid wordId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -480,7 +480,7 @@ public sealed partial class LearningActivityService
|
||||
if (!exists) throw new LearningResourceNotFoundException("word_not_found", "Word was not found.");
|
||||
}
|
||||
|
||||
private static AnswerRecordItem ToItem(AnswerRecord record, long sessionVersion)
|
||||
protected static AnswerRecordItem ToItem(AnswerRecord record, long sessionVersion)
|
||||
{
|
||||
return new AnswerRecordItem(
|
||||
record.Id,
|
||||
@@ -495,7 +495,7 @@ public sealed partial class LearningActivityService
|
||||
record.AnsweredAt);
|
||||
}
|
||||
|
||||
private static WordProgressItem ToItem(UserWordProgress item)
|
||||
protected static WordProgressItem ToItem(UserWordProgress item)
|
||||
{
|
||||
return new WordProgressItem(
|
||||
item.WordId,
|
||||
@@ -511,7 +511,7 @@ public sealed partial class LearningActivityService
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
private static PracticeSessionItem ToItem(PracticeSession item)
|
||||
protected static PracticeSessionItem ToItem(PracticeSession item)
|
||||
{
|
||||
return new PracticeSessionItem(
|
||||
item.Id,
|
||||
@@ -538,7 +538,7 @@ public sealed partial class LearningActivityService
|
||||
item.LastClientSequence);
|
||||
}
|
||||
|
||||
private static PracticeSessionReportItem ToItem(PracticeSessionReport item)
|
||||
protected static PracticeSessionReportItem ToItem(PracticeSessionReport item)
|
||||
{
|
||||
return new PracticeSessionReportItem(
|
||||
item.Id,
|
||||
@@ -568,7 +568,7 @@ public sealed partial class LearningActivityService
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
private static List<Guid> ReadGuidArray(JsonElement value)
|
||||
protected static List<Guid> ReadGuidArray(JsonElement value)
|
||||
{
|
||||
if (value.ValueKind is not JsonValueKind.Array) return [];
|
||||
|
||||
@@ -581,7 +581,7 @@ public sealed partial class LearningActivityService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static void EnsureAnswerSessionState(
|
||||
protected static void EnsureAnswerSessionState(
|
||||
PracticeSession session,
|
||||
SubmitAnswerCommand command)
|
||||
{
|
||||
@@ -596,7 +596,7 @@ public sealed partial class LearningActivityService
|
||||
"Client sequence must increase within a practice session.");
|
||||
}
|
||||
|
||||
private static JsonElement BuildGradingRules(QuestionSelection selection)
|
||||
protected static JsonElement BuildGradingRules(QuestionSelection selection)
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
@@ -607,7 +607,7 @@ public sealed partial class LearningActivityService
|
||||
});
|
||||
}
|
||||
|
||||
private static string HashAnswer(SubmitAnswerCommand command)
|
||||
protected static string HashAnswer(SubmitAnswerCommand command)
|
||||
{
|
||||
return Hash(JsonSerializer.Serialize(new
|
||||
{
|
||||
@@ -619,7 +619,7 @@ public sealed partial class LearningActivityService
|
||||
}));
|
||||
}
|
||||
|
||||
private static string HashSubmission(SubmitPracticeSessionCommand command)
|
||||
protected static string HashSubmission(SubmitPracticeSessionCommand command)
|
||||
{
|
||||
return Hash(JsonSerializer.Serialize(new
|
||||
{
|
||||
@@ -628,12 +628,12 @@ public sealed partial class LearningActivityService
|
||||
}));
|
||||
}
|
||||
|
||||
private static string Hash(string value)
|
||||
protected static string Hash(string value)
|
||||
{
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ResolvePracticeSessionHistoryStatus(PracticeSession session, DateTimeOffset now)
|
||||
protected static string ResolvePracticeSessionHistoryStatus(PracticeSession session, DateTimeOffset now)
|
||||
{
|
||||
if (session.Status is PracticeSessionStatus.Submitted or PracticeSessionStatus.PendingReview) return "finished";
|
||||
|
||||
@@ -644,7 +644,7 @@ public sealed partial class LearningActivityService
|
||||
return "active";
|
||||
}
|
||||
|
||||
private static string NormalizeMode(string? mode)
|
||||
protected static string NormalizeMode(string? mode)
|
||||
{
|
||||
return NormalizeEnumValue(mode) switch
|
||||
{
|
||||
@@ -658,17 +658,17 @@ public sealed partial class LearningActivityService
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryParseWordProgressStatus(string? value, out WordProgressStatus status)
|
||||
protected static bool TryParseWordProgressStatus(string? value, out WordProgressStatus status)
|
||||
{
|
||||
return Enum.TryParse(NormalizeEnumValue(value), true, out status);
|
||||
}
|
||||
|
||||
private static int ResolveLimit(int? limit)
|
||||
protected static int ResolveLimit(int? limit)
|
||||
{
|
||||
return Math.Clamp(limit ?? DefaultLimit, 1, MaxLimit);
|
||||
}
|
||||
|
||||
private static string? NormalizeEnumValue(string? value)
|
||||
protected static string? NormalizeEnumValue(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
@@ -676,7 +676,7 @@ public sealed partial class LearningActivityService
|
||||
.Replace("-", string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed record PracticeAssembly(
|
||||
protected sealed record PracticeAssembly(
|
||||
string Mode,
|
||||
string? TargetType,
|
||||
Guid? TargetId,
|
||||
@@ -688,7 +688,7 @@ public sealed partial class LearningActivityService
|
||||
int? DurationMinutes,
|
||||
decimal? TotalScore);
|
||||
|
||||
private sealed record QuestionSelection(
|
||||
protected sealed record QuestionSelection(
|
||||
Guid QuestionReferenceId,
|
||||
Guid QuestionOwnerTenantId,
|
||||
Guid QuestionId,
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
internal sealed record LearningServiceDependencies(
|
||||
TikuDbContext DbContext,
|
||||
IQuestionReferenceService QuestionReferenceService,
|
||||
IPublicQuestionAccessPolicy PublicQuestionAccessPolicy,
|
||||
ITenantExecutionScope TenantExecutionScope,
|
||||
ILogger<LearningActivityServiceBase> Logger);
|
||||
|
||||
internal abstract partial class LearningActivityServiceBase(LearningServiceDependencies dependencies)
|
||||
{
|
||||
protected TikuDbContext dbContext { get; } = dependencies.DbContext;
|
||||
protected IQuestionReferenceService questionReferenceService { get; } = dependencies.QuestionReferenceService;
|
||||
protected IPublicQuestionAccessPolicy publicQuestionAccessPolicy { get; } = dependencies.PublicQuestionAccessPolicy;
|
||||
protected ITenantExecutionScope tenantExecutionScope { get; } = dependencies.TenantExecutionScope;
|
||||
protected ILogger<LearningActivityServiceBase> logger { get; } = dependencies.Logger;
|
||||
|
||||
protected const int DefaultLimit = 100;
|
||||
protected const int MaxLimit = 500;
|
||||
protected static readonly Meter LearningMeter = new("Tiku.Learning");
|
||||
|
||||
protected static readonly Counter<long> IdempotencyReplays =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.idempotency.replays");
|
||||
|
||||
protected static readonly Counter<long> AnswerConflicts =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.answer.conflicts");
|
||||
|
||||
protected static readonly Counter<long> SubmissionConflicts =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.submission.conflicts");
|
||||
|
||||
protected static readonly Counter<long> ScoringFailures =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.scoring.failures");
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService(
|
||||
TikuDbContext dbContext,
|
||||
IQuestionReferenceService questionReferenceService,
|
||||
IPublicQuestionAccessPolicy publicQuestionAccessPolicy,
|
||||
ITenantExecutionScope tenantExecutionScope,
|
||||
ILogger<LearningActivityService> logger) : ILearningActivityService
|
||||
{
|
||||
private const int DefaultLimit = 100;
|
||||
private const int MaxLimit = 500;
|
||||
private static readonly Meter LearningMeter = new("Tiku.Learning");
|
||||
|
||||
private static readonly Counter<long> IdempotencyReplays =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.idempotency.replays");
|
||||
|
||||
private static readonly Counter<long> AnswerConflicts =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.answer.conflicts");
|
||||
|
||||
private static readonly Counter<long> SubmissionConflicts =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.submission.conflicts");
|
||||
|
||||
private static readonly Counter<long> ScoringFailures =
|
||||
LearningMeter.CreateCounter<long>("tiku.learning.scoring.failures");
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Learning;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
internal sealed class PracticeReportService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), IPracticeReportService
|
||||
{
|
||||
public async Task<PracticeSessionReportItem> GetPracticeSessionReportAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!filter.PracticeSessionId.HasValue)
|
||||
throw new LearningValidationException("practice_session_id_required", "Practice session id is required.");
|
||||
|
||||
var report = await dbContext.PracticeSessionReports
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(
|
||||
item =>
|
||||
item.TenantId == actor.TenantId &&
|
||||
item.UserId == actor.UserId &&
|
||||
item.PracticeSessionId == filter.PracticeSessionId.Value,
|
||||
cancellationToken);
|
||||
|
||||
if (report is null)
|
||||
throw new LearningResourceNotFoundException("practice_report_not_found",
|
||||
"Practice session report was not found.");
|
||||
|
||||
return ToItem(report);
|
||||
}
|
||||
|
||||
public async Task<LearningList<PracticeSessionReportItem>> GetPracticeReportsAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.PracticeSessionReports
|
||||
.AsNoTracking()
|
||||
.Where(report =>
|
||||
report.TenantId == actor.TenantId &&
|
||||
report.UserId == actor.UserId);
|
||||
|
||||
if (filter.BlueprintId.HasValue) query = query.Where(report => report.BlueprintId == filter.BlueprintId.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(report => report.Mode == filter.Mode.Trim());
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(report => report.SubmittedAt)
|
||||
.Take(ResolveLimit(filter.Limit))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new LearningList<PracticeSessionReportItem>(items.Select(ToItem).ToArray());
|
||||
}
|
||||
|
||||
public async Task<LearningList<PracticeHistoryItem>> GetPracticeHistoryAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.PracticeSessions
|
||||
.AsNoTracking()
|
||||
.Where(session =>
|
||||
session.TenantId == actor.TenantId &&
|
||||
session.UserId == actor.UserId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(session => session.Mode == filter.Mode.Trim());
|
||||
|
||||
var rows = await query
|
||||
.GroupJoin(
|
||||
dbContext.PracticeSessionReports.AsNoTracking(),
|
||||
session => new { session.TenantId, PracticeSessionId = session.Id },
|
||||
report => new { report.TenantId, report.PracticeSessionId },
|
||||
(session, reports) => new { session, report = reports.FirstOrDefault() })
|
||||
.OrderByDescending(row => row.session.FinishedAt ?? row.session.StartedAt)
|
||||
.Take(ResolveLimit(filter.Limit))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var items = rows
|
||||
.Select(row => new PracticeHistoryItem(
|
||||
row.session.Id,
|
||||
row.session.Mode,
|
||||
row.session.TargetType,
|
||||
row.session.TargetId,
|
||||
row.session.BlueprintId,
|
||||
row.session.CollectionId,
|
||||
row.session.EntryId,
|
||||
row.session.ContentNodeId,
|
||||
row.session.QuestionCount,
|
||||
row.report?.AnsweredCount ?? 0,
|
||||
row.report?.CorrectCount ?? 0,
|
||||
row.report?.WrongCount ?? 0,
|
||||
row.session.StartedAt,
|
||||
row.session.FinishedAt,
|
||||
row.session.ExpiresAt,
|
||||
ResolvePracticeSessionHistoryStatus(row.session, now),
|
||||
row.report?.Id,
|
||||
row.report?.Score,
|
||||
row.report?.TotalScore,
|
||||
row.report?.Accuracy))
|
||||
.Where(item => string.IsNullOrWhiteSpace(filter.Status) || item.Status == filter.Status.Trim())
|
||||
.ToArray();
|
||||
|
||||
return new LearningList<PracticeHistoryItem>(items);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ using Tiku.Domain.Learning;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal sealed class PracticeSessionService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), IPracticeSessionService
|
||||
{
|
||||
public async Task<PracticeSessionItem> CreatePracticeSessionAsync(
|
||||
LearningActor actor,
|
||||
@@ -237,102 +238,4 @@ public sealed partial class LearningActivityService
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<PracticeSessionReportItem> GetPracticeSessionReportAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!filter.PracticeSessionId.HasValue)
|
||||
throw new LearningValidationException("practice_session_id_required", "Practice session id is required.");
|
||||
|
||||
var report = await dbContext.PracticeSessionReports
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(
|
||||
item =>
|
||||
item.TenantId == actor.TenantId &&
|
||||
item.UserId == actor.UserId &&
|
||||
item.PracticeSessionId == filter.PracticeSessionId.Value,
|
||||
cancellationToken);
|
||||
|
||||
if (report is null)
|
||||
throw new LearningResourceNotFoundException("practice_report_not_found",
|
||||
"Practice session report was not found.");
|
||||
|
||||
return ToItem(report);
|
||||
}
|
||||
|
||||
public async Task<LearningList<PracticeSessionReportItem>> GetPracticeReportsAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.PracticeSessionReports
|
||||
.AsNoTracking()
|
||||
.Where(report =>
|
||||
report.TenantId == actor.TenantId &&
|
||||
report.UserId == actor.UserId);
|
||||
|
||||
if (filter.BlueprintId.HasValue) query = query.Where(report => report.BlueprintId == filter.BlueprintId.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(report => report.Mode == filter.Mode.Trim());
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(report => report.SubmittedAt)
|
||||
.Take(ResolveLimit(filter.Limit))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new LearningList<PracticeSessionReportItem>(items.Select(ToItem).ToArray());
|
||||
}
|
||||
|
||||
public async Task<LearningList<PracticeHistoryItem>> GetPracticeHistoryAsync(
|
||||
LearningActor actor,
|
||||
PracticeSessionFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.PracticeSessions
|
||||
.AsNoTracking()
|
||||
.Where(session =>
|
||||
session.TenantId == actor.TenantId &&
|
||||
session.UserId == actor.UserId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Mode)) query = query.Where(session => session.Mode == filter.Mode.Trim());
|
||||
|
||||
var rows = await query
|
||||
.GroupJoin(
|
||||
dbContext.PracticeSessionReports.AsNoTracking(),
|
||||
session => new { session.TenantId, PracticeSessionId = session.Id },
|
||||
report => new { report.TenantId, report.PracticeSessionId },
|
||||
(session, reports) => new { session, report = reports.FirstOrDefault() })
|
||||
.OrderByDescending(row => row.session.FinishedAt ?? row.session.StartedAt)
|
||||
.Take(ResolveLimit(filter.Limit))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var items = rows
|
||||
.Select(row => new PracticeHistoryItem(
|
||||
row.session.Id,
|
||||
row.session.Mode,
|
||||
row.session.TargetType,
|
||||
row.session.TargetId,
|
||||
row.session.BlueprintId,
|
||||
row.session.CollectionId,
|
||||
row.session.EntryId,
|
||||
row.session.ContentNodeId,
|
||||
row.session.QuestionCount,
|
||||
row.report?.AnsweredCount ?? 0,
|
||||
row.report?.CorrectCount ?? 0,
|
||||
row.report?.WrongCount ?? 0,
|
||||
row.session.StartedAt,
|
||||
row.session.FinishedAt,
|
||||
row.session.ExpiresAt,
|
||||
ResolvePracticeSessionHistoryStatus(row.session, now),
|
||||
row.report?.Id,
|
||||
row.report?.Score,
|
||||
row.report?.TotalScore,
|
||||
row.report?.Accuracy))
|
||||
.Where(item => string.IsNullOrWhiteSpace(filter.Status) || item.Status == filter.Status.Trim())
|
||||
.ToArray();
|
||||
|
||||
return new LearningList<PracticeHistoryItem>(items);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ using Tiku.Domain.Learning;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal sealed class QuestionReviewService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), IQuestionReviewService
|
||||
{
|
||||
public async Task<LearningList<FavoriteQuestionItem>> GetFavoriteQuestionsAsync(
|
||||
LearningActor actor,
|
||||
@@ -5,7 +5,8 @@ using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Infrastructure.Learning;
|
||||
|
||||
public sealed partial class LearningActivityService
|
||||
internal sealed class WordLearningService(LearningServiceDependencies dependencies)
|
||||
: LearningActivityServiceBase(dependencies), IWordLearningService
|
||||
{
|
||||
public async Task<LearningList<WordProgressItem>> GetWordProgressAsync(
|
||||
LearningActor actor,
|
||||
@@ -8,7 +8,13 @@ internal static class LearningModule
|
||||
{
|
||||
internal static IServiceCollection AddLearningModule(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ILearningActivityService, LearningActivityService>();
|
||||
services.AddScoped<LearningServiceDependencies>();
|
||||
services.AddScoped<ILearningAnalyticsService, LearningAnalyticsService>();
|
||||
services.AddScoped<IAnsweringService, AnsweringService>();
|
||||
services.AddScoped<IQuestionReviewService, QuestionReviewService>();
|
||||
services.AddScoped<IWordLearningService, WordLearningService>();
|
||||
services.AddScoped<IPracticeSessionService, PracticeSessionService>();
|
||||
services.AddScoped<IPracticeReportService, PracticeReportService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests
|
||||
var root = FindRepositoryRoot();
|
||||
var legacyLineBudgets = new Dictionary<string, int>(StringComparer.Ordinal)
|
||||
{
|
||||
["LearningActivityService"] = 1781,
|
||||
["PlatformAdminService"] = 1719,
|
||||
["ContentManagementService"] = 1096,
|
||||
["PlatformQuestionBankService"] = 1033,
|
||||
|
||||
@@ -328,7 +328,7 @@ public sealed class PhaseThreeTenantIsolationTests
|
||||
Guid firstSessionId;
|
||||
using (var scope = factory.CreateTenantScope(tenantId, "tenant-a"))
|
||||
{
|
||||
var created = await scope.ServiceProvider.GetRequiredService<ILearningActivityService>()
|
||||
var created = await scope.ServiceProvider.GetRequiredService<IPracticeSessionService>()
|
||||
.CreatePracticeSessionAsync(
|
||||
new LearningActor(tenantId, userId),
|
||||
new PracticeSessionCommand(
|
||||
@@ -358,7 +358,7 @@ public sealed class PhaseThreeTenantIsolationTests
|
||||
Guid secondSessionId;
|
||||
using (var scope = factory.CreateTenantScope(tenantId, "tenant-a"))
|
||||
{
|
||||
var created = await scope.ServiceProvider.GetRequiredService<ILearningActivityService>()
|
||||
var created = await scope.ServiceProvider.GetRequiredService<IPracticeSessionService>()
|
||||
.CreatePracticeSessionAsync(
|
||||
new LearningActor(tenantId, userId),
|
||||
new PracticeSessionCommand(
|
||||
|
||||
Reference in New Issue
Block a user