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