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 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);
|
||||
|
||||
@@ -13,19 +13,6 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
return dbSet.GetService<ICurrentDbContext>().Context;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ internal sealed class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal sealed class FetchState
|
||||
internal sealed class GachaLogFetchState
|
||||
{
|
||||
/// <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
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly IGachaLogExportService gachaLogExportService;
|
||||
private readonly IGachaLogImportService gachaLogImportService;
|
||||
|
||||
private readonly IEnumerable<IGachaLogQueryProvider> urlProviders;
|
||||
private readonly GachaInfoClient gachaInfoClient;
|
||||
private readonly IMetadataService metadataService;
|
||||
@@ -39,19 +42,12 @@ internal sealed class GachaLogService : IGachaLogService
|
||||
private readonly ILogger<GachaLogService> logger;
|
||||
private readonly DbCurrent<GachaArchive, Message.GachaArchiveChangedMessage> dbCurrent;
|
||||
|
||||
private readonly Dictionary<string, Item> itemBaseCache = new();
|
||||
|
||||
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;
|
||||
private GachaLogServiceContext context;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿记录服务
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="urlProviders">Url提供器集合</param>
|
||||
/// <param name="gachaInfoClient">祈愿记录客户端</param>
|
||||
@@ -60,6 +56,7 @@ internal sealed class GachaLogService : IGachaLogService
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public GachaLogService(
|
||||
IServiceProvider serviceProvider,
|
||||
AppDbContext appDbContext,
|
||||
IEnumerable<IGachaLogQueryProvider> urlProviders,
|
||||
GachaInfoClient gachaInfoClient,
|
||||
@@ -68,6 +65,9 @@ internal sealed class GachaLogService : IGachaLogService
|
||||
ILogger<GachaLogService> logger,
|
||||
IMessenger messenger)
|
||||
{
|
||||
gachaLogExportService = serviceProvider.GetRequiredService<IGachaLogExportService>();
|
||||
gachaLogImportService = serviceProvider.GetRequiredService<IGachaLogImportService>();
|
||||
|
||||
this.appDbContext = appDbContext;
|
||||
this.urlProviders = urlProviders;
|
||||
this.gachaInfoClient = gachaInfoClient;
|
||||
@@ -85,51 +85,24 @@ internal sealed class GachaLogService : IGachaLogService
|
||||
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/>
|
||||
public async ValueTask<bool> 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<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<string, Model.Metadata.Avatar.Avatar> nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(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;
|
||||
}
|
||||
else
|
||||
@@ -139,21 +112,24 @@ internal sealed class GachaLogService : IGachaLogService
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
// Return statistics
|
||||
if (archive != null)
|
||||
{
|
||||
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
|
||||
IQueryable<GachaItem> 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<GachaItem> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGachaLogQueryProvider? GetGachaLogQueryProvider(RefreshOption option)
|
||||
public Task<UIGF> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ImportFromUIGFAsync(List<UIGFItem> 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<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;
|
||||
CurrentArchive = await gachaLogImportService.ImportFromUIGFAsync(context, list, uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
@@ -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<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;
|
||||
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<GachaItem> 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<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);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取可用于绑定的存档集合
|
||||
/// 获取可用于绑定的存档集合
|
||||
/// </summary>
|
||||
/// <returns>存档集合</returns>
|
||||
Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取祈愿日志Url提供器
|
||||
/// </summary>
|
||||
/// <param name="option">刷新模式</param>
|
||||
/// <returns>祈愿日志Url提供器</returns>
|
||||
IGachaLogQueryProvider? GetGachaLogQueryProvider(RefreshOption option);
|
||||
ObservableCollection<GachaArchive> GetArchiveCollection();
|
||||
|
||||
/// <summary>
|
||||
/// 获得对应的祈愿统计
|
||||
/// </summary>
|
||||
/// <param name="archive">存档</param>
|
||||
/// <returns>祈愿统计</returns>
|
||||
Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive = null);
|
||||
Task<GachaStatistics> GetStatisticsAsync(GachaArchive? archive);
|
||||
|
||||
/// <summary>
|
||||
/// 异步从UIGF导入数据
|
||||
@@ -71,7 +64,7 @@ internal interface IGachaLogService
|
||||
/// <param name="progress">进度</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <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>
|
||||
/// 删除存档
|
||||
|
||||
@@ -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]
|
||||
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>
|
||||
/// 构造一个新的对话框
|
||||
@@ -31,9 +31,9 @@ internal sealed partial class GachaLogRefreshProgressDialog : ContentDialog
|
||||
/// <summary>
|
||||
/// 刷新状态
|
||||
/// </summary>
|
||||
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
|
||||
/// 接收进度更新
|
||||
/// </summary>
|
||||
/// <param name="state">状态</param>
|
||||
public void OnReport(FetchState state)
|
||||
public void OnReport(GachaLogFetchState state)
|
||||
{
|
||||
State = state;
|
||||
GachaItemsPresenter.Header = state.AuthKeyTimeout
|
||||
|
||||
@@ -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<GachaArchive>? archives;
|
||||
private GachaArchive? selectedArchive;
|
||||
@@ -49,6 +50,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel
|
||||
pickerFactory = serviceProvider.GetRequiredService<IPickerFactory>();
|
||||
contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
|
||||
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
HutaoCloudViewModel = serviceProvider.GetRequiredService<HutaoCloudViewModel>();
|
||||
|
||||
@@ -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<GachaArchive> 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<FetchState> progress = new(dialog.OnReport);
|
||||
Progress<GachaLogFetchState> progress = new(dialog.OnReport);
|
||||
bool authkeyValid;
|
||||
|
||||
try
|
||||
|
||||
@@ -37,6 +37,14 @@ internal struct GachaLogQueryOptions
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -55,18 +63,6 @@ internal struct GachaLogQueryOptions
|
||||
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>
|
||||
@@ -90,6 +86,8 @@ internal struct GachaLogQueryOptions
|
||||
/// <returns>匹配的查询字符串</returns>
|
||||
public string AsQuery()
|
||||
{
|
||||
// Make the cached end id into query.
|
||||
innerQuery.Set("end_id", EndId);
|
||||
return innerQuery.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user