Files
ft/CodexOutput/upserver/B3BatchMigration/M2/M2-DP1独立方案审查.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

168 lines
24 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 DP1 独立方案审查(双提供方独立 KV 用户服务抽离)
日期:2026-09-29。审查人:原 ZCode(独立侧)。只读审查,未修改任何生产/测试/服务器代码,未调用正式 101/102,未打包。
## 0. 受审基线核验
- [18 项受审基线](M2-DP1受审基线.json)逐项按字节数与 SHA256 重算:**18/18 一致,无漂移**。
- 清单自身 SHA256 实测 `858C43EF26361EB348D4CE36C7F3AFC8AF4DAF85E754A45F3D960A2D078145FA`,与提示词记录一致。
- 本报告据无漂移基线作出,可作为主会话裁决输入。基线外只读扩展源码调用点(`PlayerKVEditorValidationHost.cs`、`EventFishingDuelPanel.cs`、`FriendService.cs`、`LeadboardData` 等业务文件、`S101LoginWindow.cs`、`CodexOutput/upserver/KVGameLoadTests/ServiceHost/*`)仅用于证据,相关发现单独标注"基线外"。
## 总裁决
**限定通过(需修订后可进入白名单裁决)**。方案 C 的最小纵切方向成立:调用图证据显示正式 KV 路径可以在 DP1 内完全脱离 `PlayFabUserService` 运行时实例与 PlayFab SDK 调用,同时 PF/AGG/受控验证/kv-loop 四条既有通道不需要行为变化。无 P1 阻断。有 3 项 P2 需修订(R1/R2/R3)与 3 项 P3 登记(见 §8),全部是"把方案自身遗留的未决点落成可执行裁决",不是结构性缺陷。
---
## 1. 三方案比较是否成立(重点:方案 C 能否真正移除 PlayFabUserService 与 SDK 依赖)
**结论:通过(附 R1/R2 修订条件)。**
源码证据链:
- 启动装配:`SubBoostrap.cs:37-42` 一次捕获 `PlayerKVRuntimeSelection`;`SubBoostrap.cs:103` `if(!selection.IsKV) rootCtxGo.AddComponent<PlayFabMgr>()` —— KV 模式下 `PlayFabMgr` 组件根本不创建,`PlayFabMgr.HasLiveInstanceForKVLoad` 恒假(`PlayFabMgr.cs:31`)。
- 登录注册:`LoginStage.cs:24` 目前无条件 `Register<IUserService, PlayFabUserService>()`,是正式 KV 依赖旧类的唯一注入源;方案 C 只需在此按冻结选择分支注册新 KV 类。
- 登录行为:`UserService.cs:195-226` `LoginCore` 中 `if(IsFormalKV) return await LoginKVFormalAsync(...)`(:200)在 PlayFab SDK 全部步骤(`LoginPlayFab`/`OnYinDunLogin`/`InitTapJoy`/`LoginApiServer`,:207-225)之前返回;`PFLoginAct.cs:94-115` 正式 KV 分支在 `MailService.Init(); iapService.Init()`(:139-140)之前 return。即正式 KV 现有执行路径已零 PlayFab SDK 调用,抽离后把这段逻辑整体搬进新类即可,无需改语义。
- 正式 KV 编排全部位于 `UserService.KVLoad.cs` partial(`LoginKVFormalAsync` :82-147、`LogoutFormalKV` :67-75、`BindFormalPromptServices` :148-171、`CreateFormalKVHandler` :64-65、`OnFormalKVApplicationQuitting` :66),对旧类其余成员的依赖只有:`config`/`customServerMgr` 注入属性(:104-106)、`kvControlledInstance`/`runtimeSelection` 字段(:16-17)、`logoutGeneration`/`loginLifetime`(`UserService.cs:169-172`)、模块 31 的 `ResetForKVLoad`/`ReplaceFromKVDecoded`/`KVIntents`(:222-251)。这些均可随迁或在窄接口上重建,见 §3。
- 旧类保留通道不受损:
- PF 模式:`LoginCore` 去掉 `IsFormalKV` 分支后即恢复纯 PF 语义;`LoginStage` 非 KV 分支注册不变。
- AGG:`PlayerKVRuntimeSelection.Capture` 在 agg+kv 时直接抛异常(`PlayerKVRuntimeSelection.cs:29-30`),AGG 构建永远走 `AggUserService`(`LoginStage.cs:20-22`),不因 DP1 改变。
- kv-loop:`UserService.KVGameplayLoop.cs:537-586` `LoginKVGameplayLoopAsync` 深度依赖 PF 身份(`LoginPlayFab` :552、`LoginApiServer` :553、`PlayFabMgr.Instance.StartKVGameplayLoopSync()` :576、`ForgetAllCredentials` :613),它必须也应当留在 `PlayFabUserService` 上;`Bind(this)`(:570)在 Bind 改窄接口后仍编译(旧类实现该接口)。
- 受控验证:`PlayerKVEditorValidationHost.cs:236,265-267` 手工 `new AggUserService(...)` 并直接调用 `user.ResetForKVLoad`/`ReplaceFromKVDecoded`/`KVIntents` —— 签名不变即不受影响(基线外证据)。
- 编译级约束(须写入方案):新 KV 用户服务源文件**不得**引入 `PlayFab.*` using、不得引用 `PlayFabMgr`/`PlayFabUserService` 具体类型;"双模式证明"不得以能编译冒充,须按 §7 矩阵以注册结果与 `HasLiveInstanceForKVLoad` 断言。
方案 A/B 的否决理由与既有一致(A 无法满足双提供方目标,B 需要未核的线上合同),维持。
**为什么是"附条件"**:方案把两个决定性问题(`IUserService` 实现义务、登录/退出阶段的中立面)留作未决点(方案 §"完整评审需集中裁决的未决点" 1、4),本审查已给出确定性答案(见 §2、§6),方案文本需按此落成可执行裁决,否则白名单无法精确冻结。
## 2. DI/构造路径追踪:DP1 是否必须让独立 KV 类实现当前宽泛 IUserService
**结论:必须实现。追据如下(R1 的核心证据)。**
- `PlayerKVGameplayBindings.Bind` 在正式登录 `LoginKVFormalAsync` 内执行(`UserService.KVLoad.cs:100`),`Require<GameCore.SmallGameData>()`(`PlayerKVGameplayBindings.cs:28`)触发容器构造 `SmallGameData`。
- `DataCenter.SmallGame.cs:26-30`:`public SmallGameData(cfg.Tables tables, IUserService userService, ICustomServerMgr customServerMgr)` —— **构造函数注入** `IUserService`,是 31 个模块中唯一的构造参数级依赖(其余 4 个 DataCenter 文件均为方法内 `Resolve<>` 或属性注入:`DataCenter.PlayerData.cs:599`、`DataCenter.FishingEventData.cs:514` 等、`DataCenter.LeadboardData.cs:137`)。
- 模块 14 `ClubService` 有 `[Inject] public IUserService userService { get; set; }`(`ClubService.cs:86`),在 Bind 解析时做属性注入,同样要求容器内已注册 `IUserService`。
- `PFLoginAct.cs:15` `[Inject] public IUserService` 是登录页的必经注入。
- 结论:若 KV 类不实现 `IUserService`,KV 模式下 `SmallGameData` 无法构造、Bind 失败。替代路线(把 `SmallGameData` 构造与 `ClubService` 属性改为窄契约)需把 `DataCenter.SmallGame.cs`、`ClubService.cs` 加入白名单,改动两处业务文件,但 DP1 执行路径上**没有任何成员被调用**(`SmallGameData` 构造函数只存引用不调用成员,`DataCenter.SmallGame.cs:26-34`;受控验证 fixture 甚至传 `null`,`PlayerKVEditorValidationHost.cs:237` 基线外证据),因此该替代在 DP1 无收益,**不推荐本批做**,登记为 DP2+ 可选项。
**必须同时给出的成员三分表**(否则"显式失败"不可审计):
| 类别 | 成员 | KV 类处理 | 依据 |
|---|---|---|---|
| 真实实现 | `Login`/`Logout`/`LoginLifetime`/`CustomId` | 迁入正式 KV 逻辑 | `PFLoginAct.cs:38,88,96,111,168` 是登录页仅有的接口调用;`LoginLifetime` 用于链接取消 |
| 可观察状态(如实反映"未发布") | `IsLogin`/`IsAuthorized`/`UserId`/`ApiPlayerId`/`InternalId`/`entityKey`/`DisplayName`/`AvatarUrl` | 恒 `false`/`null`/`0`,与今日正式 KV 现状逐项一致 | `UserService.cs:110-115` + `LoginKVFormalAsync` 从不写这些字段;`LogoutFormalKV` 置空(`UserService.KVLoad.cs:71`);不是伪造,是如实反映"身份未发布" |
| 显式失败(throw InvalidOperationException,消息含"formal KV identity/profile contract pending") | `ChangeDisplayName`/`UpdateAvatarUrl`/`UpdateLanguage`/`LinkAccount`/`GetLinkState`/`SetPlayerLv`/`GetPlayLv`/`GetPlayerLv`/`GetPlayInfo`/`SetPlayInfo`/`ClearPlayInfos`/`UpdateProfile`/`UpdateProfileLv` | 全部显式失败并登记清账 | 全部调用点位于 FishingStage 闸后(社交/资料/UI)或 PF 专属链;DP1 执行路径零调用(§6 证据) |
| 登记裁决 | `CreateTime`/`LastLoginTime`/`LoginTime`/`CreateTotalSeconds`/`ContinentCode`/`Language`/`EAccoutUrl`/`Dictionary EAccoutUrl` | `EAccoutUrl` 按模块 31 置换真实 KV 值(`UserService.KVLoad.cs:249`);时间/大陆/语言字段维持默认值并在清账清单登记(P3-1) | `FishingEventData`/`PlayerShopData` 读 `CreateTime`(基线外,闸后);默认值行为与今日正式 KV 完全一致,不新增伪造 |
不接受项复核:方案明令禁止静默空实现、假 EntityKey、假 PlayFab ID、KV 失败回退 PF——上表不触碰任何一项;`GetDefaultName` 不属于上表(见 §4)。
**P2 风险 R1**:方案 §"未决点 1" 仍以问句留空。按本节证据应改写为确定裁决:"DP1 的独立 KV 类实现 `IUserService`,成员按上表三分;窄化 `SmallGameData` 构造依赖延后到 DP2+,登记最终清账"。
## 3. 第 31 模块的最小中立接收契约
**结论:通过。契约成员恰好三个,方案描述准确,可按下述精确化。**
事务侧全部消费点(`PlayerKVGameplayBindings.cs:55-56`):
```csharp
modules.Add(new PlayerKVLoadModule(31, user.ResetForKVLoad,
d => user.ReplaceFromKVDecoded(d.Module<PlayerKVLoginState>(31), d.Epoch, d.Raw)));
return new PlayerKVGameLoadTransaction(modules, ..., () => user.KVIntents?.Discard());
```
即窄契约 = `ResetForKVLoad()` + `ReplaceFromKVDecoded(PlayerKVLoginState, long epoch, IReadOnlyDictionary<string,string> raw)` + `KVIntents`(或折叠为 `DiscardKVIntents()` 委托源)。三个调用通道:正式(`UserService.KVLoad.cs:100`)、kv-loop(`UserService.KVGameplayLoop.cs:570`)、受控验证(`PlayerKVEditorValidationHost.cs:265` 基线外,手工组模块、直调具体方法、不经接口)。接口只需服务 Bind 一处调用者,两实现类(`PlayFabUserService` 供验证/kv-loop、新 KV 类供正式),符合 AGENTS.md"实际调用者"要求。
**字段/成员归属清单**(方案要求"哪些留在旧类、哪些必须迁出",此处给出完整盘点,全部出自 `UserService.KVLoad.cs` 行号):
| 归属 | 成员 | 行号 |
|---|---|---|
| 迁入新 KV 类(正式专属) | `kvHandoffEpoch`/`kvHandoffHandle`/`kvFormalLoading`/`kvFormalAttempted`/`kvFormalPromptsBoundEver`/`kvFormalClosing` | :22-26 |
| 迁入新 KV 类 | `IsFormalKV`/`KVHandoffReady`/`TryGetCurrentGameHandle`(epoch 对账) | :27-39 |
| 迁入新 KV 类 | `LoginKVFormalAsync`/`LogoutFormalKV`/`TrackFormalClose`/`BindFormalPromptServices`/`CreateFormalKVHandler`/`OnFormalKVApplicationQuitting` | :64-171 |
| 迁入新 KV 类 | `CaptureSaveBackend`/`RejectSaveBackend` 的 formal 分支(:47-51)——旧类保留 PF/kv-loop/受控分支 | :40-55 |
| 迁入新 KV 类 | `ObserveKVQuitAsync` 的 formal 观察语义 | :194 |
| 留在旧类(受控验证/kv-loop 专属) | `ConfigureKVForValidation`/`LoadKVForValidationAsync`/`CloseKVForValidationAsync`/`KVLoadedForValidation`/`KVReceiverCountForValidation`/`EnterKVReceiver`/`ExitKVReceiver`/`kvReceiver`/`kvReceiverUsers`/`MMTestForKVValidation`/`IsControlledKVInstance` | :56-63,172-203,219-220 |
| 两边各自实现(窄契约接口) | `ResetForKVLoad`/`ReplaceFromKVDecoded`/`KVIntents`/`KVLoadingCopy`/`MMTestValue`/`KVTapJoyBalance`/`EAccoutUrl`/`KVLastError` | :21,216-218,222-251 |
| 留在旧类(旧类自身还需) | `kvControlledInstance`/`runtimeSelection`(受控实例判定) | :16-17 |
拆分时必须保持的安全语义(方案已列,逐条核过源码成立):31 模块定序与主线程 Apply(`PlayerKVGameLoadTransaction.cs:37-45`)、失败 `ResetAll`+`discard` 回滚(:77-83)、`admitted` 准入在两个实现里各自收紧(旧类删掉 formal 项后剩 `kvControlledInstance || KVLoopLoading`;新 KV 类为"formal loading/closing 且无 handle 且 `InternalId==0` 且无 api 会话")、`AggUserService` 的 `internal override ResetForKVLoad/ReplaceKVLinked`(`AggUserService.cs:20-26`)不受影响。
## 4. IUserService 身份成员的分提供方语义
**结论:需修订(并入 R1 的成员表);DP1 不修改共享接口。**
- `entityKey`:真实非注释读取全客户端仅 2 处,均在 Duel 事件 UI:`EventFishingDuelPanel.cs:634-635`(`Id`/`Type`)、`:748`(成员比对)——FishingStage 闸后(基线外证据)。KV 无 Entity 合同,新类 `entityKey => null`(与今日正式 KV 一致,今日也从未赋值)。Duel 两处属 DP2+ 身份/业务合同批次;null 会在该 UI 触发 NRE,闸未开前无影响,但必须登记到最终门控清账清单(P3-2)。
- `UserId`:PF 语义为 PlayFabId;正式 KV 今日恒 `null`(`LoginKVFormalAsync` 不写、`LogoutFormalKV` :71 置空)。社交/缓存键(如 `DataCenter.SmallGame.cs:390,434,513`)全部闸后。KV 侧最终以哪个 ID(`101`/`102` 规范名还是服务端内部 ID)作社交键,属 DP2+ 身份合同,本批不得提前拍板。
- `ApiPlayerId`:`IsAuthorized ? apiSession.PlayerId : null`(`UserService.cs:115`),正式 KV 无 `apiSession` 恒 null,新类照实返回 null。
- `InternalId`:正式 KV 恒 0(不经过 `LoginApiServerCore`)。
- `IsLogin`/`IsAuthorized`:正式 KV 现状恒 false(`UserService.cs:110-112`),新类照实。**不得**在 `KVHandoffReady` 后翻转这两个值——那是首场景身份发布(CP2B 后续)的裁决,DP1 的"就绪"只通过 KV 专用状态面暴露(见 §6 R3)。
- `GetDefaultName`:`id[0..5]`(`UserService.cs:146`)对 `"101"`/`"102"` 会抛 `ArgumentOutOfRangeException`(长度 3 < 5)。调用点约 10 处全在闸后(`FriendService.cs:164`、`LeadboardData.cs:239`、各社交/伙伴 UI,基线外)。方案已如实记录;登记清账(P3-2),DP1 不改。
**临时门控与清账条件**:DP1 的门 = `LoginStage.RequestNextAct` 的 KV 早退(`LoginStage.cs:114-115`)+ 显式失败成员表;清账条件 = 线上身份/资料合同(DP2+)落地后逐成员改为真实语义或迁移到窄接口,届时重审。
## 5. 启动开关、冻结、AGG、URL、准入与生命周期安全性
**结论:通过。方案继承的时序在源码全部成立,可整体迁入新类。**
- 唯一开关与冻结时点:`SubBoostrap.RegisterServices` 在任何用户服务注册/登录之前捕获一次(`SubBoostrap.cs:37-42`);`LoginStage` 只读 `SubBoostrap.RuntimeSelection`(:35,114)。会话内不可换:`LoginKVFormalAsync` 的 `kvFormalAttempted` 单发 + `loginGate` 信号量 + generation/requestedId 对账(`UserService.cs:172-193`、`UserService.KVLoad.cs:85-119`)阻止二次登录/换号,符合用户已裁决的重启式重登。
- `SaveBackend` 再读取:登录时 `CaptureSaveBackend` 重读配置(`UserService.cs:173`)并经 `RejectSaveBackend` 与冻结选择对账(`UserService.KVLoad.cs:47-51`:formal 下非 `"kv"` 即 `Input` 错)。这是对同一事实的防御性复读,不是第二开关;**拆分后新 KV 类必须保留等价校验**(读到的后端串必须等于 `"kv"`),旧类保留 PF/kv-loop/受控分支。
- 编辑器直开:`InternalEditorBoost.Init` 仅允许 playfab,KV 选择直接抛异常(`SubBoostrap.cs:378-392`)。不因 DP1 改变。
- AGG:`Capture` 在 agg+kv 时抛(`PlayerKVRuntimeSelection.cs:29-30`),早于 `LoginStage` 的 `#if AGG` 注册(`LoginStage.cs:20-22`),"AGG-KV 启动失败、不静默回退"已由现状保证。
- 正式 URL:`Capture` 严格比对 `FormalApiUrl`(:31-32);`LoginKVFormalAsync` 再核 `server.ApiBaseUri` 与同 URL(`UserService.KVLoad.cs:92-95`)。
- 准入:`TryAdmitPlayer` 仅 101/102 且规范化(`PlayerKVRuntimeSelection.cs:36-43`);"有现存档"由加载三态语义保证(无档≠假空,此前批次已裁定)。
- KV 无 PF 活动实例:`SubBoostrap.cs:103` 不挂组件 + `LoginKVFormalAsync:94` 核 `HasLiveInstanceForKVLoad`。
- 生命周期可迁性:401/403 经 `server.KVUnauthorized` 事件 → 版本对账 → `LogoutFormalKV`(`UserService.KVLoad.cs:105-109`、`CustomServerMgr.cs:180-184`);`Application.quitting` 订阅(`UserService.cs:156`)在旧类删除 formal 分支后由新类构造器订阅;`Logout`/`QuitAct` 先同步失效再观察关闭(`LogoutFormalKV` 先 `DisableKVBusiness`/置空 handle/取消 LoginLifetime,再 `TrackFormalClose`,:67-75);Close 失败保留 `kvFormalClosing` 屏障(:76-81);在途旧响应与同账号新 epoch 由 `TryGetCurrentGameHandle` 的 epoch/引用对账(:30-39)+ `CustomServerMgr.SessionVersion`(`CustomServerMgr.cs:177`)拒绝。以上全部位于可整体搬迁的 formal 段,无旧类私有耦合。
## 6. 跨批依赖与隐藏必经路径
**结论:通过(附白名单修订 R2/R3)。**
- DP1 执行路径(SubBoostrap → LoginStage → PFLoginAct → Bind/Apply → 交接 → 停在登录页)上的 `IUserService` 实际调用面 = `Login`/`Logout`/`LoginLifetime`/`CustomId` 四个(`PFLoginAct.cs:38,88,96,111,168`);`SmallGameData` 构造只存引用(`DataCenter.SmallGame.cs:26-34`);`ClubService` 属性注入后在 Reset/Replace 中不触 `userService`(`ClubService.cs` M14 段无引用,社交请求全部 `IsAuthorized`/`ApiPlayerId` 守卫且闸后,:245-330,384-589)。
- DP1 执行路径上的 `PlayFabMgr.Instance` 直调 = **0**:组件不创建(`SubBoostrap.cs:103`),登录页 KV 分支不触 TapJoy/邮件/IAP(`PFLoginAct.cs:114` 提前 return)。全客户端 225 处 `PlayFabMgr.Instance`(83 文件)与 213 处 `IUserService` 文本引用全部位于 FishingStage 闸后或 PF/AGG 模式——本批无须迁移任何一个;打开闸门前(DP2+)必须成组处理,方案已如实声明。
- 本批必须迁移的最小调用者(与方案候选白名单一致):`LoginStage.cs`(注册分支)、`PFLoginAct.cs`(中立检测/状态/输入)、`QuitAct.cs`(退出观察)、`PlayerKVGameplayBindings.cs`(Bind 窄接口)、`UserService.cs`+`UserService.KVLoad.cs`(formal 段搬出与旧类分支清理)、新增 KV 类。
- `FishingStage` 闸保持:`LoginStage.RequestNextAct`(:114-115)不动。
- 不把客户端路由字面量或本机服务器代码当合同:本审查未读服务器目录;`mail/fetch` 等仅为 CP2B-T 的路径示例,不构成线上合同。
**R2(P2)— 测试迁移与不改文件的编译耦合核验义务**:`CodexOutput/upserver/KVGameLoadTests/ServiceHost/Program.cs` 与 `Stubs.cs` 直接 `new PlayFabUserService()`/`new AggUserService()` 驱动 formal 登录、401、quitting、双登等用例(:50,64,73,88,105,109,124,153,基线外证据)。formal 成员搬出后这批用例必须改指向新 KV 类;同时 `UserService.KVGameplayLoop.cs`、`AggUserService.cs`、`PlayerKVEditorValidationHost.cs` 不在白名单也确实无须改动,但白名单文件须**显式记录**"这三个文件依赖窄契约签名不变"的核验义务,防止实施时顺手扩面。方案文本需补这两点,否则"必要合成测试"不可审计。
**R3(P2)— 登录/退出阶段的中立面须落成具体机制**:方案只说"提供方中立的登录结果或窄 KV 会话能力"。具体缺陷三处:
1. `PFLoginAct.cs:88` `playFabUserService.CustomId = configCustomId` 与 :167-170 的下转——新类注册后 `playFabUserService` 为 null 会 NRE;应改 `userService.CustomId`(接口已有成员,`UserService.cs:70`),彻底去掉该下转。
2. formal 检测(:94,:316)应以 `SubBoostrap.RuntimeSelection?.IsKV==true` 为准,不得以"注入对象可下转到某具体类"为准。
3. `QuitAct.cs:40,61` 两个下转在新类下全部失效(validation 退出观察与 formal 退出观察分别落在两个类上)。建议一个 internal 窄接口(例如含 `KVHandoffReady`、`KVLastError`、`ObserveKVQuitAsync` 三成员),`PFLoginAct`/`QuitAct` 为其仅有的两个消费者,PF/受控语义由旧类实现、formal 语义由新类实现;或明确采用"新类具体下转 + 旧类下转并存",二选一须在方案定死,避免实施时各写各的。
## 7. AGENTS.md 简洁性核对
**结论:通过(附条件)。**
- 新增抽象仅两件,均有实际调用者:31 模块窄契约(消费者=`Bind` 一处,实现=旧类[验证/kv-loop]+新类[formal]);KV 状态/退出观察窄接口(消费者=`PFLoginAct`/`QuitAct`,实现=两类)。无第二套 Coordinator/Guard/存档管理器;方案明令禁止,源码确认无需。
- 开关单一:复用 `RuntimeSelection` 冻结捕获;kv-loop `Enabled` 是编辑器测试通道许可(permit 文件驱动),`CustomServerMgr.kvOnly/kvBusinessReady` 是运输层状态,各有不可省职责,DP1 不新增任何开关。
- 闲置路径清账已在方案 §DP2+ 与约束文件清单中登记;本报告补充:旧类拆分后残留的 formal 分支(ctor :156、`EvtAPISvrUnauthorized` :159、`IsLogin` :110、`LoginCore` :200、`Logout` :684 的 `IsFormalKV` 项)必须同批清干净,不得留死代码——这正是 `UserService.cs` 入白名单的原因,方案候选已含。
- 最小必测矩阵(建议写入验收):
1. PF 回归:同构建选 playfab → `Resolve<IUserService>()` 为 `PlayFabUserService`(AGG 配置为 `AggUserService`),现有 PF/kv-loop/受控验证套件原样通过;
2. KV 正式:选 kv → `Resolve<IUserService>()` 为新 KV 类 且 `PlayFabMgr.HasLiveInstanceForKVLoad==false` 且全进程零 `PlayFabUserService` 构造(以注册/类型断言,不以编译冒充);
3. 既有 formal 用例迁移后全绿:成功/无档/非 101/102/错误 URL/AGG-KV 拒绝、401/403、双登单发、`Logout`/`Application.quitting`、Close 失败屏障、旧响应/新 epoch、31 模块定序与回滚、`KVHandoffReady` 与 `EnableKVBusiness` 单次发布;
4. 显式失败成员逐个断言抛出(防静默空实现回潮);
5. Unity CLI Loop Editor 编译 0 错误;可取得的真实 Player 编译另记。
- 不能声称已通过的限制:线上业务路由/响应合同、`IsLogin`/`ApiPlayerId` 正式发布、SignalR 真连接、首场景链路、真实 Player 编译(工具受限时)。方案 §DP1 验收已如实声明,维持。
## 8. 发现清单(严重度汇总)
| 编号 | 严重度 | 内容 | 最小修订 |
|---|---|---|---|
| R1 | P2 | 方案未决点 1(IUserService 实现义务)应按 §2 证据落成确定裁决 + 成员三分表 | 修订方案文本,白名单裁决时引用本报告 §2 表 |
| R2 | P2 | 既有 formal 合成测试(ServiceHost Program/Stubs)迁移义务与三个"不改但受签名耦合"文件的核验义务未写入方案 | 补入 DP1 白名单说明与验收 |
| R3 | P2 | PFLoginAct/QuitAct 中立面机制未定死(CustomId NRE 风险 :88/:167-170;QuitAct 双下转失效 :40/:61) | 按 §6 R3 定死窄接口或双下转方案 |
| P3-1 | P3 | KV 类时间/大陆/语言只读成员维持默认值(与今日一致),`CreateTotalSeconds` 返回值无意义 | 登记最终清账;DP2+ 身份合同批次裁决 |
| P3-2 | P3 | `GetDefaultName` 短 ID 抛异常;Duel `entityKey` 两处读取在身份发布后 NRE | 登记清账清单,指定 DP2+ 批次 |
| P3-3 | P3 | 基线外观察:`S101LoginWindow.cs` 编辑器最小认证探针与 IUserService 无关,不构成双提供方影响 | 无须动作,记录在案 |
## 9. 可执行修订建议(供主会话直接裁决)
1. 方案按 R1 改写未决点 1 为确定裁决(§2 表原样落入方案)。
2. DP1 白名单定格:生产 = `LoginStage.cs`、`PFLoginAct.cs`、`QuitAct.cs`、`PlayerKVGameplayBindings.cs`、`UserService.cs`、`UserService.KVLoad.cs`、新增 `Services/PlayerKVUserService.cs`(命名可裁);测试 = `KVGameLoadTests/ServiceHost/Program.cs`、`Stubs.cs` 及既有 formal 用例迁移、必要新增断言。`SubBoostrap.cs` 现状无 DI/冻结缺口,不入白名单(若实施发现缺口须先补审)。
3. 明确禁止项写入白名单:新类禁 PlayFab using/具体类型引用;不碰 `UserService.KVGameplayLoop.cs`、`AggUserService.cs`、`PlayerKVEditorValidationHost.cs`、`DataCenter.SmallGame.cs`、`ClubService.cs`、场景/Prefab/服务器/付费/社交路由。
4. R3 的中立面机制二选一定死后才可编码;建议窄接口(消费者明确、两类实现语义分离)。
5. `PlayerKV*` 改名、`Core/KVMgr` 物理依赖、公开身份发布、GetDefaultName/时间字段语义——全部留在既有延期清单,本批不动。
**再次声明**:本报告落盘不等于批准编码。主会话须先重核 18/18 hash(本报告 §0 可复核),再逐项裁决并落精确白名单。