Files
ft/CodexOutput/upserver/阶段4B分期与4B1登录存档准备方案-待评审-2026-09-23.md

172 lines
18 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.
# 阶段4B分期与4B1登录存档准备方案(待独立评审)
日期:2026-09-23。前置:[4A实施复核裁决与关闭](阶段4A实施复核裁决与关闭-2026-09-23.md)已完成。
**本文是下一步提案,不是实施批准。当前仅完成文档准备,4B尚未编码,游戏仍使用原存档路径。** 沿用主会话写方案→用户指定外部AI审查→主会话逐条裁决→明确边界后编码。正式地址固定 `http://s101.local:18080/`;Unity CLI Loop;不使用UnitySkills;暂不打包。
## 1. 现状与分期理由
| 已核源码 | 观察 | 设计约束 |
|---|---|---|
| UserService.cs,LoginCore、LoginPlayFab、InitData | LoginPlayFab在API登录前调用InitData;InitData调用CompareDatas、MMTest写入、LoadPlayerData和OnInitData | 不能只在当前登录尾部插入一次KV Get,否则已产生PF初始化及写入副作用 |
| UserService.cs,LoginApiServerCore、Logout、401订阅 | 重登/注销直接Dispose apiSession;401触发后台LoginApiServer | KV Owner关闭前不能换Cookie或销毁session;401必须撤销当前存档上下文,不能在旧Owner上静默重新认证 |
| PlayFabMgr.cs,CompareDatas、Awake、UpdateUserDataValue、SyncData | PF时间戳与PlayerPrefs合并可写回,Awake启动40秒任务,普通写为void,全局Instance在回调执行时解析 | KV路径不能复用PF缓存比较;全局当前账号路由无法识别旧回调归属;必须审查调用起点 |
| AggUserService.cs,Login、InitData | OnInitData订阅只在base.Login等待期间有效;初始化会补写渠道标记并修改传入字典 | 不能把不可变KV快照直接传给现有初始化,也不能延后到base.Login返回后再调用 |
| DataCenter.PlayerData.cs,LoadPlayerData、LoadPlayerDataNew | 先设置默认值再解析;NewPlayerDataDic仅在key存在时替换;Init/体力恢复链可保存 | 空KV、缺主档、解析失败和旧业务对象残留必须分别阻断;通用字符串字典合法不代表完整游戏档合法 |
| QuitAct.cs,StartAsync | 注销先同步失效,再await Hub释放,最后销毁RootCtx | 保留先撤销身份的语义,后续增加可等待KV关闭;不得以等待网络为由延迟身份失效 |
上述观察是本轮静态读取,不宣称已经实现或运行验证。
## 2. 路线比较与推荐边界
| 方案 | 优点 | 代价/本提案选择 |
|---|---|---|
| 一次实施完整4B(登录、业务初始化、所有延后读写与退出) | 一次交付实际游戏效果 | 跨全局facade、渠道回调和大量DataCenter/UI调用,当前只有分期合同,缺完整逐调用点清单;不推荐直接开工 |
| **4B内再分三个验收单元** | 每次可给出完整文件白名单和独立用例;先锁定session所有权与认证/读取顺序 | **推荐**;4B1只做准备内核,不宣称游戏已切换,4B2/4B3继续独立详审 |
| 按业务key逐个PF/KV混用 | 单次改动表面小 | 同次初始化可能拼接两份档;跨key依赖和自动回退难证明,维持原方案不采用 |
| 单元 | 要解决的具体问题 | 编码前所需审查 | 启用范围 |
|---|---|---|---|
| 4B1 登录存档准备 | API session所有权、认证→Bridge Open/Get→安全快照、失败清理、注销/取消真正排水 | 本文§3–§8完整合同、文件边界与矩阵 | 仅离线合成宿主;不接UserService/游戏,不向正式服发请求 |
| 4B2 游戏加载与内存隔离 | PF只供暂留资料;严格业务档校验;清空/重建本账号业务对象;初始化副作用和OnInitData时序;401/退出接线 | 单独列完整加载字段/模块、缺失与损坏策略、默认值来源、写入副作用、初始化失败清理和精确文件清单 | 在完整启用门前只做替身/受控加载验收 |
| 4B3 普通读写入口 | 固定后端facade、固定句柄、逐个延后回调捕获、结构化持久化失败、定时/暂停Flush | 重新生成调用盘点,逐项标明同步/异步、句柄捕获起点、取消点、可变业务对象归属及允许修改文件 | 4C消费流程、tickets有界历史和正式场景数据边界未完成前,不启用实际含消费玩法 |
此分期仅细化4B,不撤回整体阶段4目标。4B1通过不等于4B通过;4B通过不等于G4通过。
## 3. 4B1职责与公开合同
建议新增 `PlayerKVLoginContext`(每次登录尝试一个实例),命名空间沿用 `game.PlayerDataStorage`。它拥有一个新建的S101AccountSession和一个4A Bridge,持有但不向外发布固定Handle。它不引用Unity、PlayFab、GContext、全局manager或任何玩法类。
与4A的职责差异:4A要求外部已经认证且不拥有session;4B1负责建立认证session、按顺序取档,并保证Bridge真正Close后才Dispose该session。复用4A串行/票据/读接口,不复制或修改其实现。
### 3.1 输入与输出
- 创建时捕获数字ID、绝对缓存根、long epoch、轻量 `Func<long,bool> isCurrent`,之后不可变;ID规则复用现有正数Int64十进制规则并规范化。正式公开构造使用固定s101基址,测试替身/API覆盖仅internal。
- `PrepareAsync(CancellationToken lifetimeToken)`:单次操作;同实例重复调用返回同一任务,不再登录、不换Cookie、不产生第二个Owner。首个调用的token定义本次上下文生命期,后续调用的token不替换它;接口文档和测试明确这一点。调用者若仅停止等待,不得据此新建上下文绕过Close。
- 返回 `PlayerKVLoginPrepareResult`:Prepared、Error、AccountError、OpenError、OwnerError、KVError、SyncStatus、HTTP状态(可空)、CleanupFailed,以及非敏感身份字段PlayerId/Epoch/OwnerInstanceId。失败时Data=null。
- Prepared仅表示“认证成功且取到当前Ready快照”,**不表示新档/有效业务档/已初始化/可以进游戏**。成功Data为独立不可变字符串字典;允许成功且Count=0,必须显式 `IsEmpty=true`。该空结果来源于成功Get,不能被消费方当作建档批准。
- `CloseAsync()`:首次调用同步撤销上下文(方法返回Task前已不可再发布有效结果),返回共享可等待任务;重复调用返回同一任务。不提供同步Dispose或超时后强制释放。
- `IsCurrent`:当前代际仍有效、未撤销、session/Cookie仍匹配;业务不能仅凭历史Prepared布尔值判断当前有效。已返回快照不会被销毁,但其归属过期后不得用于初始化或写入。
- 本阶段不公开session、Cookie、Handle、Update/Delete/Flush接口,不调用CustomServerMgr.UseAccountSession;未来接线若需非拥有式session绑定或写句柄,必须在4B2/4B3方案中明确扩展,不临时绕过私有所有权。
### 3.2 错误分类
| Error | 条件 | 数据/收尾规则 |
|---|---|---|
| None | 认证、Open、Read及最终归属检查成功 | Prepared=true;返回快照与身份;IsEmpty只表示空字典 |
| Validation | ID/缓存根/委托等无效 | 不创建传输;Data=null |
| Cancelled / Stale | 本次token取消/外部代际失效/关闭 | 不发布可用快照;清理完成前不完成失败返回(CloseFailed除外),不借新session |
| AuthenticationFailed | S101登录结构化失败 | 原AccountError和HTTP状态保留;Dispose本次session,未发KV Get |
| OwnerRejected | Bridge Open失败 | 原OpenError/OwnerError/KVError/SyncStatus按可得信息保留,不用默认Ready代替未知状态 |
| NotReady | Open完成但Read拒绝,如Conflict/Unknown/StorageError | 原状态保留,Data=null,不读旧PF/PlayerPrefs补档 |
| Unexpected | 工厂/回调或预期外异常 | Data=null;只记录异常类别,不记录值/请求/Cookie;仍执行清理 |
| CloseFailed | Bridge或session清理失败 | CleanupFailed=true,保留原失败原因与所持对象/任务;禁止重开;不把失败Close标成功 |
AccountError等现有错误字段没有适用值时用可空字段,不能用None暗示该阶段已成功。
## 4. 顺序、关闭与并发规则
### 4.1 准备链
1. 同步验证固定输入和外部归属;进入单次Preparing。取消或过期请求不分配新传输。
2. 新建S101AccountSession,await LoginAsync;失败不调用Bridge。成功后再次核对epoch、生命周期及session身份。
3. 使用同一session和固定缓存根/epoch调用4A Bridge.OpenAsync,接受4A“真正收尾后才放行”的保证。期间任何取消必须继续等待底层构造/传输收尾,不只看上层排队取消返回。
4. 固定Handle.ReadLocalSnapshot;保留Ready/NotReady/Stale区别,获取不可变快照。最后同步检查上下文撤销标志和外部归属后,才完成Prepared结果。
5. 不调用业务初始化、默认建档、PF加载、PlayerPrefs、写入或Flush。任何失败走统一关闭路径。
### 4.2 单次实例与跨实例责任
一个context只代表一次登录,不提供重登方法。调用方必须await旧context.CloseAsync成功后才能创建下一context;这一顺序在4B1组合测试中演示,并作为4B2登录Gate的必测合同。4B1不宣称能阻止任意外部代码故意新建多个context。Close失败的对象必须保留,禁止通过重新new对象规避关闭失败;同账号缓存锁只是兜底,不能替代跨账号关闭屏障。
### 4.3 防止失败与Close互等
必须区分两项任务:底层prepare工作结束信号(workSettled)与对外Prepare结果。workSettled只表示认证/打开/读取不再执行;对外失败结果可继续等待清理。
- Close先原子撤销本context,锁外取消context自身的CTS,再await workSettled,随后await bridge.CloseAsync,最后Dispose账号session。
- prepare路径成功或失败都在finally完成workSettled;失败路径只能在完成该信号后等待统一Close。Close不得等待对外Prepare任务,避免“Prepare等Close、Close又等Prepare”的环。
- workSettled完成时Bridge内部可能仍有排队收尾;仍必须await Bridge.CloseAsync,不能把它省掉。
- Close在Prepare之前被调用时,将本实例永久关闭,并使工作信号无需等待;后来Prepare返回结构化Cancelled,不发请求。
- Prepare/Close并发发布、同实例重复Prepare/Close、token注册/取消/释放的临界区必须统一设计。锁内只改状态/捕获任务,不Cancel、不调用外部委托、不执行I/O、不await。
- 取消回调只同步撤销并触发非阻塞收尾,不等待Close;后台收尾任务必须保留并可由Close观察,不能遗失异常。
- 外部epoch可以任意时刻变化;4B1不轮询。下一边界检查立即拒绝失效结果,调用方在代际切换时必须调用Close。已返回快照不是永久授权。
### 4.4 资源关闭次序
`撤销业务准入 → 取消本次生命周期 → 等prepare工作settled → await Bridge.CloseAsync → Dispose S101AccountSession → 释放context CTS/注册`。
Bridge Close失败时保留session、bridge及故障任务,不提前销毁Cookie/HttpClient;失败状态永久阻塞该实例。session.Dispose失败也保留CloseFailed,不能当作可继续登录。Close从不隐式Flush,Unknown不自动重试。关闭期间不处理游戏退出、Hub或渠道SDK;这些由4B2在组合层按此顺序接入。
### 4.5 外部归属委托
采纳4A P3-3:只读取捕获ID/epoch及撤销标志;不I/O、不等待任务、不触发登录/关闭,不获取与Close反向依赖的锁。异常视为失效并保留异常类别诊断,进入安全清理。不要使用“当前全局账号相等”替代本次context身份;同号重登也必须区分epoch。
## 5. 业务档及未来接线的硬边界
- 4B1返回成功空快照是协议事实,不是新档批准;`newCreate=true`亦不自动授权。4B2默认策略为现有账号空KV阻塞,不复制PF/PlayerPrefs,不写默认档。新玩家建档另定显式流程。
- 4B1不验证NewPlayerDataDic、GuideSaveData、MMTest等业务格式,不声明可玩。4B2在调用现有LoadPlayerData前完成完整业务校验与清理方案;不能只验证一项JSON就宣布完整档有效。
- 4B2需要为InitData/MMTest/Agg渠道标记/体力恢复等写入副作用逐项选择延迟、暂存或禁写策略,并验证失败回滚/重建业务内存;未审清不得接游戏。
- 4B3必须按回调创建时的context捕获归属。仅让PlayFabMgr.Instance转发到“当前Handle”不能阻止旧回调写新账号。每条修改还须防止旧回调先修改共享业务对象、再被存储层拒写的情况。
- PF身份暂留作资料及外围依赖,ApiPlayerId作KV身份;不全局改UserId、不删SDK。普通保存本地Accepted与远端Confirmed明确区分;立即消费留4C结构化处理。
- tickets有界历史仍为长期会话启用门,4B1不新增票据、不修改Coordinator来顺便解决此问题。
## 6. 4B1文件白名单(仅待审提案)
| 类型 | 精确路径/职责 |
|---|---|
| 新生产文件 | `Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs`:单次认证/准备/资源所有权与统一关闭 |
| 新结果模型 | `Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginPrepareResult.cs`:错误、阶段证据、不可变快照及身份 |
| 新meta | 上述两文件对应 `.meta` |
| 新合成测试目录 | `CodexOutput/upserver/KVLoginContextTests/`:Program.cs、显式链接真实生产源码的csproj、独立runs及诊断输出 |
| 文档 | 本方案、4B1任务/实施/交付/审查文件、主日志与共享状态入口 |
禁止修改S101AccountSession、4A Bridge/Owner/结果、3A状态/磁盘、3B Coordinator、KV Client、旧测试Program/csproj、UserService、PlayFabMgr、DataCenter、Stages、服务器、包/asmdef。如发现现有公开接口不足或旧测试包含通配符导致编译问题,先记录最小差异并另行补审,不能边写边扩大范围。
测试需注入创建session/bridge的internal工厂,用现有S101AccountSession的handlerFactory和Bridge internal handler构造,不新增公共任意传输入口。生产默认固定地址、Cookie和禁重定向沿用现有实现。工厂只允许离线测试故障注入,不替换生产所有权约束。
## 7. 4B1验收矩阵
以下均为计划,尚未运行。并发使用TaskCompletionSource/事件闸门,不用Sleep断言先后。所有登录/Get只走synthetic.invalid替身;缓存位于本次run目录。
| 编号 | 场景 | 必须观察结果 |
|---|---|---|
| B1-01 | 正常认证/Get/读取 | 请求顺序正确,同一Cookie归属;Prepared及快照正确;无Set/Delete/PF/初始化回调 |
| B1-02 | 非法ID/根/空委托、提前取消、提前代际失效 | Data=null,0传输,结构化错误 |
| B1-03 | 认证HTTP/协议/拒绝/网络失败 | 保留具体AccountError/HTTP状态;0 KV Get;本session已清理 |
| B1-04 | 初始Get失败、Busy、Conflict、Unknown、StorageError | 不返回可玩数据,保留原错误和状态,不回退PF、不写空档;清理成功后新context可由调用者打开 |
| B1-05 | 成功Get返回空字典,分别newCreate true/false | Prepared且IsEmpty=true;无建档、初始化或任何变更请求 |
| B1-06 | 同context重复Prepare(不同token) | 同一任务、仅一次登录/Get,后续token不替换首个生命期;定义有测试 |
| B1-07 | 认证忽略取消时Close | 同步失效,Close与对外失败结果等真实返回;不提前Dispose传输;底层结束后清理 |
| B1-08 | Get忽略取消时Close,之后准备102 | 101底层任务与Bridge排水完成、session再Dispose;测试调用者等待旧Close后才启动102;无Cookie/快照串号 |
| B1-09 | Read前/后epoch失效及同号重登 | 失效优先,不发布旧快照;旧结果归属检查false;Owner不复用 |
| B1-10 | Prepare失败与Close同时发生 | workSettled/对外结果分离,无互等;测试以闸门推进并有整体超时保护 |
| B1-11 | Prepare前Close、并发重复Close、取消回调重入 | 关闭幂等、任务不遗失、CTS无重复释放问题;关闭后Prepare零请求拒绝 |
| B1-12 | Bridge Close/清理异常 | CloseFailed与原错误都保留;session未提前Dispose;context不重开;不假成功 |
| B1-13 | isCurrent/工厂异常 | 结构化Unexpected或Stale,Data=null;可释放资源全部按顺序释放,失败仍记录 |
| B1-14 | pending/Unknown已有缓存 | pending只读呈现且Close零写;Unknown拒读且不重试;不改变既有恢复语义 |
| B1-15 | 输出与不可变性 | 仅用例名/状态/计数/脱敏身份摘要;无Cookie/Token/完整值/用户路径;快照脱离内部字典 |
| B1-16 | 回归与编译 | 4A18组、42/86/29、Review、3C离线8/8、KVLiveTests只build、Unity CLI Loop编译;未授权修改文件hash不变 |
采纳P3-1:每条命令保留完整stdout/stderr和真实退出码;若86套件再次失败,保留首轮用例名及原输出,再诊断,不覆盖或只报告后续成功。已有偶发失败根因未定位。
## 8. 交付、评审问题与自审
实施获批后交付:生产/测试文件完整SHA256、实施前后范围hash、逐行矩阵、各命令/退出码、每个检查点日志、限定自审及外部实施复核提示词。所有实际测试以独立run记录;不访问正式服,不运行KVLiveTests或3C --live-s101。
外部审查必须分别裁决:
1. 是否接受4B分期,以及4B1仅为准备内核、不提前接游戏的边界。
2. session所有权、workSettled与失败结果分离、Close异常保留是否完整,是否存在取消/重复操作互等或资源泄漏。
3. 成功空快照与失败Data=null是否足够清楚,是否会被误解为游戏初始化放行。
4. internal替身工厂和精确白名单是否足以实现;若需扩旧接口,先给最小差异,禁止默认扩大。
5. 矩阵是否覆盖生命周期竞争,P3待查项是否准确保留,4B2/4B3启用门是否存在被绕过的路径。
主会话文档自审:分期目的、职责/非职责、错误、生命周期、资源关闭、文件边界和测试均已写明;没有将后续业务档校验或所有回调隔离当成4B1已经解决。4B2/4B3明确属于未来独立方案,不能以本文件批准其编码。原工作区改动与历史证据保留,不自动提交Git。
流程检查表:
- [x] 读取既有阶段方案、4A复核、登录/存档/退出及渠道初始化相关源码。
- [x] 沿用已有用户约束,比较三条路线并提出明确推荐;本轮无需重问地址、工具或打包策略。
- [x] 写出4B1完整提案和逐条验收合同,完成文档自审。
- [ ] 用户指定外部AI独立审查本方案;主会话核对版本后逐条裁决。
- [ ] 书面方案及边界获批后再实施;当前未开始编码。