Files
ft/CodexOutput/upserver/B3BatchMigration/M2/M2-合批实施方案工作稿-2026-09-29.md
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

119 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M2 合批实施方案工作稿
日期:2026-09-29。状态:**开发侧连续准备中,未送审、未获新代码白名单**。本稿把独立 KV 用户服务、CP2B 首场景及同合同 CP3 调用放在一份方案中;不为每个接口或第 31 模块单独送审。遵守[合批节奏裁决](M2-合批审核节奏纠偏-2026-09-29.md)。现有 CP2B-T 运输限定关闭不等于首场景功能已验收。用户进一步裁决:原游戏服接口按原逻辑接线,只需服务端集中确认接受新的玩家 ID,不逐项追索旧接口全套合同。
**本期总迁移规则**:在 KV 模式,原来经 PlayFab 读写的玩家存档、玩家统计等数据改经 KV 读写;从现有 KV 数据能推导的值不盲目重复存储。现有 PF 模式保留原 SDK 路线。上述规则已由用户确定,不再逐个字段追问“要不要迁 KV”。逐调用仍需在完整方案中落实读写时点、失败/Unknown、奖励票据与回归,不能以静默跳过充当迁移。
## 目标和当前安全门
同一个 `SaveBackend` 启动选择,在创建用户服务、存档和登录前冻结。PF/AGG 继续使用 `PlayFabUserService → PlayFabMgr → PlayFab SDK`;KV 使用独立用户服务、31 模块同步 Apply 与原 `PlayerKVGameSessionHandle`,只准 101/102 已有 KV 档。**用户已定 KV 使用新玩家 ID;来源是认证会话的 `PlayerKVAccountDescriptor.PlayerId`,旧 PlayFab ID 只留在 PF 模式,不做映射。用户也已定等级分数存 KV、从 KV 读取。** 两个模式均须回归,KV 不伪造 PlayFab 身份、不静默回退。首场景仍有跨玩家等级榜聚合读取和客户端完整方案未定,`LoginStage` 的 KV 跳场景闸保持;本稿不授权提前解除。
## 同一启动链上的代码事实
| 次序 | 当前入口 | 合批必须处理的依赖 |
|---|---|---|
| 1 启动 | `SubBoostrap.RegisterServices` 捕获 `PlayerKVRuntimeSelection`;KV 不创建 `PlayFabMgr` | `LoginStage` 仍向 `IUserService` 注册旧 PF 类;正式 KV 被其 partial 承载。`SaveBackend` 在旧类登录时又读取,需消除二次决策。AGG 仍只准 PF。 |
| 2 登录与加载 | `PFLoginAct` 向 `PlayFabUserService` 下转设置 `CustomId`、读取 `IsFormalKV`/错误/交接状态;`UserService.KVLoad` 管 Coordinator、31 模块、原 handle、401/退出 | 独立 KV 类及第31模块窄接收契约必须保持 Guard/Apply/回滚/通知/epoch。`SmallGameData` 构造函数要求 `IUserService`,不能只新增窄登录接口而忽略 DI。PF/Editor `kv-loop`/受控验证保留真实消费者,迁移后闲置 partial 最终清理。 |
| 3 登录后入口 | `PFLoginAct` 的 KV 分支在 Mail/IAP 前返回;`LoginStage.RequestNextAct` 对 KV 不跳 `FishingStage` | 若合批开放首场景,需同时恢复 Mail/IAP/RT 的明确行为;不能只删阶段闸。`QuitAct` 仍下转旧类观察 KV 关闭,也须随独立服务改造。 |
| 4 场景同步初始化 | `FishingStage.StartAsync` 等授权最多 5 秒,随后 `InitData()`、`InitManager()`,注册 `SurveyService`;`InitData` 发 `LoginDays` 事件 | KV 公开登录态须在当前新 `PlayerId` 会话与原 handle 真正就绪后发布;动态活动订阅与存档写需要配置命中核查。Survey 构造即读旧 `PlayFabMgr` 两键;`SmallGameData` 等初始化可能写旧存档。 |
| 5 场景自动网络 | `LeadboardData.SetLvData` 直调 PlayFab 统计;随后 `SurveyService.GetSurveyMatchlist` 以旧 `UserId` 十六进制抽样;`AntiCheatManager.CheckBan`、`FriendService.GetInvitedListCount`、AIHelp/AutoRestart | 客户端用新 `PlayerId`,问卷抽样改为新 ID 的稳定规则,个人等级分数优先从现有 KV 的 `Lv`/`InfiniteBuildingData` 推导;全服/地区前 50 名需 KV 数据的跨玩家聚合读取入口。现有游戏服请求沿用原逻辑,功能回归时验证。 |
| 6 条件与恢复 | 已入 Club 时 `GetSelfClub`、Mail Fetch、RT 通知、暂停恢复触发重连及再次 Fetch | 每条请求记录所属 epoch/SessionVersion;401/403、退出与旧回包丢弃;失败不得清已知状态或重复奖励。 |
## 与首场景同批迁移的存档调用
[224 项调用矩阵](M2-当前调用矩阵-待核证.json)仍是语法候选,不是批准清单。本稿首场景已知最短链包括 `SurveyService` 的 `B3-C179/C180` 两键读和 `B3-C181/C182` 已读/领奖写;写奖励前需确定原票据、Unknown 和重复领取合同。`SmallGameData` 的 `B3-C087/C088/C089` 三种小游戏次数写在相应玩法触发时发生,不是首场景同步必写,待同业务动作归并。`FriendService` 的 `B3-C175/C176` 邀请奖励读写也须随领取链归并,不能因 `GetInvitedListCount` 查询自动扩大到其领奖。`UserService.cs` 中 B3-C183–C190 属 PF 资料/初始化兼容或 GM,不能机械替换并误删 PF 路线。
首场景 `LoginDays` 事件可能触发 `BattlePassData` 的 `BattlePassRecordTask` 写;还需从实际配置与订阅者列出其它激活写入。CP3 同批范围最终按**可达业务动作**而非文件或 `PlayFabMgr` 方法名定,读、内存变化、写、票据、失败停点一并审。其余约 200 候选待按已确认合同合批迁移,不列作本次首场景自动必需项,也不默认获得编码许可。
## 一次前审前需定的边界,不拆成小审查
| 事项 | 当前证据 | 裁决或待处理 |
|---|---|---|
| 新玩家 ID 的客户端接线 | CP2A 原 handle/101、102 有档准入,`PlayerKVAccountDescriptor.PlayerId` 已承载认证会话的新 ID | **ID 来源和不映射旧 PlayFab 已由用户定下**;客户端仍须把 `IUserService`、反作弊/俱乐部/问卷等旧消费者接到新 `PlayerId`,在会话与原 handle 就绪后发布登录态,并把 `EntityKey` 留给 PF 路线。这是实现工作,不再向服务端反复问“用哪个 ID”。 |
| Mail | `mail/fetch`、`mail/send`、`mail/updateState` 请求体均无 PlayFab ID,认证会话 Cookie 承载请求身份;`MailCache.Load(UserId)` 只用 ID 隔离本地缓存和同步游标 | 用户已定沿用原邮件流程;KV 以会话新 `PlayerId` 初始化本地邮件键,PF 保持原键。无需因 ID 更换单独等待邮件新合同;实际线上功能仍须验收,若返回有具体差异再集中处理。 |
| AntiCheat/Friend/Club | 客户端路由、请求字段及既有 PF 模式解析见[客户端矩阵](M2-CP2B客户端首场景线上合同待核矩阵-2026-09-29.md) | 这些是普通游戏服 API,不是 PlayFab SDK;显式玩家 ID 在 KV 模式使用新 `PlayerId`,其余逻辑沿用原合同。服务端只需集中确认接受新 ID;具体协议差异出现后再处理。 |
| Survey/等级榜 | 当前问卷和等级榜直接依赖旧 PF 存档/ID/SDK,代码可见;`PlayerKVClient` 只访问认证账号自己的 KV;存档已有 `Lv`/`InfiniteBuildingData` | 客户端用新 `PlayerId` 与 KV 替代问卷存档;个人等级榜分数优先从现有 KV 字段按原算法推导,避免未经评审重复存储。全服/地区前 50 名不能仅由个人 KV 计算,服务端开发侧提供以 KV 为数据源的聚合读取入口及最小调用方式。活动榜已有游戏服 `event/GetLeaderboards`,按旧流程继续。领取幂等和保存顺序在客户端同批设计。 |
| 动态活动与支付 | `FishingStage.InitData/InitManager`、IAP/RT/暂停恢复有条件动作 | 客户端核当前配置命中的订阅链、同批首场景最早写入及奖励失败/Unknown、AGG 的提供方政策;既有游戏服调用沿用原行为并回归。 |
对原游戏服接口,不再把完整线上文档作为编码前置条件;服务端只需确认新 ID 被接受,客户端按原协议接线并验收。等级榜的**存储选择已定为 KV**;尚缺的是从多账号 KV 汇总全服/地区前 50 名的读取入口,不能用本机服务器源码或历史合成 200 冒充线上验收。
跨端问题已收窄为[集中确认项](M2-客户端致服务端线上合同集中问题单-2026-09-29.md);客户端 Agent 不读取或修改服务器代码。
## 实施与验证顺序(待一次完整前审裁决)
### 预审后的确定实现选择
本节依据[预审主会话逐项裁决](M2-合批预审主会话裁决-2026-09-29.md),覆盖上文和 DP1 底稿中的同题未决句。不是额外小批,不凭本节先改代码。
1. 正式 KV 新类实现 `IUserService`,满足 SmallGameData 构造和 ClubService 属性注入;不继承 PlayFabUserService。`CustomId` 在共用接口上增加 setter,两个既有具体类均已支持该 setter,登录输入直接写共用接口。
2. 第 31 模块窄契约只含 `ResetForKVLoad`、`ReplaceFromKVDecoded(state, epoch, raw)`、`DiscardKVIntents`,消费者只有 `PlayerKVGameplayBindings.Bind`;旧类供受控/kv-loop,新类供正式 KV。初始化意图计数供新类自身编排读取,不为此扩成另一套存档接口。
3. 生命周期窄契约含 `KVHandoffReady`、`KVLastError`、`CloseKVAsync`、`ObserveKVQuitAsync`,消费者只有 PFLoginAct/QuitAct;原 handle 继续由现有 Coordinator 提供,不经此契约暴露给任意调用者。模式判断只读冻结 RuntimeSelection;失败无 PF 回退。
4. 正式 KV 登录只信启动冻结选择,不再从 config 重读 SaveBackend 作第二次后端决策。PF/AGG/kv-loop 原入口保留;静态重扫清除旧 PF 类搬迁后闲置 formal 分支。
5. `GetDefaultName` 对非空 ID 取最多前 5 字符;短 ID 101/102 保留全长,空输入采用既有 UI 默认名且不访问网络。Survey 的 KV 抽样按规范十进制 ID `%10`,对应现有 TailNumberList;PF 的十六进制算法保持。两模式分别测试,不能误用 `%16`。
6. 地区码从 KV 资料的 `ContinentCode` 读取;空/非法表示未知,地区榜查询入口显式处理该状态,不能与全局榜同名,也不从系统语言推断玩家地理位置。地区榜功能目标保留,真实地区资料和 KV 聚合读取到齐后验收;这属于实现依赖,不再向用户询问是否改用 KV。
### IUserService 成员责任与身份时序
| 成员 | KV 来源与行为 |
|---|---|
| Login/Logout/LoginLifetime/CustomId | 新服务持有正式登录编排和取消;保留现有单发、退出撤销及关闭失败屏障。CustomId 为登录输入,新 PlayerId 为认证结果,二者职责区分。 |
| UserId/ApiPlayerId/InternalId/IsLogin/IsAuthorized | 原 handle、当前 epoch 和普通请求租约就绪后,发布 AccountDescriptor.PlayerId/InternalId;公开态先失效再执行退出/401 的异步关闭,旧回包不得重置。 |
| CreateTime/LoginTime/CreateTotalSeconds | CreateTimeUtc/ServerTimeUtc 已由 S101AccountSession 解析并进入 AccountDescriptor;只从当前成功加载的同一描述对象取得,CreateTotalSeconds 使用真实创建时间。需在 Coordinator 成功结果中携带该不可变描述,失败结果不携带可发布身份。 |
| LastLoginTime | 从 KV 保存的上次登录记录取得;缺失为未知,不伪造成创建时间或当前时间。TimeRefreshManager 已有 nullable 读取入口,须将缺失映射为 null,并沿现有首次登录分支。当前登录时间的保存及确认时点在完整写入清单中固定。 |
| DisplayName/AvatarUrl/Language/EAccoutUrl | 本人资料从 KV 读取;无昵称用安全默认名,无头像按现有默认头像规则,语言采用已有玩家设置;绑定信息仍用模块31已解码数据。写资料走原 handle 的 KVMgr 与票据确认,Unknown 不发布成功。 |
| ContinentCode | KV 资料的真实地区值;未知状态不冒充任何已知地区、不拼接全局榜名。 |
| ChangeDisplayName/UpdateAvatarUrl/UpdateLanguage/UpdateProfile/UpdateProfileLv | 本人资料更新写 KV;既有游戏服资料通知用新 ID 和原路由。仍须列明具体键与消费者更新顺序,无确认不能回报已保存。 |
| SetPlayerLv/GetPlayerLv/SetPlayInfo/ClearPlayInfos/GetDefaultName | 复用现有客户端缓存职责,KV 模式缓存键为新 ID;本人等级来自 KV 已加载 PlayerData,服务端返回他人资料可直接填缓存。 |
| GetPlayLv/他人 GetPlayInfo | 本人可从 KV 读;他人未命中不能私读当前账号 KV 冒充对方,也不能调 PF。现有游戏服响应已提供的字段复用,其余跨账号公开资料读取与等级榜聚合集中记为实现依赖。 |
| LinkAccount/GetLinkState/entityKey | 账号绑定不等同于任意 KV 字符串写入;现有 KV 模块的已加载绑定信息可用于展示,认证绑定变更仍须其真实调用链。EntityKey 只属于 PF;Duel 的两段 SDK 调用须通过 PF 专属能力消费,不能留在 KV 共用身份假值中。该部分进入同一方案完善,不批准 null 成功替身。 |
`IUserService.entityKey` 当前直接使用 PlayFab DTO。因此“新 KV 类实现现接口”与“新类完全不引用任何 PF 类型”不能同时不改接口成立。完整方案选择把 PF 专属身份从共用用户契约移出,现有 Duel PF 调用点纳入迁移核查;不靠 fully-qualified 名字隐瞒类型依赖。具体 Duel KV 功能仍须按原功能目标迁移,不能以空 EntityKey 开场景。
身份发布顺序固定为:认证与 KV 准备 → 31 模块同步 Apply/通知 → 释放加载 Guard → 取得原 handle → 绑定提示服务和同租约普通请求 → 准备真实身份/资料 → 同步发布公开身份 → 发布一次 EvtAPISvrLoginSuccess → Login 返回 Done → 场景切换前再次确认同一有效会话。任何等待返回后都检查原 epoch。现有阶段闸仅在全部首场景调用完成迁移并具备运行验收条件后解除。
FishingStage KV 入口需硬检查有效身份;不得依赖原 5 秒软等待继续初始化。OnApplicationResumedAsync 只在有效身份下发 RT 重连/邮件读取;AutoRestart.Init 只接受真实 LoginTime。撤销后的恢复回调与旧重连完成不得重新拉取/更新 UI。
### 首场景请求与直接 KV 调用边界
| 调用 | 处理及失败边界 |
|---|---|
| Mail Init/Fetch/状态与领奖 | 本地键用新 ID,同 Cookie 原请求不变;读取失败不推进游标,不清已知邮件。领取涉及远端邮件状态和本地物品 KV,完整方案必须把这条业务动作的存档失败一起列出。 |
| AntiCheat CheckBan | 原 `playerID` 字段传新 UserId;保留原响应业务语义,当前 epoch/会话失效后的回包不得改变封禁/退出状态。 |
| Friend GetInvitedListCount | 继续空请求体;失败保留已知计数,返回后校验原会话再更新红点。邀请领奖另外追踪原动作的存档键,不混同计数读取。 |
| Club GetSelfClub | 继续 ApiPlayerId 与现有 SessionVersion 保护;空结果和失败区别保持,不用失败清掉已知俱乐部。 |
| Survey 两读/已读写/领奖写 | 两键原名迁 KV;缺键为原默认空列表,畸形数据不当空档。已读写与奖励写须使用同一原 handle。领奖的物品变化与已领奖标识作为同一业务动作审查,不声称两个独立 Set 天然具备事务。 |
| 等级榜个人分与全服/地区榜 | 从已存 Lv/InfiniteBuildingData 按原上限和算法推导分数;不另存未经论证的冗余分数。多人榜从 KV 数据聚合,地区码未知不混入全局。活动榜继续原 event 请求和新 ID。 |
| AIHelp/AutoRestart | 使用已发布的新 ID、真实创建/登录时间;不把 pfid 变量名称当必须继续发送旧身份的理由。 |
首场景动态调用继续在本开发侧补全。已核 `InitData` 先逐模块 Init,末尾发布 LoginDays;订阅者包括 BattlePassData、AchievementDataManager、FishingEventData、TriggerPackData,另有配置条件触发的 MiniBattlePassDataProvider/LuckyTaskData。不能只迁 BattlePassRecordTask 就认为此事件只有一个写入点。必须同时核 Init 是否覆盖31模块已加载值,按实际可达动作列 KV 写键;此项不是服务端或用户确认事项。
| LoginDays 可达消费者 | 本次源码追踪到的实际存档路径 | 方案处理 |
|---|---|---|
| BattlePassData.UpdateTaskData | SaveBattleTask → SaveBattlePassRecordTask → `BattlePassRecordTask` | 迁同一个原组合值;注释中的 BattlePassRecord/BattleTask 独立写不是活调用,不扩大成三个写。 |
| AchievementDataManager.UpdateData | 只改内存;直接 AchievementData 写已被注释。PlayerData.SaveNewPlayerData 将其合入 `NewPlayerDataDic["ad"]` | 把后续真实合档保存列入动作,不恢复旧注释写或另造第二个事实源。 |
| TriggerPackData.Init/UpdateTriggerPackData | InitGiftVIPDData、ConditionTypeAdd 等按配置写 `GiftVIPDData`/`TriggerPackDataList`/`FixedTimeShopDic` | 首次 Init 本身就可能写;不能只围绕末尾 LoginDays 事件做迁移。 |
| FishingEventData.OnEventDataChange | 遍历配置 ConditionList → SetEvent → 按活动类型初始化/存档 | 需追踪当前配置命中分支,包含 FixedTimeEventDic 等写和 LastPaymentTime 等读;不能按单一固定键概括。 |
| MiniBattlePassDataModel.UpdateTaskData | SaveBattlePassRecord → provider.SetMiniBattlePassRecord → `MiniBPKeyNew`;首次读取同键 | 配置 TaskType 命中 LoginDays 时同批迁读写,仍保持原数据字典形状。 |
| LuckyTaskData.UpdateTaskData | 条件命中 → `ILuckyTaskGetData.SaveTaskData()` | 后续按实际模型落到具体 KV 键;不把接口名当成已穷尽写入点。 |
### 候选修改文件与验收责任
该表是待正式前审的候选范围,**未批准编码**;最终新增文件名、全部业务写入调用方和完整测试列表仍需随同一方案冻结。
| 范围 | 文件/责任 |
|---|---|
| 生命周期与 DI | UserService.cs、UserService.KVLoad.cs、新增独立 KV 用户服务及两个有实际消费者的窄契约、PlayerKVGameplayBindings.cs、PlayerKVGameLoadCoordinator.cs、LoginStage.cs、PFLoginAct.cs、QuitAct.cs。只搬正式职责;既有加载/存档协议不重写。 |
| 首场景及资料 | FishingStage.cs、SurveyService.cs、InGameMailHandler.cs、FriendService.cs、ClubService.cs、AntiCheat/AntiCheatManager.cs、DataCenter.LeadboardData.cs、TimeManager/TimeRefreshManager.cs。按上述具体行为修改,未列业务不自动授权。 |
| 共用契约去 PF 类型 | Duel/UI/EventFishingDuelPanel.cs 是 EntityKey 实际消费者;完整方案需给出 PF 能力访问及 KV 对应路径,不只删除属性让消费者编译失败。 |
| formal 合成宿主 | KVGameLoadTests/ServiceHost/Program.cs、Stubs.cs、ServiceHost.csproj;迁入新类源码,formal fixture 改用真实新 KV 类,不用 stub 偷偷模拟成功登录。 |
| 不改但必须回归 | UserService.KVGameplayLoop.cs、AggUserService.cs、PlayerKVEditorValidationHost.cs 的第31模块签名兼容;SmallGameData 构造依赖、SubBoostrap 冻结选择、RTService/AutoRestart 的新身份消费时序。 |
合批验收必须覆盖:PF/KV 真实注册类型;KV 无 PlayFabUserService 构造/SDK 调用;101/102现存档和拒绝边界;双登单发、31模块回滚、原handle交接;身份发布前后、401/退出与旧回包;短 ID 101/102、Survey 十进制尾数与PF旧规则;地区缺失不得与全局同名;创建/登录时间真来源;formal 测试迁移后回归、PF/AGG/kv-loop兼容;邮件/好友/俱乐部失败不清状态;同动作存档 Confirmed/Unknown/重启;Unity CLI Loop Editor 编译。真实 Player、SignalR、线上操作没有运行就继续记未验,不打包。
1. 先在同一方案内明确身份/服务契约、31 模块 DI 和启动/退出语义,再定精确文件白名单。保留 PF/AGG 原行为;KV 失败不回退。第 31 模块、原 handle、CP2B-T Cookie/运输时点保持。
2. 白名单批准后一个编码端连续完成独立 KV 服务、首场景必要请求与同业务动作存档迁移;每检查点记录首败/修复和测试,不因常规编译/合成失败重新送审。阶段闸最后才解除。
3. 批内测两模式注册、登录/退出/401/旧 epoch、31 模块、存档三态/票据、首场景自动与条件触发、账号重启;跑合成套件和 Unity CLI Loop Editor。Player 真实脚本编译若 CLI Loop 仍无无打包入口则保持未验,不用 Editor 冒充。
4. 最终按[临时门控清单](M2-最终通关前临时门控与忽略路径清理清单-2026-09-29.md)逐项放开/销账、清闲置过渡实现、复核 PF 和 KV 真调用链,再做一次全量独立实施复核。`PlayerKV*` 改名及 `Core/KVMgr` 物理分层另按已定时点处理。
**当前结论**:可继续完善客户端完整方案、单次前审输入和精确白名单;原游戏服接口不因逐项合同问题阻塞。等级分数 KV 接线可纳入方案,跨玩家聚合读取须等服务器开发侧给出现有或新增入口后才能称等级榜完整迁移。本稿尚未完成评审/裁决,不能据此修改新生产代码。