Files
flatrender/services/content/FlatRender.ContentSvc/Application/Services/TaxonomyService.cs
T
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

229 lines
8.7 KiB
C#

using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models.Requests;
using FlatRender.ContentSvc.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Application.Services;
public class TaxonomyService(ContentDbContext db)
{
public async Task<List<CategoryResponse>> GetCategoryTreeAsync()
{
var all = await db.Categories
.Where(x => x.ParentId == null)
.Include(x => x.Children)
.OrderBy(x => x.Sort)
.ToListAsync();
return all.Select(MapCategory).ToList();
}
public async Task<CategoryResponse> CreateCategoryAsync(CreateCategoryRequest req)
{
var cat = new Category
{
ParentId = req.ParentId,
Name = req.Name,
Slug = req.Slug,
Description = req.Description,
ImageUrl = req.ImageUrl,
Icon = req.Icon,
MetaTitle = req.MetaTitle,
MetaDescription = req.MetaDescription,
MetaKeywords = req.MetaKeywords,
BotFollow = req.BotFollow,
Sort = req.Sort,
IsActive = req.IsActive
};
db.Categories.Add(cat);
await db.SaveChangesAsync();
return MapCategory(cat);
}
public async Task<CategoryResponse> UpdateCategoryAsync(Guid id, UpdateCategoryRequest req)
{
var cat = await db.Categories.FindAsync(id)
?? throw new KeyNotFoundException($"Category {id} not found");
cat.ParentId = req.ParentId;
cat.Name = req.Name;
cat.Slug = req.Slug;
cat.Description = req.Description;
cat.ImageUrl = req.ImageUrl;
cat.Icon = req.Icon;
cat.MetaTitle = req.MetaTitle;
cat.MetaDescription = req.MetaDescription;
cat.MetaKeywords = req.MetaKeywords;
cat.BotFollow = req.BotFollow;
cat.Sort = req.Sort;
cat.IsActive = req.IsActive;
cat.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return MapCategory(cat);
}
public async Task DeleteCategoryAsync(Guid id)
{
var cat = await db.Categories.FindAsync(id)
?? throw new KeyNotFoundException($"Category {id} not found");
cat.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<TagResponse>> GetTagsAsync(int page, int pageSize, string? search)
{
var q = db.Tags.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<TagResponse>(
items.Select(t => new TagResponse(t.Id, t.Name, t.LatinName, t.Slug, t.AppliesToMode, t.IsActive)),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<TagResponse> CreateTagAsync(CreateTagRequest req)
{
var tag = new Tag
{
Name = req.Name,
LatinName = req.LatinName,
Slug = req.Slug,
AppliesToMode = req.AppliesToMode,
IsActive = req.IsActive
};
db.Tags.Add(tag);
await db.SaveChangesAsync();
return new TagResponse(tag.Id, tag.Name, tag.LatinName, tag.Slug, tag.AppliesToMode, tag.IsActive);
}
public async Task<TagResponse> UpdateTagAsync(Guid id, UpdateTagRequest req)
{
var tag = await db.Tags.FindAsync(id)
?? throw new KeyNotFoundException($"Tag {id} not found");
tag.Name = req.Name;
tag.LatinName = req.LatinName;
tag.Slug = req.Slug;
tag.AppliesToMode = req.AppliesToMode;
tag.IsActive = req.IsActive;
tag.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return new TagResponse(tag.Id, tag.Name, tag.LatinName, tag.Slug, tag.AppliesToMode, tag.IsActive);
}
public async Task DeleteTagAsync(Guid id)
{
var tag = await db.Tags.FindAsync(id)
?? throw new KeyNotFoundException($"Tag {id} not found");
tag.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<FontResponse>> GetFontsAsync(int page, int pageSize, string? search, string? direction)
{
var q = db.Fonts.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
if (!string.IsNullOrWhiteSpace(direction))
q = q.Where(x => x.Direction == direction);
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Sort).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<FontResponse>(
items.Select(MapFont),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<FontResponse> CreateFontAsync(CreateFontRequest req)
{
var font = new Font
{
Name = req.Name, OriginalName = req.OriginalName, SystemName = req.SystemName,
Family = req.Family, Weight = req.Weight, Style = req.Style, Direction = req.Direction,
FileUrl = req.FileUrl, SampleImageUrl = req.SampleImageUrl, IsPremium = req.IsPremium,
IsActive = req.IsActive, InstalledOnNodes = req.InstalledOnNodes, Sort = req.Sort
};
db.Fonts.Add(font);
await db.SaveChangesAsync();
return MapFont(font);
}
public async Task<FontResponse> UpdateFontAsync(Guid id, UpdateFontRequest req)
{
var font = await db.Fonts.FindAsync(id)
?? throw new KeyNotFoundException($"Font {id} not found");
font.Name = req.Name; font.OriginalName = req.OriginalName; font.SystemName = req.SystemName;
font.Family = req.Family; font.Weight = req.Weight; font.Style = req.Style; font.Direction = req.Direction;
font.FileUrl = req.FileUrl; font.SampleImageUrl = req.SampleImageUrl; font.IsPremium = req.IsPremium;
font.IsActive = req.IsActive; font.InstalledOnNodes = req.InstalledOnNodes; font.Sort = req.Sort;
font.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return MapFont(font);
}
public async Task DeleteFontAsync(Guid id)
{
var font = await db.Fonts.FindAsync(id)
?? throw new KeyNotFoundException($"Font {id} not found");
font.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<MusicTrackResponse>> GetMusicTracksAsync(int page, int pageSize, string? search)
{
var q = db.MusicTracks.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Sort).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<MusicTrackResponse>(
items.Select(MapMusic),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<MusicTrackResponse> CreateMusicTrackAsync(CreateMusicTrackRequest req)
{
var track = new MusicTrack
{
Name = req.Name, Caption = req.Caption, Keywords = req.Keywords, Url = req.Url,
WaveformData = req.WaveformData, DurationSec = req.DurationSec, Bpm = req.Bpm,
Genre = req.Genre, Mood = req.Mood, IsPremium = req.IsPremium, IsActive = req.IsActive, Sort = req.Sort
};
db.MusicTracks.Add(track);
await db.SaveChangesAsync();
return MapMusic(track);
}
public async Task DeleteMusicTrackAsync(Guid id)
{
var track = await db.MusicTracks.FindAsync(id)
?? throw new KeyNotFoundException($"MusicTrack {id} not found");
track.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
private static CategoryResponse MapCategory(Category c) => new(
c.Id, c.ParentId, c.Name, c.Slug, c.Description, c.ImageUrl, c.Icon,
c.IsActive, c.Sort, c.Children.Select(MapCategory).ToList()
);
private static FontResponse MapFont(Font f) => new(
f.Id, f.Name, f.OriginalName, f.SystemName, f.Family, f.Weight, f.Style,
f.Direction, f.FileUrl, f.SampleImageUrl, f.IsPremium, f.IsActive, f.InstalledOnNodes
);
private static MusicTrackResponse MapMusic(MusicTrack m) => new(
m.Id, m.Name, m.Caption, m.Url, m.WaveformData, m.DurationSec, m.Bpm, m.Genre, m.Mood, m.IsPremium
);
}