90ac0b81d1
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>
229 lines
8.7 KiB
C#
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
|
|
);
|
|
}
|