Files
tiku-backend.net/Tiku.Infrastructure/Assets/AssetQueryService.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

292 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Domain.Content;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Assets;
public sealed class AssetQueryService(IContentAssetPersistence dbContext) : IAssetQueryService
{
private const int DefaultLimit = 100;
private const int MaxLimit = 500;
public async Task<CatalogList<ContentAssetCatalogItem>> GetContentAssetsAsync(
AssetFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.ContentAssets
.AsNoTracking()
.Where(asset => asset.TenantId == filter.TenantId);
if (!filter.IncludeInactive) query = query.Where(asset => asset.Status == ContentStatus.Active);
if (!filter.IncludeLocked)
query = query.Where(asset => asset.Visibility == ContentVisibility.Public || asset.IsPublic);
if (filter.RegionId.HasValue)
query = query.Where(asset => asset.RegionId == filter.RegionId.Value || asset.RegionId == null);
if (filter.SubjectId.HasValue)
query = query.Where(asset => asset.SubjectId == filter.SubjectId.Value || asset.SubjectId == null);
if (filter.CategoryId.HasValue)
query = query.Where(asset => asset.CategoryId == filter.CategoryId.Value || asset.CategoryId == null);
if (filter.ContentNodeId.HasValue)
query = query.Where(asset =>
asset.ContentNodeId == filter.ContentNodeId.Value || asset.ContentNodeId == null);
if (filter.AssetId.HasValue) query = query.Where(asset => asset.Id == filter.AssetId.Value);
if (!string.IsNullOrWhiteSpace(filter.AssetType) &&
Enum.TryParse<ContentAssetType>(filter.AssetType, true, out var assetType))
query = query.Where(asset => asset.AssetType == assetType);
if (!string.IsNullOrWhiteSpace(filter.Category))
{
var category = filter.Category.Trim();
query = query.Where(asset => asset.Category == category);
}
if (!string.IsNullOrWhiteSpace(filter.AssetKey))
{
var assetKey = filter.AssetKey.Trim();
query = query.Where(asset => asset.AssetKey == assetKey);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(asset =>
(asset.Title != null && asset.Title.Contains(keyword)) ||
(asset.FileName != null && asset.FileName.Contains(keyword)) ||
(asset.Description != null && asset.Description.Contains(keyword)) ||
(asset.AssetKey != null && asset.AssetKey.Contains(keyword)));
}
var items = await query
.OrderBy(asset => asset.SortOrder)
.ThenByDescending(asset => asset.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(asset => new ContentAssetCatalogItem(
asset.Id,
asset.LegacyId,
asset.RegionId,
asset.SubjectId,
asset.CategoryId,
asset.ContentNodeId,
asset.AssetKey,
asset.Title,
asset.Category,
asset.Description,
asset.FileName,
asset.CdnUrl,
asset.IsPublic,
asset.AssetType,
asset.StorageProvider,
asset.Bucket,
asset.ObjectKey,
asset.MimeType,
asset.FileSizeBytes,
asset.ChecksumSha256,
asset.Visibility,
asset.PreviewUrl,
asset.Status,
asset.SortOrder,
asset.UploadStatus,
asset.VerifiedAt,
asset.PreviewStatus,
asset.SecurityScanStatus,
asset.Metadata))
.ToArrayAsync(cancellationToken);
return new CatalogList<ContentAssetCatalogItem>(items);
}
public async Task<CatalogList<ImageAssetCatalogItem>> GetImagesAsync(
AssetFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.Images
.AsNoTracking()
.Where(image => image.TenantId == filter.TenantId);
if (!filter.IncludeLocked) query = query.Where(image => image.IsPublic);
if (!string.IsNullOrWhiteSpace(filter.Category))
{
var category = filter.Category.Trim();
query = query.Where(image => image.Category == category);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(image =>
(image.Title != null && image.Title.Contains(keyword)) ||
(image.FileName != null && image.FileName.Contains(keyword)));
}
var items = await query
.OrderBy(image => image.Category)
.ThenBy(image => image.Title)
.Take(ResolveLimit(filter.Limit))
.Select(image => new ImageAssetCatalogItem(
image.Id,
image.LegacyId,
image.Title,
image.Category,
image.FileName,
image.IsPublic,
image.CdnUrl,
image.Metadata))
.ToArrayAsync(cancellationToken);
return new CatalogList<ImageAssetCatalogItem>(items);
}
public async Task<CatalogList<AppAssetCatalogItem>> GetAppAssetsAsync(
AssetFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.AppAssets
.AsNoTracking()
.Where(asset => asset.TenantId == filter.TenantId);
if (!string.IsNullOrWhiteSpace(filter.AssetKey))
{
var assetKey = filter.AssetKey.Trim();
query = query.Where(asset => asset.AssetKey == assetKey);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(asset =>
asset.AssetKey.Contains(keyword) ||
(asset.FileName != null && asset.FileName.Contains(keyword)) ||
(asset.Description != null && asset.Description.Contains(keyword)));
}
var items = await query
.OrderBy(asset => asset.AssetKey)
.Take(ResolveLimit(filter.Limit))
.Select(asset => new AppAssetCatalogItem(
asset.Id,
asset.LegacyId,
asset.AssetKey,
asset.FileName,
asset.Description,
asset.Metadata))
.ToArrayAsync(cancellationToken);
return new CatalogList<AppAssetCatalogItem>(items);
}
public async Task<CatalogList<VideoExplanationCatalogItem>> GetVideoExplanationsAsync(
AssetFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.VideoExplanations
.AsNoTracking()
.Where(video => video.TenantId == filter.TenantId);
if (!filter.IncludeInactive) query = query.Where(video => video.IsActive);
if (filter.SubjectId.HasValue)
query = query.Where(video => video.SubjectId == filter.SubjectId.Value || video.SubjectId == null);
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(video =>
video.Title.Contains(keyword) ||
(video.Description != null && video.Description.Contains(keyword)));
}
var items = await query
.OrderBy(video => video.SortOrder)
.ThenBy(video => video.Title)
.Take(ResolveLimit(filter.Limit))
.Select(video => new VideoExplanationCatalogItem(
video.Id,
video.LegacyId,
video.SubjectId,
video.Title,
video.Description,
video.VideoUrl,
video.ThumbnailUrl,
video.DurationSeconds,
video.KnowledgeTags,
video.IsGeneral,
video.Difficulty,
video.SortOrder,
video.IsActive,
video.Metadata))
.ToArrayAsync(cancellationToken);
return new CatalogList<VideoExplanationCatalogItem>(items);
}
public async Task<CatalogList<QuestionVideoCatalogItem>> GetQuestionVideosAsync(
AssetFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.QuestionVideos
.AsNoTracking()
.Where(video => video.TenantId == filter.TenantId);
if (filter.QuestionId.HasValue) query = query.Where(video => video.QuestionId == filter.QuestionId.Value);
if (filter.AssetId.HasValue) query = query.Where(video => video.VideoId == filter.AssetId.Value);
var videoExplanations = dbContext.VideoExplanations.AsNoTracking();
var items = await query
.OrderBy(video => video.SortOrder)
.ThenBy(video => video.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.GroupJoin(
videoExplanations,
questionVideo => new { questionVideo.TenantId, Id = questionVideo.VideoId },
video => new { video.TenantId, Id = (Guid?)video.Id },
(questionVideo, videos) => new
{
QuestionVideo = questionVideo,
Video = videos.FirstOrDefault()
})
.Select(row => new QuestionVideoCatalogItem(
row.QuestionVideo.Id,
row.QuestionVideo.LegacyId,
row.QuestionVideo.QuestionId,
row.QuestionVideo.VideoId,
row.QuestionVideo.VideoType,
row.QuestionVideo.SortOrder,
row.QuestionVideo.Metadata,
row.Video == null
? null
: new VideoExplanationCatalogItem(
row.Video.Id,
row.Video.LegacyId,
row.Video.SubjectId,
row.Video.Title,
row.Video.Description,
row.Video.VideoUrl,
row.Video.ThumbnailUrl,
row.Video.DurationSeconds,
row.Video.KnowledgeTags,
row.Video.IsGeneral,
row.Video.Difficulty,
row.Video.SortOrder,
row.Video.IsActive,
row.Video.Metadata)))
.ToArrayAsync(cancellationToken);
return new CatalogList<QuestionVideoCatalogItem>(items);
}
private static int ResolveLimit(int? limit)
{
return Math.Clamp(limit ?? DefaultLimit, 1, MaxLimit);
}
}