refactor(bootstrap): migrate mypage-index globals to seed files
This commit is contained in:
@@ -1,229 +0,0 @@
|
||||
{
|
||||
"data_headers": {
|
||||
"short_udid": 411054851,
|
||||
"viewer_id": 906243102,
|
||||
"sid": "",
|
||||
"servertime": 1779553959,
|
||||
"result_code": 1
|
||||
},
|
||||
"data": {
|
||||
"user_info": {
|
||||
"device_type": "2",
|
||||
"name": "combusty7",
|
||||
"country_code": "KOR",
|
||||
"max_friend": "20",
|
||||
"last_play_time": "2026-05-23 16:32:39",
|
||||
"is_received_two_pick_mission": "1",
|
||||
"birth": "19600101",
|
||||
"selected_emblem_id": "701441011",
|
||||
"selected_degree_id": "300003",
|
||||
"mission_change_time": "2017-09-17 14:47:13",
|
||||
"mission_receive_type": "0",
|
||||
"is_official": "0",
|
||||
"is_official_mark_displayed": "0"
|
||||
},
|
||||
"sealed_info": {
|
||||
"enable": 1,
|
||||
"crystal_cost": 600,
|
||||
"rupy_cost": 600,
|
||||
"ticket_cost": 4,
|
||||
"is_join": false,
|
||||
"pack_info": [
|
||||
10032,
|
||||
10032,
|
||||
10031,
|
||||
10030,
|
||||
10029
|
||||
],
|
||||
"deck_using_num_min": 30,
|
||||
"schedule_id": 21,
|
||||
"is_deck_code_maintenance": false,
|
||||
"sales_period_info": {
|
||||
"sales_period_series": 33
|
||||
}
|
||||
},
|
||||
"colosseum_info": {
|
||||
"colosseum_id": "165",
|
||||
"is_display_tips": "0",
|
||||
"tips_id": "0",
|
||||
"card_pool_name": "Take Two (Dragonblade–Rivenbrandt)",
|
||||
"is_colosseum_period": true,
|
||||
"is_round_period": true,
|
||||
"deck_format": "3",
|
||||
"is_normal_two_pick": "1",
|
||||
"is_special_mode": "10",
|
||||
"is_all_card_enabled": 0,
|
||||
"start_time": "2026-05-21 06:00:00",
|
||||
"colosseum_name": "Rivenbrandt Take Two Cup",
|
||||
"now_round": "1",
|
||||
"end_time": "2026-05-25 19:59:59",
|
||||
"sales_period_info": {
|
||||
"sales_period_time": "2026-05-25 19:59:59"
|
||||
}
|
||||
},
|
||||
"is_available_colosseum_free_entry": true,
|
||||
"arena_info": [
|
||||
{
|
||||
"mode": 1,
|
||||
"enable": 1,
|
||||
"cost": 150,
|
||||
"rupy_cost": 150,
|
||||
"ticket_cost": 1,
|
||||
"is_join": false,
|
||||
"format_info": {
|
||||
"two_pick_type": "1",
|
||||
"card_pool_name": "Take Two (Dragonblade–Rivenbrandt)",
|
||||
"announce_id": 0,
|
||||
"last_card_pack_set_id": "10029",
|
||||
"start_time": "2026-05-01 02:00:00",
|
||||
"end_time": "2026-06-01 01:59:59"
|
||||
},
|
||||
"sales_period_info": {
|
||||
"sales_period_time": "2026-06-01 01:59:59"
|
||||
}
|
||||
}
|
||||
],
|
||||
"is_arena_challenge_period": true,
|
||||
"is_hidden_boss_appeared": false,
|
||||
"competition_info": {
|
||||
"is_competition_period": false
|
||||
},
|
||||
"treasure_info": null,
|
||||
"unread_present_count": 0,
|
||||
"unreceived_mission_reward_count": 0,
|
||||
"lottery_period_info": null,
|
||||
"master_point_ranking_period": {
|
||||
"id": "119",
|
||||
"period_num": "118",
|
||||
"necessary_score": "0",
|
||||
"begin_time": "2026-05-01 02:00:00",
|
||||
"end_time": "2026-06-01 01:59:59"
|
||||
},
|
||||
"last_announce_id": "3353",
|
||||
"last_announce_update_time": "2026-05-15 10:22:11",
|
||||
"unfinished_battle_exists": false,
|
||||
"is_joined_room": false,
|
||||
"receive_friend_apply_count": 0,
|
||||
"feature_maintenance_list": [],
|
||||
"can_give_daily_login_bonus": false,
|
||||
"friend_battle_invite_count": 0,
|
||||
"user_config": {
|
||||
"receive_invitation": "1",
|
||||
"receive_invitation_in_battle": "1",
|
||||
"receive_invitation_in_offline": "1",
|
||||
"receive_friend_apply": "1",
|
||||
"is_allow_send_adjust": "1",
|
||||
"is_foil_preferred": "0",
|
||||
"is_prize_preferred": "0"
|
||||
},
|
||||
"banner": [
|
||||
{
|
||||
"image_name": "banner_000788",
|
||||
"click": "account_transition_with_two",
|
||||
"status": "10",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000906",
|
||||
"click": "colosseum",
|
||||
"status": "",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000220",
|
||||
"click": "deck_intro_rotation",
|
||||
"status": "17",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000840",
|
||||
"click": "mission",
|
||||
"status": "2",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
}
|
||||
],
|
||||
"sub_banner": null,
|
||||
"sub_banner_list": [],
|
||||
"user_mypage_info": {
|
||||
"user_mypage_setting": {
|
||||
"mypage_id": "0",
|
||||
"select_type": "0",
|
||||
"mypage_id_list": []
|
||||
}
|
||||
},
|
||||
"user_offline_event": [],
|
||||
"convention": {
|
||||
"is_join_tournament": false,
|
||||
"recent_start_date": null,
|
||||
"is_admin_watch_user": false
|
||||
},
|
||||
"special_crystal_info": [],
|
||||
"room_type_in_session": {
|
||||
"special_deck_format_list": [
|
||||
{
|
||||
"deck_format": "5",
|
||||
"end_time": "2030-06-26 19:59:59"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guild_notification": {
|
||||
"guild_id": null,
|
||||
"guild_room_message_id": null,
|
||||
"is_join_request": false,
|
||||
"is_invited": false
|
||||
},
|
||||
"shop_notification": {
|
||||
"card_pack": {
|
||||
"is_open_free_gacha_campaign": false,
|
||||
"can_free_gacha": false
|
||||
},
|
||||
"build_deck": [],
|
||||
"sleeve": [],
|
||||
"leader_skin": []
|
||||
},
|
||||
"pre_release_status": 0,
|
||||
"gathering_info": {
|
||||
"has_invite": 0,
|
||||
"is_entry": 0
|
||||
},
|
||||
"quest": {
|
||||
"is_open": false,
|
||||
"is_display_badge": false,
|
||||
"is_daily_first_access": false,
|
||||
"end_time": "",
|
||||
"name": ""
|
||||
},
|
||||
"basic_puzzle": {
|
||||
"is_display_badge": true
|
||||
},
|
||||
"all_card_enabled_period": null,
|
||||
"user_item_list": [
|
||||
{
|
||||
"item_id": "1",
|
||||
"number": "19"
|
||||
},
|
||||
{
|
||||
"item_id": "10011",
|
||||
"number": "1"
|
||||
},
|
||||
{
|
||||
"item_id": "80001",
|
||||
"number": "1"
|
||||
}
|
||||
],
|
||||
"is_battle_pass_period": true,
|
||||
"story_notification": {
|
||||
"is_display_ribbon": false,
|
||||
"is_display_badge": false
|
||||
},
|
||||
"home_dialog_list": []
|
||||
}
|
||||
}
|
||||
38
SVSim.Bootstrap/Data/seeds/banners.json
Normal file
38
SVSim.Bootstrap/Data/seeds/banners.json
Normal file
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"image_name": "banner_000788",
|
||||
"click": "account_transition_with_two",
|
||||
"status": "10",
|
||||
"change_time": 10,
|
||||
"remaining_time": 0,
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"image_name": "banner_000906",
|
||||
"click": "colosseum",
|
||||
"status": "",
|
||||
"change_time": 10,
|
||||
"remaining_time": 0,
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"image_name": "banner_000220",
|
||||
"click": "deck_intro_rotation",
|
||||
"status": "17",
|
||||
"change_time": 10,
|
||||
"remaining_time": 0,
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"image_name": "banner_000840",
|
||||
"click": "mission",
|
||||
"status": "2",
|
||||
"change_time": 10,
|
||||
"remaining_time": 0,
|
||||
"image_paths": []
|
||||
}
|
||||
]
|
||||
20
SVSim.Bootstrap/Data/seeds/colosseum.json
Normal file
20
SVSim.Bootstrap/Data/seeds/colosseum.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": 1,
|
||||
"colosseum_id": "165",
|
||||
"colosseum_name": "Rivenbrandt Take Two Cup",
|
||||
"card_pool_name": "Take Two (Dragonblade–Rivenbrandt)",
|
||||
"deck_format": "3",
|
||||
"start_time": "2026-05-21 06:00:00",
|
||||
"end_time": "2026-05-25 19:59:59",
|
||||
"now_round": "1",
|
||||
"is_display_tips": "0",
|
||||
"tips_id": "0",
|
||||
"is_colosseum_period": true,
|
||||
"is_round_period": true,
|
||||
"is_normal_two_pick": "1",
|
||||
"is_special_mode": "10",
|
||||
"is_all_card_enabled": 0,
|
||||
"sales_period_info": {
|
||||
"sales_period_time": "2026-05-25 19:59:59"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"id": 119,
|
||||
"period_num": 118,
|
||||
"necessary_score": 0,
|
||||
"begin_time": "2026-05-01 02:00:00",
|
||||
"end_time": "2026-06-01 01:59:59"
|
||||
}
|
||||
]
|
||||
21
SVSim.Bootstrap/Data/seeds/sealed-season.json
Normal file
21
SVSim.Bootstrap/Data/seeds/sealed-season.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": 1,
|
||||
"enable": 1,
|
||||
"crystal_cost": 600,
|
||||
"rupy_cost": 600,
|
||||
"ticket_cost": 4,
|
||||
"deck_using_num_min": 30,
|
||||
"schedule_id": 21,
|
||||
"is_join": false,
|
||||
"is_deck_code_maintenance": false,
|
||||
"pack_info": [
|
||||
10032,
|
||||
10032,
|
||||
10031,
|
||||
10030,
|
||||
10029
|
||||
],
|
||||
"sales_period_info": {
|
||||
"sales_period_series": 33
|
||||
}
|
||||
}
|
||||
7
SVSim.Bootstrap/Data/seeds/special-deck-formats.json
Normal file
7
SVSim.Bootstrap/Data/seeds/special-deck-formats.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"deck_format": "5",
|
||||
"end_time": "2030-06-26 19:59:59"
|
||||
}
|
||||
]
|
||||
@@ -27,7 +27,6 @@ public class GlobalsImporter
|
||||
Console.WriteLine($"[GlobalsImporter] Loading captures from {capturesDir}...");
|
||||
|
||||
JsonElement? loadIndex = LoadCapture(capturesDir, "load-index");
|
||||
JsonElement? mypageIndex = LoadCapture(capturesDir, "mypage-index");
|
||||
JsonElement? deckInfo = LoadCapture(capturesDir, "deck-info");
|
||||
JsonElement? packInfo = LoadCapture(capturesDir, "pack-info");
|
||||
|
||||
@@ -51,15 +50,6 @@ public class GlobalsImporter
|
||||
total += await UpdateRotationCardSetFlags(context, loadIndex.Value);
|
||||
}
|
||||
|
||||
if (mypageIndex.HasValue)
|
||||
{
|
||||
total += await ImportBanners(context, mypageIndex.Value);
|
||||
total += await ImportColosseum(context, mypageIndex.Value);
|
||||
total += await ImportSealed(context, mypageIndex.Value);
|
||||
total += await ImportMasterPointRankingPeriod(context, mypageIndex.Value);
|
||||
total += await ImportRoomTypeInSession(context, mypageIndex.Value);
|
||||
}
|
||||
|
||||
if (deckInfo.HasValue)
|
||||
{
|
||||
total += await ImportDefaultDecks(context, deckInfo.Value);
|
||||
@@ -521,135 +511,6 @@ public class GlobalsImporter
|
||||
return updated;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Banners ----------
|
||||
|
||||
private async Task<int> ImportBanners(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("banner", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
|
||||
// Banners have no wire ID; we treat the capture as authoritative — clear and rewrite.
|
||||
var existing = await context.Banners.ToListAsync();
|
||||
context.Banners.RemoveRange(existing);
|
||||
|
||||
int created = 0;
|
||||
int idx = 1;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
context.Banners.Add(new BannerEntry
|
||||
{
|
||||
Id = idx++,
|
||||
ImageName = GetString(el, "image_name"),
|
||||
Click = GetString(el, "click"),
|
||||
Status = GetString(el, "status"),
|
||||
ChangeTime = GetInt(el, "change_time"),
|
||||
RemainingTime = GetInt(el, "remaining_time"),
|
||||
ImagePaths = el.TryGetProperty("image_paths", out var ip) ? Serialize(ip) : "[]"
|
||||
});
|
||||
created++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] Banners: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Colosseum (singleton) ----------
|
||||
|
||||
private async Task<int> ImportColosseum(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("colosseum_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new ColosseumConfig { Id = 1 };
|
||||
entry.ColosseumId = GetString(info, "colosseum_id");
|
||||
entry.ColosseumName = GetString(info, "colosseum_name");
|
||||
entry.CardPoolName = GetString(info, "card_pool_name");
|
||||
entry.DeckFormat = GetString(info, "deck_format");
|
||||
entry.StartTime = ParseWireDateTime(GetString(info, "start_time"));
|
||||
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
|
||||
entry.NowRound = GetString(info, "now_round");
|
||||
entry.IsDisplayTips = GetString(info, "is_display_tips");
|
||||
entry.TipsId = GetString(info, "tips_id");
|
||||
entry.IsColosseumPeriod = GetBool(info, "is_colosseum_period");
|
||||
entry.IsRoundPeriod = GetBool(info, "is_round_period");
|
||||
entry.IsNormalTwoPick = GetString(info, "is_normal_two_pick");
|
||||
entry.IsSpecialMode = GetString(info, "is_special_mode");
|
||||
entry.IsAllCardEnabled = GetInt(info, "is_all_card_enabled");
|
||||
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
|
||||
if (existing is null) context.Colosseums.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] Colosseum: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Sealed (singleton) ----------
|
||||
|
||||
private async Task<int> ImportSealed(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("sealed_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new SealedConfig { Id = 1 };
|
||||
entry.Enable = GetInt(info, "enable");
|
||||
entry.CrystalCost = GetInt(info, "crystal_cost");
|
||||
entry.RupyCost = GetInt(info, "rupy_cost");
|
||||
entry.TicketCost = GetInt(info, "ticket_cost");
|
||||
entry.DeckUsingNumMin = GetInt(info, "deck_using_num_min");
|
||||
entry.ScheduleId = GetInt(info, "schedule_id");
|
||||
entry.IsJoin = GetBool(info, "is_join");
|
||||
entry.IsDeckCodeMaintenance = GetBool(info, "is_deck_code_maintenance");
|
||||
entry.PackInfo = info.TryGetProperty("pack_info", out var pi) ? Serialize(pi) : "[]";
|
||||
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
|
||||
if (existing is null) context.SealedSeasons.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] Sealed: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Master Point Ranking Period ----------
|
||||
|
||||
private async Task<int> ImportMasterPointRankingPeriod(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("master_point_ranking_period", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
int id = GetInt(info, "id");
|
||||
if (id == 0) return 0;
|
||||
|
||||
var existing = await context.MasterPointRankingPeriods.FirstOrDefaultAsync(e => e.Id == id);
|
||||
var entry = existing ?? new MasterPointRankingPeriodEntry { Id = id };
|
||||
entry.PeriodNum = GetInt(info, "period_num");
|
||||
entry.NecessaryScore = GetLong(info, "necessary_score");
|
||||
entry.BeginTime = ParseWireDateTime(GetString(info, "begin_time"));
|
||||
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
|
||||
if (existing is null) context.MasterPointRankingPeriods.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] MasterPointRankingPeriod (id={id}): {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Room Type In Session (special deck formats) ----------
|
||||
|
||||
private async Task<int> ImportRoomTypeInSession(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("room_type_in_session", out var rt) || rt.ValueKind != JsonValueKind.Object) return 0;
|
||||
if (!rt.TryGetProperty("special_deck_format_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
|
||||
// Same shape semantics as Banners — the wire has no stable id, treat the capture as
|
||||
// authoritative and clear-and-rewrite with a synthetic ordinal.
|
||||
var existing = await context.SpecialDeckFormats.ToListAsync();
|
||||
context.SpecialDeckFormats.RemoveRange(existing);
|
||||
|
||||
int created = 0;
|
||||
int idx = 1;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
context.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
|
||||
{
|
||||
Id = idx++,
|
||||
DeckFormat = GetString(el, "deck_format"),
|
||||
EndTime = ParseWireDateTime(GetString(el, "end_time"))
|
||||
});
|
||||
created++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] SpecialDeckFormats: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Deck/info: Default Decks ----------
|
||||
|
||||
private async Task<int> ImportDefaultDecks(SVSimDbContext context, JsonElement deckInfo)
|
||||
|
||||
185
SVSim.Bootstrap/Importers/MyPageGlobalsImporter.cs
Normal file
185
SVSim.Bootstrap/Importers/MyPageGlobalsImporter.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Bootstrap.Models.Seed;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent upsert of /mypage/index-derived globals from per-table seed files.
|
||||
/// Banners and SpecialDeckFormats use CLEAR-AND-REWRITE semantics (no stable wire ID, capture is authoritative).
|
||||
/// Colosseum and SealedSeason are singletons (Id=1). MasterPointRankingPeriod upserts by wire id.
|
||||
/// </summary>
|
||||
public class MyPageGlobalsImporter
|
||||
{
|
||||
public async Task<int> ImportBannersAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var seed = SeedLoader.LoadList<BannerSeed>(Path.Combine(seedDir, "banners.json"));
|
||||
if (seed.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[MyPageGlobalsImporter] No banner seed rows; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear-and-rewrite: banners have no stable wire ID, the capture is authoritative.
|
||||
var existing = await context.Banners.ToListAsync();
|
||||
context.Banners.RemoveRange(existing);
|
||||
|
||||
foreach (var s in seed)
|
||||
{
|
||||
context.Banners.Add(new BannerEntry
|
||||
{
|
||||
Id = s.Id,
|
||||
ImageName = s.ImageName,
|
||||
Click = s.Click,
|
||||
Status = s.Status,
|
||||
ChangeTime = s.ChangeTime,
|
||||
RemainingTime = s.RemainingTime,
|
||||
ImagePaths = s.ImagePaths.ValueKind == JsonValueKind.Undefined
|
||||
? "[]"
|
||||
: JsonSerializer.Serialize(s.ImagePaths),
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[MyPageGlobalsImporter] Banners: -{existing.Count}/+{seed.Count}");
|
||||
return seed.Count;
|
||||
}
|
||||
|
||||
public async Task<int> ImportColosseumAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var s = SeedLoader.LoadObject<ColosseumSeed>(Path.Combine(seedDir, "colosseum.json"));
|
||||
if (s is null)
|
||||
{
|
||||
Console.WriteLine("[MyPageGlobalsImporter] No colosseum seed; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existing = await context.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new ColosseumConfig { Id = 1 };
|
||||
|
||||
entry.ColosseumId = s.ColosseumId;
|
||||
entry.ColosseumName = s.ColosseumName;
|
||||
entry.CardPoolName = s.CardPoolName;
|
||||
entry.DeckFormat = s.DeckFormat;
|
||||
entry.StartTime = ImporterBase.ParseWireDateTime(s.StartTime);
|
||||
entry.EndTime = ImporterBase.ParseWireDateTime(s.EndTime);
|
||||
entry.NowRound = s.NowRound;
|
||||
entry.IsDisplayTips = s.IsDisplayTips;
|
||||
entry.TipsId = s.TipsId;
|
||||
entry.IsColosseumPeriod = s.IsColosseumPeriod;
|
||||
entry.IsRoundPeriod = s.IsRoundPeriod;
|
||||
entry.IsNormalTwoPick = s.IsNormalTwoPick;
|
||||
entry.IsSpecialMode = s.IsSpecialMode;
|
||||
entry.IsAllCardEnabled = s.IsAllCardEnabled;
|
||||
entry.SalesPeriodInfo = s.SalesPeriodInfo.ValueKind == JsonValueKind.Undefined
|
||||
? "{}"
|
||||
: JsonSerializer.Serialize(s.SalesPeriodInfo);
|
||||
|
||||
if (existing is null) context.Colosseums.Add(entry);
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[MyPageGlobalsImporter] Colosseum: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
public async Task<int> ImportSealedAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var s = SeedLoader.LoadObject<SealedSeasonSeed>(Path.Combine(seedDir, "sealed-season.json"));
|
||||
if (s is null)
|
||||
{
|
||||
Console.WriteLine("[MyPageGlobalsImporter] No sealed-season seed; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existing = await context.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new SealedConfig { Id = 1 };
|
||||
|
||||
entry.Enable = s.Enable;
|
||||
entry.CrystalCost = s.CrystalCost;
|
||||
entry.RupyCost = s.RupyCost;
|
||||
entry.TicketCost = s.TicketCost;
|
||||
entry.DeckUsingNumMin = s.DeckUsingNumMin;
|
||||
entry.ScheduleId = s.ScheduleId;
|
||||
entry.IsJoin = s.IsJoin;
|
||||
entry.IsDeckCodeMaintenance = s.IsDeckCodeMaintenance;
|
||||
entry.PackInfo = s.PackInfo.ValueKind == JsonValueKind.Undefined
|
||||
? "[]"
|
||||
: JsonSerializer.Serialize(s.PackInfo);
|
||||
entry.SalesPeriodInfo = s.SalesPeriodInfo.ValueKind == JsonValueKind.Undefined
|
||||
? "{}"
|
||||
: JsonSerializer.Serialize(s.SalesPeriodInfo);
|
||||
|
||||
if (existing is null) context.SealedSeasons.Add(entry);
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[MyPageGlobalsImporter] SealedSeason: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
public async Task<int> ImportMasterPointRankingPeriodAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var seed = SeedLoader.LoadList<MasterPointRankingPeriodSeed>(
|
||||
Path.Combine(seedDir, "master-point-ranking-periods.json"));
|
||||
if (seed.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[MyPageGlobalsImporter] No master-point-ranking-period seed rows; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existing = await context.MasterPointRankingPeriods.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
|
||||
foreach (var s in seed)
|
||||
{
|
||||
if (s.Id == 0) continue;
|
||||
|
||||
var entry = existing.TryGetValue(s.Id, out var ex)
|
||||
? ex : new MasterPointRankingPeriodEntry { Id = s.Id };
|
||||
|
||||
entry.PeriodNum = s.PeriodNum;
|
||||
entry.NecessaryScore = s.NecessaryScore;
|
||||
entry.BeginTime = ImporterBase.ParseWireDateTime(s.BeginTime);
|
||||
entry.EndTime = ImporterBase.ParseWireDateTime(s.EndTime);
|
||||
|
||||
if (ex is null)
|
||||
{
|
||||
context.MasterPointRankingPeriods.Add(entry);
|
||||
existing[s.Id] = entry;
|
||||
created++;
|
||||
}
|
||||
else updated++;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[MyPageGlobalsImporter] MasterPointRankingPeriod: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
public async Task<int> ImportSpecialDeckFormatsAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var seed = SeedLoader.LoadList<SpecialDeckFormatSeed>(
|
||||
Path.Combine(seedDir, "special-deck-formats.json"));
|
||||
if (seed.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[MyPageGlobalsImporter] No special-deck-format seed rows; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear-and-rewrite: same semantics as banners — no stable wire ID, capture is authoritative.
|
||||
var existing = await context.SpecialDeckFormats.ToListAsync();
|
||||
context.SpecialDeckFormats.RemoveRange(existing);
|
||||
|
||||
foreach (var s in seed)
|
||||
{
|
||||
context.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
|
||||
{
|
||||
Id = s.Id,
|
||||
DeckFormat = s.DeckFormat,
|
||||
EndTime = ImporterBase.ParseWireDateTime(s.EndTime),
|
||||
});
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[MyPageGlobalsImporter] SpecialDeckFormats: -{existing.Count}/+{seed.Count}");
|
||||
return seed.Count;
|
||||
}
|
||||
}
|
||||
15
SVSim.Bootstrap/Models/Seed/BannerSeed.cs
Normal file
15
SVSim.Bootstrap/Models/Seed/BannerSeed.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class BannerSeed
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
[JsonPropertyName("image_name")] public string ImageName { get; set; } = "";
|
||||
[JsonPropertyName("click")] public string Click { get; set; } = "";
|
||||
[JsonPropertyName("status")] public string Status { get; set; } = "";
|
||||
[JsonPropertyName("change_time")] public int ChangeTime { get; set; }
|
||||
[JsonPropertyName("remaining_time")] public int RemainingTime { get; set; }
|
||||
[JsonPropertyName("image_paths")] public JsonElement ImagePaths { get; set; }
|
||||
}
|
||||
24
SVSim.Bootstrap/Models/Seed/ColosseumSeed.cs
Normal file
24
SVSim.Bootstrap/Models/Seed/ColosseumSeed.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class ColosseumSeed
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; } = 1;
|
||||
[JsonPropertyName("colosseum_id")] public string ColosseumId { get; set; } = "";
|
||||
[JsonPropertyName("colosseum_name")] public string ColosseumName { get; set; } = "";
|
||||
[JsonPropertyName("card_pool_name")] public string CardPoolName { get; set; } = "";
|
||||
[JsonPropertyName("deck_format")] public string DeckFormat { get; set; } = "";
|
||||
[JsonPropertyName("start_time")] public string StartTime { get; set; } = "";
|
||||
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
|
||||
[JsonPropertyName("now_round")] public string NowRound { get; set; } = "";
|
||||
[JsonPropertyName("is_display_tips")] public string IsDisplayTips { get; set; } = "";
|
||||
[JsonPropertyName("tips_id")] public string TipsId { get; set; } = "";
|
||||
[JsonPropertyName("is_colosseum_period")] public bool IsColosseumPeriod { get; set; }
|
||||
[JsonPropertyName("is_round_period")] public bool IsRoundPeriod { get; set; }
|
||||
[JsonPropertyName("is_normal_two_pick")] public string IsNormalTwoPick { get; set; } = "";
|
||||
[JsonPropertyName("is_special_mode")] public string IsSpecialMode { get; set; } = "";
|
||||
[JsonPropertyName("is_all_card_enabled")] public int IsAllCardEnabled { get; set; }
|
||||
[JsonPropertyName("sales_period_info")] public JsonElement SalesPeriodInfo { get; set; }
|
||||
}
|
||||
12
SVSim.Bootstrap/Models/Seed/MasterPointRankingPeriodSeed.cs
Normal file
12
SVSim.Bootstrap/Models/Seed/MasterPointRankingPeriodSeed.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class MasterPointRankingPeriodSeed
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
[JsonPropertyName("period_num")] public int PeriodNum { get; set; }
|
||||
[JsonPropertyName("necessary_score")] public long NecessaryScore { get; set; }
|
||||
[JsonPropertyName("begin_time")] public string BeginTime { get; set; } = "";
|
||||
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
|
||||
}
|
||||
19
SVSim.Bootstrap/Models/Seed/SealedSeasonSeed.cs
Normal file
19
SVSim.Bootstrap/Models/Seed/SealedSeasonSeed.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class SealedSeasonSeed
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; } = 1;
|
||||
[JsonPropertyName("enable")] public int Enable { get; set; }
|
||||
[JsonPropertyName("crystal_cost")] public int CrystalCost { get; set; }
|
||||
[JsonPropertyName("rupy_cost")] public int RupyCost { get; set; }
|
||||
[JsonPropertyName("ticket_cost")] public int TicketCost { get; set; }
|
||||
[JsonPropertyName("deck_using_num_min")] public int DeckUsingNumMin { get; set; }
|
||||
[JsonPropertyName("schedule_id")] public int ScheduleId { get; set; }
|
||||
[JsonPropertyName("is_join")] public bool IsJoin { get; set; }
|
||||
[JsonPropertyName("is_deck_code_maintenance")] public bool IsDeckCodeMaintenance { get; set; }
|
||||
[JsonPropertyName("pack_info")] public JsonElement PackInfo { get; set; }
|
||||
[JsonPropertyName("sales_period_info")] public JsonElement SalesPeriodInfo { get; set; }
|
||||
}
|
||||
10
SVSim.Bootstrap/Models/Seed/SpecialDeckFormatSeed.cs
Normal file
10
SVSim.Bootstrap/Models/Seed/SpecialDeckFormatSeed.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class SpecialDeckFormatSeed
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
[JsonPropertyName("deck_format")] public string DeckFormat { get; set; } = "";
|
||||
[JsonPropertyName("end_time")] public string EndTime { get; set; } = "";
|
||||
}
|
||||
@@ -83,6 +83,13 @@ public static class Program
|
||||
await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir);
|
||||
await puzzleImporter.ImportMissionsAsync(context, opts.SeedDir);
|
||||
|
||||
var mypage = new MyPageGlobalsImporter();
|
||||
await mypage.ImportBannersAsync(context, opts.SeedDir);
|
||||
await mypage.ImportColosseumAsync(context, opts.SeedDir);
|
||||
await mypage.ImportSealedAsync(context, opts.SeedDir);
|
||||
await mypage.ImportMasterPointRankingPeriodAsync(context, opts.SeedDir);
|
||||
await mypage.ImportSpecialDeckFormatsAsync(context, opts.SeedDir);
|
||||
|
||||
// BuildDeck pipeline: series CSV → catalog JSON → package CSV. Catalog must run after
|
||||
// series CSV (FK on products → series) and before package CSV (so the catalog-side
|
||||
// enriched rows take precedence over stub creation).
|
||||
|
||||
187
SVSim.UnitTests/Importers/MyPageGlobalsImporterTests.cs
Normal file
187
SVSim.UnitTests/Importers/MyPageGlobalsImporterTests.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Importers;
|
||||
|
||||
public class MyPageGlobalsImporterTests
|
||||
{
|
||||
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
|
||||
|
||||
[Test]
|
||||
public async Task Imports_banners_from_seed_file()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportBannersAsync(db, SeedDir);
|
||||
|
||||
int count = await db.Banners.CountAsync();
|
||||
Assert.That(count, Is.GreaterThan(0), "seed must contain banners");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Imports_colosseum_singleton()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportColosseumAsync(db, SeedDir);
|
||||
|
||||
var row = await db.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
Assert.That(row, Is.Not.Null);
|
||||
Assert.That(row!.Id, Is.EqualTo(1));
|
||||
Assert.That(row.ColosseumId, Is.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Imports_sealed_singleton()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportSealedAsync(db, SeedDir);
|
||||
|
||||
var row = await db.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
Assert.That(row, Is.Not.Null);
|
||||
Assert.That(row!.Id, Is.EqualTo(1));
|
||||
// pack_info is a JSON array column, must be non-empty in the captured seed.
|
||||
Assert.That(row.PackInfo, Does.StartWith("["));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Imports_master_point_ranking_period()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportMasterPointRankingPeriodAsync(db, SeedDir);
|
||||
|
||||
int count = await db.MasterPointRankingPeriods.CountAsync();
|
||||
Assert.That(count, Is.GreaterThan(0), "seed must contain at least one ranking period");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Imports_special_deck_formats()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportSpecialDeckFormatsAsync(db, SeedDir);
|
||||
|
||||
int count = await db.SpecialDeckFormats.CountAsync();
|
||||
Assert.That(count, Is.GreaterThan(0), "seed must contain special deck formats");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Banners_are_clear_and_rewrite()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Pre-seed a legacy banner with Id outside the seed range — the importer must wipe it.
|
||||
db.Banners.Add(new BannerEntry
|
||||
{
|
||||
Id = 999,
|
||||
ImageName = "legacy_banner",
|
||||
Click = "legacy",
|
||||
Status = "9",
|
||||
ChangeTime = 0,
|
||||
RemainingTime = 0,
|
||||
ImagePaths = "[]",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportBannersAsync(db, SeedDir);
|
||||
|
||||
var stale = await db.Banners.FindAsync(999);
|
||||
Assert.That(stale, Is.Null,
|
||||
"ImportBannersAsync must clear-and-rewrite — pre-existing legacy rows must be removed");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Special_deck_formats_are_clear_and_rewrite()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
db.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
|
||||
{
|
||||
Id = 999,
|
||||
DeckFormat = "99",
|
||||
EndTime = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportSpecialDeckFormatsAsync(db, SeedDir);
|
||||
|
||||
var stale = await db.SpecialDeckFormats.FindAsync(999);
|
||||
Assert.That(stale, Is.Null,
|
||||
"ImportSpecialDeckFormatsAsync must clear-and-rewrite — pre-existing legacy rows must be removed");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Singletons_are_idempotent_on_rerun()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportColosseumAsync(db, SeedDir);
|
||||
await importer.ImportSealedAsync(db, SeedDir);
|
||||
|
||||
await importer.ImportColosseumAsync(db, SeedDir);
|
||||
await importer.ImportSealedAsync(db, SeedDir);
|
||||
|
||||
Assert.That(await db.Colosseums.CountAsync(), Is.EqualTo(1),
|
||||
"Colosseum singleton must remain a single row on re-run");
|
||||
Assert.That(await db.SealedSeasons.CountAsync(), Is.EqualTo(1),
|
||||
"SealedSeason singleton must remain a single row on re-run");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Master_point_ranking_period_leaves_existing_rows_untouched()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const int legacyId = 88888;
|
||||
db.MasterPointRankingPeriods.Add(new MasterPointRankingPeriodEntry
|
||||
{
|
||||
Id = legacyId,
|
||||
PeriodNum = 87,
|
||||
NecessaryScore = 12345,
|
||||
BeginTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
EndTime = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var importer = new MyPageGlobalsImporter();
|
||||
await importer.ImportMasterPointRankingPeriodAsync(db, SeedDir);
|
||||
|
||||
var legacy = await db.MasterPointRankingPeriods.FindAsync(legacyId);
|
||||
Assert.That(legacy, Is.Not.Null,
|
||||
"MasterPointRankingPeriod upserts by id — legacy rows not in seed must survive");
|
||||
Assert.That(legacy!.PeriodNum, Is.EqualTo(87));
|
||||
Assert.That(legacy.NecessaryScore, Is.EqualTo(12345));
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,13 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
||||
await puzzleImporter.ImportGroupsAsync(ctx, seedDir);
|
||||
await puzzleImporter.ImportPuzzlesAsync(ctx, seedDir);
|
||||
await puzzleImporter.ImportMissionsAsync(ctx, seedDir);
|
||||
|
||||
var mypage = new MyPageGlobalsImporter();
|
||||
await mypage.ImportBannersAsync(ctx, seedDir);
|
||||
await mypage.ImportColosseumAsync(ctx, seedDir);
|
||||
await mypage.ImportSealedAsync(ctx, seedDir);
|
||||
await mypage.ImportMasterPointRankingPeriodAsync(ctx, seedDir);
|
||||
await mypage.ImportSpecialDeckFormatsAsync(ctx, seedDir);
|
||||
}
|
||||
|
||||
/// <summary>Convenience: bake the X-Test-Viewer-Id header into a fresh client.</summary>
|
||||
|
||||
Reference in New Issue
Block a user