118 lines
5.6 KiB
C#
118 lines
5.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Learning;
|
|
using Tiku.Domain.Learning;
|
|
using ZLinq;
|
|
|
|
namespace Tiku.Infrastructure.Learning;
|
|
|
|
internal sealed class LearningAnalyticsService(LearningServiceDependencies dependencies)
|
|
: LearningActivityServiceBase(dependencies), ILearningAnalyticsService
|
|
{
|
|
public async Task<LearningStatsItem> GetStatsAsync(
|
|
LearningActor actor,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var answers = learningPersistence.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 learningPersistence.WrongQuestions.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.ResolvedAt == null,
|
|
cancellationToken),
|
|
await learningPersistence.FavoriteQuestions.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId && item.UserId == actor.UserId,
|
|
cancellationToken),
|
|
await learningPersistence.UserWordFavorites.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId && item.UserId == actor.UserId,
|
|
cancellationToken),
|
|
await learningPersistence.UserWordProgress.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId && item.UserId == actor.UserId,
|
|
cancellationToken),
|
|
await learningPersistence.PracticeSessions.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId && item.UserId == actor.UserId,
|
|
cancellationToken),
|
|
await learningPersistence.PracticeSessionReports.AsNoTracking().CountAsync(
|
|
item => item.TenantId == actor.TenantId &&
|
|
item.UserId == actor.UserId &&
|
|
item.Status == PracticeReportStatus.Final,
|
|
cancellationToken));
|
|
}
|
|
|
|
public async Task<LearningList<LearningTrendItem>> GetTrendAsync(
|
|
LearningActor actor,
|
|
LearningLimitFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var since = DateTimeOffset.UtcNow.AddDays(-ResolveLimit(filter.Limit));
|
|
var rows = await learningPersistence.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<LearningTrendItem>(items);
|
|
}
|
|
|
|
public async Task<LearningLeaderboardResult> GetLeaderboardAsync(
|
|
LearningActor actor,
|
|
LearningLimitFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var rows = await learningPersistence.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 identityPersistence.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);
|
|
}
|
|
}
|