using Microsoft.EntityFrameworkCore; using Tiku.Application.Learning; using Tiku.Domain.Learning; using ZLinq; namespace Tiku.Infrastructure.Learning; public sealed partial class LearningActivityService { public async Task GetStatsAsync( LearningActor actor, CancellationToken cancellationToken = default) { var answers = dbContext.AnswerRecords.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.IsCurrent && item.GradingStatus != AnswerGradingStatus.PendingReview && item.GradingStatus != AnswerGradingStatus.LegacyUnverified); return new LearningStatsItem( await answers.CountAsync(cancellationToken), await answers.CountAsync(item => item.IsCorrect == true, cancellationToken), await dbContext.WrongQuestions.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.ResolvedAt == null, cancellationToken), await dbContext.FavoriteQuestions.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, cancellationToken), await dbContext.UserWordFavorites.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, cancellationToken), await dbContext.UserWordProgress.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, cancellationToken), await dbContext.PracticeSessions.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId, cancellationToken), await dbContext.PracticeSessionReports.AsNoTracking().CountAsync( item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.Status == PracticeReportStatus.Final, cancellationToken)); } public async Task> GetTrendAsync( LearningActor actor, LearningLimitFilter filter, CancellationToken cancellationToken = default) { var since = DateTimeOffset.UtcNow.AddDays(-ResolveLimit(filter.Limit)); var rows = await dbContext.AnswerRecords.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.IsCurrent && item.GradingStatus != AnswerGradingStatus.PendingReview && item.GradingStatus != AnswerGradingStatus.LegacyUnverified && item.AnsweredAt >= since) .Select(item => new { item.AnsweredAt, item.IsCorrect }) .ToArrayAsync(cancellationToken); var items = rows .AsValueEnumerable() .GroupBy(item => DateOnly.FromDateTime(item.AnsweredAt.UtcDateTime)) .OrderBy(group => group.Key) .Select(group => new LearningTrendItem( group.Key, group.Count(), group.Count(item => item.IsCorrect == true), group.Count(item => item.IsCorrect == false))) .ToArray(); return new LearningList(items); } public async Task GetLeaderboardAsync( LearningActor actor, LearningLimitFilter filter, CancellationToken cancellationToken = default) { var rows = await dbContext.AnswerRecords.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.IsCurrent && item.GradingStatus != AnswerGradingStatus.PendingReview && item.GradingStatus != AnswerGradingStatus.LegacyUnverified) .GroupBy(item => item.UserId) .Select(group => new { UserId = group.Key, AnswerCount = group.Count(), CorrectCount = group.Count(item => item.IsCorrect == true), WrongCount = group.Count(item => item.IsCorrect == false) }) .OrderByDescending(item => item.CorrectCount) .ThenByDescending(item => item.AnswerCount) .Take(ResolveLimit(filter.Limit)) .ToArrayAsync(cancellationToken); var userIds = rows.Select(item => item.UserId).ToArray(); var names = await dbContext.Users.AsNoTracking() .Where(item => userIds.Contains(item.Id)) .ToDictionaryAsync(item => item.Id, item => item.Name ?? item.Phone, cancellationToken); var items = rows .Select(item => new LearningLeaderboardItem( item.UserId, names.GetValueOrDefault(item.UserId), item.AnswerCount, item.CorrectCount, item.WrongCount, item.AnswerCount == 0 ? 0 : decimal.Round((decimal)item.CorrectCount / item.AnswerCount, 4))) .ToArray(); return new LearningLeaderboardResult( "correct_count", "all", items, items.FirstOrDefault(item => item.UserId == actor.UserId), DateTimeOffset.UtcNow); } }