Files
ft/CodexOutput/upserver/客户端迁移详细修改方案.md

24 KiB
Raw Blame History

客户端迁移详细修改方案 v1

2026-09-16执行顺序更新:按用户要求优先确认PlayFab存档到KV的读写适配,当前阶段顺序与验收门以TODO.md为准。本文A/B/C保留作技术范围分组。KV联调所需的最小测试登录属于测试准备,不要求先重构完整游戏登录。

日期:2026-09-15。状态:技术评审完成,待用户确认本文的开发边界与行为裁决。

本文是当前修改方案入口,覆盖此前草案中被新 KV 实现替代的结论。本文不代表代码已实现或正式联调已通过。

1. 基线、目标与交付边界

  • 客户端:C:/projects/ft,feat/upserver,f40a816cc;工作区已有 RTService 的 SkipNegotiation + WebSockets 修改。
  • 服务端:C:/projects/N3-Server-CN,main,858fe7c;KV 功能提交 a3d3faa。
  • 正式 API:http://s101.local:18080/;Hub:http://s101.local:18080/gamehub。
  • Unity:2022.3.62f2c1;沿用现有 HttpClient、Newtonsoft.Json、官方 .NET SignalR 客户端。
  • 身份唯一来源:涂游 SDK 数字 userId,传输统一为十进制字符串。禁止把旧 PlayFab hex ID 转成数字后当作涂游 ID。

交付划分

批次 可审查交付 完成定义
A 正式地址、独立 KV 客户端、缓存与同步模块 配置与契约测试通过,KV 模块不依赖 PlayFab
B 涂游登录、KV 初始化、会话和实时连接接入 专用账号可完成登录、恢复本人存档、写后读和业务消息验证
C 好友/资料/排行/公告/匹配等外围迁移,移除 SDK 所有保留业务能力有替代实现,Unity/热更新编译与相关回归通过,包和程序集不再依赖 SDK

B 必须先关闭第 10 节列出的登录资料来源问题;C 不包含在“仅修改 KV 接口”的放行范围内。当前可完成 A 的具体实现;B/C 的条件不满足时不通过删除功能、空响应或临时生成假身份绕过。

2. 已核实的接口契约

2.1 登录

POST /account/Login,JSON 请求:

{"playerId":"10006464"}

playerProfile 是可选字段。首次读取存档前不上传本地默认等级/昵称/头像,避免 AccountService 将默认值写回已有玩家资料。

成功响应字段:state=0、internalId、createTime、svrTime、newCreate;成功时签发 Cookie。HTTP 200 不代表 state 成功。客户端接收专用 DTO,字段显式映射;日期按 UTC 解析。缺少必需字段、失败 state、非法 JSON 都不能进入游戏。

2.2 KV

全部为 POST、JSON、Cookie 鉴权。用户 ID 取 NameIdentifier,不放在 KV 请求 body 中。

路由 请求示例 成功响应 行为
playerdata/Get {} {"data":{"Lv":"12"}} 读取当前账号全部键值
playerdata/Get {"keys":["Lv","NewPlayerDataDic"]} {"data":{...}} 缺失 key 省略
playerdata/Set {"data":{"Lv":"13"}} 200,空正文 新增/覆盖提交的键,保留其他键
playerdata/Delete {"keys":["Lv"]} 200,空正文 删除指定键,缺失键无操作
  • Get 的 keys 省略/null/空列表均读全部;Delete 的空列表无操作;Set 的空字典无操作。
  • Get 成功必须有非 null 的 data 对象。空正文、data 缺失或格式错误不是空存档。
  • V 是字符串;JSON 对象由客户端先序列化成字符串,再放进 data。不能把整个存档对象直接替换成 HTTP data 对象的内部字段。
  • MySQL PlayerKV:主键 (PlayerId,K),K 最大128字符,V 为 longtext,UpdateTime 由服务端写入。
  • Get 不返回 UpdateTime 或版本。当前服务端无条件更新,没有条件版本写入/幂等请求号,不能承诺跨设备并发安全。
  • Set/Delete 以 HTTP 成功判定,不能用响应内容是否为空判定。

2.3 SignalR

保留 CookieContainer,WebSockets-only,SkipNegotiation=true,仍执行 SignalR 协议握手。业务登录用的 HttpClient 与 Hub 必须共享当前会话 Cookie,旧账号会话不能被新账号复用。

3. 模块与文件修改清单

以下是计划路径,尚未创建的源文件用“新增”注明;实施时不修改服务器源码。

文件(相对 Client) 改动职责
Assets/Scripts/AOTScripts/GConstant.cs 共用正式 API 默认常量
Assets/Editor/DebugConfig.cs 更新 s101 API 常量;菜单校验同时核对 API 字段
Assets/Scripts/Core/CustomServerMgr.cs 同一基址、显式 HTTP 结果、会话捕获/失效、请求取消;保留旧业务方法签名以控制影响面
Assets/Scripts/Services/PlayerData/PlayerKVModels.cs(新增) Get/Set/Delete DTO、业务错误种类、同步状态
Assets/Scripts/Services/PlayerData/PlayerKVClient.cs(新增) 远端请求、严格响应解析、空响应成功判定;无 Unity 生命周期逻辑
Assets/Scripts/Services/PlayerData/PlayerKVSyncState.cs(新增) 账号缓存、待写队列、序号、批次确认与三方比较;不依赖 MonoBehaviour
Assets/Scripts/Services/PlayerData/PlayerKVCacheStore.cs(新增) 按环境/账号隔离保存快照与待同步记录、加载校验、备份及写入失败报告
Assets/Scripts/Core/PlayFabMgr.cs B批切换时保留 MonoBehaviour 类名和 .meta GUID,作为业务兼容入口;内部委托新 KV 模块,同批替换登录调用的 UserDataRecord 签名。A批不激活替换,避免旧登录流程调用未初始化的新KV
Assets/Scripts/Services/UserService.cs 新登录 DTO、await 授权/KV 初始化、身份/时间/状态管理;PlayFab 调用按批次拆除
Assets/Scripts/Services/AggUserService.cs loginInfo.StrUserId 接入,成功/失败进度、绑定信息与资料初始化顺序
Assets/Scripts/Stages/PFLoginAct.cs、LoginStage.cs、LoginStageCtx.cs 明确涂游入口/编辑器入口,移除把 PlayFab Title 当初始化依据的条件;防止非 AGG 分支漏改
Assets/Scripts/Services/RTService.cs 已有直连选项;补首次连接注册、释放订阅和换号会话隔离
Assets/Scripts/SubBoostrap.cs 注册 KV 依赖,校验最终配置在服务构造前生效
Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs 立即同步失败后玩法状态/重试语义,不能仅日志忽略
Assets/Scripts/UI/Turntable/FishingTurntablePanel.cs 仅成功时设置 isDataSynced;重试同一存档值,不重复扣费

保留 PlayFabMgr 类名是为了避免所有场景和玩法引用一起重命名;这不代表继续依赖 SDK。最终包依赖检查与类名是否包含 PlayFab 分别判断。新代码使用业务命名。

4. 配置和身份规则

  1. API 与 Hub 均从 EventApiURL 解析,缺省用共用常量;URL 必须为绝对 HTTP(S),规范末尾斜杠后组合相对路由。
  2. 远端配置先加载、本地 debugconfig.json 后覆盖。实施需定位实际 persistentDataPath,仅改 EventApiURL;不得通过整套调试菜单顺带覆盖静态资源、RTM 和 Func 字段。
  3. 云配置发布不在客户端代码批次内。包上配置若仍提供旧值,默认常量不会覆盖它;验收需检查实际最终值。
  4. 发布客户端必须由涂游 SDK 登录获取 ID;ID 缺失或非法立即失败,不回退设备 ID、PlayFab ID 或随机 ID。
  5. 编辑器允许显式输入分配给联调的十进制测试 ID,不能把短数字默认拼接成真实用户,也不能把任意测试 ID 当作已完成渠道认证。
  6. 在字符串层保留 ID;需要验证范围时使用 long.TryParse + InvariantCulture。当前涂游包装字段为 int,不能通过自行改类型声称上游已支持 long。
  7. 清理三个已定位 hex 转换点:FriendService 邀请码、SurveyService 用户尾号、DinggingSceneData 随机种子;修改必须连同编码/解码及玩法可复现测试评审。
  8. 历史 JSON 字段 PlayFabId/FriendPlayFabId 仍按服务端现有契约使用,值是涂游 ID,不能单端改字段名。

5. 登录和退出时序

登录状态

Idle → ChannelAuthenticated → ApiAuthenticated → LoadingKV → Initializing → Ready。 任一步失败进入 Failed;存档冲突进入 Conflict;退出/换号增加 sessionGeneration,失效所有旧请求回调。

  1. 确认最终配置,初始化新的会话容器。
  2. 涂游登录成功,验证数字 ID;已有 SDK 登录态取 TYAccountInfo.userId。
  3. await account/Login;检查 HTTP、DTO、state;暂存身份及 Cookie。
  4. 用 svrTime 校时,用 createTime/newCreate 初始化账号生命周期标志。
  5. await playerdata/Get({}),将云端字典与当前账号本地记录按第6节比较。失败不 LoadPlayerData、不启动写入、不返回 Done。
  6. 缓存模块先就绪,允许 LoadPlayerData 与 Agg InitData 中现有同步调用记录变更;此时网络同步仍暂停。
  7. 完成必要资料来源处理及 LoadPlayerData、OnInitData,初始化成功后启动同步循环。
  8. 准备好聊天/邮件/实时订阅后发布现有 EvtAPISvrLoginSuccess,避免这些消费者先于存档初始化读取状态。内部 ApiAuthenticated 状态不通过旧公共事件提前广播。
  9. Login 返回 Done;SignalR 若连接失败独立报告实时连接状态。完整联调必须再确认其连接与业务收发,不能以 Done 代替实时成功。

重登和退出

  • 同一会话多个401/403合并为一次重登,登录请求本身不触发递归重登。
  • KV请求捕获发起时的 ID、会话 generation、Cookie 所属 HttpClient;旧请求不能在排队后使用新账号客户端发送。
  • 退出先停止接收业务写入并保存待同步记录,再停止/释放 Hub 和网络请求;不依赖应用退出时网络请求一定完成。
  • 换号创建新 CookieContainer;旧回调不得确认或修改新账号队列。
  • 已发出的旧请求即使取消,也可能在服务器提交;取消只表示客户端停止等待,不能解释成服务端未写入。

6. 没有服务器时间戳时的存档方案

6.1 路线评审

方案 优点 问题 裁决
云端全量覆盖本地 简单,适合干净首次登录 离线或失败待写修改丢失 仅用于没有待写记录时
本地时间较新就覆盖云端 接近旧实现 新API无远端时间,客户端时钟也不可靠 不采用
云端基线 + 待写日志 + 值的三方比较 保留失败修改,检测可见冲突,不需修改服务器 无法提供服务器版本锁、跨设备强一致性 推荐本期有限保证方案
服务端新增revision/条件写/幂等号 可解决并发覆盖和超时确认问题 超出已有API,需双方另行实现 多设备强一致需求的前置工作

推荐方案仅保证本客户端正常顺序发送的队列正确性;不承诺多设备并发或超时请求延迟提交下绝不回退。若本期要求后两者,裁决改为先补服务端契约,不能仅靠客户端重试满足。

6.2 本地持久化

使用版本化缓存文件,路径包含环境指纹和涂游 ID,例如 persistentDataPath/player-kv/v1//.json;originHash 由规范化 API origin 计算,避免主机特殊字符与跨服混用。

单份快照包含:formatVersion、环境、playerId、最后确认的云端键值、pending 修改及其本地序号/base存在标记/baseValue、本地专用值。每个键最多一个待写“最新目标值”,正在发送的批次使用独立不可变快照。

  • 文件写入临时文件后替换,保留上个完整备份;加载校验归属与格式。主文件损坏读备份;都损坏则报告恢复失败并保留原文件,不能静默当新账号。
  • 修改后尽快保存完整记录;更新方法仍为同步入口,优先保证待写记录持久化后再安排网络写入。写入失败进入 StorageError,立即同步返回失败,不能声称已可靠保存。
  • 不复制 LocalData_<旧PlayFabId>/LastUpdateTime_<旧PlayFabId>。即使旧 ID 恰好只含数字,也不能认定与涂游 ID 同源。
  • remoteStore=false 的值保存在 localOnly 区,仅本人本环境使用,不加入远端队列,避免和远端确认基线混为一谈。

6.3 每键比较规则

首次登录无本地pending:云端为基线,成功空字典表示服务器当前无键。业务默认值仅在存档模块初始化后按现有玩法规则生成。

有本地pending时,取 B=编辑前最后确认的云端值,L=本地待写值,R=本次Get值,存在/不存在单独记录;空字符串不等于不存在。

条件 处理
R == L 云端已具备目标值,确认该pending;适用于上次响应丢失后的常见恢复
R == B 未观察到云端变化,保留L并允许按队列提交
R与B、L均不同 标记Conflict,保留三份数据,不自动覆盖任何一侧
没有可靠B 除R==L可确认外均标记Conflict,不猜测先后

Conflict 状态不开放玩法写入,用现有错误/重试入口提示存档存在冲突;重试不自动丢弃本地记录。日志只记键名、错误类别和摘要,不输出完整存档。人工数据恢复工具不包含在本批。

字典值中的JSON采用字符串精确比较;序列化格式差异可能产生保守冲突,禁止为了减少冲突把未知业务JSON合并。

7. 写入队列与立即同步

正常批量

  • 保留40秒周期作为常规同步触发;每批最多10个key属于客户端节流策略,不宣称是新服务器限制。
  • 取消旧5个缓冲桶最多50个不同key的限制;新的待写字典不能静默丢弃第51个key。
  • 同一会话只允许一个KV变更HTTP请求正在等待;普通Set、立即Set、Delete共用排队入口。
  • 发送时复制 key/value/sequence,不将后续可修改字典直接交给请求。
  • 成功确认只移除当前pending中sequence与批次一致的项;若发送v1期间产生v2,v1确认后v2继续待写,并更新其base为已确认v1。
  • 失败保留目标值和基线,不把失败项伪装为已同步。

失败分类

情况 动作
未认证401/403 暂停写入、合并重登;恢复时重新读取并比较
400及明确无效请求 停止该项自动重试,报告key/类型;保留原始记录
连接错误/超时/500 写入结果可能未知,冻结后续写并进入重读恢复流程;不得立即发送反向扣款/回滚
Get也失败 保留本地状态并等待重连,不默认空云端
缓存持久化失败 StorageError,阻止新的需可靠存档的操作

恢复重读可以发现很多“已提交但响应丢失”的情况,但不能排除旧请求稍后提交,也不能提供服务器事务序号保证。并发及超时严格一致性仍受第6.1节边界约束。

SyncDataImmidiate 兼容语义

保留现有签名 Task SyncDataImmidiate(string key,string data),内部以同一队列立即调度;true只在该请求目标已获远端成功确认时返回。失败或结果不确定返回false,同时保留状态和待恢复记录。

调用者不能在超时后重新执行一次“减余额”生成新值,应重试已捕获的目标存档。false不等于服务端未执行。

  • 捕鱼:原代码先本地扣体力再发请求;需要冻结该操作的重复触发,在未确认时走恢复/重试,禁止简单补回再重复扣导致双写。该玩法状态处理与KV入口一起验收。
  • 转盘:仅true时设置isDataSynced;false走重试/恢复,不因返回了布尔值就解锁结果。
  • KV存储不是扣款事务接口,不能承诺支付或扣道具的exactly-once语义;如玩法需要此保证,应由服务器业务接口提供操作幂等性。

删除

PlayerKVClient 提供 DeleteAsync 以覆盖现有服务器契约,写入规则同样按key顺序串行;删除意图用显式标记,不将null或空字符串当作删除。当前无业务删除调用时不新增玩法入口;删除恢复也用存在标记做三方比较。

8. HTTP 层与接口建议

新增结构化返回:Success、StatusCode(无HTTP响应时为空)、ErrorKind、Body,不改变已有CustomServerPost的string/null返回契约以避免扩大影响。

// 方案接口,尚未实施;所有请求绑定调用时的会话。
Task<KVGetResult> GetAsync(IReadOnlyList<string> keys, CancellationToken token);
Task<KVWriteResult> SetAsync(IReadOnlyDictionary<string, string> data, CancellationToken token);
Task<KVWriteResult> DeleteAsync(IReadOnlyList<string> keys, CancellationToken token);

KVGetResult = 成功标志 + Dictionary<string,string> Data + HTTP/错误信息;KVWriteResult = 成功标志 + HTTP/错误信息。Get解析失败必须失败,Set/Delete不解析空正文。所有例外映射到取消、网络、HTTP、协议四类,不吞异常返回空存档。

沿用15秒单次请求超时;恢复重试使用有上限的退避(2/5/10/30秒),仅在会话有效且无Conflict/StorageError时继续。退出取消所有后台循环,Task.Delay带取消令牌。

9. SignalR 配套修正

  1. 保留已修改的两个直连选项及Cookie。
  2. 在每次新HubConnection创建后,先注册全部已有业务处理器,再StartAsync;不把首次注册仅挂在Reconnected。
  3. 同一HubConnection重连保留已有处理器,不重复注册;新连接则从持久订阅表重建。
  4. WaitForAPISvrLogin保存/释放订阅,重复调用不重复建连;Release等待停止/释放或由会话协调器观察其完成。
  5. 会话更新先结束旧Hub,再绑定新Cookie重建;旧连接回调按generation过滤。
  6. 用服务器已有Test(arg1,arg2)返回OK验证请求/响应;推送验证使用测试环境已有消息,不调用生产强制下线管理接口。

10. 登录资料与SDK彻底移除的剩余裁决

新增KV解决存档承接,但不自动生成旧PlayFab提供的所有字段。

能力 当前事实 本方案裁决
本人昵称/头像 登录不返回;SearchFriend可返回基础PlayerInfo,但缓存失败可能返回Unknown默认数据 可作为查询候选,禁止把默认值当可靠资料再回写;B批需确定权威来源或扩展登录资料响应
GetHomePage 返回好友邀请/推荐列表 明确排除为本人资料恢复接口,修正旧文档候选建议
上次登录时间 LoginResp不返回 优先服务端返回登录前时间;若采用新KV键记录需明确首次缺失策略,不将当前时间冒充历史时间
地区 LoginResp和KV均不自动提供 地区榜适配前确定来源,不默认伪造国家/大洲
渠道绑定 SDK与旧KV均有标志 渠道SDK为账号绑定权威;旧KV标志不得自行升级认证状态
好友 有GetFriendsList,Friends是JSON字符串数组 可迁移;Level直接映射,LastLogin缺失需单独展示裁决
他人KV 新KV仅限本人 GetPlayLv等改用资料查询;不向私有KV接口增加任意playerId
普通/地区等级榜 当前服务端有活动榜 不能直接互换,需要等价契约或明确产品范围
公告 未确认等价接口 C批条件未满足
PlayFab匹配票据 DuelRT不是票据服务 C批条件未满足
EntityKey 新登录不提供 移除其真实消费者后才能删除;不能构造伪EntityKey骗过编译

因此SDK包删除必须最后执行。阶段内SDK包暂留不代表整体目标已完成,也不允许正式进入仍按旧身份调用PlayFab的功能。

11. 验证矩阵

新增纯逻辑测试放入 Client/Assets/Tests/PlayerData/(新Editor测试程序集;引用按现有HotUpdate与Unity Test Framework定义),测试核心状态机使用内存远端/缓存替身;测试不用硬编码正式账号。

编号 场景 明确预期
L01 涂游ID缺失/非数字/超范围 不发业务登录,不调用PlayFab回退
L02 HTTP200但state失败或字段缺失 不发布登录成功,不加载KV/玩法
L03 登录成功、KV失败 不返回Done,不生成空存档写回
L04 KV成功、初始化失败 不启动网络写队列/登录成功事件
K01 Get {} 返回data={} 成功空存档,与网络失败区分
K02 指定keys含缺失项 只返回已有项;不构造值为null的伪记录
K03 Set/Delete 200空正文 成功;其他HTTP错误均保留待写
K04 发送v1时编辑v2 v1确认不删除v2;最终再次提交v2
K05 51个以上不同key 全部记录并可分批同步,无静默丢失
K06 请求已提交但响应丢失,重登R=L 恢复确认,无二次扣减操作
K07 R=B与R不同于B/L 前者保留待写;后者Conflict且不写云端
K08 空值、缺失、删除 三种状态区分,删除缺失键幂等
K09 remoteStore=false 保留本地专用值,不上传
K10 本地文件损坏/磁盘写失败 备份恢复或StorageError,不重置存档
K11 A账号/服切到B,A请求晚返回 B缓存/队列/会话不受影响
K12 暂停/恢复/退出 单同步循环、待写已保存,不依赖退出HTTP完成
G01 转盘立即写失败 不设置isDataSynced,不重复生成扣减
G02 捕鱼扣体力结果未知 进入恢复,不补回后重扣
S01 连接前已订阅事件,首次连接收到推送 首次即可处理,重连无重复回调
S02 WebSocket直连及重连 无negotiate请求,Cookie鉴权、Test返回OK

正式联调用已分配测试账号:写独立测试key → Get核对 → 清除该账号本地测试缓存影响并重新登录读取 → Delete测试key → 再读确认不存在。只清理本次创建的测试key,不清空真实玩家存档。记录客户端版本、目标地址、部署版本、迁移状态、HTTP状态与脱敏结果。

测试分层:纯逻辑测试通过 → Unity脚本编译 → 测试环境HTTP+MySQL持久化 → 正式部署专用账号验证。服务端现有7个InMemory测试只能证明其覆盖的单进程语义;数据库主键/事务、HTTP授权和部署迁移需实际验证。

12. 执行任务与验收门

  • A1:配置入口修改,验证最终API/Hub基址,保留其他服务地址。
  • A2:DTO/HTTP/KV客户端实现,覆盖Get解析、200空响应、授权与超时分类测试。
  • A3:纯KV状态与持久化实现,先覆盖K04~K12失败用例,再接MonoBehaviour。
  • A4:审核所有CompareDatas/立即同步/remoteStore=false调用,以测试宿主验证新KV入口;保留当前生产登录及PlayFabMgr调用链,防止半切换破坏已有初始化。
  • B1:关闭第10节资料来源条件,完成身份/登录阶段/SDK回调具体映射的补充评审。
  • B2:同一批次切换PlayFabMgr内部与登录调用签名,按第5节接入登录与会话、按第9节修正实时订阅,运行L/S用例。
  • B3:审核捕鱼与转盘完整交互路径,运行G用例,验证恢复不会重复消费。
  • B4:专用账号KV与实时端到端验收,输出脱敏记录。
  • C1:为剩余SDK直调功能确定替代接口与产品裁决。
  • C2:移除所有运行/编译依赖,清理manifest、lock、asmdef GUID、StripHint、link和热更新输入;Unity重新生成派生文件。
  • C3:执行相关玩法回归和热更新构建验证,全部通过后才能宣称SDK移除完成。

13. 评审裁决与待确认项

评审项 裁决 依据
正式地址/涂游身份/三项KV路由/Cookie 已核实 用户确认及服务端源码
空响应成功、全量读空keys、本人隔离 通过 DTO/Controller/Service实现一致
兼容业务入口、独立KV模块、失败不丢队列 通过 控制影响面并覆盖现有可靠性缺口
无时间戳缓存恢复 条件通过 用户接受第6节有限保证与冲突阻止写入;不是跨设备强一致
A批开发边界 技术评审通过,待用户确认 本文给出职责、契约、失败处理和验收;不要求改服务端
B批完整游戏登录 条件未关闭 昵称/头像、历史时间、地区与仍存SDK消费者需明确处理
C批彻底删除SDK 暂不放行 公告/等级榜/匹配等尚无完整替代裁决
执行结果 未执行 本次仅文档,已有SignalR选项修改保持不变

建议本次确认:先实施A批;采用第6节“云端基线+待写日志+冲突阻止自动覆盖”,接受现有接口有限并发保证。若要求多设备/超时条件下严格防覆盖,则先增加服务端版本条件写入,再放行存档模块。