# 阶段4B3分期与4B3A固定存档入口方案(待独立评审) 日期:2026-09-24。作者:主会话。前置:[4B2关闭裁决](阶段4B2实施复核裁决与关闭-2026-09-24.md)。 **本包申请两项独立裁决:A=4B3内部分期是否合理;B=4B3A限定方案是否完整、可实施。不是整个4B3已经完成设计,更不是批准全量游戏接线。** 4B3B/C的业务调用起点、可变对象隔离及启用接线仍须分别形成完整方案;本包不以扫描表代替这些方案。当前没有4B3业务代码。 ## 1. 目标、现状和最小改动原则 整个4B3最终交付:单次登录固定后端;普通KV读写使用创建操作时捕获的固定句柄;旧回调在改变共享内存前即失效;持久化失败可辨;定时/暂停/退出属于原会话。游戏正式启用仍受4C、tickets有界历史、身份专项、4D、P2-α真实档专项及用户产品确认约束。 当前4A固定句柄已有安全快照、Update/Delete/Flush/WaitForTicket,4B1/4B2已负责所有权和受控加载。**不再造Owner、缓存、传输、会话管理器或另一套验证专用普通存档框架。** 首单元只在既有PlayFabMgr增加显式固定句柄入口及一个键读取结果类型;不替换旧调用、不改模块字段、不放开旧guard、不公开Context的私有handle。 文本初扫234行/84文件包含注释和定义。Roslyn语法盘点及逐位置台账见[调用事实与分期附件](阶段4B3调用事实与分期附件-2026-09-24.md)和`B3DesignAudit/calls.json`。它记录实际语法调用及按名称匹配的上游候选;没有语义绑定,也未证明所有事件、反射、UnityEvent持久订阅和接口实现链完整。这个事实是分期理由,不能被隐藏为“全链隔离已经设计完成”。 ## 2. 路线选择与分期裁决请求 | 路线 | 具体影响 | 建议 | |---|---|---| | 让旧PlayFabMgr.Instance自动取当前KV句柄 | 改动少,但A回调到B时会写B;保存前的共享内存污染也未解决 | 不采用 | | 一次改全部普通调用、消费、支付和身份 | 跨越现有4C/身份边界,逐调用证据不足,白名单过大 | 不采用 | | **显式固定入口→业务操作隔离→加载和生命周期接线** | 复用已验基础;每次只批准已完整定义的单元;直到全链通过仍禁用正式玩法 | 推荐,待独立评审 | | 单元 | 交付与关闭条件 | 不包含/下一前置 | |---|---|---| | **4B3A 固定存档入口** | 本方案§3~8全部通过,旧PF路径字节保持、4B1/4B2回归及独立实施复核关闭 | 不发布游戏写权限、不更换任何业务调用、不接定时器;不能代替4B3B/C | | 4B3B 业务操作及内存隔离 | 每个业务入口明确捕获点/对象归属/取消点;同步、await、广告/支付/事件/协程跨账号验证;接口拒读不再当缺档 | 完整逐调用合同及白名单另审;未完整审查的入口不得在KV模式运行;4C交易结果语义不在此伪造 | | 4B3C 加载发布与生命周期 | 私有handle受限发布、后端冻结、初始化意图、40秒周期/暂停/恢复/退出,真实服务组合与31对象回归 | 必须先完成B;生产准入仍关;不能删除B2guard或把LoadedForValidation改为Done | A→B→C全部独立关闭才可称4B3完成。每个单元完成后,主会话按用户最新授权自动做无需复审的后续准备,不在“是否继续”处停顿;遇到完整新方案/必要差异/独立实施复核门时,准备好可审材料后交接。**本次没有请求免除既定审查门。** ## 3. 4B3A开发范围与接口 新增方法放在`Core/PlayFabMgr.cs`一个有起止注释的`KV explicit fixed-handle access`块中,均为`internal static`;不访问`Instance`、Unity场景、PlayerPrefs、旧缓冲、SDK或任何全局“当前账号”。静态方法不触发组件Awake;不新增静态字段、timer、事件或内部重试。生产Player编译保留这些普通内部方法;不存在用户配置/公共登录路径调用它们。 调用者必须显式提供已有`PlayerKVGameSessionHandle`,方法只调用这个实例,绝不寻找替代句柄。该类型由4A Bridge产生,4B1 Context的private handle保持private,4B2 LoadedForValidation不会因此获得它。 | 计划接口 | 输出与用途 | |---|---| | `ReadKV(handle, string key)` | 新`PlayerKVKeyReadResult`;严格区分有效快照内缺键与拒读 | | `WriteKV(handle, string key, string value, bool remoteStore = true)` | 既有`PlayerKVEditResult`,完整保留本地接受、原始错误、票据和不确定提交标志 | | `FlushKVAsync(handle, CancellationToken waitToken = default)` | 既有`Task`;透传水位和分类结果,不合并成bool | | `WaitForKVTicketAsync(handle, PlayerKVSyncTicket ticket, CancellationToken waitToken = default)` | 既有`Task`;确认的是原句柄的原票据 | 不新增Delete facade:当前业务没有显式Delete调用需求,空字符串写入仍是存在值,不能偷改为删除。底层已有Delete保持;未来确有调用需求再纳入对应单元合同。原`GetLocalData/UpdateUserDataValue/SyncDataImmidiate/CompareDatas`签名和实现一律不改。 ### 3.1 新键读取结果 新增`Services/PlayerData/PlayerKVKeyReadResult.cs`,命名空间`game.PlayerDataStorage`。只含internal enum和internal sealed不可变结果;不增加业务DTO或可变字典。 `PlayerKVKeyReadError`:None、InvalidInput、LoadGuardActive、StaleOrClosing、NotReady。 结果属性:`Error`、`Success => Error == None`、可空bool `Found`、string `Value`、`OwnerInstanceId`、`PlayerId`、long? `Epoch`。创建器internal;没有set、隐式bool/string转换。失败时Found=null且Value=null,禁止构造伪缺键;null handle时身份全null;有效handle失败仍可带该原始身份用于诊断,但不带Cookie/存档正文。 读取顺序固定: 1. 先调用已有`PlayerKVGameLoadGuard.Reject(ReadLocal)`。命中则返回LoadGuardActive,记录已有违规计数,零快照读取。不能另开不计数的guard旁路。 2. null handle或`!KVSnapshot.ValidKey(key)`返回InvalidInput。使用已有规则:非null、非空、长度≤128;不Trim、不改大小写、不替换key、不额外拒绝当前规则允许的空格。 3. 仅调用指定handle的`ReadLocalSnapshot()`一次。其StaleOrClosing/NotReady分别映射;不再读取全局身份或重新Get远端。 4. 成功快照按Ordinal键查询:存在→Found=true/原字符串;缺键→Found=false/Value=null;空字符串是Found=true。 5. 读取成功是一次快照观察,不是未来业务操作许可。调用后账号失效时,B3B仍须在首次内存修改前重检其操作身份。A不宣称返回值能自动撤销已经拷出的字符串。 既有Snapshot实现负责操作租约及读后归属检查。4B3A不修改其并发线性化点,也不把普通结果对象当授权令牌。 ### 3.2 写入与错误合同 先调用`guard.Reject(Update)`;命中返回`PlayerKVEditResult(StateResult=Blocked)`,无票据。null handle返回StateResult.Validation。随后**原样返回**指定handle.Update(key,value,remoteStore);key/value非法与Ready/关闭优先级沿用底层,不在facade复制第二套校验流程。 | 结果 | 可以作出的判断 | 不允许作出的判断 | |---|---|---| | Accepted=true、remoteStore=true | 该原Owner缓存已接受,Ticket保留 | 远端已确认、可发奖、可重放交易 | | Accepted=true、remoteStore=false | localOnly接受,通常无Ticket | 该key所有历史远端pending被撤销,或永远不会发送历史值 | | Validation | 本次输入未被接受,保留原字段 | 自动规范化输入后重试 | | Blocked | 原Owner/状态不准入 | 改取当前B账号句柄再写 | | StorageError | 保留AttemptSequence/Revision/LocalCommitMayHaveSucceeded | 将不确定提交改成false并回滚/重试业务动作 | remoteStore=false沿用3A:写localOnly不清除同key已有remote pending;以后Flush可能发送此前remote值。先remote后localOnly、localOnly后remote必须分别测,不按旧PF名称猜语义。value=null不等于删除,沿用Validation/Blocked结果;value=""是正常存在值。 写操作与Close竞争时,已被底层准入的A操作可以完成于A;结果保留实际发生的接受事实。它永远不改发B,也不因返回后失效而把可能已持久化的Accepted伪装成未提交。新的调用在旧handle失效后由原Owner拒绝。B3B负责结果返回后的UI/内存归属,不能凭Accepted绕过。 底层预期错误结构化透传;未预期编程异常不catch成缺档、成功或可安全重试。允许向调用方传播,后续B3B入口按故障态中止,不能继续业务。A测试显式验证不吞异常。 ### 3.3 Flush与票据 Flush先`guard.Reject(Sync)`;命中返回计数Confirmed=0/Superseded=0/Blocked=1/Unknown=0、TransportError=Cancelled、CacheStatus=handle?.Status或NotLoaded。null handle同样Blocked=1,但TransportError=Validation、CacheStatus=NotLoaded。通过后直接返回handle.FlushAsync(waitToken),不另开Task.Run/定时器/水位,不改Confirmed/Superseded/Unknown。 票据等待先`guard.Reject(Sync)`;命中返回Status=Blocked、TransportError=Cancelled及原handle.Status/NotLoaded。null handle返回Blocked/Validation/NotLoaded。通过后直接返回handle.WaitForTicketAsync(ticket,waitToken)。票据null/外来Owner/epoch/sequence由已有Coordinator检查,失效handle优先级沿用4A;不创建替代ticket。 取消token只结束调用方等待,不撤销已经发出的操作,不重试,不释放Owner资源。已有Owner跟踪的内部任务继续收尾,Close继续等待排水。预取消token也沿用现有4A调用语义,不在facade声称“保证未发送”。票据Confirmed仅表示该原票据确认;Superseded不等于当前值确认,Unknown不能当失败可重扣。等待不主动触发Flush。 ## 4. 4B2兼容与账号归属 四个新入口全部服从现有B2加载guard。guard活跃期间任何新入口调用都拒绝,并增加现有计数;不允许为测试普通写临时释放B2guard。4B2仍是单认证、加载事务、零业务写、LoadedForValidation,旧受控工厂与私有handle不改。 这里的拒绝点是调用入口处的guard观察,不是跨整个异步操作持有的全局租约。已经在guard建立前准入的原Owner操作仍由原Owner排水,不能宣称新静态方法凭一次检查就解决“普通写→下一加载”的切换竞争。A没有生产调用者,也不开放加载后写权限;C必须在上一普通会话撤销/排水后才能进入新加载guard,不能与未排水普通访问并存。若外部认为此限制仍不足以允许单独实现A,应退回分期或要求最小准入合同补审,不能在实施中临时修改Guard。 普通A单元测试通过**现有4A合成session+Bridge**取得句柄,独立调用新facade;这不等于“4B2加载后写入已接通”,二者组合属于C。Editor验证沿用固定反射入口模式,但新验证host仅用既有internal合成工厂,没有运行时可用的网络工厂、反射通用执行器或配置开关。 验证host固定使用`http://kv-route.invalid/`,合成handler直接返回响应且检查固定host/path,禁止调用默认HttpClientHandler或任何转发代理;不更改项目正式地址。缓存仅在`CodexOutput/upserver/KVSaveRouteTests/unity-runs//`。不创建PlayFabMgr组件/不触发Awake,不启动游戏容器。guard用TryEnter取得本次所有权,失败即停止该用例,不释放他人的guard;finally只释放本次取得的guard,先Close Bridge排水再Dispose认证session,异常和未完成Task保留为失败。不得覆盖既有静态单例来让测试通过。 固定身份为OwnerInstanceId+PlayerId+Epoch;同账号再次登录也必须是新操作归属。方法参数永不替换。A验证旧A句柄在B打开后不能读/写/Flush B,并核对B缓存和请求数,而不只看返回错误。 ## 5. 后续业务合同(约束,不是本轮实施批准) | 场景 | B/C必须满足的合同 | |---|---| | 同步按钮/业务入口 | 在首次可变状态写、事件或奖励前建立原会话操作归属。只在末端保存加校验不足。 | | await/协程/SDK回调 | 发起时捕获操作;恢复后先重检再Resolve/修改业务对象、更新UI或发奖;同账号新epoch也拒绝旧结果。 | | 常驻订阅/计时器 | 注册时捕获epoch与对象生命周期,关闭先撤销再退订;已经入队的回调仍需复查。 | | 原地Reset/Replace的31对象 | 旧引用可能仍指向已装入B数据的同一实例,必须在变更入口校验原epoch;不能只比较ReferenceEquals或ID。 | | 仓库读取异常 | EventDataRepository等不能把Stale/NotReady转换为new T并继续保存;只有有效快照的缺键可进入明确的新数据策略。 | | 奖励/支付/消费 | B只处理归属与中止;持久化失败后的奖励一致性必须有单独业务裁决,不能凭本地Accepted发奖或重新扣减。两个立即保存消费点继续归4C。 | | 普通写入失败 | 一次操作内停止后续依赖副作用;不以改写全局缓存/从新账号重试补救;是否可恢复以具体业务合同为准。 | | 生命周期 | C才能建立原会话40秒周期、暂停Flush/恢复、同步撤销及有界退出观察;等待超时不释放占用资源、不宣称已确认。 | 上述合同尚需B/C逐入口落到源方法/字段/事件和文件白名单,未落完则B/C不得编码或关闭。本方案没有将全部业务方法的安全性归功于四个静态facade方法。 ## 6. 4B3A精确白名单 | 文件 | 允许的唯一内容 | |---|---| | `Client/Assets/Scripts/Core/PlayFabMgr.cs` | 新增§3四个internal static方法及块内私有结果构造helper;旧代码块字节保持,不调整Awake/Compare/普通PF/立即PF/生命周期 | | `Client/Assets/Scripts/Services/PlayerData/PlayerKVKeyReadResult.cs`及meta | §3.1不可变结果和枚举 | | `Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveRouteEditorValidationHost.cs`及meta | 整个文件UNITY_EDITOR包裹;固定合成传输、固定case,实际调用新增facade,无动态代码执行 | | `Client/Assets/Editor/KVSaveRouteValidation.cs`及meta | Editor固定类型/方法/case白名单反射转接,无任意类型/路径/参数执行 | | `CodexOutput/upserver/KVSaveRouteTests/` | 新显式链接源码测试项目/替身/证据;链接现有底层和PlayFabMgr,不复制实现 | | 本阶段文档、入口、状态、TODO、日志 | 主会话检查点、送审/交付/hash材料;保留旧受审证据 | 禁止改:User/Agg/Context/Bridge/Owner/Guard/Registry/Schema/State/Cache/Coordinator、CustomServerMgr、QuitAct、任何业务DataCenter/UI、场景/字体/配置/包/SDK、旧测试源码与csproj。当前唯一显式链接PlayFabMgr的旧项目是`KVGameLoadTests/ServiceHost/ServiceHost.csproj`,PlayerKV源码为通配纳入;新增Editor host需使用UNITY_EDITOR且额外`!SERVICE_HOST`条件排除内容,避免旧宿主误编译验证实现。既有旧Editor host排除项不改。 普通新结果文件被该通配纳入,旧宿主不需要csproj补丁。其他旧显式Compile项目不链接PlayFabMgr,新增facade依赖不影响它们。如实施发现依赖与此不同,停止相关修改先提出精确差异,不默认扩大白名单。 新meta使用新GUID;不重新序列化场景或资产。新测试依赖用现有Unity Newtonsoft/PlayFab程序集与本地.NET SDK,不安装新包。测试stub仅替代SDK/Unity边界,必须说明它不能证明真实渠道全流程。 ## 7. 4B3A验收矩阵 全部测试合成账号/传输,缓存位于独立运行目录;无正式服请求。按语义组计数,不预造断言总数。每条应有实际日志和对应源测试名。 | 编号 | 场景与必须观察到的结果 | |---|---| | B3A-01 | Ready读存在/缺键/空字符串,Found分别true/false/true;精确大小写、空格、Unicode/边界128字符按现有ValidKey规则;无新Get。 | | B3A-02 | null handle、null/空/129字符key为InvalidInput;guard活跃优先LoadGuardActive;失败Found=null/Value=null。 | | B3A-03 | 旧/关闭handle返回StaleOrClosing;先沿用4A Seed制造真实Conflict/Unknown,state.json.tmp目录冲突制造StorageError,合成401制造授权故障,验证失败Found=null。ReadFailed等底层状态覆盖沿用4A/3A回归;本层证明两个Snapshot错误的映射,不冒称所有状态均由新facade端到端重造。 | | B3A-04 | remote=true写接受、返回真实票据;网络尚未发送,读取见本地合并值;Accepted不被测试命名为远端成功。 | | B3A-05 | remote=false接受无ticket;remote→localOnly后Flush仍可发送旧pending;localOnly→remote后读/发送新remote值,分别验证。 | | B3A-06 | value=null、非法key按底层Validation/Blocked,空串保留;无自动Delete、无规范化输入。 | | B3A-07 | 沿用KVGameBridgeTests.NonReady:在本run唯一state.json旁创建state.json.tmp目录使实际写失败,facade返回StorageError及实际sequence/revision/uncertain=false,无重试。uncertain=true的AfterReplace分支沿用KVSyncTests现有五阶段fault回归+facade原样return源码审计,不宣称新facade已端到端注入AfterReplace;不得修改readonly字段或底层增加故障钩子。 | | B3A-08 | 正常Flush真实计数/水位;调用后的新写不被旧水位虚报确认;同keySuperseded保留,后续ticket必须独立确认。 | | B3A-09 | 合成Set已执行但响应丢失→Unknown;401及前置拒绝保留原结果;无再次Set、无改发PF、无自动Refresh。 | | B3A-10 | null/外来Owner/同账号新epoch票据拒绝;旧A票据Confirmed也不代表B的新值确认;Wait不主动发送。 | | B3A-11 | Flush等待取消/预取消仅取消观察;忽略取消传输仍排水,Close未完成前不能复用;保留原Task直至收尾。 | | B3A-12 | A句柄捕获→关闭→B打开→A的四个facade调用拒绝,B缓存、请求、业务替身内存不变;同账号新epoch复测。内存断言仅证明测试回调自身遵循合同,不称所有业务已迁移。 | | B3A-13 | Close与Write/Read/Flush竞争:允许底层已准入A操作完成在A;不改写接受事实,无跨Owner请求,随后调用拒绝。 | | B3A-14 | B2guard活跃时四入口全部拒绝;ReadLocal/Update/Sync计数符合次数;SDK/Prefs/Set/Delete均0,guard不释放。 | | B3A-15 | facade静态调用无需MonoBehaviour/Instance;未触发Awake/旧timer/缓冲/Prefs;四个方法及helper移除后PlayFabMgr与基线字节一致。 | | B3A-16 | 新源码宿主两平台条件;无UNITY_EDITOR时验证host消失,四普通入口仍存在但无生产调用者;旧B2 ServiceHost双条件和PlayerProbe保持。 | | B3A-17 | Unity CLI Loop实际HotUpdate程序集调用固定case,合成A/B固定句柄读写/guard测试,关闭所有资源并恢复静态guard;0编译错误/新文件0警告,无PlayMode。 | | B3A-18 | 原4A18组、4B1全23组、4B2全75组/关闭失败三进程/服务双条件/Unity B2-20,42/86/29/Review/3C离线回归;KVLive及旧NetworkTransport仅build。间歇首败完整保留,不以重跑覆盖。 | | B3A-19 | 源码范围/hash一致,新增生产依赖正确,限定自审、外部实施复核与主会话逐条裁决完成才关闭A;不将A通过等同B3或G4通过。 | 不在A测试真实广告、支付、PlayFab渠道、活动启动或全场景。这样的测试不属于A能力,不能声称通过。实际普通游戏启用证据属于后续B/C/4C/4D。 ## 8. 实施检查点(仅在本方案独立通过并裁决后生效) 0. 核对受审hash、A/B两部分裁决及白名单;保存PlayFabMgr开工副本、现有源hash;记录实施批准。任一P1/P2未关闭不编码。 1. 新键结果与ReadKV;先测B3A-01~03、14/15相关项,旧PF区字节比对,更新日志。 2. WriteKV/FlushKVAsync/WaitForKVTicketAsync透传;B3A-04~14全部故障/竞争用例,记录最初失败与修复;不增加后台机制,更新日志。 3. 两平台真实源码宿主、非Editor表面、Unity CLI Loop及4B1/4B2全量回归;观察首败根因不明时保留原状态,必要问题单独裁决,更新日志。 4. 范围/hash/限定自审/交付矩阵及外部实施复核提示词封板;外部接受后主会话逐条裁决关闭A。接着自动开始B的完整方案准备,不询问是否继续;B方案未经审查不编码。 每检查点更新`开发日志.md`与新`阶段4B3A实施记录-2026-09-24.md`。只有文档准备时写“设计检查点”,不冒充实施检查点已完成。不Git提交、不打包、不运行正式转发代理。 ## 9. 作者自审及外部重点 已核对A依赖复用、guard计数、无新会话拥有权、key规则、localOnly与已有pending语义、异步取消、旧PF不变、旧宿主通配依赖和Editor/Player隔离。作者自审不代替独立评审。 外部应分别回答:分期是否合理,A是否过薄/重复或存在必要接口缺口;四入口是否留下guard/所有权旁路;只改PlayFabMgr新增块是否可验证PF保持;A的测试是否可用现有故障能力实现;后续B/C不完整是否被正确限制为未批准,而非包装成全量完成。 审查若认为必须先完整证明全部业务调用再实施A,可明确退回分期A部分;主会话按报告裁决,不以“未来不启用”自动豁免任何本单元实际副作用问题。