forked from xiongyuxing/tiku-backend.net
285 lines
9.2 KiB
C#
285 lines
9.2 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Catalog;
|
|
using Tiku.Application.StudyContent;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.StudyContent;
|
|
|
|
public sealed class StudyContentQueryService(TikuDbContext dbContext) : IStudyContentQueryService
|
|
{
|
|
private const int DefaultLimit = 500;
|
|
private const int MaxLimit = 2000;
|
|
|
|
public async Task<CatalogList<VocabularyUnitCatalogItem>> GetVocabularyUnitsAsync(
|
|
StudyContentFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = dbContext.VocabularyUnits
|
|
.AsNoTracking()
|
|
.Where(unit =>
|
|
unit.TenantId == filter.TenantId &&
|
|
unit.IsActive);
|
|
|
|
if (filter.RegionId.HasValue)
|
|
{
|
|
query = query.Where(unit => unit.RegionId == filter.RegionId.Value || unit.RegionId == null);
|
|
}
|
|
|
|
if (filter.EntryId.HasValue)
|
|
{
|
|
query = query.Where(unit => unit.EntryId == filter.EntryId.Value);
|
|
}
|
|
|
|
if (filter.ContentNodeId.HasValue)
|
|
{
|
|
query = query.Where(unit => unit.ContentNodeId == filter.ContentNodeId.Value);
|
|
}
|
|
|
|
query = ApplyNameKeyword(query, filter.Keyword);
|
|
|
|
var items = await query
|
|
.OrderBy(unit => unit.SortOrder)
|
|
.ThenBy(unit => unit.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(unit => new VocabularyUnitCatalogItem(
|
|
unit.Id,
|
|
unit.LegacyId,
|
|
unit.RegionId,
|
|
unit.EntryId,
|
|
unit.ContentNodeId,
|
|
unit.Name,
|
|
unit.Description,
|
|
unit.WordCount,
|
|
unit.SortOrder,
|
|
unit.Metadata))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return new CatalogList<VocabularyUnitCatalogItem>(items);
|
|
}
|
|
|
|
public async Task<CatalogList<VocabularyWordCatalogItem>> GetVocabularyWordsAsync(
|
|
StudyContentFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = dbContext.VocabularyWords
|
|
.AsNoTracking()
|
|
.Where(word =>
|
|
word.TenantId == filter.TenantId &&
|
|
word.IsActive);
|
|
|
|
if (filter.UnitId.HasValue)
|
|
{
|
|
query = query.Where(word => word.UnitId == filter.UnitId.Value);
|
|
}
|
|
|
|
if (filter.EntryId.HasValue)
|
|
{
|
|
query = query.Where(word => word.EntryId == filter.EntryId.Value);
|
|
}
|
|
|
|
if (filter.ContentNodeId.HasValue)
|
|
{
|
|
query = query.Where(word => word.ContentNodeId == filter.ContentNodeId.Value);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Keyword))
|
|
{
|
|
var keyword = filter.Keyword.Trim();
|
|
query = query.Where(word =>
|
|
word.Word.Contains(keyword) ||
|
|
(word.Meaning != null && word.Meaning.Contains(keyword)));
|
|
}
|
|
|
|
var items = await query
|
|
.OrderBy(word => word.SortOrder)
|
|
.ThenBy(word => word.Word)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(word => new VocabularyWordCatalogItem(
|
|
word.Id,
|
|
word.LegacyId,
|
|
word.UnitId,
|
|
word.EntryId,
|
|
word.ContentNodeId,
|
|
word.Word,
|
|
word.Phonetic,
|
|
word.Meaning,
|
|
word.Example,
|
|
word.ExampleTranslation,
|
|
word.Difficulty,
|
|
word.Tags,
|
|
word.SortOrder,
|
|
word.Metadata))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return new CatalogList<VocabularyWordCatalogItem>(items);
|
|
}
|
|
|
|
public async Task<CatalogList<HandbookSubjectCatalogItem>> GetHandbookSubjectsAsync(
|
|
StudyContentFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = dbContext.HandbookSubjects
|
|
.AsNoTracking()
|
|
.Where(subject =>
|
|
subject.TenantId == filter.TenantId &&
|
|
subject.IsActive);
|
|
|
|
if (filter.RegionId.HasValue)
|
|
{
|
|
query = query.Where(subject => subject.RegionId == filter.RegionId.Value || subject.RegionId == null);
|
|
}
|
|
|
|
if (filter.EntryId.HasValue)
|
|
{
|
|
query = query.Where(subject => subject.EntryId == filter.EntryId.Value);
|
|
}
|
|
|
|
if (filter.ContentNodeId.HasValue)
|
|
{
|
|
query = query.Where(subject => subject.ContentNodeId == filter.ContentNodeId.Value);
|
|
}
|
|
|
|
query = ApplyNameKeyword(query, filter.Keyword);
|
|
|
|
var items = await query
|
|
.OrderBy(subject => subject.SortOrder)
|
|
.ThenBy(subject => subject.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(subject => new HandbookSubjectCatalogItem(
|
|
subject.Id,
|
|
subject.LegacyId,
|
|
subject.RegionId,
|
|
subject.SchoolId,
|
|
subject.MajorId,
|
|
subject.EntryId,
|
|
subject.ContentNodeId,
|
|
subject.Name,
|
|
subject.Type,
|
|
subject.Icon,
|
|
subject.Color,
|
|
subject.Description,
|
|
subject.SortOrder,
|
|
subject.Metadata))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return new CatalogList<HandbookSubjectCatalogItem>(items);
|
|
}
|
|
|
|
public async Task<CatalogList<HandbookChapterCatalogItem>> GetHandbookChaptersAsync(
|
|
StudyContentFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = dbContext.HandbookChapters
|
|
.AsNoTracking()
|
|
.Where(chapter =>
|
|
chapter.TenantId == filter.TenantId &&
|
|
chapter.IsActive);
|
|
|
|
if (filter.SubjectId.HasValue)
|
|
{
|
|
query = query.Where(chapter => chapter.SubjectId == filter.SubjectId.Value);
|
|
}
|
|
|
|
if (filter.EntryId.HasValue)
|
|
{
|
|
query = query.Where(chapter => chapter.EntryId == filter.EntryId.Value);
|
|
}
|
|
|
|
if (filter.ContentNodeId.HasValue)
|
|
{
|
|
query = query.Where(chapter => chapter.ContentNodeId == filter.ContentNodeId.Value);
|
|
}
|
|
|
|
query = ApplyNameKeyword(query, filter.Keyword);
|
|
|
|
var items = await query
|
|
.OrderBy(chapter => chapter.SortOrder)
|
|
.ThenBy(chapter => chapter.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(chapter => new HandbookChapterCatalogItem(
|
|
chapter.Id,
|
|
chapter.LegacyId,
|
|
chapter.SubjectId,
|
|
chapter.EntryId,
|
|
chapter.ContentNodeId,
|
|
chapter.Name,
|
|
chapter.Description,
|
|
chapter.SortOrder,
|
|
chapter.Metadata))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return new CatalogList<HandbookChapterCatalogItem>(items);
|
|
}
|
|
|
|
public async Task<CatalogList<HandbookEntryCatalogItem>> GetHandbookEntriesAsync(
|
|
StudyContentFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = dbContext.HandbookEntries
|
|
.AsNoTracking()
|
|
.Where(entry =>
|
|
entry.TenantId == filter.TenantId &&
|
|
entry.IsActive);
|
|
|
|
if (filter.ChapterId.HasValue)
|
|
{
|
|
query = query.Where(entry => entry.ChapterId == filter.ChapterId.Value);
|
|
}
|
|
|
|
if (filter.EntryId.HasValue)
|
|
{
|
|
query = query.Where(entry => entry.EntryId == filter.EntryId.Value);
|
|
}
|
|
|
|
if (filter.ContentNodeId.HasValue)
|
|
{
|
|
query = query.Where(entry => entry.ContentNodeId == filter.ContentNodeId.Value);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Keyword))
|
|
{
|
|
var keyword = filter.Keyword.Trim();
|
|
query = query.Where(entry =>
|
|
entry.Title.Contains(keyword) ||
|
|
(entry.Summary != null && entry.Summary.Contains(keyword)));
|
|
}
|
|
|
|
var items = await query
|
|
.OrderBy(entry => entry.SortOrder)
|
|
.ThenBy(entry => entry.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.Select(entry => new HandbookEntryCatalogItem(
|
|
entry.Id,
|
|
entry.LegacyId,
|
|
entry.ChapterId,
|
|
entry.EntryId,
|
|
entry.ContentNodeId,
|
|
entry.Title,
|
|
entry.Summary,
|
|
filter.IncludeContent ? entry.Content : null,
|
|
entry.Tags,
|
|
entry.SortOrder,
|
|
entry.Metadata))
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return new CatalogList<HandbookEntryCatalogItem>(items);
|
|
}
|
|
|
|
private static IQueryable<T> ApplyNameKeyword<T>(IQueryable<T> query, string? keyword)
|
|
where T : class
|
|
{
|
|
if (string.IsNullOrWhiteSpace(keyword))
|
|
{
|
|
return query;
|
|
}
|
|
|
|
var trimmed = keyword.Trim();
|
|
return query.Where(entity => EF.Property<string>(entity, "Name").Contains(trimmed));
|
|
}
|
|
|
|
private static int ResolveLimit(int? limit)
|
|
{
|
|
return Math.Clamp(limit ?? DefaultLimit, 1, MaxLimit);
|
|
}
|
|
}
|