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

136 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用户服务、首场景接线、现有玩家存档调用的同批迁移与迁移期清理。本文取代工作稿中的待定实现选择;历史工作稿和两份预审报告保留不改。**当前为正式前审输入,不是已获批实现。**
## 1. 一次审查的范围与终点
KV模式把原PlayFab玩家存档改经KV,使用认证结果的新PlayerId;PF/AGG继续原PlayFab路线。首批只101、102已有KV档;固定 `http://s101.local:18080/`。不读/改服务器代码,不改配置规避触发,不调用UnitySkills,不打包,不自动实施线上写入或付费验证。
本批把服务抽离、启动/退出、首场景、条件活动、存档调用与对应测试一起审。文件上限见《M2-合批正式候选白名单.json》,调用粒度见《M2-合批存档调用实施台账.json》。白名单不是对文件所有内容的任意重构许可。224项旧矩阵是迁移核对索引,含PF内部、验证及GM调用;不能全部机械替换,也不能把语法项数当运行覆盖。
本批可实现的客户端合同与外部能力缺口分开记录:原游戏服API按原路由/字段/逻辑接线;跨账号KV等级榜/公开资料、原PF好友列表和PF匹配替代能力,集中列明真实依赖。缺少这些入口时不得编造URL、假数据或声称完整通关;也不阻断已审的独立服务和个人KV存档编码。阶段闸在真实可达链未完整就绪时保留,登记为未完成验收,不把隐藏功能当产品延期。
奖励迁移维持原业务协议;本方案不新增跨服务事务,不承诺客户端可实现跨设备恰好一次。原有跨系统一致性缺陷如实保留为风险或明确修复,禁止以“多键Set等于事务”消除风险。若正式前审判定某奖励动作必须增加持久事务协议才能安全放开,应一次指出受影响动作及必要改动,不再询问存储方向。
## 2. 启动、用户契约与关闭
1. `SubBoostrap`捕获一次`PlayerKVRuntimeSelection`。`LoginStage`按该对象注册PF/AGG或新`KVUserService`,以及对应存档实现。KV服务不继承、不构造、不调用`PlayFabUserService`,不引用PlayFab DTO。正式KV不二次读取SaveBackend。两种模式失败均不自动切换提供方。
2. `IUserService.CustomId`当前只有getter,本批增加setter;`PFLoginAct`两处输入直接设置共用接口。输入CustomId与认证PlayerId分开验证,保持101/102规范化及已有档门。
3. 从`IUserService`移除`EntityKey entityKey`。PF具体类保留属性;Duel的PF专属分支明确取得`PlayFabUserService`能力(AGG继承仍兼容),KV绝不走该分支。这里不新造一个只包装一属性的PF接口。KV匹配所需服务能力列集中问题单,未接通前不启动PF票据。
4. 新`IKVUserLoadReceiver`只含`ResetForKVLoad`、`ReplaceFromKVDecoded(state,epoch,raw)`、`DiscardKVIntents`;实际消费者为`PlayerKVGameplayBindings.Bind`。旧PF类供仍使用它的受控/kv-loop,第31模块正式加载由新KV服务实现。31模块、Guard、Apply/回滚、通知顺序不改。
5. 新`IKVUserLifecycle`只含`KVHandoffReady`、`KVLastError`、`CloseKVForValidationAsync`、`ObserveKVQuitAsync`;实际消费者为PFLoginAct/QuitAct。复用旧签名,不再引入`CloseKVAsync`。两个实现均只委托自己的Coordinator.Revoke/ObserveQuit;`StopKVGameplayLoopAsync`保留其验证流程编排责任,不创建另一关闭器。
6. 只从`UserService.KVLoad.cs`搬走正式KV字段与分支。受控加载、kv-loop、Editor宿主仍有调用者则保留;静态重扫后删除无人调用的formal方法,不能整文件删除。关闭先同步撤销身份、普通请求和存档访问,再await原关闭;关闭失败屏障保持。
## 3. 身份、资料与发布时间
Coordinator成功`GameLoaded`结果增加当前不可变`PlayerKVAccountDescriptor`;错误结果不携带可发布身份。只传已有认证事实,不新增认证请求或伪造值。
顺序:认证与KV读取→31模块Apply/通知→释放Guard→取得原handle→绑定本次存档和提示服务→启用同租约普通请求→准备真实资料→同步发布UserId/ApiPlayerId/InternalId/IsAuthorized→发布一次EvtAPISvrLoginSuccess→Login返回Done。公开身份前任一失败都撤销本次实例,禁止发成功事件。事件现有消费者Chat/Mail/RT均只能看见已可用会话。
| 成员/资料 | 本批确定行为 |
|---|---|
| UserId、ApiPlayerId、InternalId | AccountDescriptor的认证结果;IsLogin/IsAuthorized还需原handle及当前会话有效。退出、401首先清空公开态。 |
| CreateTime、LoginTime | 同一描述对象的CreateTimeUtc、ServerTimeUtc;缺失有效时间使正式登录失败,不拿本机当前时间伪造认证事实。 |
| LastLoginTime | 新KV资料键`LastLoginTimeUtc`(UTC往返格式`O`)。本次登录先保存旧值到服务内存;缺键为DateTime.MinValue哨兵,TimeRefreshManager映射为null;畸形值报资料错误。首场景初始化与首次跨天消费完成后才写本次LoginTime并等待确认,避免加载前推进日期漏掉每日刷新。PF读取保持原逻辑。 |
| DisplayName、AvatarUrl、Language、ContinentCode | KV同名键;缺昵称采用Angler#加ID最多5字符,短ID不切越界;头像缺失沿用既有默认;语言缺失使用现有SettingService语言。地区缺失保持未知,不从系统语言推断。资料键写入采用原handle票据;名字/头像成功后再更新UI/缓存和原游戏服UpdateProfile通知。 |
| EAccoutUrl、绑定展示 | 复用第31模块已解码值;非AGG原LinkAccount返回失败(1,0),KV保留这个真实能力边界,不把写绑定字符串当认证绑定成功。AGG路线不迁KV。 |
| 本人等级/缓存 | 已Apply的PlayerData/原档为事实源;默认名修复同时覆盖PF短ID回归。GetPlayInfo/SetPlayInfo缓存按新PlayerId隔离。 |
| 他人资料与等级 | 优先消费原游戏服响应自带字段。未命中不能读自己KV代替别人,不能调用PF补齐;对接集中问题单列出的公开资料能力,失败保留已知数据,未加载明确区别于空档。 |
新增资料键属于普通个人KV,无需服务端重新定义玩家存储;不重写31模块已有格式/别名,不在未知字段处丢数据。用户名/语言旧接口具体返回类型保留,KV错误明确返回原失败形态,不用空成功吞Unknown。
## 4. 存档入口:一个共用契约,两条已有提供方路线
本批新增`Services/PlayerData/IPlayerSaveStore.cs`(契约和结果)及`PlayerSaveStores.cs`(两实现)。实际消费者是台账内业务读写;没有此契约,每个业务文件都需重复模式分流。该层只承担提供方选择后的读写语义、固定会话及确认,不能再加静态全局路由/第二个选择开关/新Owner。
- `Read(key)`返回Success/Found/Value/Error;只有成功缺键进入原默认分支,加载失败/失效/畸形值不转换为缺键。
- `Write(key,value,remoteStore=true)`返回接受结果和不透明回执;保留原键、原字符串编码、空串和remoteStore含义。KV实现直用既有KVMgr四入口及原handle;PF实现委托原PlayFabMgr缓存/写入。回执固定所属实例,不能移交另一账号。
- `WriteAndConfirmAsync(key,value,token)`:KV调用Write、Flush并等待原票据;PF委托原SyncDataImmidiate。只在确定Confirmed时返回成功,Unknown/Superseded/Cancelled不转true。原即时消费调用先保存确认再继续,不用普通buffer写替换。
- `FlushAsync(token)`用于KV周期保存、暂停、动作完成及退出前保存。普通Write接受并非云端确认。KV采用原PF的40秒节奏,只有本次登录Lifetime中的一个调度者;失败记录待同步状态,不另建重试Owner。退出取消调度并复用原关闭屏障;PF原定时器保持。
- KV存档实例在本次原handle交接时绑定一次,生命周期内不可重新绑定。读取或写入入口不从全局容器临时取“最新账号handle”;模块/异步动作捕获本次store,旧实例永远不能向下个账号写。加载Guard期间不绑定业务store,不产生写副作用。
- 契约的公共结果不含PF DTO。KV票据保留为实现内不透明内容;PF普通buffer回执不能伪标KV Confirmed。
台账规则:业务GetLocalData/UpdateUserDataValue迁共用store;PFMgr内部、PFUserService初始化和AGG专属调用留原提供方。Setting/Club已走KV的提示接线复用既有结果保护,不能为对称覆盖成无保护字符串读。GM调用也按已选提供方写,但不是本批正式账号验收动作。注释不恢复为活代码。
异步调用必须在首次await前捕获store、身份和租约,await后验有效性;存档对象固定还不足以允许旧回包修改新UI。构造/Init绑定的模块随RootCtx释放;单例/延迟事件要解除订阅并清旧store。带回调、奖励、消耗的成员按第6节处理,不能只文本替换方法名。
`Core/KVMgr`物理反向依赖不在本批顺手搬目录:它是既有问题,仍未解决,保留最终清理待办。新存档实现放Services/PlayerData,高层使用底层,不向Core新增上层服务依赖。以后移动时需覆盖所有Compile Include和.meta引用,不以本批新增中立入口冒充物理分层已修复。
## 5. 首场景和动态活动的完整处理边界
FishingStage启动前KV硬检查当前身份、store和原handle;原5秒软等待不能作为KV放行。InitData保留业务初始化和订阅,但不得重新用默认值覆盖31模块已Apply档。第一次Init和重复Init分别测试;在活动切换确需新活动档时保留原业务条件。
| 入口 | 同批迁移与验收 |
|---|---|
| Mail.Init、RT/Chat | 注册/初始化时序保证不漏掉唯一成功事件;可先订阅,或Init发现已授权即开始原流程。Mail本地cache/cursor以新UserId隔离;失败不推进游标。恢复只对当前身份重连/Fetch,旧完成不改UI。 |
| IAP.Init | 恢复被KV早返回跳过的初始化。当前非AGG DevIAPService.Init只重绑RT订单事件;不宣称其Pay假实现证明真实购买。原渠道与真实支付独立验收,无付费操作。 |
| AntiCheat/Friend/Club | 保留路由和请求字段,只替换显式ID;同SessionVersion/epoch保护。GetInvitedListCount可进一步调用SetLinkAwardData,必须同批迁InviteLinkRewardCount读和红点。好友列表GetFriendsListAsync属于直接PF能力,与普通friend/*请求区别,见外部能力项。 |
| Survey | SurveyData/SurveyReadSidData原键、原List字符串;缺键为空,坏JSON为错误。KV使用十进制PlayerId%10,PF保留十六进制转数再%10。GetSurveyAnswer维持原问卷URL语义,已读写需检查接受结果;领奖走第6节。 |
| 等级榜 | 个人分仍lv*100000+building,原clamp保持;从现有Lv/InfiniteBuildingData读取,不新增LevelB冗余键、不写PF统计。全球/地区前50名等聚合入口就绪后接线;地区未知不拼成全球名。活动榜event/GetLeaderboards保持原请求。 |
| AIHelp、AutoRestart | 用已发布新ID与真实时间,过期回调不发新会话请求;不能以pfid变量名沿用旧值。 |
动态活动不能只选101/102当前恰好激活分支。随包《M2-合批动态配置投影.json》记录本地配置及hash,仅证明本地输入,不冒充线上配置或账号实际可达结果:当前FishingEvent共1466项、70组Type/SubType,直接LoginDays条件为0;TriggerPack同条件为0。首场景Init仍遍历配置,后续跨日/等级/道具事件也会激活,因此本批存档范围覆盖全部当前代码分支;新配置出现新代码/新存档语义才是真正变更,不以配置表行数替代运行验收。
| 动作链 | 持久化事实和处理 |
|---|---|
| BattlePass LoginDays/任务刷新 | SaveBattleTask→SaveBattlePassRecordTask,单键BattlePassRecordTask组合串;不复活已注释两键写。 |
| Achievement条件变化 | 先改内存,后由PlayerData合档NewPlayerDataDic[ad];不另造AchievementData第二事实源。 |
| TriggerPack Init/条件刷新 | GiftVIPDData、TriggerPackDataList、FixedTimeShopDic;首次Init也可能写,按原条件迁,不略过。 |
| FishingEvent SetEvent/跨日 | FixedTimeEventDic、LimitedTimeEventDic,LastPaymentTime条件读,以及Type/SubType下游模块;保持原EventId一致性判断和时间条件。 |
| Type2签到、Type3礼包/战令、Type6礼包 | MonthlySignEvent、BattlePassRecordTask、优惠券/ChainPack/ThanksGiving/Pack1A2等台账原键。 |
| Type4及Type7动态活动 | 沙滩DigCount/DigGrid、伙伴、魔药、弹珠、Bingo、CubeMelt、射击、Break、Can、GatherCore、Mow、Smash等;通用EventDataRepository/AEventDataManager也迁,覆盖泛型动态键。以台账参数表达式为准,不另造简写键。 |
| Type8 LuckyTask | EventLuckyGameModel.SaveTaskData→Save→EventLuckGameRecord;EventLuckMagicModel.SaveTaskData→ToPfData().Save→EventLuckMagicPlayfabData。保留原活动模型和序列化。 |
| Type9及其它当前分支 | MiniBattlePassDataProvider读写MiniBPKeyNew、OfferJourney及其等级触发档、其它通用活动仓库;MiniBP/LuckyTask任务按当前配置命中,不声称每个登录必写。 |
| 道具/活动联动 | PlayerItemData先发ItemAddedEvent并按Type/SubType分发货币、鱼卡、杆、代币、Buff等。奖励不是只改NewPlayerDataDic;台账全部消费者和动态泛型仓库都要进入KV路线。 |
具体运行验收记录配置hash、活动ID、动作、读写键、确认结果、重启后值;不得通过冻结活动、删除LoginDays发布或禁掉ItemAddedEvent获得通过。
## 6. 奖励、消耗和失败:本批不偷换业务协议
正常存档保持原业务顺序及序列化。奖励/消费动作增加本会话重入保护,并收集该动作实际产生的KV回执;收集窗口仅覆盖主线程同步业务片段,await期间不使用全局可写收集标志。嵌套同步事件沿用该动作;异步后续先核会话再开明确片段。动作结束强制原必要主档保存,等所有最终票据Confirmed才报告“已保存”。同键中间票据Superseded只有被**同动作**最终Confirmed值覆盖才能解释为成功。
本批在`PlayerSaveStores.cs`中实现这个有实际消费者的轻量动作记录,不再新增奖励Manager、Owner或事务协议。需要影响UI的领奖方法改Task并由实际按钮await;可先保留内存业务变化,但奖励成功展示要在确认之后,必须处理PlayerItemData内提前ShowData及其嵌套通知,不能只移外层一行ShowData。PF保持原流程。
| 动作 | 原流程与明确修改 |
|---|---|
| Survey.GetSurveyCheckAnswer | 捕获sid和奖励定义,拒绝同sid重入;原AddItemByDrop及SurveyData标识同一动作收集,主档保存后确认,成功才刷新领取成功展示。按钮/失焦触发调用同Task并处理错误。 |
| Friend.OnReceiveLinkAward | 按原阈值计算奖励和最终count;AddItemByDropList与InviteLinkRewardCount及真实连带键同一动作;EventInviteFriends按钮await,不能先返回新count冒充已保存。 |
| Mail.TakeReward单/批 | 沿用mail/updateState后本地发物品的原协议、Cookie、缓存隔离。批量请求修为RewardTaken,并使用请求前冻结的未领邮件集合发奖;不能先请求HasRead再筛HasReward。保留个人邮件LureInflation差异。远端状态成功后,在同一身份下发物品、保存确认,再向上返回领取成功。 |
| 即时消耗 | FishingBehaviorB.ConsumeBaitOnPlayfab、FishingTurntablePanel.ConsumeOnPlayfab迁WriteAndConfirmAsync,未Confirmed不开始后续随机/动画/奖励,不把Unknown当可再次扣费。 |
| 其它同合同活动/商店动作 | 台账内保存统一迁KV;带奖励/扣费的业务仍需按真实调用栈收集最终保存与成功回调,批内连续实现和测试,不增加按模块送审。支付回调不能因重复通知重复发物品,原订单ID/原去重语义保留。 |
失败边界:Write未接受时不继续成功路径;Unknown/Cancelled/关闭或确认失败不自动重复奖励、不自动切账号补写、不谎报已同步。保留原KV pending/unknown恢复和审计证据;UI恢复为明确失败/待处理,禁止在同会话不确定状态下重复该动作。旧回包在修改任何内存/本地cache前即丢弃。
**准确的持久性限制**:单个动作多键确认不提供事务回滚;进程在部分键提交后崩溃可能出现部分结果。邮件还有“服务器已置已领、物品尚未落KV”的跨系统窗口。现有客户端代码就存在相应窗口,本批不伪称只等票据即可解决,不能靠自动重放AddItem补偿,否则可能重复发奖。故本批代码迁移和普通成功回归可评审实施,但这类故障恢复未被真实验证/裁决前,不标“奖励完整通关”;最终报告必须逐动作列出风险。需要持久事务/服务端原子领奖时再围绕具体缺口集中提出,不泛化为所有旧接口需新合同。
## 7. 外部能力与PF专属功能
[集中问题单](M2-客户端致服务端线上合同集中问题单-2026-09-29.md)统一承载最小缺口:KV等级榜聚合、他人公开资料、PF好友关系列表替代、PF匹配替代。它们不是“ID究竟用什么”或“个人数据是否存KV”的产品选择。
收到接口说明后,若路由、身份、字段和失败处理可由本文既定模式承接,在本批最终集成记录中补具体响应夹具;若需要新认证协议/跨账号写权限/事务协议,则是合同变化,才需集中补充审查。当前不实现猜测路由,也不创建返回空成功的生产适配器。保留旧PF具体实现,KV场景全通关必须有其真实替代路径。
账号绑定现状非AGG本来返回失败,本批不擅自扩展渠道能力;配置为AGG+KV仍拒绝。旧DevIAP支付stub属于原能力限制,不能作为KV支付通关证据。
## 8. 实施检查点与验证(不等于审查轮次)
1. **C0**:正式报告hash核证、逐条裁决、锁定文件/动作范围,保存既有工作树基线。正式审查未通过前不改源码或测试。
2. **C1**:共用契约、独立服务、DI、生命周期、身份发布、31模块与formal宿主迁移。先暴露真实旧实现失败,再实现;保留PF/AGG/kv-loop回归。产出可用的独立服务,不停在空类。
3. **C2**:同批迁存档台账及首场景/活动链、异步上下文、保存调度、奖励确认和UI;每完成一组真实动作更新简短日志,常规编译/测试失败自行修复,不发新送审。
4. **C3**:合成回归和CLI Loop Editor编译;受控运行只用被授权窗口与账号,记录首场景真实配置及读写。当前不自动执行正式写入/付费。缺真实Player编译支持如实未验,不用Editor编译或合成PlayerProbe冒充。
5. **C4**:闲置formal分支和开发期冻结/忽略清账、双模式静态重扫,整批一次独立实施复核;未解除或未证实的门控逐项保留TODO,不能关闭M2。PlayerKV命名按用户指定留到整体移植后。
测试职责:
- KVGameLoadTests/ServiceHost的Program、Stubs、csproj改用真实KVUserService,PF SDK stub记调用计数;不能只stub出成功身份。原90组加载等测试计数可能有竞态,比较命名用例和失败数,不夸大断言差额。
- 新合批用例放既有测试工程:账号101/102、拒绝其他账号/无档;真实注册类型;初始/完成/退出身份;单发、旧epoch、401、关闭失败;31模块回滚/第31签名;PF/AGG/kv-loop兼容。
- 读失败≠缺键、坏JSON;remoteStore=false;40秒调度不串账号;即时消费Unknown不继续;同动作同键覆盖规则;延迟回包丢弃;多键部分成功不返回成功。
- Survey十进制尾数/PF十六进制旧算法;短ID;地区缺失不误查全球;邀请计数读取;邮件单/批不同类型、重复点击、失败不推进游标、状态成功后KV失败、跨账号回包。
- 真实初始化链而非只测假模块:InitData/Activity仓库/ItemAddedEvent/登录时间消费;首场景PF调用计数必须为0才能解除KV阶段闸。完整集成尚不可运行就保留闸,不伪造通过。
- Unity只用CLI Loop;编译错误0才通过编译,警告列变化;真实Player、SignalR、线上档、购买未跑即未验。测试必须覆盖业务结果与失败后状态,不只镜像方法实现。
## 9. 最终清理与不可混淆的完成声明
删除迁走后无调用的旧formal成员/重复开关/无消费者契约;保留PF、AGG和仍用的验证入口。清单中的“为开发方便暂时跳过”逐条恢复;身份/已有档准入/Guard/epoch/Unknown保护属于必要正确性边界,不作为临时冻结删掉。全链实证前不声称M2完成,也不以已建文档算实现进度。
正式前审应一次裁决本方案是否足够完整、哪些精确范围可连续实施、外部能力和持久性限制是否正确;只给“方向正确/事实属实”的报告不视为编码许可。下一轮目标是正式设计裁决,不再做工作稿预审。