From 332722c860ca8ffba5e698b87cebd3ab070ca6d4 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 5 Sep 2022 13:01:36 +0800 Subject: [PATCH] async queued database logger --- .../Snap.Hutao.Win32/Snap.Hutao.Win32.csproj | 4 + .../UI/WindowsAndMessaging/MINMAXINFO.cs | 8 + src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs | 22 -- .../Snap.Hutao/Core/CoreEnvironment.cs | 15 +- .../Snap.Hutao/Core/Logging/DatebaseLogger.cs | 3 +- .../Snap.Hutao/Core/Logging/LogEntry.cs | 5 + .../Snap.Hutao/Core/Logging/LogEntryQueue.cs | 85 +++--- .../DispatherQueueSwitchOperation.cs | 13 +- .../Snap.Hutao/Core/Threading/Watcher.cs | 80 ----- .../Core/Windowing/WindowSubclassManager.cs | 7 +- .../Extension/EnumerableExtensions.cs | 21 ++ src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs | 1 + .../LogDb/20220903071033_LogTime.Designer.cs | 55 ++++ .../LogDb/20220903071033_LogTime.cs | 27 ++ .../LogDb/LogDbContextModelSnapshot.cs | 7 +- src/Snap.Hutao/Snap.Hutao/Program.cs | 4 +- .../Service/Achievement/AchievementService.cs | 4 +- .../Service/GachaLog/GachaLogService.cs | 12 + .../Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs | 67 ++-- .../DynamicSecret/DynamicSecretProvider.cs | 2 +- .../Hk4e/Event/GachaInfo/GachaConfigType.cs | 35 +++ .../Hk4e/Event/GachaInfo/GachaInfoClient.cs | 41 +++ .../Event/GachaInfo/GachaLogConfigration.cs | 63 ++++ .../Hk4e/Event/GachaInfo/GachaLogItem.cs | 77 +++++ .../Hk4e/Event/GachaInfo/GachaLogPage.cs | 42 +++ .../Web/Request/QueryString/QueryString.cs | 288 ++++++++++++++++++ .../QueryString/QueryStringParameter.cs | 37 +++ 27 files changed, 798 insertions(+), 227 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/UI/WindowsAndMessaging/MINMAXINFO.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogPage.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj index 10c2cd7a..465625c5 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/UI/WindowsAndMessaging/MINMAXINFO.cs b/src/Snap.Hutao/Snap.Hutao.Win32/UI/WindowsAndMessaging/MINMAXINFO.cs new file mode 100644 index 00000000..40ff153e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Win32/UI/WindowsAndMessaging/MINMAXINFO.cs @@ -0,0 +1,8 @@ +namespace Windows.Win32.UI.WindowsAndMessaging; +public partial struct MINMAXINFO +{ + public static unsafe ref MINMAXINFO FromPointer(nint value) + { + return ref *(MINMAXINFO*)value; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs deleted file mode 100644 index 1c57ab21..00000000 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Unsafe.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Windows.Win32.UI.WindowsAndMessaging; - -namespace Snap.Hutao.Win32; - -/// -/// 包装不安全的代码 -/// -public class Unsafe -{ - /// - /// 使用指针操作简化封送 - /// - /// lParam - /// 最小宽度 - /// 最小高度 - public static unsafe void SetMinTrackSize(nint lParam, double minWidth, double minHeight) - { - MINMAXINFO* info = (MINMAXINFO*)lParam; - info->ptMinTrackSize.x = (int)Math.Max(minWidth, info->ptMinTrackSize.x); - info->ptMinTrackSize.y = (int)Math.Max(minHeight, info->ptMinTrackSize.y); - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index af60d300..9863f0fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -11,20 +11,11 @@ namespace Snap.Hutao.Core; /// internal static class CoreEnvironment { - // Used DS1 History - // 2.34.1 9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7 - // 2.35.2 N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep - /// - /// 动态密钥1的盐 - /// - public const string DynamicSecret1Salt = "N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep"; - - /// - /// 动态密钥2的盐 + /// 动态密钥的盐 /// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd /// - public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; + public const string DynamicSecretSalt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"; /// /// 米游社请求UA @@ -34,7 +25,7 @@ internal static class CoreEnvironment /// /// 米游社 Rpc 版本 /// - public const string HoyolabXrpcVersion = "2.35.2"; + public const string HoyolabXrpcVersion = "2.36.1"; /// /// 标准UA diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs index 26988044..c961e1ac 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/DatebaseLogger.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Context.Database; - namespace Snap.Hutao.Core.Logging; /// @@ -54,6 +52,7 @@ internal sealed partial class DatebaseLogger : ILogger LogEntry entry = new() { + Time = DateTimeOffset.Now, Category = name, LogLevel = logLevel, EventId = eventId.Id, diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs index cbc4b4d6..6c43298f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs @@ -19,6 +19,11 @@ public class LogEntry [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid InnerId { get; set; } + /// + /// 日志时间 + /// + public DateTimeOffset Time { get; set; } + /// /// 类别 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntryQueue.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntryQueue.cs index bacdacbd..9733ab00 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntryQueue.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntryQueue.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Snap.Hutao.Context.Database; using Snap.Hutao.Context.FileSystem; +using Snap.Hutao.Extension; using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; @@ -15,50 +16,19 @@ namespace Snap.Hutao.Core.Logging; /// public sealed class LogEntryQueue : IDisposable { - private static readonly object LogDbContextCreationLock = new(); - private readonly ConcurrentQueue entryQueue = new(); - private readonly CancellationTokenSource disposeCancellationTokenSource = new(); - private readonly TaskCompletionSource writeDbTaskCompletionSource = new(); - - // the provider is created per logger, we don't want to create too much - private volatile LogDbContext? logDbContext; + private readonly CancellationTokenSource disposeTokenSource = new(); + private readonly TaskCompletionSource writeDbCompletionSource = new(); + private readonly LogDbContext logDbContext; /// /// 构造一个新的日志队列 /// public LogEntryQueue() { - Execute(); - } + logDbContext = InitializeDbContext(); - private LogDbContext LogDbContext - { - get - { - if (logDbContext == null) - { - lock (LogDbContextCreationLock) - { - // prevent re-entry call - if (logDbContext == null) - { - HutaoContext myDocument = new(new()); - logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}"); - if (logDbContext.Database.GetPendingMigrations().Any()) - { - Debug.WriteLine("[Debug] Performing LogDbContext Migrations"); - logDbContext.Database.Migrate(); - } - - logDbContext.Logs.RemoveRange(logDbContext.Logs); - logDbContext.SaveChanges(); - } - } - } - - return logDbContext; - } + Task.Run(async () => await WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget(); } /// @@ -74,45 +44,62 @@ public sealed class LogEntryQueue : IDisposable [SuppressMessage("", "VSTHRD002")] public void Dispose() { - disposeCancellationTokenSource.Cancel(); - writeDbTaskCompletionSource.Task.GetAwaiter().GetResult(); + // notify the write task to complete. + disposeTokenSource.Cancel(); - LogDbContext.Dispose(); + // Wait the db operation complete. + writeDbCompletionSource.Task.GetAwaiter().GetResult(); + + logDbContext.Dispose(); } - [SuppressMessage("", "VSTHRD100")] - private async void Execute() + private static LogDbContext InitializeDbContext() { - await Task.Run(async () => await ExecuteCoreAsync(disposeCancellationTokenSource.Token)); + HutaoContext myDocument = new(new()); + LogDbContext logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}"); + if (logDbContext.Database.GetPendingMigrations().Any()) + { + Debug.WriteLine("[Debug] Performing LogDbContext Migrations"); + logDbContext.Database.Migrate(); + } + + logDbContext.Logs.RemoveRange(logDbContext.Logs); + logDbContext.SaveChanges(); + + return logDbContext; } - private async Task ExecuteCoreAsync(CancellationToken token) + private async Task WritePendingLogsAsync(CancellationToken token) { bool hasAdded = false; while (true) { if (entryQueue.TryDequeue(out LogEntry? logEntry)) { - LogDbContext.Logs.Add(logEntry); + logDbContext.Logs.Add(logEntry); hasAdded = true; } else { if (hasAdded) { - LogDbContext.SaveChanges(); + logDbContext.SaveChanges(); hasAdded = false; } if (token.IsCancellationRequested) { - writeDbTaskCompletionSource.TrySetResult(); + writeDbCompletionSource.TrySetResult(); break; } - await Task - .Delay(1000, CancellationToken.None) - .ConfigureAwait(false); + try + { + await Task.Delay(5000, token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs index 2a86b9da..e1ea3071 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/DispatherQueueSwitchOperation.cs @@ -14,17 +14,15 @@ public struct DispatherQueueSwitchOperation : IAwaitable - /// 构造一个新的同步上下文等待器 + /// 构造一个新的调度器队列等待器 /// - /// 同步上下文 + /// 调度器队列 public DispatherQueueSwitchOperation(DispatcherQueue dispatherQueue) { this.dispatherQueue = dispatherQueue; } - /// - /// 是否完成 - /// + /// public bool IsCompleted { get => dispatherQueue.HasThreadAccess; @@ -41,10 +39,7 @@ public struct DispatherQueueSwitchOperation : IAwaitable - /// 获取等待器 - /// - /// 等待器 + /// public DispatherQueueSwitchOperation GetAwaiter() { return this; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs deleted file mode 100644 index 70822137..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/Watcher.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Model; - -namespace Snap.Hutao.Core.Threading; - -/// -/// 提供简单易用的状态提示信息 -/// 用于任务的状态跟踪 -/// 同时继承了 -/// -public class Watcher : Observable -{ - private readonly bool isReusable; - private bool hasUsed; - private bool isWorking; - private bool isCompleted; - - /// - /// 构造一个新的工作监视器 - /// - /// 是否可以重用 - public Watcher(bool isReusable = true) - { - this.isReusable = isReusable; - } - - /// - /// 是否正在工作 - /// - public bool IsWorking - { - get => isWorking; - - private set => Set(ref isWorking, value); - } - - /// - /// 工作是否完成 - /// - public bool IsCompleted - { - get => isCompleted; - - private set => Set(ref isCompleted, value); - } - - /// - /// 对某个操作进行监视, - /// - /// 一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成 - /// 重用了一个不可重用的监视器 - public IDisposable Watch() - { - Verify.Operation(!IsWorking, $"此 {nameof(Watcher)} 已经处于检查状态"); - Verify.Operation(isReusable || !hasUsed, $"此 {nameof(Watcher)} 不允许多次使用"); - - hasUsed = true; - IsWorking = true; - - return new WatchDisposable(this); - } - - private struct WatchDisposable : IDisposable - { - private readonly Watcher watcher; - - public WatchDisposable(Watcher watcher) - { - this.watcher = watcher; - } - - public void Dispose() - { - watcher.IsWorking = false; - watcher.IsCompleted = true; - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs index 4874d8d8..9b30785d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowSubclassManager.cs @@ -1,9 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System; using System.Diagnostics; +using System.Runtime.CompilerServices; using Windows.Win32.Foundation; using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; using static Windows.Win32.PInvoke; namespace Snap.Hutao.Core.Windowing; @@ -85,7 +88,9 @@ internal class WindowSubclassManager : IDisposable case WM_GETMINMAXINFO: { double scalingFactor = Persistence.GetScaleForWindow(hwnd); - Win32.Unsafe.SetMinTrackSize(lParam, MinWidth * scalingFactor, MinHeight * scalingFactor); + ref MINMAXINFO info = ref MINMAXINFO.FromPointer(lParam); + info.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.x); + info.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.y); break; } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs index 4f6722bd..0b3a1793 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs @@ -78,6 +78,27 @@ public static partial class EnumerableExtensions return source.FirstOrDefault(predicate) ?? source.FirstOrDefault(); } + /// + /// 移除表中首个满足条件的项 + /// + /// 项的类型 + /// 表 + /// 是否应当移除 + /// 是否移除了元素 + public static bool RemoveFirstWhere(this IList list, Func shouldRemovePredicate) + { + for (int i = 0; i < list.Count; i++) + { + if (shouldRemovePredicate.Invoke(list[i])) + { + list.RemoveAt(i); + return true; + } + } + + return false; + } + /// /// 表示一个对 类型的计数器 /// diff --git a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs index 25fda613..ada8ec67 100644 --- a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs +++ b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs @@ -18,6 +18,7 @@ global using System; global using System.Collections.Generic; global using System.ComponentModel; global using System.Diagnostics.CodeAnalysis; +global using System.Linq; global using System.Text.Json; global using System.Text.Json.Serialization; global using System.Threading; diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs new file mode 100644 index 00000000..0eba23dc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs @@ -0,0 +1,55 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations.LogDb +{ + [DbContext(typeof(LogDbContext))] + [Migration("20220903071033_LogTime")] + partial class LogTime + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + + modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("Exception") + .HasColumnType("TEXT"); + + b.Property("LogLevel") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("logs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.cs new file mode 100644 index 00000000..74d1e26d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.cs @@ -0,0 +1,27 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations.LogDb +{ + public partial class LogTime : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Time", + table: "logs", + type: "TEXT", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Time", + table: "logs"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/LogDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/LogDbContextModelSnapshot.cs index a2fba3a3..f79e5a53 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/LogDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/LogDbContextModelSnapshot.cs @@ -1,6 +1,8 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Snap.Hutao.Context.Database; #nullable disable @@ -13,7 +15,7 @@ namespace Snap.Hutao.Migrations.LogDb protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b => { @@ -38,6 +40,9 @@ namespace Snap.Hutao.Migrations.LogDb .IsRequired() .HasColumnType("TEXT"); + b.Property("Time") + .HasColumnType("TEXT"); + b.HasKey("InnerId"); b.ToTable("logs"); diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index c54d55aa..c0702732 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -67,8 +67,8 @@ public static class Program // Microsoft extension .AddLogging(builder => builder - .AddDebug() - .AddDatabase()) + .AddDatabase() + .AddDebug()) .AddMemoryCache() // Hutao extensions diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index cbb6f93e..158b8276 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -141,10 +141,10 @@ internal class AchievementService : IAchievementService case ImportOption.Overwrite: { - IEnumerable newData = list + IEnumerable orederedUIAF = list .Select(uiaf => EntityAchievement.Create(archiveId, uiaf)) .OrderBy(a => a.Id); - return achievementDbOperation.Overwrite(archiveId, newData); + return achievementDbOperation.Overwrite(archiveId, orederedUIAF); } default: diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs new file mode 100644 index 00000000..a1928fa7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.GachaLog; + +/// +/// 祈愿记录服务器 +/// +internal class GachaLogService +{ + +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs index ba644369..e5a8e846 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ApiEndpoints.cs @@ -15,12 +15,25 @@ internal static class ApiEndpoints /// /// 公告列表 /// - public const string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}"; + public const string AnnList = $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery}"; /// /// 公告内容 /// - public const string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}"; + public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}"; + #endregion + + #region GachaInfo + + /// + /// 获取祈愿记录 + /// + /// query string + /// 祈愿记录信息Url + public static string GachaInfoGetGachaLog(string query) + { + return $"{Hk4eApiGachaInfoApi}/getGachaLog?{query}"; + } #endregion #region GameRecord @@ -53,49 +66,6 @@ internal static class ApiEndpoints } #endregion - #region SignIn - - /// - /// 签到活动Id - /// - public const string SignInRewardActivityId = "e202009291139501"; - - /// - /// 签到 - /// - public const string SignInRewardHome = $"{ApiTakumi}/event/bbs_sign_reward/home?act_id={SignInRewardActivityId}"; - - /// - /// 签到信息 - /// - /// uid - /// 签到信息字符串 - public static string SignInRewardInfo(PlayerUid uid) - { - return $"{ApiTakumi}/event/bbs_sign_reward/info?act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}"; - } - - /// - /// 签到 - /// - public const string SignInRewardReSign = $"{ApiTakumi}/event/bbs_sign_reward/resign"; - - /// - /// 补签信息 - /// - /// uid - /// 补签信息字符串 - public static string SignInRewardResignInfo(PlayerUid uid) - { - return $"{ApiTakumi}/event/bbs_sign_reward/resign_info?act_id=e202009291139501®ion={uid.Region}&uid={uid.Value}"; - } - - /// - /// 签到 - /// - public const string SignInRewardSign = $"{ApiTakumi}/event/bbs_sign_reward/sign"; - #endregion - #region UserFullInfo /// @@ -119,16 +89,21 @@ internal static class ApiEndpoints /// /// 用户游戏角色 /// - public const string UserGameRoles = $"{ApiTakumi}/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn"; + public const string UserGameRoles = $"{ApiTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_cn"; #endregion // consts private const string ApiTakumi = "https://api-takumi.mihoyo.com"; + private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api"; private const string ApiTakumiRecord = "https://api-takumi-record.mihoyo.com"; private const string ApiTakumiRecordApi = $"{ApiTakumiRecord}/game_record/app/genshin/api"; + private const string BbsApi = "https://bbs-api.mihoyo.com"; private const string BbsApiUserApi = $"{BbsApi}/user/wapi"; + private const string Hk4eApi = "https://hk4e-api.mihoyo.com"; + private const string Hk4eApiAnnouncementApi = $"{Hk4eApi}/common/hk4e_cn/announcement/api"; + private const string Hk4eApiGachaInfoApi = $"{Hk4eApi}/event/gacha_info/api"; private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000"; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs index 197bf6ce..cca5c54c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/DynamicSecretProvider.cs @@ -33,7 +33,7 @@ internal abstract class DynamicSecretProvider : Md5Convert string q = string.Join("&", new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x)); // check - string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); + string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecretSalt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant(); return $"{t},{r},{check}"; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs new file mode 100644 index 00000000..e9d6ea01 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaConfigType.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +/// +/// 祈愿配置类型 +/// +public enum GachaConfigType +{ + /// + /// 新手池 + /// + NoviceWish = 100, + + /// + /// 常驻池 + /// + PermanentWish = 200, + + /// + /// 角色1池 + /// + AvatarEventWish = 301, + + /// + /// 武器池 + /// + WeaponEventWish = 302, + + /// + /// 角色2池 + /// + AvatarEventWish2 = 400, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs new file mode 100644 index 00000000..977d3b24 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs @@ -0,0 +1,41 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Response; +using System.Net.Http; +using System.Net.Http.Json; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +/// +/// 祈愿记录客户端 +/// +[HttpClient(HttpClientConfigration.Default)] +internal class GachaInfoClient +{ + private readonly HttpClient httpClient; + private readonly JsonSerializerOptions options; + + /// + /// 构造一个新的祈愿记录客户端 + /// + /// http客户端 + /// Json序列化选项 + public GachaInfoClient(HttpClient httpClient, JsonSerializerOptions options) + { + this.httpClient = httpClient; + this.options = options; + } + + /// + /// 获取记录页面 + /// + /// 查询 + /// 取消令牌 + /// 单个祈愿记录页面 + public Task?> GetGachaLogPageAsync(GachaLogConfigration config, CancellationToken token = default) + { + return httpClient.GetFromJsonAsync>(ApiEndpoints.GachaInfoGetGachaLog(config.AsQuery()), options, token); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs new file mode 100644 index 00000000..b1eb1b5e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogConfigration.cs @@ -0,0 +1,63 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Request.QueryString; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +/// +/// 祈愿记录请求配置 +/// +public struct GachaLogConfigration +{ + private readonly QueryString innerQuery; + + /// + /// 尺寸 + /// + public int Size + { + set => innerQuery.Set("size", value); + } + + /// + /// 类型 + /// + public GachaConfigType Type + { + set => innerQuery.Set("gacha_type", (int)value); + } + + /// + /// 结束Id + /// + public ulong EndId + { + set => innerQuery.Set("end_id", value); + } + + /// + /// 构造一个新的祈愿记录请求配置 + /// + /// 原始查询字符串 + /// 祈愿类型 + /// 终止Id + public GachaLogConfigration(string query, GachaConfigType type, ulong endId = 0UL) + { + innerQuery = QueryString.Parse(query); + innerQuery.Set("lang", "zh-cn"); + + Size = 20; + Type = type; + EndId = endId; + } + + /// + /// 转换到查询字符串 + /// + /// 匹配的查询字符串 + public string AsQuery() + { + return innerQuery.ToString(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs new file mode 100644 index 00000000..466d118d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogItem.cs @@ -0,0 +1,77 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +/// +/// 祈愿记录物品 +/// +public class GachaLogItem +{ + /// + /// 玩家Uid + /// + [JsonPropertyName("uid")] + public string Uid { get; set; } = default!; + + /// + /// 祈愿类型 + /// + [JsonPropertyName("gacha_type")] + public GachaConfigType GachaType { get; set; } = default!; + + /// + /// 总为 + /// + [Obsolete("API removed this property")] + [JsonPropertyName("item_id")] + public string ItemId { get; set; } = default!; + + /// + /// 个数 + /// 一般为 1 + /// + [JsonPropertyName("count")] + public string? Count { get; set; } = default!; + + /// + /// 时间 + /// + [JsonPropertyName("time")] + [JsonConverter(typeof(DateTimeOffsetConverter))] + public DateTimeOffset Time { get; set; } + + /// + /// 物品名称 + /// + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + /// + /// 语言 + /// + [JsonPropertyName("lang")] + public string Language { get; set; } = default!; + + /// + /// 物品类型 + /// + [JsonPropertyName("item_type")] + public string ItemType { get; set; } = default!; + + /// + /// 物品稀有等级 + /// + [JsonPropertyName("rank_type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ItemQuality Rank { get; set; } = default!; + + /// + /// Id + /// + [JsonPropertyName("id")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public long Id { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogPage.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogPage.cs new file mode 100644 index 00000000..3b95306c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaLogPage.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo; + +/// +/// 祈愿记录分页 +/// +public class GachaLogPage +{ + /// + /// 页码 + /// + [JsonPropertyName("page")] + public string Page { get; set; } = default!; + + /// + /// 尺寸 + /// + [JsonPropertyName("size")] + public string Size { get; set; } = default!; + + /// + /// 总页数 + /// + [Obsolete("总是为 0")] + [JsonPropertyName("total")] + public string Total { get; set; } = default!; + + /// + /// 总页数 + /// 总是为 0 + /// + [JsonPropertyName("list")] + public List List { get; set; } = default!; + + /// + /// 地区 + /// + [JsonPropertyName("region")] + public string Region { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs new file mode 100644 index 00000000..3c4941f8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs @@ -0,0 +1,288 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using System.Collections; +using System.Linq; + +namespace Snap.Hutao.Web.Request.QueryString; + +/// +/// querystring serializer/deserializer +/// +public struct QueryString +{ + private readonly Dictionary> dictionary = new(); + + /// + /// Nothing + /// + public QueryString() + { + } + + /// + /// Gets the first value of the first parameter with the matching name. + /// Throws if a parameter with a matching name could not be found. + /// O(n) where n = Count of the current object. + /// + /// The parameter name to find. + /// query + public string this[string name] + { + get + { + if (TryGetValue(name, out string? value)) + { + return value; + } + + throw new KeyNotFoundException($"A parameter with name '{name}' could not be found."); + } + } + + /// + /// Parses a query string into a object. Keys/values are automatically URL decoded. + /// + /// + /// The query string to deserialize. + /// This should NOT have a leading ? character. + /// Valid input would be something like "a=1&b=5". + /// URL decoding of keys/values is automatically performed. + /// Also supports query strings that are serialized using ; instead of &, like "a=1;b=5" + /// A new QueryString represents the url query + public static QueryString Parse(string? queryString) + { + if (string.IsNullOrWhiteSpace(queryString)) + { + return new QueryString(); + } + + string[] pairs = queryString.Split('&', ';'); + QueryString answer = new(); + foreach (string pair in pairs) + { + string name; + string? value; + int indexOfEquals = pair.IndexOf('='); + if (indexOfEquals == -1) + { + name = pair; + value = string.Empty; + } + else + { + name = pair[..indexOfEquals]; + value = pair[(indexOfEquals + 1)..]; + } + + answer.Add(name, value); + } + + return answer; + } + + /// + /// Gets the first value of the first parameter with the matching name. If no parameter with a matching name exists, returns false. + /// + /// The parameter name to find. + /// The parameter's value will be written here once found. + /// value + public bool TryGetValue(string name, [NotNullWhen(true)] out string? value) + { + if (dictionary.TryGetValue(name, out List? values)) + { + value = values.First(); + return true; + } + + value = null; + return false; + } + + /// + /// Gets the values of the parameter with the matching name. If no parameter with a matching name exists, sets to null and returns false. + /// + /// The parameter name to find. + /// The parameter's values will be written here once found. + /// values + public bool TryGetValues(string name, [NotNullWhen(true)] out string?[]? values) + { + if (dictionary.TryGetValue(name, out List? storedValues)) + { + values = storedValues.ToArray(); + return true; + } + + values = null; + return false; + } + + /// + /// Returns the count of parameters in the current query string. + /// + /// count of the queries + public int Count() + { + return dictionary.Select(i => i.Value.Count).Sum(); + } + + /// + /// Adds a query string parameter to the query string. + /// + /// The name of the parameter. + /// The optional value of the parameter. + public void Add(string name, string value) + { + if (!dictionary.TryGetValue(name, out List? values)) + { + values = new List(); + dictionary[name] = values; + } + + values.Add(value); + } + + /// + public void Set(string name, object value) + { + Set(name, value.ToString() ?? string.Empty); + } + + /// + /// Sets a query string parameter. If there are existing parameters with the same name, they are removed. + /// + /// The name of the parameter. + /// The optional value of the parameter. + public void Set(string name, string value) + { + dictionary[name] = new() { value }; + } + + /// + /// Determines if the query string contains at least one parameter with the specified name. + /// + /// The parameter name to look for. + /// True if the query string contains at least one parameter with the specified name, else false. + public bool Contains(string name) + { + return dictionary.ContainsKey(name); + } + + /// + /// Determines if the query string contains a parameter with the specified name and value. + /// + /// The parameter name to look for. + /// The value to look for when the name has been matched. + /// True if the query string contains a parameter with the specified name and value, else false. + public bool Contains(string name, string value) + { + return dictionary.TryGetValue(name, out List? values) && values.Contains(value); + } + + /// + /// Removes the first parameter with the specified name. + /// + /// The name of parameter to remove. + /// True if the parameters were removed, else false. + public bool Remove(string name) + { + if (dictionary.TryGetValue(name, out List? values)) + { + if (values.Count == 1) + { + dictionary.Remove(name); + } + else + { + values.RemoveAt(0); + } + + return true; + } + + return false; + } + + /// + /// Removes all parameters with the specified name. + /// + /// The name of parameters to remove. + /// True if the parameters were removed, else false. + public bool RemoveAll(string name) + { + return dictionary.Remove(name); + } + + /// + /// Removes the first parameter with the specified name and value. + /// + /// The name of the parameter to remove. + /// value + /// True if parameter was removed, else false. + public bool Remove(string name, string value) + { + if (dictionary.TryGetValue(name, out List? values)) + { + if (values.RemoveFirstWhere(i => Equals(i, value))) + { + // If removed last value, remove the key + if (values.Count == 0) + { + dictionary.Remove(name); + } + + return true; + } + } + + return false; + } + + /// + /// Removes all parameters with the specified name and value. + /// + /// The name of parameters to remove. + /// The value to match when deciding whether to remove. + /// The count of parameters removed. + public int RemoveAll(string name, string value) + { + if (dictionary.TryGetValue(name, out List? values)) + { + int countRemoved = values.RemoveAll(i => Equals(i, value)); + + // If removed last value, remove the key + if (values.Count == 0) + { + dictionary.Remove(name); + } + + return countRemoved; + } + + return 0; + } + + /// + /// Serializes the key-value pairs into a query string, using the default & separator. + /// Produces something like "a=1&b=5". + /// URL encoding of keys/values is automatically performed. + /// Null values are not written (only their key is written). + /// + /// query + public override string ToString() + { + return string.Join('&', GetParameters()); + } + + private IEnumerable GetParameters() + { + foreach ((string name, List values) in dictionary) + { + foreach (string value in values) + { + yield return new QueryStringParameter(name, value); + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs new file mode 100644 index 00000000..641e6f17 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryStringParameter.cs @@ -0,0 +1,37 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Request.QueryString; + +/// +/// A single query string parameter (name and value pair). +/// +public struct QueryStringParameter +{ + /// + /// The name of the parameter. Cannot be null. + /// + public string Name; + + /// + /// The value of the parameter (or null if there's no value). + /// + public string Value; + + /// + /// Initializes a new query string parameter with the specified name and optional value. + /// + /// The name of the parameter. Cannot be null. + /// The optional value of the parameter. + internal QueryStringParameter(string name, string value) + { + Name = name; + Value = value; + } + + /// + public override string ToString() + { + return $"{Name}={Value}"; + } +}