feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

View File

@@ -0,0 +1,329 @@
using System;
using System.Collections.Generic;
using asap.core;
using UnityEngine;
namespace Activity
{
/// <summary>
/// <see cref="Services.IEventTokenService"/> 入账/扣账成功后由 <see cref="Services.EventTokenService"/> 发布,
/// 供各活动自行判断是否刷新入口合并红点等(避免订阅者与具体玩法耦合)。
/// </summary>
public readonly struct ActivityEventTokenBalanceChanged
{
public int EventId { get; }
public ActivityEventTokenBalanceChanged(int eventId)
{
EventId = eventId;
}
}
}
namespace Activity.Services
{
/// <summary>
/// 活动代币服务实现
/// 负责管理所有活动的代币数据,提供统一的存储接口
/// </summary>
public class EventTokenService : IEventTokenService
{
#region 常量与字段
private const string TransitionDataKey = "TransitionData";
private Dictionary<int, int> _tokenData = new Dictionary<int, int>();
private readonly object _lock = new object();
private bool _isInitialized;
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
public EventTokenService(game.PlayerDataStorage.IPlayerSaveStore store)
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
#endregion
#region 初始化
public void Initialize()
{
lock (_lock)
{
if (!store.IsCurrent) throw new InvalidOperationException("Activity token session is no longer current.");
if (_isInitialized) return;
var read = store.Read(TransitionDataKey);
var value = read.ValueOrThrow();
Dictionary<int, int> loaded;
if (read.Found == false) loaded = new Dictionary<int, int>();
else
{
if (string.IsNullOrWhiteSpace(value))
throw new Newtonsoft.Json.JsonSerializationException("Stored activity tokens cannot be empty.");
loaded = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<int, int>>(value);
if (loaded == null) throw new Newtonsoft.Json.JsonSerializationException("Stored activity tokens cannot be null.");
}
_tokenData = loaded;
_isInitialized = true;
}
}
// Publish the candidate balance only after the current store accepts its exact JSON.
// Acceptance remains distinct from cloud confirmation, which belongs to the enclosing action.
private void SaveTokens(Dictionary<int, int> candidate)
{
store.Write(TransitionDataKey, Newtonsoft.Json.JsonConvert.SerializeObject(candidate)).RequireAccepted();
_tokenData = candidate;
}
#endregion
#region 基础操作
public void AddToken(int eventId, int count)
{
if (eventId <= 0)
{
Debug.LogWarning($"[EventTokenService] Invalid eventId: {eventId}");
return;
}
if (count == 0)
return;
lock (_lock)
{
try
{
EnsureInitialized();
var currentCount = GetTokenCount(eventId);
var candidate = new Dictionary<int, int>(_tokenData);
candidate[eventId] = currentCount + count;
SaveTokens(candidate);
#if UNITY_EDITOR
Debug.Log($"[EventTokenService] Added {count} tokens to event {eventId}, new total: {_tokenData[eventId]}");
#endif
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] AddToken failed for event {eventId}: {e.Message}");
throw;
}
}
PublishActivityEventTokenBalanceChanged(eventId);
}
public int RemoveToken(int eventId, int count)
{
if (eventId <= 0)
{
Debug.LogWarning($"[EventTokenService] Invalid eventId: {eventId}");
return 0;
}
int actualRemoved = 0;
lock (_lock)
{
try
{
EnsureInitialized();
var currentCount = GetTokenCount(eventId);
actualRemoved = Mathf.Min(count, currentCount);
var newCount = currentCount - actualRemoved;
var candidate = new Dictionary<int, int>(_tokenData);
if (newCount <= 0)
{
candidate.Remove(eventId);
}
else
{
candidate[eventId] = newCount;
}
SaveTokens(candidate);
#if UNITY_EDITOR
Debug.Log($"[EventTokenService] Removed {actualRemoved} tokens from event {eventId}, remaining: {newCount}");
#endif
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] RemoveToken failed for event {eventId}: {e.Message}");
throw;
}
}
PublishActivityEventTokenBalanceChanged(eventId);
return actualRemoved;
}
public int GetTokenCount(int eventId)
{
if (eventId <= 0) return 0;
lock (_lock)
{
try
{
EnsureInitialized();
return _tokenData.GetValueOrDefault(eventId, 0);
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] GetTokenCount failed for event {eventId}: {e.Message}");
throw;
}
}
}
public void SetToken(int eventId, int count)
{
if (eventId <= 0)
{
Debug.LogWarning($"[EventTokenService] Invalid eventId: {eventId}");
return;
}
lock (_lock)
{
try
{
EnsureInitialized();
var candidate = new Dictionary<int, int>(_tokenData);
candidate[eventId] = count;
SaveTokens(candidate);
#if UNITY_EDITOR
Debug.Log($"[EventTokenService] Set event {eventId} token count to {count}");
#endif
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] SetToken failed for event {eventId}: {e.Message}");
throw;
}
}
}
public void ClearToken(int eventId)
{
if (eventId <= 0) return;
lock (_lock)
{
try
{
EnsureInitialized();
var candidate = new Dictionary<int, int>(_tokenData);
if (candidate.Remove(eventId))
{
SaveTokens(candidate);
}
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] ClearToken failed for event {eventId}: {e.Message}");
throw;
}
}
}
#endregion
#region 批量操作
public Dictionary<int, int> GetAllTokens()
{
lock (_lock)
{
EnsureInitialized();
return new Dictionary<int, int>(_tokenData);
}
}
public void SetTokens(Dictionary<int, int> tokens)
{
if (tokens == null) return;
lock (_lock)
{
try
{
EnsureInitialized();
var candidate = new Dictionary<int, int>();
foreach (var kvp in tokens)
{
if (kvp.Key > 0)
{
candidate[kvp.Key] = kvp.Value;
}
}
SaveTokens(candidate);
#if UNITY_EDITOR
Debug.Log($"[EventTokenService] Set {tokens.Count} token entries");
#endif
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] SetTokens failed: {e.Message}");
throw;
}
}
}
public void ClearTokens(IEnumerable<int> eventIds)
{
if (eventIds == null) return;
lock (_lock)
{
try
{
EnsureInitialized();
var candidate = new Dictionary<int, int>(_tokenData);
bool changed = false;
foreach (var eventId in eventIds)
{
if (candidate.Remove(eventId))
{
changed = true;
}
}
if (changed)
{
SaveTokens(candidate);
}
}
catch (Exception e)
{
Debug.LogError($"[EventTokenService] ClearTokens failed: {e.Message}");
throw;
}
}
}
#endregion
#region 私有辅助方法
private void EnsureInitialized()
{
Initialize();
}
private static void PublishActivityEventTokenBalanceChanged(int eventId)
{
try
{
GContext.Publish(new Activity.ActivityEventTokenBalanceChanged(eventId));
}
catch
{
// GContext 未就绪等
}
}
#endregion
}
}