forked from xiongyuxing/tiku-backend.net
feat: add study content readonly endpoints
This commit is contained in:
@@ -5,11 +5,13 @@ using Tiku.Application.Auth;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.StudyContent;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Catalog;
|
||||
using Tiku.Infrastructure.Content;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
using Tiku.Infrastructure.QuestionBanks;
|
||||
using Tiku.Infrastructure.StudyContent;
|
||||
|
||||
namespace Tiku.Infrastructure;
|
||||
|
||||
@@ -37,6 +39,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<ICatalogQueryService, CatalogQueryService>();
|
||||
services.AddScoped<IContentNavigationQueryService, ContentNavigationQueryService>();
|
||||
services.AddScoped<IQuestionBankQueryService, QuestionBankQueryService>();
|
||||
services.AddScoped<IStudyContentQueryService, StudyContentQueryService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
284
Tiku.Infrastructure/StudyContent/StudyContentQueryService.cs
Normal file
284
Tiku.Infrastructure/StudyContent/StudyContentQueryService.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user