async queued database logger

This commit is contained in:
DismissedLight
2022-09-05 13:01:36 +08:00
parent 4e7336c238
commit 332722c860
27 changed files with 798 additions and 227 deletions

View File

@@ -14,4 +14,8 @@
<PackageReference Include="Microsoft.Windows.SDK.Win32Metadata" Version="25.0.28-preview" />
</ItemGroup>
<ItemGroup>
<Folder Include="Foundation\" />
</ItemGroup>
</Project>

View File

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

View File

@@ -1,22 +0,0 @@
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Win32;
/// <summary>
/// 包装不安全的代码
/// </summary>
public class Unsafe
{
/// <summary>
/// 使用指针操作简化封送
/// </summary>
/// <param name="lParam">lParam</param>
/// <param name="minWidth">最小宽度</param>
/// <param name="minHeight">最小高度</param>
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);
}
}

View File

@@ -11,20 +11,11 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
// Used DS1 History
// 2.34.1 9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7
// 2.35.2 N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep
/// <summary>
/// 动态密钥1的盐
/// </summary>
public const string DynamicSecret1Salt = "N50pqm7FSy2AkFz2B3TqtuZMJ5TOl3Ep";
/// <summary>
/// 动态密钥2的盐
/// 动态密钥的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
public const string DynamicSecretSalt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA
@@ -34,7 +25,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.35.2";
public const string HoyolabXrpcVersion = "2.36.1";
/// <summary>
/// 标准UA

View File

@@ -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;
/// <summary>
@@ -54,6 +52,7 @@ internal sealed partial class DatebaseLogger : ILogger
LogEntry entry = new()
{
Time = DateTimeOffset.Now,
Category = name,
LogLevel = logLevel,
EventId = eventId.Id,

View File

@@ -19,6 +19,11 @@ public class LogEntry
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 日志时间
/// </summary>
public DateTimeOffset Time { get; set; }
/// <summary>
/// 类别
/// </summary>

View File

@@ -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;
/// </summary>
public sealed class LogEntryQueue : IDisposable
{
private static readonly object LogDbContextCreationLock = new();
private readonly ConcurrentQueue<LogEntry> 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;
/// <summary>
/// 构造一个新的日志队列
/// </summary>
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();
}
/// <summary>
@@ -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)
{
}
}
}
}

View File

@@ -14,17 +14,15 @@ public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOpe
private readonly DispatcherQueue dispatherQueue;
/// <summary>
/// 构造一个新的同步上下文等待器
/// 构造一个新的调度器队列等待器
/// </summary>
/// <param name="dispatherQueue">同步上下文</param>
/// <param name="dispatherQueue">调度器队列</param>
public DispatherQueueSwitchOperation(DispatcherQueue dispatherQueue)
{
this.dispatherQueue = dispatherQueue;
}
/// <summary>
/// 是否完成
/// </summary>
/// <inheritdoc/>
public bool IsCompleted
{
get => dispatherQueue.HasThreadAccess;
@@ -41,10 +39,7 @@ public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOpe
{
}
/// <summary>
/// 获取等待器
/// </summary>
/// <returns>等待器</returns>
/// <inheritdoc/>
public DispatherQueueSwitchOperation GetAwaiter()
{
return this;

View File

@@ -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;
/// <summary>
/// 提供简单易用的状态提示信息
/// 用于任务的状态跟踪
/// 同时继承了 <see cref="Observable"/>
/// </summary>
public class Watcher : Observable
{
private readonly bool isReusable;
private bool hasUsed;
private bool isWorking;
private bool isCompleted;
/// <summary>
/// 构造一个新的工作监视器
/// </summary>
/// <param name="isReusable">是否可以重用</param>
public Watcher(bool isReusable = true)
{
this.isReusable = isReusable;
}
/// <summary>
/// 是否正在工作
/// </summary>
public bool IsWorking
{
get => isWorking;
private set => Set(ref isWorking, value);
}
/// <summary>
/// 工作是否完成
/// </summary>
public bool IsCompleted
{
get => isCompleted;
private set => Set(ref isCompleted, value);
}
/// <summary>
/// 对某个操作进行监视,
/// </summary>
/// <returns>一个可释放的对象,用于在操作完成时自动提示监视器工作已经完成</returns>
/// <exception cref="InvalidOperationException">重用了一个不可重用的监视器</exception>
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;
}
}
}

View File

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

View File

@@ -78,6 +78,27 @@ public static partial class EnumerableExtensions
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
/// <typeparam name="T">项的类型</typeparam>
/// <param name="list">表</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>是否移除了元素</returns>
public static bool RemoveFirstWhere<T>(this IList<T> list, Func<T, bool> shouldRemovePredicate)
{
for (int i = 0; i < list.Count; i++)
{
if (shouldRemovePredicate.Invoke(list[i]))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// </summary>

View File

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

View File

@@ -0,0 +1,55 @@
// <auto-generated />
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<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,27 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class LogTime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
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");
}
}
}

View File

@@ -1,6 +1,8 @@
// <auto-generated />
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<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");

View File

@@ -67,8 +67,8 @@ public static class Program
// Microsoft extension
.AddLogging(builder => builder
.AddDebug()
.AddDatabase())
.AddDatabase()
.AddDebug())
.AddMemoryCache()
// Hutao extensions

View File

@@ -141,10 +141,10 @@ internal class AchievementService : IAchievementService
case ImportOption.Overwrite:
{
IEnumerable<EntityAchievement> newData = list
IEnumerable<EntityAchievement> orederedUIAF = list
.Select(uiaf => EntityAchievement.Create(archiveId, uiaf))
.OrderBy(a => a.Id);
return achievementDbOperation.Overwrite(archiveId, newData);
return achievementDbOperation.Overwrite(archiveId, orederedUIAF);
}
default:

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录服务器
/// </summary>
internal class GachaLogService
{
}

View File

@@ -15,12 +15,25 @@ internal static class ApiEndpoints
/// <summary>
/// 公告列表
/// </summary>
public const string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}";
public const string AnnList = $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery}";
/// <summary>
/// 公告内容
/// </summary>
public const string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}";
public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}";
#endregion
#region GachaInfo
/// <summary>
/// 获取祈愿记录
/// </summary>
/// <param name="query">query string</param>
/// <returns>祈愿记录信息Url</returns>
public static string GachaInfoGetGachaLog(string query)
{
return $"{Hk4eApiGachaInfoApi}/getGachaLog?{query}";
}
#endregion
#region GameRecord
@@ -53,49 +66,6 @@ internal static class ApiEndpoints
}
#endregion
#region SignIn
/// <summary>
/// 签到活动Id
/// </summary>
public const string SignInRewardActivityId = "e202009291139501";
/// <summary>
/// 签到
/// </summary>
public const string SignInRewardHome = $"{ApiTakumi}/event/bbs_sign_reward/home?act_id={SignInRewardActivityId}";
/// <summary>
/// 签到信息
/// </summary>
/// <param name="uid">uid</param>
/// <returns>签到信息字符串</returns>
public static string SignInRewardInfo(PlayerUid uid)
{
return $"{ApiTakumi}/event/bbs_sign_reward/info?act_id={SignInRewardActivityId}&region={uid.Region}&uid={uid.Value}";
}
/// <summary>
/// 签到
/// </summary>
public const string SignInRewardReSign = $"{ApiTakumi}/event/bbs_sign_reward/resign";
/// <summary>
/// 补签信息
/// </summary>
/// <param name="uid">uid</param>
/// <returns>补签信息字符串</returns>
public static string SignInRewardResignInfo(PlayerUid uid)
{
return $"{ApiTakumi}/event/bbs_sign_reward/resign_info?act_id=e202009291139501&region={uid.Region}&uid={uid.Value}";
}
/// <summary>
/// 签到
/// </summary>
public const string SignInRewardSign = $"{ApiTakumi}/event/bbs_sign_reward/sign";
#endregion
#region UserFullInfo
/// <summary>
@@ -119,16 +89,21 @@ internal static class ApiEndpoints
/// <summary>
/// 用户游戏角色
/// </summary>
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&region=cn_gf01&level=55&uid=100000000";
}

View File

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

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
/// <summary>
/// 祈愿配置类型
/// </summary>
public enum GachaConfigType
{
/// <summary>
/// 新手池
/// </summary>
NoviceWish = 100,
/// <summary>
/// 常驻池
/// </summary>
PermanentWish = 200,
/// <summary>
/// 角色1池
/// </summary>
AvatarEventWish = 301,
/// <summary>
/// 武器池
/// </summary>
WeaponEventWish = 302,
/// <summary>
/// 角色2池
/// </summary>
AvatarEventWish2 = 400,
}

View File

@@ -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;
/// <summary>
/// 祈愿记录客户端
/// </summary>
[HttpClient(HttpClientConfigration.Default)]
internal class GachaInfoClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
/// <summary>
/// 构造一个新的祈愿记录客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="options">Json序列化选项</param>
public GachaInfoClient(HttpClient httpClient, JsonSerializerOptions options)
{
this.httpClient = httpClient;
this.options = options;
}
/// <summary>
/// 获取记录页面
/// </summary>
/// <param name="config">查询</param>
/// <param name="token">取消令牌</param>
/// <returns>单个祈愿记录页面</returns>
public Task<Response<GachaLogPage>?> GetGachaLogPageAsync(GachaLogConfigration config, CancellationToken token = default)
{
return httpClient.GetFromJsonAsync<Response<GachaLogPage>>(ApiEndpoints.GachaInfoGetGachaLog(config.AsQuery()), options, token);
}
}

View File

@@ -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;
/// <summary>
/// 祈愿记录请求配置
/// </summary>
public struct GachaLogConfigration
{
private readonly QueryString innerQuery;
/// <summary>
/// 尺寸
/// </summary>
public int Size
{
set => innerQuery.Set("size", value);
}
/// <summary>
/// 类型
/// </summary>
public GachaConfigType Type
{
set => innerQuery.Set("gacha_type", (int)value);
}
/// <summary>
/// 结束Id
/// </summary>
public ulong EndId
{
set => innerQuery.Set("end_id", value);
}
/// <summary>
/// 构造一个新的祈愿记录请求配置
/// </summary>
/// <param name="query">原始查询字符串</param>
/// <param name="type">祈愿类型</param>
/// <param name="endId">终止Id</param>
public GachaLogConfigration(string query, GachaConfigType type, ulong endId = 0UL)
{
innerQuery = QueryString.Parse(query);
innerQuery.Set("lang", "zh-cn");
Size = 20;
Type = type;
EndId = endId;
}
/// <summary>
/// 转换到查询字符串
/// </summary>
/// <returns>匹配的查询字符串</returns>
public string AsQuery()
{
return innerQuery.ToString();
}
}

View File

@@ -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;
/// <summary>
/// 祈愿记录物品
/// </summary>
public class GachaLogItem
{
/// <summary>
/// 玩家Uid
/// </summary>
[JsonPropertyName("uid")]
public string Uid { get; set; } = default!;
/// <summary>
/// 祈愿类型
/// </summary>
[JsonPropertyName("gacha_type")]
public GachaConfigType GachaType { get; set; } = default!;
/// <summary>
/// 总为 <see cref="string.Empty"/>
/// </summary>
[Obsolete("API removed this property")]
[JsonPropertyName("item_id")]
public string ItemId { get; set; } = default!;
/// <summary>
/// 个数
/// 一般为 1
/// </summary>
[JsonPropertyName("count")]
public string? Count { get; set; } = default!;
/// <summary>
/// 时间
/// </summary>
[JsonPropertyName("time")]
[JsonConverter(typeof(DateTimeOffsetConverter))]
public DateTimeOffset Time { get; set; }
/// <summary>
/// 物品名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 语言
/// </summary>
[JsonPropertyName("lang")]
public string Language { get; set; } = default!;
/// <summary>
/// 物品类型
/// </summary>
[JsonPropertyName("item_type")]
public string ItemType { get; set; } = default!;
/// <summary>
/// 物品稀有等级
/// </summary>
[JsonPropertyName("rank_type")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ItemQuality Rank { get; set; } = default!;
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long Id { get; set; } = default!;
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
/// <summary>
/// 祈愿记录分页
/// </summary>
public class GachaLogPage
{
/// <summary>
/// 页码
/// </summary>
[JsonPropertyName("page")]
public string Page { get; set; } = default!;
/// <summary>
/// 尺寸
/// </summary>
[JsonPropertyName("size")]
public string Size { get; set; } = default!;
/// <summary>
/// 总页数
/// </summary>
[Obsolete("总是为 0")]
[JsonPropertyName("total")]
public string Total { get; set; } = default!;
/// <summary>
/// 总页数
/// 总是为 0
/// </summary>
[JsonPropertyName("list")]
public List<GachaLogItem> List { get; set; } = default!;
/// <summary>
/// 地区
/// </summary>
[JsonPropertyName("region")]
public string Region { get; set; } = default!;
}

View File

@@ -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;
/// <summary>
/// querystring serializer/deserializer
/// </summary>
public struct QueryString
{
private readonly Dictionary<string, List<string>> dictionary = new();
/// <summary>
/// Nothing
/// </summary>
public QueryString()
{
}
/// <summary>
/// <para>Gets the first value of the first parameter with the matching name. </para>
/// <para>Throws <see cref="KeyNotFoundException"/> if a parameter with a matching name could not be found. </para>
/// <para>O(n) where n = Count of the current object.</para>
/// </summary>
/// <param name="name">The parameter name to find.</param>
/// <returns>query</returns>
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.");
}
}
/// <summary>
/// Parses a query string into a <see cref="QueryString"/> object. Keys/values are automatically URL decoded.
/// </summary>
/// <param name="queryString">
/// The query string to deserialize.
/// This should NOT have a leading ? character.
/// Valid input would be something like "a=1&amp;b=5".
/// URL decoding of keys/values is automatically performed.
/// Also supports query strings that are serialized using ; instead of &amp;, like "a=1;b=5"</param>
/// <returns>A new QueryString represents the url query</returns>
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;
}
/// <summary>
/// Gets the first value of the first parameter with the matching name. If no parameter with a matching name exists, returns false.
/// </summary>
/// <param name="name">The parameter name to find.</param>
/// <param name="value">The parameter's value will be written here once found.</param>
/// <returns>value</returns>
public bool TryGetValue(string name, [NotNullWhen(true)] out string? value)
{
if (dictionary.TryGetValue(name, out List<string>? values))
{
value = values.First();
return true;
}
value = null;
return false;
}
/// <summary>
/// Gets the values of the parameter with the matching name. If no parameter with a matching name exists, sets <paramref name="values"/> to null and returns false.
/// </summary>
/// <param name="name">The parameter name to find.</param>
/// <param name="values">The parameter's values will be written here once found.</param>
/// <returns>values</returns>
public bool TryGetValues(string name, [NotNullWhen(true)] out string?[]? values)
{
if (dictionary.TryGetValue(name, out List<string>? storedValues))
{
values = storedValues.ToArray();
return true;
}
values = null;
return false;
}
/// <summary>
/// Returns the count of parameters in the current query string.
/// </summary>
/// <returns>count of the queries</returns>
public int Count()
{
return dictionary.Select(i => i.Value.Count).Sum();
}
/// <summary>
/// Adds a query string parameter to the query string.
/// </summary>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The optional value of the parameter.</param>
public void Add(string name, string value)
{
if (!dictionary.TryGetValue(name, out List<string>? values))
{
values = new List<string>();
dictionary[name] = values;
}
values.Add(value);
}
/// <inheritdoc cref="Set(string, string)"/>
public void Set(string name, object value)
{
Set(name, value.ToString() ?? string.Empty);
}
/// <summary>
/// Sets a query string parameter. If there are existing parameters with the same name, they are removed.
/// </summary>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The optional value of the parameter.</param>
public void Set(string name, string value)
{
dictionary[name] = new() { value };
}
/// <summary>
/// Determines if the query string contains at least one parameter with the specified name.
/// </summary>
/// <param name="name">The parameter name to look for.</param>
/// <returns>True if the query string contains at least one parameter with the specified name, else false.</returns>
public bool Contains(string name)
{
return dictionary.ContainsKey(name);
}
/// <summary>
/// Determines if the query string contains a parameter with the specified name and value.
/// </summary>
/// <param name="name">The parameter name to look for.</param>
/// <param name="value">The value to look for when the name has been matched.</param>
/// <returns>True if the query string contains a parameter with the specified name and value, else false.</returns>
public bool Contains(string name, string value)
{
return dictionary.TryGetValue(name, out List<string>? values) && values.Contains(value);
}
/// <summary>
/// Removes the first parameter with the specified name.
/// </summary>
/// <param name="name">The name of parameter to remove.</param>
/// <returns>True if the parameters were removed, else false.</returns>
public bool Remove(string name)
{
if (dictionary.TryGetValue(name, out List<string>? values))
{
if (values.Count == 1)
{
dictionary.Remove(name);
}
else
{
values.RemoveAt(0);
}
return true;
}
return false;
}
/// <summary>
/// Removes all parameters with the specified name.
/// </summary>
/// <param name="name">The name of parameters to remove.</param>
/// <returns>True if the parameters were removed, else false.</returns>
public bool RemoveAll(string name)
{
return dictionary.Remove(name);
}
/// <summary>
/// Removes the first parameter with the specified name and value.
/// </summary>
/// <param name="name">The name of the parameter to remove.</param>
/// <param name="value">value</param>
/// <returns>True if parameter was removed, else false.</returns>
public bool Remove(string name, string value)
{
if (dictionary.TryGetValue(name, out List<string>? 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;
}
/// <summary>
/// Removes all parameters with the specified name and value.
/// </summary>
/// <param name="name">The name of parameters to remove.</param>
/// <param name="value">The value to match when deciding whether to remove.</param>
/// <returns>The count of parameters removed.</returns>
public int RemoveAll(string name, string value)
{
if (dictionary.TryGetValue(name, out List<string>? 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;
}
/// <summary>
/// Serializes the key-value pairs into a query string, using the default &amp; separator.
/// Produces something like "a=1&amp;b=5".
/// URL encoding of keys/values is automatically performed.
/// Null values are not written (only their key is written).
/// </summary>
/// <returns>query</returns>
public override string ToString()
{
return string.Join('&', GetParameters());
}
private IEnumerable<QueryStringParameter> GetParameters()
{
foreach ((string name, List<string> values) in dictionary)
{
foreach (string value in values)
{
yield return new QueryStringParameter(name, value);
}
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Request.QueryString;
/// <summary>
/// A single query string parameter (name and value pair).
/// </summary>
public struct QueryStringParameter
{
/// <summary>
/// The name of the parameter. Cannot be null.
/// </summary>
public string Name;
/// <summary>
/// The value of the parameter (or null if there's no value).
/// </summary>
public string Value;
/// <summary>
/// Initializes a new query string parameter with the specified name and optional value.
/// </summary>
/// <param name="name">The name of the parameter. Cannot be null.</param>
/// <param name="value">The optional value of the parameter.</param>
internal QueryStringParameter(string name, string value)
{
Name = name;
Value = value;
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Name}={Value}";
}
}