mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
async queued database logger
This commit is contained in:
@@ -14,4 +14,8 @@
|
||||
<PackageReference Include="Microsoft.Windows.SDK.Win32Metadata" Version="25.0.28-preview" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Foundation\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,6 +19,11 @@ public class LogEntry
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid InnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志时间
|
||||
/// </summary>
|
||||
public DateTimeOffset Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类别
|
||||
/// </summary>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
55
src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs
generated
Normal file
55
src/Snap.Hutao/Snap.Hutao/Migrations/LogDb/20220903071033_LogTime.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -67,8 +67,8 @@ public static class Program
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder
|
||||
.AddDebug()
|
||||
.AddDatabase())
|
||||
.AddDatabase()
|
||||
.AddDebug())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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}®ion={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®ion={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®ion=cn_gf01&level=55&uid=100000000";
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
288
src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs
Normal file
288
src/Snap.Hutao/Snap.Hutao/Web/Request/QueryString/QueryString.cs
Normal 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&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"</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 & 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).
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user