feat: add asset and video readonly endpoints

This commit is contained in:
xiong
2026-07-26 14:48:15 +08:00
parent c6051f95f2
commit 0ff97e86cd
7 changed files with 798 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
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(TikuDbContext 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, ignoreCase: 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);
}
}