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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)] [Produces("application/json")] [Route("api/learning")] public sealed class LearningController( ILearningActivityService learningActivityService, ICurrentUser currentUser, ITenantContext currentTenant) : ControllerBase { [HttpGet("stats")] [EndpointSummary("查询学习统计")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetStats(CancellationToken cancellationToken) { return Ok(await learningActivityService.GetStatsAsync(ResolveActor(), cancellationToken)); } [HttpGet("trend")] [EndpointSummary("查询学习趋势")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetTrend( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("leaderboard")] [EndpointSummary("查询学习排行榜")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetLeaderboard( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetLeaderboardAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("practice-sessions")] [EndpointSummary("创建练习会话")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task> CreatePracticeSession( CreatePracticeSessionDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.CreatePracticeSessionAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("practice-sessions/detail")] [EndpointSummary("获取练习会话详情")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetPracticeSessionDetail( [FromQuery] PracticeSessionQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetPracticeSessionDetailAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("practice-sessions/submit")] [EndpointSummary("提交练习会话并生成报告")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> SubmitPracticeSession( SubmitPracticeSessionDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.SubmitPracticeSessionAsync( ResolveActor(), request.ToFilter(), cancellationToken)); } [HttpGet("practice-sessions/report")] [EndpointSummary("获取练习会话报告")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetPracticeSessionReport( [FromQuery] PracticeSessionQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetPracticeSessionReportAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("practice-reports")] [EndpointSummary("查询练习报告列表")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetPracticeReports( [FromQuery] PracticeSessionQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetPracticeReportsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("practice-sessions/history")] [EndpointSummary("查询练习历史")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetPracticeHistory( [FromQuery] PracticeSessionQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetPracticeHistoryAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("answers")] [EndpointSummary("提交题目答案")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> SubmitAnswer( SubmitAnswerDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.SubmitAnswerAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("favorites/questions")] [EndpointSummary("查询收藏题目")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetFavoriteQuestions( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetFavoriteQuestionsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("favorites/questions")] [EndpointSummary("收藏或取消收藏题目")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> ToggleFavoriteQuestion( QuestionActionDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.ToggleFavoriteQuestionAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("wrong-questions")] [EndpointSummary("查询错题列表")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetWrongQuestions( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetWrongQuestionsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("wrong-questions/review-plan")] [EndpointSummary("生成错题复习计划")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetWrongQuestionReviewPlan( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetWrongQuestionReviewPlanAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("wrong-questions/resolve")] [EndpointSummary("将错题标记为已解决")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> ResolveWrongQuestion( QuestionActionDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.ResolveWrongQuestionAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("vocabulary/progress")] [EndpointSummary("查询单词学习进度")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetWordProgress( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetWordProgressAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("vocabulary/review-plan")] [EndpointSummary("生成单词复习计划")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetWordReviewPlan( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetWordReviewPlanAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("vocabulary/progress")] [EndpointSummary("更新单词学习进度")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> UpdateWordProgress( WordProgressDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.UpdateWordProgressAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpPost("vocabulary/review")] [EndpointSummary("提交单词复习结果")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> ReviewWord( WordReviewDto request, CancellationToken cancellationToken) { return Ok(await learningActivityService.ReviewWordAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("vocabulary/stats")] [EndpointSummary("查询单词学习统计")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetWordStats( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetWordStatsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("vocabulary/favorites")] [EndpointSummary("查询收藏单词")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetFavoriteWords( [FromQuery] LearningLimitQueryDto query, CancellationToken cancellationToken) { return Ok(await learningActivityService.GetFavoriteWordsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPost("vocabulary/favorites")] [EndpointSummary("收藏或取消收藏单词")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> 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.");