Files
tiku-backend.net/Tiku.Infrastructure/ContentV2/ContentCandidateReader.cs
xiong 3f5957d744
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): establish v2 learning delivery foundation
2026-08-06 09:52:18 +08:00

91 lines
4.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Domain.Content;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.ContentV2;
internal sealed class ContentCandidateReader(ITenantExecutionScope tenantExecutionScope) : IContentCandidateReader
{
public async Task<IReadOnlyCollection<ContentCandidateItem>> GetCandidatesAsync(
ContentCandidateQuery query,
CancellationToken cancellationToken = default)
{
if (query.ContentReleaseIds.Count == 0 || query.AudienceSegmentIds.Count == 0) return [];
var limit = Math.Clamp(query.Limit, 1, 500);
return await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
query.ContentOwnerTenantId,
SystemScopeCallerType.PublicQuestionBank,
nameof(ContentCandidateReader),
"Read compiled content candidates with an explicit content owner",
Guid.NewGuid().ToString("N")),
async (provider, token) =>
{
var persistence = provider.GetRequiredService<IQuestionBankPersistence>();
var releaseIds = query.ContentReleaseIds.ToArray();
var segmentIds = query.AudienceSegmentIds.ToArray();
var candidates = persistence.ContentReleaseQuestions.AsNoTracking().Where(item =>
item.TenantId == query.ContentOwnerTenantId &&
releaseIds.Contains(item.ContentReleaseId) &&
segmentIds.Contains(item.AudienceSegmentId) &&
item.Status == ContentReleaseQuestionStatus.Active);
if (query.CurriculumNodeId.HasValue)
candidates = candidates.Where(item => item.CurriculumNodeId == query.CurriculumNodeId.Value);
if (!string.IsNullOrWhiteSpace(query.QuestionType))
{
var type = query.QuestionType.Trim();
candidates = candidates.Where(item => item.QuestionType == type);
}
if (query.Difficulty.HasValue)
candidates = candidates.Where(item => item.Difficulty == query.Difficulty.Value);
if (query.CollectionReleaseIds?.Count > 0)
{
var collectionIds = query.CollectionReleaseIds.ToArray();
candidates = candidates.Where(candidate => persistence.CollectionReleaseQuestions.Any(item =>
item.TenantId == query.ContentOwnerTenantId &&
collectionIds.Contains(item.CollectionReleaseId) &&
item.ContentReleaseQuestionId == candidate.Id));
}
var count = await candidates.CountAsync(token);
if (count == 0) return Array.Empty<ContentCandidateItem>();
var start = (int)((uint)query.Seed % (uint)count);
var ordered = candidates
.OrderBy(item => item.ContentReleaseId)
.ThenBy(item => item.AudienceSegmentId)
.ThenBy(item => item.CurriculumNodeId)
.ThenBy(item => item.Ordinal)
.ThenBy(item => item.Id);
var first = await Project(ordered.Skip(start).Take(limit))
.ToArrayAsync(token);
if (first.Length == limit || start == 0) return first;
var wrapped = await Project(ordered.Take(limit - first.Length))
.ToArrayAsync(token);
return first.Concat(wrapped).DistinctBy(item => item.ContentReleaseQuestionId).ToArray();
},
cancellationToken);
}
private static IQueryable<ContentCandidateItem> Project(IQueryable<ContentReleaseQuestion> query)
{
return query.Select(item => new ContentCandidateItem(
item.Id,
item.TenantId,
item.ContentReleaseId,
item.AudienceSegmentId,
item.CurriculumNodeId,
item.QuestionPlacementId,
item.QuestionAssetOwnerTenantId,
item.QuestionAssetId,
item.QuestionRevisionId,
item.AssessmentPolicyVersionId,
item.Ordinal,
item.Difficulty,
item.QuestionType));
}
}