refactor gachalog service

This commit is contained in:
Lightczx
2023-04-12 22:34:49 +08:00
parent 3cf505d9b2
commit 27c7875c26
18 changed files with 631 additions and 293 deletions

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -23,6 +23,7 @@ internal sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
/// <summary>

View File

@@ -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;
}
}

View 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 获取状态
/// </summary>
internal sealed class FetchState
internal sealed class GachaLogFetchState
{
/// <summary>
/// 验证密钥是否过期

View File

@@ -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,
};
}
}

View File

@@ -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}"),
};
}
}
}

View File

@@ -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,
};
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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>
/// 删除存档

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}
}