feat: add operations catalog readonly endpoints

This commit is contained in:
xiong
2026-07-26 15:11:32 +08:00
parent fc80176a90
commit b5be831749
11 changed files with 16190 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Domain.Catalog;
using Tiku.Domain.Commerce;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Catalog;
@@ -326,6 +327,309 @@ public sealed class CatalogQueryService(TikuDbContext dbContext) : ICatalogQuery
return new CatalogList<CategoryCatalogItem>(items);
}
public async Task<CatalogList<BannerCatalogItem>> GetBannersAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.Banners
.AsNoTracking()
.Where(banner =>
banner.TenantId == filter.TenantId &&
banner.IsActive);
if (filter.RegionId.HasValue)
{
query = query.Where(banner => banner.RegionId == filter.RegionId.Value);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(banner =>
(banner.Title != null && banner.Title.Contains(keyword)) ||
(banner.Subtitle != null && banner.Subtitle.Contains(keyword)) ||
(banner.Content != null && banner.Content.Contains(keyword)));
}
var items = await query
.OrderBy(banner => banner.SortOrder)
.ThenBy(banner => banner.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(banner => new BannerCatalogItem(
banner.Id,
banner.LegacyId,
banner.RegionId,
banner.Title,
banner.Subtitle,
banner.Content,
banner.ButtonText,
banner.ButtonLink,
banner.BackgroundColor,
banner.BorderColor,
banner.SortOrder,
banner.IsActive))
.ToArrayAsync(cancellationToken);
return new CatalogList<BannerCatalogItem>(items);
}
public async Task<CatalogList<FaqCatalogItem>> GetFaqsAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.Faqs
.AsNoTracking()
.Where(faq =>
faq.TenantId == filter.TenantId &&
faq.IsActive);
if (filter.RegionId.HasValue)
{
query = query.Where(faq => faq.RegionId == filter.RegionId.Value);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(faq =>
(faq.Question != null && faq.Question.Contains(keyword)) ||
(faq.Answer != null && faq.Answer.Contains(keyword)));
}
var items = await query
.OrderBy(faq => faq.SortOrder)
.ThenBy(faq => faq.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(faq => new FaqCatalogItem(
faq.Id,
faq.LegacyId,
faq.RegionId,
faq.Question,
faq.Answer,
faq.SortOrder,
faq.IsActive))
.ToArrayAsync(cancellationToken);
return new CatalogList<FaqCatalogItem>(items);
}
public async Task<CatalogList<AnnouncementCatalogItem>> GetAnnouncementsAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.Announcements
.AsNoTracking()
.Where(announcement =>
announcement.TenantId == filter.TenantId &&
announcement.IsActive);
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(announcement =>
announcement.Content != null && announcement.Content.Contains(keyword));
}
var items = await query
.OrderBy(announcement => announcement.SortOrder)
.ThenBy(announcement => announcement.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(announcement => new AnnouncementCatalogItem(
announcement.Id,
announcement.LegacyId,
announcement.Content,
announcement.Link,
announcement.BackgroundColor,
announcement.SortOrder,
announcement.IsActive))
.ToArrayAsync(cancellationToken);
return new CatalogList<AnnouncementCatalogItem>(items);
}
public async Task<CatalogList<ExamDateCatalogItem>> GetExamDatesAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.ExamDates
.AsNoTracking()
.Where(examDate =>
examDate.TenantId == filter.TenantId &&
examDate.IsActive);
if (filter.RegionId.HasValue)
{
query = query.Where(examDate =>
examDate.RegionId == filter.RegionId.Value ||
examDate.RegionId == null);
}
if (filter.SchoolId.HasValue)
{
query = query.Where(examDate =>
examDate.SchoolId == filter.SchoolId.Value ||
examDate.SchoolId == null);
}
if (!string.IsNullOrWhiteSpace(filter.Type))
{
query = query.Where(examDate => examDate.ExamType == filter.Type.Trim());
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(examDate =>
examDate.ExamName.Contains(keyword) ||
(examDate.Description != null && examDate.Description.Contains(keyword)));
}
var today = DateTimeOffset.UtcNow.Date;
var rows = await query
.OrderBy(examDate => examDate.ExamAt == null)
.ThenBy(examDate => examDate.ExamAt)
.ThenBy(examDate => examDate.SortOrder)
.Take(ResolveLimit(filter.Limit))
.Select(examDate => new
{
examDate.Id,
examDate.LegacyId,
examDate.RegionId,
examDate.SchoolId,
examDate.ExamName,
examDate.ExamAt,
examDate.ExamType,
examDate.Description,
examDate.Metadata,
examDate.SortOrder,
examDate.IsActive
})
.ToArrayAsync(cancellationToken);
var items = rows
.Select(examDate => new ExamDateCatalogItem(
examDate.Id,
examDate.LegacyId,
examDate.RegionId,
examDate.SchoolId,
examDate.ExamName,
examDate.ExamAt,
examDate.ExamType,
examDate.Description,
examDate.Metadata,
examDate.SortOrder,
examDate.IsActive,
examDate.ExamAt.HasValue
? (int?)(examDate.ExamAt.Value.Date - today).Days
: null))
.ToArray();
return new CatalogList<ExamDateCatalogItem>(items);
}
public async Task<CatalogList<ProductCatalogItem>> GetProductsAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.Products
.AsNoTracking()
.Where(product =>
product.TenantId == filter.TenantId &&
product.IsActive);
if (filter.RegionId.HasValue)
{
query = query.Where(product => product.RegionId == filter.RegionId.Value);
}
if (TryParseProductType(filter.Type, out var productType))
{
query = query.Where(product => product.Type == productType);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(product => product.Title.Contains(keyword));
}
var items = await query
.OrderBy(product => product.SortOrder)
.ThenByDescending(product => product.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(product => new ProductCatalogItem(
product.Id,
product.LegacyId,
product.RegionId,
product.Title,
null,
product.LegacyPriceText,
product.Link,
product.Type,
product.Tags,
product.Cover,
product.PreviewIframe,
product.DetailImages,
product.SortOrder,
product.IsActive))
.ToArrayAsync(cancellationToken);
return new CatalogList<ProductCatalogItem>(items);
}
public async Task<CatalogList<SvipPlanCatalogItem>> GetSvipPlansAsync(
CatalogFilter filter,
CancellationToken cancellationToken = default)
{
var query = dbContext.SvipPlans
.AsNoTracking()
.Where(plan =>
plan.TenantId == filter.TenantId &&
plan.IsActive);
if (filter.RegionId.HasValue)
{
query = query.Where(plan =>
plan.RegionId == filter.RegionId.Value ||
plan.RegionId == null);
}
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(plan =>
plan.Name.Contains(keyword) ||
(plan.Description != null && plan.Description.Contains(keyword)));
}
var items = await query
.OrderBy(plan => plan.SortOrder)
.ThenBy(plan => plan.PriceCents)
.Take(ResolveLimit(filter.Limit))
.Select(plan => new SvipPlanCatalogItem(
plan.Id,
plan.LegacyId,
plan.RegionId,
plan.Name,
plan.PriceCents,
plan.PriceCents / 100m,
plan.OriginalPriceCents,
plan.OriginalPriceCents.HasValue ? plan.OriginalPriceCents.Value / 100m : null,
plan.Days,
plan.Description,
plan.PerDayLabel,
plan.Badge,
plan.Recommended,
plan.CouponOnly,
plan.VpProductId,
plan.VpEnabled,
plan.SortOrder,
plan.IsActive))
.ToArrayAsync(cancellationToken);
return new CatalogList<SvipPlanCatalogItem>(items);
}
private static IQueryable<T> ApplyKeyword<T>(IQueryable<T> query, string? keyword)
where T : class
{
@@ -358,6 +662,11 @@ public sealed class CatalogQueryService(TikuDbContext dbContext) : ICatalogQuery
return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out type);
}
private static bool TryParseProductType(string? value, out ProductType type)
{
return Enum.TryParse(NormalizeEnumValue(value), ignoreCase: true, out type);
}
private static string? NormalizeEnumValue(string? value)
{
return string.IsNullOrWhiteSpace(value)