feat(upserver): 接入认证与KV存档并完成限定钓鱼闭环

This commit is contained in:
Liubing\LB
2026-09-28 18:14:01 +08:00
parent f40a816cc4
commit 0eb43b2b40
129 changed files with 11612 additions and 573 deletions

View File

@@ -0,0 +1,105 @@
using System;
using System.IO;
using System.Security;
using System.Text;
using Newtonsoft.Json;
namespace game.PlayerDataStorage
{
// Internal seam for deterministic disk-failure tests; no production callback is supplied.
internal enum KVCacheWriteStage { BeforeWrite, AfterWrite, AfterFlush, BeforeReplace, AfterReplace }
internal sealed class PlayerKVCacheStore : IDisposable
{
const long MaxBytes = 64L * 1024 * 1024;
readonly KVIdentity identity;
readonly FileStream lease;
readonly Action<KVCacheWriteStage> fault;
readonly string main, temporary, backup;
bool disposed;
PlayerKVCacheStore(KVIdentity identity, FileStream lease, Action<KVCacheWriteStage> fault)
{
this.identity = identity; this.lease = lease; this.fault = fault;
main = Path.Combine(identity.DirectoryPath, "state.json");
temporary = main + ".tmp"; backup = main + ".bak";
}
internal static PlayerKVOpenError Open(KVIdentity identity, Action<KVCacheWriteStage> fault,
out PlayerKVCacheStore store, out KVSnapshot snapshot)
{
store = null; snapshot = null;
FileStream lease = null;
try
{
Directory.CreateDirectory(identity.DirectoryPath);
try { lease = new FileStream(Path.Combine(identity.DirectoryPath, "state.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); }
catch (IOException ex) when ((ex.HResult & 0xffff) == 32 || (ex.HResult & 0xffff) == 33)
{ return PlayerKVOpenError.Busy; }
store = new PlayerKVCacheStore(identity, lease, fault);
if (!File.Exists(store.main))
{
if (File.Exists(store.backup) || File.Exists(store.temporary)) throw KVSnapshot.Invalid();
snapshot = new KVSnapshot();
}
else
{
if (new FileInfo(store.main).Length > MaxBytes) throw KVSnapshot.Invalid();
snapshot = KVSnapshot.Decode(File.ReadAllText(store.main, new UTF8Encoding(false, true)), identity);
}
return PlayerKVOpenError.None;
}
catch (Exception ex) when (ex is JsonException || ex is OverflowException || ex is FormatException || ex is DecoderFallbackException)
{
store?.Dispose(); lease?.Dispose(); store = null;
return PlayerKVOpenError.RecoveryRequired;
}
catch (Exception ex) when (DiskError(ex))
{
store?.Dispose(); lease?.Dispose(); store = null;
return PlayerKVOpenError.StorageError;
}
}
internal bool Save(KVSnapshot snapshot, out bool mayHaveCommitted)
{
mayHaveCommitted = false;
try
{
if (disposed) throw new ObjectDisposedException(nameof(PlayerKVCacheStore));
snapshot.Validate();
var bytes = new UTF8Encoding(false, true).GetBytes(snapshot.Encode(identity));
if (bytes.LongLength > MaxBytes) throw new IOException("KV cache capacity exceeded.");
fault?.Invoke(KVCacheWriteStage.BeforeWrite);
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
{
stream.Write(bytes, 0, bytes.Length);
fault?.Invoke(KVCacheWriteStage.AfterWrite);
stream.Flush(true);
fault?.Invoke(KVCacheWriteStage.AfterFlush);
}
fault?.Invoke(KVCacheWriteStage.BeforeReplace);
mayHaveCommitted = true;
// The temp file is on the same filesystem. Never delete the main file first.
if (File.Exists(main)) File.Replace(temporary, main, backup);
else File.Move(temporary, main);
fault?.Invoke(KVCacheWriteStage.AfterReplace);
mayHaveCommitted = false;
return true;
}
catch (Exception ex) when (DiskError(ex) || ex is JsonException || ex is EncoderFallbackException)
{ return false; }
}
static bool DiskError(Exception ex) => ex is IOException || ex is UnauthorizedAccessException ||
ex is SecurityException || ex is NotSupportedException || ex is ObjectDisposedException;
public void Dispose()
{
if (disposed) return;
disposed = true;
lease.Dispose();
// Keep files, including failed temporary writes, for explicit recovery diagnosis.
}
}
}