Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
using System.Text.Json;
|
||||
using Backend.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static partial class SeedDataBootstrapper
|
||||
{
|
||||
private static async Task EnsureSeedOperationalDataAsync(AwardsDbContext db, Season season)
|
||||
{
|
||||
var categories = await db.Categories
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase);
|
||||
var candidates = await db.Candidates
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToArrayAsync();
|
||||
|
||||
var normalizedLegacyState = await NormalizeLegacyDemoLabelsAsync(db);
|
||||
|
||||
if (!await db.ClipSubmissions.AnyAsync(item => item.SeasonId == season.Id))
|
||||
{
|
||||
db.ClipSubmissions.AddRange(
|
||||
new ClipSubmission
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Hoshimi Miyu"),
|
||||
SubmittedByTwitchId = "local_user_3",
|
||||
ClipUrl = "https://clips.twitch.tv/StarlitDebutMoment",
|
||||
Title = "Starlight Debut Moment",
|
||||
Creator = "Hoshimi Miyu",
|
||||
Platform = "Twitch",
|
||||
Status = "approved",
|
||||
ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.",
|
||||
ReviewedByTwitchId = "jayuhime_admin",
|
||||
CreatedFromIp = "127.0.0.1",
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 15, 0, TimeSpan.Zero),
|
||||
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 5, 0, TimeSpan.Zero),
|
||||
},
|
||||
new ClipSubmission
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Kurainu"),
|
||||
SubmittedByTwitchId = "local_user_4",
|
||||
ClipUrl = "https://clips.twitch.tv/KurainuFinaleHype",
|
||||
Title = "Finale-Hype mit Chat-Chaos",
|
||||
Creator = "Kurainu",
|
||||
Platform = "Twitch",
|
||||
Status = "approved",
|
||||
ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.",
|
||||
ReviewedByTwitchId = "jayuhime_admin",
|
||||
CreatedFromIp = "127.0.0.1",
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 35, 0, TimeSpan.Zero),
|
||||
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 10, 0, TimeSpan.Zero),
|
||||
},
|
||||
new ClipSubmission
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = ResolveCategoryId(categories, "best-gaming"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "best-gaming", "Kurainu"),
|
||||
SubmittedByTwitchId = "local_user",
|
||||
ClipUrl = "https://clips.twitch.tv/EpicGamingMoment",
|
||||
Title = "Epischer Clutch im Finale",
|
||||
Creator = "Kurainu",
|
||||
Platform = "Twitch",
|
||||
Status = "pending",
|
||||
CreatedFromIp = "127.0.0.1",
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 17, 9, 10, 0, TimeSpan.Zero),
|
||||
},
|
||||
new ClipSubmission
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
CategoryId = ResolveCategoryId(categories, "gesang-musik"),
|
||||
CandidateId = ResolveCandidateId(categories, candidates, "gesang-musik", "Melo Diva"),
|
||||
SubmittedByTwitchId = "local_user_2",
|
||||
ClipUrl = "https://www.youtube.com/watch?v=liveCoverMoment",
|
||||
Title = "Live-Cover mit Gänsehaut",
|
||||
Creator = "Melo Diva",
|
||||
Platform = "YouTube",
|
||||
Status = "approved",
|
||||
ReviewNote = "Geprüfter Clip fuer Review-Workflow.",
|
||||
ReviewedByTwitchId = "jayuhime_admin",
|
||||
CreatedFromIp = "127.0.0.1",
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 18, 10, 30, 0, TimeSpan.Zero),
|
||||
ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 0, 0, TimeSpan.Zero),
|
||||
});
|
||||
}
|
||||
|
||||
if (!normalizedLegacyState.HasRiskSeed && !await db.RiskFlags.AnyAsync(item => item.Source == "seed"))
|
||||
{
|
||||
db.RiskFlags.Add(new RiskFlag
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
TwitchUserId = "sample_user",
|
||||
Source = "seed",
|
||||
Type = "rapid_vote_updates",
|
||||
Severity = "medium",
|
||||
Status = "open",
|
||||
Summary = "Mehrere Voting-Aenderungen in kurzer Zeit erkannt.",
|
||||
CreatedFromIp = "127.0.0.1",
|
||||
UserAgent = "seed-bootstrap",
|
||||
MetadataJson = JsonSerializer.Serialize(new { recentVoteSubmissions = 3 }),
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 40, 0, TimeSpan.Zero),
|
||||
});
|
||||
}
|
||||
|
||||
if (!normalizedLegacyState.HasAuditSeed && !await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize"))
|
||||
{
|
||||
db.AdminAuditEntries.Add(new AdminAuditEntry
|
||||
{
|
||||
AdminTwitchUserId = "system",
|
||||
ActionType = "seed.initialize",
|
||||
EntityType = "database",
|
||||
EntityId = season.Year.ToString(),
|
||||
Summary = "Startinhalte wurden in der Datenbank bereitgestellt.",
|
||||
MetadataJson = JsonSerializer.Serialize(new { categories = SeedCatalog.CategorySeeds.Length }),
|
||||
CreatedFromIp = "seed",
|
||||
UserAgent = "seed-bootstrap",
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 32, 0, TimeSpan.Zero),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static int? ResolveCategoryId(IReadOnlyDictionary<string, Category> categories, string slug) =>
|
||||
categories.TryGetValue(slug, out var category) ? category.Id : null;
|
||||
|
||||
private static int? ResolveCandidateId(
|
||||
IReadOnlyDictionary<string, Category> categories,
|
||||
IEnumerable<Candidate> candidates,
|
||||
string categorySlug,
|
||||
string displayName)
|
||||
{
|
||||
var categoryId = ResolveCategoryId(categories, categorySlug);
|
||||
return categoryId is int resolvedCategoryId
|
||||
? candidates.FirstOrDefault(item =>
|
||||
item.CategoryId == resolvedCategoryId
|
||||
&& string.Equals(item.DisplayName, displayName, StringComparison.OrdinalIgnoreCase))?.Id
|
||||
: null;
|
||||
}
|
||||
|
||||
private static async Task<LegacySeedState> NormalizeLegacyDemoLabelsAsync(AwardsDbContext db)
|
||||
{
|
||||
var legacySessions = await db.UserSessions
|
||||
.Where(item => item.TwitchUserId == "admin_demo" || item.TwitchUserId == "jayuhime_demo" || item.TwitchUserId == "demo_user")
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var session in legacySessions)
|
||||
{
|
||||
session.TwitchUserId = session.TwitchUserId switch
|
||||
{
|
||||
"admin_demo" => "jayuhime_admin",
|
||||
"jayuhime_demo" => "jayuhime_viewer",
|
||||
"demo_user" => "local_user",
|
||||
_ => session.TwitchUserId,
|
||||
};
|
||||
session.DisplayName = session.DisplayName switch
|
||||
{
|
||||
"Admin Demo" => "Jayuhime Admin",
|
||||
"Demo User" => "Local User",
|
||||
_ => session.DisplayName,
|
||||
};
|
||||
}
|
||||
|
||||
var legacyClipSubmissions = await db.ClipSubmissions
|
||||
.Where(item =>
|
||||
item.SubmittedByTwitchId == "demo_user" ||
|
||||
item.SubmittedByTwitchId == "demo_user_2" ||
|
||||
item.ClipUrl.Contains("Demo") ||
|
||||
item.ClipUrl.Contains("demo") ||
|
||||
(item.ReviewNote != null && item.ReviewNote.Contains("Demo-Clip")))
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var clip in legacyClipSubmissions)
|
||||
{
|
||||
clip.SubmittedByTwitchId = clip.SubmittedByTwitchId switch
|
||||
{
|
||||
"demo_user" => "local_user",
|
||||
"demo_user_2" => "local_user_2",
|
||||
_ => clip.SubmittedByTwitchId,
|
||||
};
|
||||
clip.ClipUrl = clip.ClipUrl
|
||||
.Replace("DemoGamingMoment", "EpicGamingMoment")
|
||||
.Replace("demoSong", "liveCoverMoment");
|
||||
clip.ReviewNote = clip.ReviewNote?.Replace("Demo-Clip", "Geprüfter Clip");
|
||||
}
|
||||
await LinkExistingClipsToCandidatesAsync(db);
|
||||
|
||||
var legacyRiskFlags = await db.RiskFlags
|
||||
.Where(item =>
|
||||
item.Source == "demo" ||
|
||||
item.Summary.StartsWith("Demo:") ||
|
||||
item.TwitchUserId == "jayuhime_demo" ||
|
||||
item.TwitchUserId == "demo_user")
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var flag in legacyRiskFlags)
|
||||
{
|
||||
flag.Source = "seed";
|
||||
flag.TwitchUserId = flag.TwitchUserId switch
|
||||
{
|
||||
"demo_user" => "local_user",
|
||||
"jayuhime_demo" => "jayuhime_viewer",
|
||||
_ => flag.TwitchUserId,
|
||||
};
|
||||
flag.Summary = flag.Summary.Replace("Demo: ", string.Empty);
|
||||
flag.UserAgent = flag.UserAgent == "demo-seed" ? "seed-bootstrap" : flag.UserAgent;
|
||||
}
|
||||
|
||||
var legacyAuditEntries = await db.AdminAuditEntries
|
||||
.Where(item =>
|
||||
item.ActionType == "demo.seed" ||
|
||||
item.Summary.Contains("Demo-Inhalte") ||
|
||||
item.AdminTwitchUserId == "admin_demo" ||
|
||||
item.AdminTwitchUserId == "jayuhime_demo")
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var entry in legacyAuditEntries)
|
||||
{
|
||||
entry.AdminTwitchUserId = entry.AdminTwitchUserId switch
|
||||
{
|
||||
"admin_demo" => "jayuhime_admin",
|
||||
"jayuhime_demo" => "jayuhime_viewer",
|
||||
_ => entry.AdminTwitchUserId,
|
||||
};
|
||||
if (entry.ActionType == "demo.seed")
|
||||
{
|
||||
entry.ActionType = "seed.initialize";
|
||||
}
|
||||
if (entry.Summary.Contains("Demo-Inhalte"))
|
||||
{
|
||||
entry.Summary = "Startinhalte wurden in der Datenbank bereitgestellt.";
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacySeedState(
|
||||
legacyRiskFlags.Length > 0 || await db.RiskFlags.AnyAsync(item => item.Source == "seed"),
|
||||
legacyAuditEntries.Length > 0 || await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize"));
|
||||
}
|
||||
|
||||
private static async Task LinkExistingClipsToCandidatesAsync(AwardsDbContext db)
|
||||
{
|
||||
var clips = await db.ClipSubmissions
|
||||
.Where(item => item.CandidateId == null && item.CategoryId != null && item.Creator != string.Empty)
|
||||
.ToArrayAsync();
|
||||
if (clips.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seasonIds = clips.Select(item => item.SeasonId).Distinct().ToArray();
|
||||
var categoryIds = clips.Select(item => item.CategoryId!.Value).Distinct().ToArray();
|
||||
var candidates = await db.Candidates
|
||||
.Where(item => seasonIds.Contains(item.SeasonId) && categoryIds.Contains(item.CategoryId))
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
var creatorKey = NormalizeSeedCandidateKey(clip.Creator);
|
||||
var candidate = candidates.FirstOrDefault(item =>
|
||||
item.SeasonId == clip.SeasonId
|
||||
&& item.CategoryId == clip.CategoryId
|
||||
&& (NormalizeSeedCandidateKey(item.DisplayName) == creatorKey
|
||||
|| NormalizeSeedCandidateKey(item.ChannelSlug) == creatorKey));
|
||||
|
||||
if (candidate is not null)
|
||||
{
|
||||
clip.CandidateId = candidate.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeSeedCandidateKey(string value) =>
|
||||
new(
|
||||
value
|
||||
.Trim()
|
||||
.TrimStart('@')
|
||||
.ToLowerInvariant()
|
||||
.Where(char.IsLetterOrDigit)
|
||||
.ToArray());
|
||||
|
||||
private sealed record LegacySeedState(bool HasRiskSeed, bool HasAuditSeed);
|
||||
}
|
||||
Reference in New Issue
Block a user