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