diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/TypeInternalAnalyzer.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/TypeInternalAnalyzer.cs index 4f2fba51..967b3bb1 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/TypeInternalAnalyzer.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/TypeInternalAnalyzer.cs @@ -40,19 +40,27 @@ internal sealed class TypeInternalAnalyzer : DiagnosticAnalyzer bool privateExists = false; bool internalExists = false; + bool fileExists = false; + foreach(SyntaxToken token in syntax.Modifiers) { if (token.IsKind(SyntaxKind.PrivateKeyword)) { privateExists = true; } + if (token.IsKind(SyntaxKind.InternalKeyword)) { internalExists = true; } + + if (token.IsKind(SyntaxKind.FileKeyword)) + { + fileExists = true; + } } - if (!privateExists && !internalExists) + if (!privateExists && !internalExists && !fileExists) { Location location = syntax.Identifier.GetLocation(); Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs index aae0bc36..603b09c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs @@ -13,19 +13,6 @@ namespace Snap.Hutao.Core.Database; [HighQuality] internal static class DbSetExtension { - /// - /// 获取对应的数据库上下文 - /// - /// 实体类型 - /// 数据库集 - /// 对应的数据库上下文 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DbContext Context(this DbSet dbSet) - where TEntity : class - { - return dbSet.GetService().Context; - } - /// /// 添加并保存 /// @@ -37,7 +24,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.Add(entity); - return dbSet.Context().SaveChanges(); + DbContext dbContext = dbSet.Context(); + int count = dbContext.SaveChanges(); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -51,7 +41,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.Add(entity); - return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); + DbContext dbContext = dbSet.Context(); + int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -65,7 +58,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.AddRange(entities); - return dbSet.Context().SaveChanges(); + DbContext dbContext = dbSet.Context(); + int count = dbSet.Context().SaveChanges(); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -79,7 +75,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.AddRange(entities); - return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); + DbContext dbContext = dbSet.Context(); + int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -93,7 +92,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.Remove(entity); - return dbSet.Context().SaveChanges(); + DbContext dbContext = dbSet.Context(); + int count = dbContext.SaveChanges(); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -107,7 +109,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.Remove(entity); - return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); + DbContext dbContext = dbSet.Context(); + int count = await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -121,7 +126,10 @@ internal static class DbSetExtension where TEntity : class { dbSet.Update(entity); - return dbSet.Context().SaveChanges(); + DbContext dbContext = dbSet.Context(); + int count = dbContext.SaveChanges(); + dbContext.ChangeTracker.Clear(); + return count; } /// @@ -137,4 +145,11 @@ internal static class DbSetExtension dbSet.Update(entity); return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static DbContext Context(this DbSet dbSet) + where TEntity : class + { + return dbSet.GetService().Context; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs index f341f703..375f2d86 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Snap.Hutao.Core.Diagnostics; @@ -36,6 +37,18 @@ internal readonly struct ValueStopwatch return new(Stopwatch.GetTimestamp()); } + /// + /// 测量运行时间 + /// + /// 日志器 + /// 调用方法名称 + /// 结束测量 + public static IDisposable MeasureExecution(ILogger logger, [CallerMemberName] string callerName = default!) + { + ValueStopwatch stopwatch = StartNew(); + return new MeasureExecutionDisposable(stopwatch, logger, callerName); + } + /// /// 获取经过的时间 /// @@ -63,3 +76,24 @@ internal readonly struct ValueStopwatch return new TimeSpan(GetElapsedTimestamp()); } } + +[SuppressMessage("", "SA1400")] +[SuppressMessage("", "SA1600")] +file readonly struct MeasureExecutionDisposable : IDisposable +{ + private readonly ValueStopwatch stopwatch; + private readonly ILogger logger; + private readonly string callerName; + + public MeasureExecutionDisposable(ValueStopwatch stopwatch, ILogger logger, string callerName) + { + this.stopwatch = stopwatch; + this.logger = logger; + this.callerName = callerName; + } + + public void Dispose() + { + logger.LogInformation("{caller} toke {time} ms.", callerName, stopwatch.GetElapsedTime().TotalMilliseconds); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs index c9f63aa9..6e563b9c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Database/AppDbContext.cs @@ -23,6 +23,7 @@ internal sealed class AppDbContext : DbContext public AppDbContext(DbContextOptions options) : base(options) { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs index e3d39cbb..c5aa76b7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/GachaArchive.cs @@ -1,7 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -38,4 +43,92 @@ internal sealed class GachaArchive : ISelectable { return new() { Uid = uid }; } + + /// + /// 初始化或跳过 + /// + /// 存档 + /// uid + /// 数据库集 + /// 集合 + public static void SkipOrInit([NotNull] ref GachaArchive? archive, string uid, DbSet gachaArchives, ObservableCollection collection) + { + if (archive == null) + { + Init(out archive, uid, gachaArchives, collection); + } + } + + /// + /// 初始化 + /// + /// 存档 + /// uid + /// 数据库集 + /// 集合 + public static void Init([NotNull] out GachaArchive? archive, string uid, DbSet gachaArchives, ObservableCollection collection) + { + archive = collection.SingleOrDefault(a => a.Uid == uid); + + if (archive == null) + { + GachaArchive created = Create(uid); + gachaArchives.AddAndSave(created); + ThreadHelper.InvokeOnMainThread(() => collection!.Add(created)); + archive = created; + } + } + + /// + /// 保存祈愿物品 + /// + /// 待添加物品 + /// 是否懒惰 + /// 结尾Id + /// 数据集 + public void SaveItems(List itemsToAdd, bool isLazy, long endId, DbSet gachaItems) + { + if (itemsToAdd.Count > 0) + { + // 全量刷新 + if (!isLazy) + { + gachaItems + .Where(i => i.ArchiveId == InnerId) + .Where(i => i.Id >= endId) + .ExecuteDelete(); + } + + gachaItems.AddRangeAndSave(itemsToAdd); + } + } + + /// + /// 按卡池类型获取数据库中的最大 Id + /// + /// 卡池类型 + /// 数据集 + /// 最大 Id + public long GetEndId(GachaConfigType configType, DbSet gachaItems) + { + GachaItem? item = null; + + try + { + // TODO: replace with MaxBy + // https://github.com/dotnet/efcore/issues/25566 + // .MaxBy(i => i.Id); + item = gachaItems + .Where(i => i.ArchiveId == InnerId) + .Where(i => i.QueryType == configType) + .OrderByDescending(i => i.Id) + .FirstOrDefault(); + } + catch (SqliteException ex) + { + ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex); + } + + return item?.Id ?? 0L; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs new file mode 100644 index 00000000..d6d94be1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaArchives.cs @@ -0,0 +1,49 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.InterChange.GachaLog; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.Service.GachaLog.Factory; +using Snap.Hutao.Service.GachaLog.QueryProvider; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.GachaLog; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using Snap.Hutao.Web.Response; +using System.Collections.Immutable; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 存档操作 +/// +internal static class GachaArchives +{ + /// + /// 初始化存档集合 + /// + /// 数据库上下文 + /// 集合 + public static void Initialize(AppDbContext appDbContext, out ObservableCollection collection) + { + try + { + collection = appDbContext.GachaArchives.AsNoTracking().ToObservableCollection(); + } + catch (SqliteException ex) + { + string message = string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message); + throw ThrowHelper.UserdataCorrupted(message, ex); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs new file mode 100644 index 00000000..3070fc0d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogExportService.cs @@ -0,0 +1,46 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.InterChange.GachaLog; +using Snap.Hutao.Model.Metadata.Abstraction; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录导出服务 +/// +[Injection(InjectAs.Scoped, typeof(IGachaLogExportService))] +internal sealed class GachaLogExportService : IGachaLogExportService +{ + private readonly AppDbContext appDbContext; + + /// + /// 构造一个新的祈愿记录导出服务 + /// + /// 数据库上下文 + public GachaLogExportService(AppDbContext appDbContext) + { + this.appDbContext = appDbContext; + } + + /// + public async Task ExportToUIGFAsync(GachaLogServiceContext context, GachaArchive archive) + { + await ThreadHelper.SwitchToBackgroundAsync(); + List list = appDbContext.GachaItems + .Where(i => i.ArchiveId == archive.InnerId) + .AsEnumerable() + .Select(i => i.ToUIGFItem(context.GetNameQualityByItemId(i.ItemId))) + .ToList(); + + UIGF uigf = new() + { + Info = UIGFInfo.Create(archive.Uid), + List = list, + }; + + return uigf; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs rename to src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs index 6d49ac2e..911d176b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/FetchState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogFetchState.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.GachaLog; /// /// 获取状态 /// -internal sealed class FetchState +internal sealed class GachaLogFetchState { /// /// 验证密钥是否过期 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs new file mode 100644 index 00000000..4c0bb935 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogImportService.cs @@ -0,0 +1,79 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Model.InterChange.GachaLog; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.Service.GachaLog.Factory; +using Snap.Hutao.Service.GachaLog.QueryProvider; +using Snap.Hutao.Service.Metadata; +using Snap.Hutao.ViewModel.GachaLog; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using Snap.Hutao.Web.Response; +using System.Collections.Immutable; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录导入服务 +/// +[Injection(InjectAs.Scoped, typeof(IGachaLogImportService))] +internal sealed class GachaLogImportService : IGachaLogImportService +{ + private readonly AppDbContext appDbContext; + private readonly ILogger logger; + + /// + /// 构造一个新的祈愿记录导入服务 + /// + /// 数据库上下文 + /// 日志器 + public GachaLogImportService(AppDbContext appDbContext, ILogger logger) + { + this.appDbContext = appDbContext; + this.logger = logger; + } + + /// + public async Task ImportFromUIGFAsync(GachaLogServiceContext context, List list, string uid) + { + GachaArchive.Init(out GachaArchive? archive, uid, appDbContext.GachaArchives, context.ArchiveCollection); + await ThreadHelper.SwitchToBackgroundAsync(); + Guid archiveId = archive.InnerId; + + long trimId = appDbContext.GachaItems + .Where(i => i.ArchiveId == archiveId) + .OrderBy(i => i.Id) + .FirstOrDefault()?.Id ?? long.MaxValue; + + logger.LogInformation("Last Id to trim with [{id}]", trimId); + + IEnumerable toAdd = list + .OrderByDescending(i => i.Id) + .Where(i => i.Id < trimId) + .Select(i => GachaItem.Create(archiveId, i, GetItemId(context, i))); + + await appDbContext.GachaItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false); + return archive; + } + + private static int GetItemId(GachaLogServiceContext context, GachaLogItem item) + { + return item.ItemType switch + { + "角色" => context.NameAvatarMap.GetValueOrDefault(item.Name)?.Id ?? 0, + "武器" => context.NameWeaponMap.GetValueOrDefault(item.Name)?.Id ?? 0, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs index 9b237a93..54606666 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -32,6 +32,9 @@ namespace Snap.Hutao.Service.GachaLog; internal sealed class GachaLogService : IGachaLogService { private readonly AppDbContext appDbContext; + private readonly IGachaLogExportService gachaLogExportService; + private readonly IGachaLogImportService gachaLogImportService; + private readonly IEnumerable urlProviders; private readonly GachaInfoClient gachaInfoClient; private readonly IMetadataService metadataService; @@ -39,19 +42,12 @@ internal sealed class GachaLogService : IGachaLogService private readonly ILogger logger; private readonly DbCurrent dbCurrent; - private readonly Dictionary itemBaseCache = new(); - - private Dictionary? nameAvatarMap; - private Dictionary? nameWeaponMap; - - private Dictionary? idAvatarMap; - private Dictionary? idWeaponMap; - - private ObservableCollection? archiveCollection; + private GachaLogServiceContext context; /// /// 构造一个新的祈愿记录服务 /// + /// 服务提供器 /// 数据库上下文 /// Url提供器集合 /// 祈愿记录客户端 @@ -60,6 +56,7 @@ internal sealed class GachaLogService : IGachaLogService /// 日志器 /// 消息器 public GachaLogService( + IServiceProvider serviceProvider, AppDbContext appDbContext, IEnumerable urlProviders, GachaInfoClient gachaInfoClient, @@ -68,6 +65,9 @@ internal sealed class GachaLogService : IGachaLogService ILogger logger, IMessenger messenger) { + gachaLogExportService = serviceProvider.GetRequiredService(); + gachaLogImportService = serviceProvider.GetRequiredService(); + this.appDbContext = appDbContext; this.urlProviders = urlProviders; this.gachaInfoClient = gachaInfoClient; @@ -85,51 +85,24 @@ internal sealed class GachaLogService : IGachaLogService set => dbCurrent.Current = value; } - /// - public Task ExportToUIGFAsync(GachaArchive archive) - { - List list = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .AsEnumerable() - .Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId))) - .ToList(); - - UIGF uigf = new() - { - Info = UIGFInfo.Create(archive.Uid), - List = list, - }; - - return Task.FromResult(uigf); - } - - /// - public async Task> GetArchiveCollectionAsync() - { - await ThreadHelper.SwitchToMainThreadAsync(); - try - { - archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection(); - } - catch (SqliteException ex) - { - ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message), ex); - } - - return archiveCollection; - } - /// public async ValueTask InitializeAsync(CancellationToken token) { + if (context.IsInitialized) + { + return true; + } + if (await metadataService.InitializeAsync().ConfigureAwait(false)) { - nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false); - nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false); + Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); + Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false); - idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false); - idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false); + Dictionary nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false); + Dictionary nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false); + GachaArchives.Initialize(appDbContext, out ObservableCollection collection); + context = new(idAvatarMap, idWeaponMap, nameAvatarMap, nameWeaponMap, collection); return true; } else @@ -139,21 +112,24 @@ internal sealed class GachaLogService : IGachaLogService } /// - public async Task GetStatisticsAsync(GachaArchive? archive = null) + public ObservableCollection GetArchiveCollection() + { + return context.ArchiveCollection; + } + + /// + public async Task GetStatisticsAsync(GachaArchive? archive) { archive ??= CurrentArchive; // Return statistics if (archive != null) { - ValueStopwatch stopwatch = ValueStopwatch.StartNew(); - IQueryable items = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId); - - GachaStatistics statistics = await gachaStatisticsFactory.CreateAsync(items).ConfigureAwait(false); - - logger.LogInformation("GachaStatistic Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds); - return statistics; + using (ValueStopwatch.MeasureExecution(logger)) + { + IQueryable items = appDbContext.GachaItems.Where(i => i.ArchiveId == archive.InnerId); + return await gachaStatisticsFactory.CreateAsync(items).ConfigureAwait(false); + } } else { @@ -162,44 +138,19 @@ internal sealed class GachaLogService : IGachaLogService } /// - public IGachaLogQueryProvider? GetGachaLogQueryProvider(RefreshOption option) + public Task ExportToUIGFAsync(GachaArchive archive) { - return option switch - { - RefreshOption.WebCache => urlProviders.Single(p => p.Name == nameof(GachaLogQueryWebCacheProvider)), - RefreshOption.SToken => urlProviders.Single(p => p.Name == nameof(GachaLogQuerySTokenProvider)), - RefreshOption.ManualInput => urlProviders.Single(p => p.Name == nameof(GachaLogQueryManualInputProvider)), - _ => null, - }; + return gachaLogExportService.ExportToUIGFAsync(context, archive); } /// public async Task ImportFromUIGFAsync(List list, string uid) { - await ThreadHelper.SwitchToBackgroundAsync(); - - GachaArchive? archive = null; - SkipOrInitArchive(ref archive, uid); - Guid archiveId = archive.InnerId; - - long trimId = appDbContext.GachaItems - .Where(i => i.ArchiveId == archiveId) - .OrderBy(i => i.Id) - .FirstOrDefault()?.Id ?? long.MaxValue; - - logger.LogInformation("Last Id to trim with [{id}]", trimId); - - IEnumerable toAdd = list - .OrderByDescending(i => i.Id) - .Where(i => i.Id < trimId) - .Select(i => GachaItem.Create(archiveId, i, GetItemId(i))); - - await appDbContext.GachaItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false); - CurrentArchive = archive; + CurrentArchive = await gachaLogImportService.ImportFromUIGFAsync(context, list, uid).ConfigureAwait(false); } /// - public async Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token) + public async Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token) { bool isLazy = strategy switch { @@ -209,7 +160,12 @@ internal sealed class GachaLogService : IGachaLogService }; (bool authkeyValid, GachaArchive? result) = await FetchGachaLogsAsync(query, isLazy, progress, token).ConfigureAwait(false); - CurrentArchive = result ?? CurrentArchive; + + if (result != null) + { + CurrentArchive = result; + } + return authkeyValid; } @@ -218,7 +174,7 @@ internal sealed class GachaLogService : IGachaLogService { // Sync cache await ThreadHelper.SwitchToMainThreadAsync(); - archiveCollection!.Remove(archive); + context.ArchiveCollection.Remove(archive); // Sync database await ThreadHelper.SwitchToBackgroundAsync(); @@ -233,15 +189,16 @@ internal sealed class GachaLogService : IGachaLogService return Task.Delay(TimeSpan.FromSeconds(Random.Shared.NextDouble() + 1), token); } - private async Task> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress progress, CancellationToken token) + private async Task> FetchGachaLogsAsync(GachaLogQuery query, bool isLazy, IProgress progress, CancellationToken token) { GachaArchive? archive = null; - FetchState state = new(); + GachaLogFetchState state = new(); foreach (GachaConfigType configType in GachaLog.QueryTypes) { - state.ConfigType = configType; + // 每个卡池类型重置 long? dbEndId = null; + state.ConfigType = configType; GachaLogQueryOptions options = new(query, configType); List itemsToAdd = new(); @@ -259,13 +216,13 @@ internal sealed class GachaLogService : IGachaLogService foreach (GachaLogItem item in items) { - SkipOrInitArchive(ref archive, item.Uid); - dbEndId ??= GetEndId(archive, configType); + GachaArchive.SkipOrInit(ref archive, item.Uid, appDbContext.GachaArchives, context.ArchiveCollection); + dbEndId ??= archive.GetEndId(configType, appDbContext.GachaItems); if ((!isLazy) || item.Id > dbEndId) { - itemsToAdd.Add(GachaItem.Create(archive.InnerId, item, GetItemId(item))); - state.Items.Add(GetItemBaseByName(item.Name, item.ItemType)); + itemsToAdd.Add(GachaItem.Create(archive.InnerId, item, context.GetItemId(item))); + state.Items.Add(context.GetItemByNameAndType(item.Name, item.ItemType)); options.EndId = item.Id; } else @@ -300,148 +257,10 @@ internal sealed class GachaLogService : IGachaLogService } token.ThrowIfCancellationRequested(); - SaveGachaItems(itemsToAdd, isLazy, archive, options.EndId); + archive?.SaveItems(itemsToAdd, isLazy, options.EndId, appDbContext.GachaItems); await RandomDelayAsync(token).ConfigureAwait(false); } return new(!state.AuthKeyTimeout, archive); } - - private void SkipOrInitArchive([NotNull] ref GachaArchive? archive, string uid) - { - if (archive == null) - { - archive = appDbContext.GachaArchives.AsNoTracking().SingleOrDefault(a => a.Uid == uid); - - if (archive == null) - { - GachaArchive created = GachaArchive.Create(uid); - appDbContext.GachaArchives.AddAndSave(created); - - // System.InvalidOperationException: Sequence contains no elements - // ? how this happen here? - archive = appDbContext.GachaArchives.AsNoTracking().Single(a => a.Uid == uid); - GachaArchive temp = archive; - ThreadHelper.InvokeOnMainThread(() => archiveCollection!.Add(temp)); - } - } - } - - private long GetEndId(GachaArchive? archive, GachaConfigType configType) - { - GachaItem? item = null; - - if (archive != null) - { - try - { - // TODO: replace with MaxBy - // https://github.com/dotnet/efcore/issues/25566 - // .MaxBy(i => i.Id); - item = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .Where(i => i.QueryType == configType) - .OrderByDescending(i => i.Id) - .FirstOrDefault(); - } - catch (SqliteException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex); - } - } - - return item?.Id ?? 0L; - } - - private int GetItemId(GachaLogItem item) - { - return item.ItemType switch - { - "角色" => nameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0, - "武器" => nameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0, - _ => 0, - }; - } - - private Item GetItemBaseByName(string name, string type) - { - if (!itemBaseCache.TryGetValue(name, out Item? result)) - { - result = type switch - { - "角色" => nameAvatarMap![name].ToItemBase(), - "武器" => nameWeaponMap![name].ToItemBase(), - _ => throw Must.NeverHappen(), - }; - - itemBaseCache[name] = result; - } - - return result; - } - - private INameQuality GetNameQualityByItemId(int id) - { - int place = id.Place(); - return place switch - { - 8 => idAvatarMap![id], - 5 => idWeaponMap![id], - _ => throw Must.NeverHappen($"Id places: {place}"), - }; - } - - private void SaveGachaItems(List itemsToAdd, bool isLazy, GachaArchive? archive, long endId) - { - if (itemsToAdd.Count > 0) - { - // 全量刷新 - if ((!isLazy) && archive != null) - { - appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .Where(i => i.Id >= endId) - .ExecuteDelete(); - } - - appDbContext.GachaItems.AddRangeAndSave(itemsToAdd); - } - } -} - -/// -/// 祈愿记录导出服务 -/// -internal sealed class GachaLogExportService -{ - AppDbContext appDbContext; - - /// - public Task ExportToUIGFAsync(GachaArchive archive) - { - List list = appDbContext.GachaItems - .Where(i => i.ArchiveId == archive.InnerId) - .AsEnumerable() - .Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId))) - .ToList(); - - UIGF uigf = new() - { - Info = UIGFInfo.Create(archive.Uid), - List = list, - }; - - return Task.FromResult(uigf); - } - - private INameQuality GetNameQualityByItemId(int id) - { - int place = id.Place(); - return place switch - { - 8 => idAvatarMap![id], - 5 => idWeaponMap![id], - _ => throw Must.NeverHappen($"Id places: {place}"), - }; - } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs new file mode 100644 index 00000000..eaec0c4b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogServiceContext.cs @@ -0,0 +1,133 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Metadata.Abstraction; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Weapon; +using Snap.Hutao.Model.Primitive; +using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; +using System.Collections.ObjectModel; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录服务上下文 +/// +internal readonly struct GachaLogServiceContext +{ + /// + /// 物品缓存 + /// + public readonly Dictionary ItemCache = new(); + + /// + /// Id 角色 映射 + /// + public readonly Dictionary IdAvatarMap; + + /// + /// Id 武器 映射 + /// + public readonly Dictionary IdWeaponMap; + + /// + /// 名称 角色 映射 + /// + public readonly Dictionary NameAvatarMap; + + /// + /// 名称 武器 映射 + /// + public readonly Dictionary NameWeaponMap; + + /// + /// 存档集合 + /// + public readonly ObservableCollection ArchiveCollection; + + /// + /// 是否初始化完成 + /// + public readonly bool IsInitialized; + + /// + /// 构造一个新的祈愿记录服务上下文 + /// + /// Id 角色 映射 + /// Id 武器 映射 + /// 名称 角色 映射 + /// 名称 武器 映射 + /// 存档集合 + public GachaLogServiceContext( + Dictionary idAvatarMap, + Dictionary idWeaponMap, + Dictionary nameAvatarMap, + Dictionary nameWeaponMap, + ObservableCollection archiveCollection) + { + IdAvatarMap = idAvatarMap; + IdWeaponMap = idWeaponMap; + NameAvatarMap = nameAvatarMap; + NameWeaponMap = nameWeaponMap; + ArchiveCollection = archiveCollection; + + IsInitialized = true; + } + + /// + /// 按名称获取物品 + /// + /// 名称 + /// 类型 + /// 物品 + public Item GetItemByNameAndType(string name, string type) + { + if (!ItemCache.TryGetValue(name, out Item? result)) + { + result = type switch + { + "角色" => NameAvatarMap[name].ToItemBase(), + "武器" => NameWeaponMap[name].ToItemBase(), + _ => throw Must.NeverHappen(), + }; + + ItemCache[name] = result; + } + + return result; + } + + /// + /// 按物品 Id 获取名称星级 + /// + /// Id + /// 名称星级 + public INameQuality GetNameQualityByItemId(int id) + { + int place = id.Place(); + return place switch + { + 8 => IdAvatarMap![id], + 5 => IdWeaponMap![id], + _ => throw Must.NeverHappen($"Id places: {place}"), + }; + } + + /// + /// 获取物品 Id + /// O(1) + /// + /// 祈愿物品 + /// 物品 Id + public int GetItemId(GachaLogItem item) + { + return item.ItemType switch + { + "角色" => NameAvatarMap!.GetValueOrDefault(item.Name)?.Id ?? 0, + "武器" => NameWeaponMap!.GetValueOrDefault(item.Name)?.Id ?? 0, + _ => 0, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogExportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogExportService.cs new file mode 100644 index 00000000..6d22f7f0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogExportService.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.InterChange.GachaLog; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录导出服务 +/// +internal interface IGachaLogExportService +{ + /// + /// 异步导出存档到 UIGF + /// + /// 元数据上下文 + /// 存档 + /// UIGF + Task ExportToUIGFAsync(GachaLogServiceContext context, GachaArchive archive); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogImportService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogImportService.cs new file mode 100644 index 00000000..a97f65da --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogImportService.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.InterChange.GachaLog; + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录导入服务 +/// +internal interface IGachaLogImportService +{ + /// + /// 异步从 UIGF 导入 + /// + /// 祈愿记录服务上下文 + /// 列表 + /// uid + /// 存档 + Task ImportFromUIGFAsync(GachaLogServiceContext context, List list, string uid); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs index c276a1e6..f9b8421a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/IGachaLogService.cs @@ -28,24 +28,17 @@ internal interface IGachaLogService Task ExportToUIGFAsync(GachaArchive archive); /// - /// 异步获取可用于绑定的存档集合 + /// 获取可用于绑定的存档集合 /// /// 存档集合 - Task> GetArchiveCollectionAsync(); - - /// - /// 获取祈愿日志Url提供器 - /// - /// 刷新模式 - /// 祈愿日志Url提供器 - IGachaLogQueryProvider? GetGachaLogQueryProvider(RefreshOption option); + ObservableCollection GetArchiveCollection(); /// /// 获得对应的祈愿统计 /// /// 存档 /// 祈愿统计 - Task GetStatisticsAsync(GachaArchive? archive = null); + Task GetStatisticsAsync(GachaArchive? archive); /// /// 异步从UIGF导入数据 @@ -71,7 +64,7 @@ internal interface IGachaLogService /// 进度 /// 取消令牌 /// 验证密钥是否可用 - Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token); + Task RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategy strategy, IProgress progress, CancellationToken token); /// /// 删除存档 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderExtension.cs new file mode 100644 index 00000000..57642db0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryProviderExtension.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.GachaLog.QueryProvider; + +/// +/// 祈愿记录Url提供器拓展 +/// +internal static class GachaLogQueryProviderExtension +{ + /// + /// 选出对应的祈愿 Url 提供器 + /// + /// 服务提供器 + /// 刷新选项 + /// 对应的祈愿 Url 提供器 + public static IGachaLogQueryProvider? PickProvider(this IServiceProvider serviceProvider, RefreshOption option) + { + IEnumerable providers = serviceProvider.GetServices(); + + string? name = option switch + { + RefreshOption.WebCache => nameof(GachaLogQueryWebCacheProvider), + RefreshOption.SToken => nameof(GachaLogQuerySTokenProvider), + RefreshOption.ManualInput => nameof(GachaLogQueryManualInputProvider), + _ => null, + }; + + return providers.SingleOrDefault(p => p.Name == name); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs index 313f9856..a706a905 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogRefreshProgressDialog.xaml.cs @@ -16,7 +16,7 @@ namespace Snap.Hutao.View.Dialog; [HighQuality] internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog { - private static readonly DependencyProperty StateProperty = Property.Depend(nameof(State)); + private static readonly DependencyProperty StateProperty = Property.Depend(nameof(State)); /// /// 构造一个新的对话框 @@ -31,9 +31,9 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog /// /// 刷新状态 /// - public FetchState State + public GachaLogFetchState State { - get => (FetchState)GetValue(StateProperty); + get => (GachaLogFetchState)GetValue(StateProperty); set => SetValue(StateProperty, value); } @@ -41,7 +41,7 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog /// 接收进度更新 /// /// 状态 - public void OnReport(FetchState state) + public void OnReport(GachaLogFetchState state) { State = state; GachaItemsPresenter.Header = state.AuthKeyTimeout diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 7b82d72d..ae8b303f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -31,6 +31,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel private readonly IPickerFactory pickerFactory; private readonly IContentDialogFactory contentDialogFactory; private readonly JsonSerializerOptions options; + private readonly IServiceProvider serviceProvider; private ObservableCollection? archives; private GachaArchive? selectedArchive; @@ -49,6 +50,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel pickerFactory = serviceProvider.GetRequiredService(); contentDialogFactory = serviceProvider.GetRequiredService(); options = serviceProvider.GetRequiredService(); + this.serviceProvider = serviceProvider; HutaoCloudViewModel = serviceProvider.GetRequiredService(); @@ -146,16 +148,10 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel { try { - if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(true)) + if (await gachaLogService.InitializeAsync(CancellationToken).ConfigureAwait(false)) { - ObservableCollection archives; - using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) - { - archives = await gachaLogService.GetArchiveCollectionAsync().ConfigureAwait(false); - } - await ThreadHelper.SwitchToMainThreadAsync(); - Archives = archives; + Archives = gachaLogService.GetArchiveCollection(); SetSelectedArchiveAndUpdateStatistics(Archives.SelectedOrDefault(), true); } } @@ -181,7 +177,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel private async Task RefreshInternalAsync(RefreshOption option) { - IGachaLogQueryProvider? provider = gachaLogService.GetGachaLogQueryProvider(option); + IGachaLogQueryProvider? provider = serviceProvider.PickProvider(option); if (provider != null) { @@ -195,7 +191,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel await ThreadHelper.SwitchToMainThreadAsync(); GachaLogRefreshProgressDialog dialog = new(); IDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false); - Progress progress = new(dialog.OnReport); + Progress progress = new(dialog.OnReport); bool authkeyValid; try diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs index c7d9e057..d9ed5dd5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogQueryOptions.cs @@ -37,6 +37,14 @@ internal struct GachaLogQueryOptions /// private readonly QueryString innerQuery; + /// + /// 结束Id + /// 控制API返回的分页 + /// 米哈游使用了 keyset pagination 来实现这一目标 + /// https://learn.microsoft.com/en-us/ef/core/querying/pagination#keyset-pagination + /// + public long EndId; + /// /// 构造一个新的祈愿记录请求配置 /// @@ -55,18 +63,6 @@ internal struct GachaLogQueryOptions EndId = endId; } - /// - /// 结束Id - /// 控制API返回的分页 - /// 米哈游使用了 keyset pagination 来实现这一目标 - /// https://learn.microsoft.com/en-us/ef/core/querying/pagination#keyset-pagination - /// - public long EndId - { - get => long.Parse(innerQuery["end_id"]); - set => innerQuery.Set("end_id", value); - } - /// /// 转换到查询字符串 /// @@ -90,6 +86,8 @@ internal struct GachaLogQueryOptions /// 匹配的查询字符串 public string AsQuery() { + // Make the cached end id into query. + innerQuery.Set("end_id", EndId); return innerQuery.ToString(); } } \ No newline at end of file