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,461 @@
using game.PlayerDataStorage;
using asap.core;
using asap.playfab.async;
using GameCore;
using PlayFab.ClientModels;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace game
{
public class FriendService
{
[Inject]
public IUserService userService { get; set; }
[Inject]
public ICustomServerMgr customServerMgr { get; set; }
[Inject]
public IRTService rts { get; set; }
[Inject]
public cfg.Tables tables { get; set; }
[Inject] public IPlayerSaveStore SaveStore { get; set; }
public bool IsCurrent => SaveStore.IsCurrent && !userService.LoginLifetime.IsCancellationRequested;
bool linkRewardPending;
public const string InviteLinkRewardCountKey = "InviteLinkRewardCount";
IDisposable linkCompletedDisposable;
public void GetFriendData()
{
//GetFriendLeaderboard();
GetFriendList();
//GetInvitedListCount();
}
#region 邀请Link相关
public int InviteLinkNumber { private set; get; }
int InviteLinkRewardCount = 0;
void SetLinkAwardData()
{
List<cfg.Invite> invites = tables.TbInvite.DataList;
//已经领取奖励的数量
var read = SaveStore.Read(InviteLinkRewardCountKey);
var value = read.ValueOrThrow();
if (read.Found == false) InviteLinkRewardCount = 0;
else if (!int.TryParse(value, out InviteLinkRewardCount) || InviteLinkRewardCount < 0 || InviteLinkRewardCount > invites.Count)
throw new InvalidOperationException("Invalid invitation reward count.");
int canRewardCount = InviteLinkRewardCount;
for (int i = InviteLinkRewardCount; i < invites.Count; i++)
{
//领取奖励
if (invites[i].NumInvite <= InviteLinkNumber)
{
canRewardCount = i + 1;
}
else
{
break;
}
}
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Invite, canRewardCount > InviteLinkRewardCount);
#if AGG
using (var e = GEvent.GameEvent("invite_increase"))
{
e.AddContent("inviteprogress", InviteLinkNumber)
.AddContent("rewardprogress", InviteLinkRewardCount);
}
#endif
}
public async Task<int> OnReceiveLinkAward()
{
if (!IsCurrent) throw new OperationCanceledException();
if (linkRewardPending) throw new InvalidOperationException("Invitation reward is pending or requires recovery.");
SetLinkAwardData();
int canRewardCount = InviteLinkRewardCount;
List<cfg.Invite> invites = tables.TbInvite.DataList;
List<int> DropIDs = new List<int>();
for (int i = InviteLinkRewardCount; i < invites.Count; i++)
{
//领取奖励
if (invites[i].NumInvite <= InviteLinkNumber)
{
canRewardCount = i + 1;
DropIDs.Add(invites[i].DropID);
}
else
{
break;
}
}
if (InviteLinkRewardCount != canRewardCount)
{
linkRewardPending = true;
var items = GContext.container.Resolve<PlayerItemData>();
var player = GContext.container.Resolve<PlayerData>();
await PlayerSaveAction.RunAsync(SaveStore, "invite-reward", () =>
{
items.AddItemByDropList(DropIDs, null);
PlayerSaveAction.Present(() => GContext.Publish(new ShowData()));
InviteLinkRewardCount = canRewardCount;
SaveStore.Write(InviteLinkRewardCountKey, InviteLinkRewardCount.ToString()).RequireAccepted();
}, () => player.SavePlayerData(), userService.LoginLifetime);
if (!IsCurrent) throw new OperationCanceledException();
linkRewardPending = false;
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Invite, false);
#if AGG
using (var e = GEvent.GameEvent("invite_reward"))
{
e.AddContent("inviteprogress", InviteLinkNumber)
.AddContent("rewardprogress", InviteLinkRewardCount);
}
#endif
}
return InviteLinkRewardCount;
}
#endregion 邀请Link相关
#region 判断是否好友
public bool IsFriend(string playFabID)
{
if (m_FriendList == null || m_FriendList.Count < 1 || string.IsNullOrEmpty(playFabID)) return false;
foreach (var friend in m_FriendList)
{
if (friend.playFabId.Equals(playFabID))
{
return true;
}
}
return false;
}
#endregion
#region 获取好友列表
private List<LeadboardItemData> m_FriendList = new List<LeadboardItemData>();
public List<LeadboardItemData> FriendList { get { return m_FriendList; } }
public bool IsFriendListRefreshing { get; private set; } = true;
public async void GetFriendList()
{
GetFriendsListRequest getFriendsListRequest = new GetFriendsListRequest
{
ProfileConstraints = new PlayerProfileViewConstraints()
{
ShowAvatarUrl = true,
ShowDisplayName = true,
ShowStatistics = true,
ShowLastLogin = true,
}
};
var result = await PlayFabClientAsyncAPI.GetFriendsListAsync(getFriendsListRequest);
var list = result?.Friends;
if (list != null)
{
string userId = GContext.container.Resolve<IUserService>().UserId;
List<LeadboardItemData> leadboardList = new List<LeadboardItemData>();
int count = list.Count;
foreach (FriendInfo friendInfo in list)
{
if (friendInfo.Profile == null) continue;
var listStatistic = friendInfo.Profile.Statistics;
if (listStatistic == null) continue;
if (userId.Equals(friendInfo.FriendPlayFabId))
{
continue;
}
if (string.IsNullOrEmpty(friendInfo.Profile.DisplayName))
{
friendInfo.Profile.DisplayName = userService.GetDefaultName(friendInfo.FriendPlayFabId);
}
int level = 0;
foreach (var item in listStatistic)
{
if (item == null) continue;
if (item.Name == LeadboardData.leadboardLevelName)
{
//level = item.Value / LeadboardData.LV_MODELING;
level = item.Value;
break;
}
}
userService.SetPlayInfo(friendInfo.FriendPlayFabId, friendInfo.Profile.DisplayName, friendInfo.Profile.AvatarUrl);
LeadboardItemData friendData = new LeadboardItemData();
friendData.playFabId = friendInfo.FriendPlayFabId;
friendData.displayName = friendInfo.Profile.DisplayName;
friendData.avatarUrl = friendInfo.Profile.AvatarUrl;
friendData.LastLogin = friendInfo.Profile.LastLogin;
friendData.value = level;
friendData.position = 0;
friendData.rank = 0;
leadboardList.Add(friendData);
}
leadboardList.Sort((item1, item2) =>
{
return item2.value.CompareTo(item1.value);
});
count = leadboardList.Count;
for (int i = 0; i < count; ++i)
{
LeadboardItemData itemData = leadboardList[i];
itemData.position = i;
itemData.rank = i + 1;
}
m_FriendList = leadboardList;
IsFriendListRefreshing = false;
GContext.Publish<SocialFriendListChangedData>(new SocialFriendListChangedData());
#if AGG
GEvent.SetProp("friends_count", count);
GEvent.SetUserProp("friends_count", count);
#endif
}
}
#endregion
#region 邀请好友
public async Task<string> AddFriend(string playfabID)
{
int count = m_FriendList.Count;
AddFriendReq getFriendListReq = new AddFriendReq()
{
MySelf = new BFriendInfo
{
PlayFabId = userService.UserId,
AvatarUrl = userService.AvatarUrl,
DisplayName = userService.DisplayName,
Level = GContext.container.Resolve<PlayerData>().lv
},
MyFriendCount = count,
FriendPlayFabId = playfabID,
};
FriendHelper.FixBFriendInfoData(getFriendListReq.MySelf);
return await customServerMgr.FriendRequest("AddFriend", getFriendListReq);
}
#endregion
#region 删除好友
public async Task<bool> DeleteFriend(string friendPlayfabID)
{
var req = new FriendDeleteReq()
{
FriendPlayFabId = friendPlayfabID,
};
var json = await customServerMgr.FriendRequest("DeleteFriend", req);
if (string.IsNullOrEmpty(json)) return false;
var resp = Newtonsoft.Json.JsonConvert.DeserializeObject<FriendDeleteResp>(json);
if (resp.Code == FriendHelper.Code_OK)
{
m_FriendList.RemoveAll(f => f.playFabId == friendPlayfabID);
GContext.Publish<SocialFriendListChangedData>(new SocialFriendListChangedData());
return true;
}
return false;
}
#endregion
#region 搜索好友
public async Task<string> SearchFriend(string playfabID)
{
SearchFriendReq searchFriendReq = new SearchFriendReq()
{
PlayFabId = playfabID
};
return await customServerMgr.FriendRequest("SearchFriend", searchFriendReq);
}
#endregion
#region 接受好友邀请/忽略好友邀请
// status:1:同意邀请;2:忽略, playfabID 接受/忽略 friendPlayFabID 的邀请
public async Task<string> AcceptFriend(int status, string playfabID, string friendPlayFabID)
{
AcceptFriendReq acceptFriendReq = new AcceptFriendReq()
{
Status = status,
PlayFabId = playfabID,
FriendPlayFabId = friendPlayFabID
};
return await customServerMgr.FriendRequest("AcceptFriend", acceptFriendReq);
}
#endregion
#region 好友主页
public async Task<string> GetHomePage(string playfabID, int pageNo)
{
GetHomePageReq getHomePageReq = new GetHomePageReq()
{
PlayFabId = playfabID,
PageNo = pageNo
};
return await customServerMgr.FriendRequest("GetHomePage", getHomePageReq);
}
#endregion
#region 好友邀请列表&推荐列表
public async Task<string> GetRecommendedFriends(string playfabID, int pageNo)
{
GetRecommendedFriendsReq acceptFriendReq = new GetRecommendedFriendsReq()
{
PlayFabId = playfabID,
PageNo = pageNo
};
return await customServerMgr.FriendRequest("GetRecommendedFriends", acceptFriendReq);
}
#endregion
#region 获取申请成为我的朋友的列表
public async Task<string> GetInvitedList()
{
return await customServerMgr.FriendRequest("GetInvitedList", "");
}
#endregion
#region 获取邀请自己的列表数量
public async void GetInvitedListCount()
{
if (!IsCurrent) return;
string json = await customServerMgr.FriendRequest("GetInvitedListCount", "");
if (!IsCurrent) return;
if (!string.IsNullOrEmpty(json))
{
GetInvitedListCountResp getRecommendedFriendsResp = Newtonsoft.Json.JsonConvert.DeserializeObject<GetInvitedListCountResp>(json);
if (getRecommendedFriendsResp != null)
{
if (getRecommendedFriendsResp.FriendInvitedCount >= 0)
{
await Awaiters.NextFrame;
if (!IsCurrent) return;
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Friend_Add, getRecommendedFriendsResp.FriendInvitedCount > 0);
}
else
{
Debug.LogWarning("[FriendService::GetInvitedListCount] FriendInvitedCount Failed:");
}
if (getRecommendedFriendsResp.DeepLinkInvitedCount > 0)
{
InviteLinkNumber = getRecommendedFriendsResp.DeepLinkInvitedCount;
SetLinkAwardData();
}
}
}
}
#endregion
#region 邀请码处理
const string chars = "0M4KL3FZSTG8XCY5E6VW179A2UBDHQRIJNOP";
readonly ulong baseLength = (ulong)chars.Length;
public string EncryptUlongToInvitationCode(ulong number)
{
StringBuilder encoded = new StringBuilder();
while (number > 0)
{
encoded.Insert(0, chars[(int)(number % baseLength)]);
number /= baseLength;
}
return encoded.ToString().PadLeft(14, chars[0]); // Pad with '0' if necessary
}
public ulong DecryptInvitationCodeToUlong(string invitationCode)
{
ulong number = 0;
foreach (char c in invitationCode)
{
number *= baseLength;
number += (ulong)chars.IndexOf(c);
}
return number;
}
public string GetInvitationCodeByPlayFabID(string playFabID)
{
ulong code = Convert.ToUInt64(playFabID, 16);
string invitationCode = EncryptUlongToInvitationCode(code);
return InsertFormat(invitationCode, 3, "-");
}
public string InsertFormat(string input, int interval, string value)
{
for (int i = interval; i < input.Length; i += interval + 1)
input = input.Insert(i, value);
return input;
}
#endregion
#region 获得X天内登录过的活跃好友数量
public int GetActiveFriendCount(int xDays)
{
return FriendList.Count(data =>
data.LastLogin is not null &&
ZZTimeHelper.UtcNow() - data.LastLogin <= TimeSpan.FromDays(xDays));
}
#endregion
#region 判断当前用户是否处于被邀请状态
public async Task<string> IsFriendWaitingForAccept(string playfabID, string friendPlayfabID)
{
IsFriendWaitingForAcceptReq req = new IsFriendWaitingForAcceptReq()
{
PlayFabId = playfabID,
FriendPlayFabId = friendPlayfabID
};
return await customServerMgr.FriendRequest("IsFriendWaitingForAccept", req);
}
#endregion
#region 后台操作
List<FriendAddPanelBackendOperation> m_FriendAddPanelBackendOperationList = new List<FriendAddPanelBackendOperation>();
public void StartInvite(string playfabID)
{
var backendOperation = new FriendAddPanelBackendOperation();
m_FriendAddPanelBackendOperationList.Add(backendOperation);
backendOperation.StartInvite(playfabID, OnInviteCallback);
}
public void OnInviteCallback(FriendAddPanelBackendOperation backendOperation, string playfabID, int code)
{
if (backendOperation != null)
{
backendOperation.Clear();
m_FriendAddPanelBackendOperationList.Remove(backendOperation);
}
}
#endregion
public void Release()
{
linkCompletedDisposable?.Dispose();
linkCompletedDisposable = null;
m_FriendAddPanelBackendOperationList.Clear();
}
}
}