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

21 KiB
Raw Blame History

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专属功能

集中问题单统一承载最小缺口: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完成,也不以已建文档算实现进度。

正式前审应一次裁决本方案是否足够完整、哪些精确范围可连续实施、外部能力和持久性限制是否正确;只给“方向正确/事实属实”的报告不视为编码许可。下一轮目标是正式设计裁决,不再做工作稿预审。