Files
tiku-backend.net/Tiku.Infrastructure/Learning/Analytics/LearningAnalyticsService.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

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);
}
}