Files
ft/CodexOutput/upserver/阶段4C2收线取消缺陷修复方案独立审查-2026-09-24.md

101 lines
15 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.
# 阶段4C2收线取消缺陷修复方案独立审查
2026-09-24。审查对象:《阶段4C2收线取消缺陷最小修复方案-待评审-2026-09-24.md》(sha256 47CF3AFA…FB1,本次hash核对一致)。独立只读审查,未改动任何生产代码、共享文档或测试;未运行Unity、未连接任何服务器/账号。
## 一、审查范围与方法
1. 通读方案与《阶段4C2第二次真实尝试与收线取消记录-2026-09-24.md》,并以 run 62216 一手证据复核:`KVGameplayLoopTests/runs/62216_639258472117601499/events.jsonl` 全时间线、`diagnostic-impl/run62216-final-state.json`(error="Fishing reward cancelled: controller disabled")。
2. 逐行核对源码:`Client/Assets/Scripts/Services/UserService.KVFishingReward.cs`(FishingRewardAction :26-40,字段 :36;BeginRewardEnding :167-175;BindRewardPanel :176-187;RewardCurrent :188-189;BeginRewardSegment :203-211;ConfirmRewardAsync :242-275;PresentReward :276-280;CancelReward :281-287)、`FishingUpdateSys.cs`(Update→FishEnd :1225-1228;FishEnd :1247-1263;ShowKVRewardPanelAsync :1265-1273;FishEndCore :1275-1380;OnDisable :3200-3213;OnDestroy :3250-3259)、`UserService.KVGameplayLoop.cs`(CancelFishing :192-198;Revoke/CloseAsync :278-279;Fault :281-291)、`FishingRewardPanel.cs`(OnDisable :36;ConfirmKVRewardAsync :25-35;OnShow绑定 :340-364;OnDestroy :1177-1180)、`UserService.KVFishingRewardValidation.cs` 全文、`FishingBehaviorB.cs`(BeginFishing(this) :767;CancelKVFishing :34;OnDisable/OnDestroy :2436-2439/:2462-2465)、`QuitAct.cs`(CloseAsync排水 :35-38)、`UIManager.cs`(DestroyUI,GameObject.Destroy :938)、`FishingDuelAct.cs:168`。
3. 全库grep核对 `CancelReward`/`CancelFishing`/`BeginRewardEnding`/`BeginRewardDrawing`/`BeginFishing`/`DestroyUI(FishingPanel)` 的全部调用方,确认守卫影响面闭合。
4. 按 `KVFishingRewardTests/controller-cancel-fix-review-hashes.json` 以 SHA256(大写hex)+字节数逐项核对10个文件(工作目录 C:\projects\ft,python只读计算)。
5. 以最近一次全绿固定验证 `KVGameplayLoopTests/reward-fixed-runs/20260924T092707_*/result.json` 核对"43组168断言"基线(实测 success=true, groups=43, assertions=168,与方案一致)。
关键实证(对裁决最重要的一条):events.jsonl 时间线为 11:54:00.766 第4次special-skill-check(即BeginRewardEnding→CheckRewardContext→CheckRewardGuide 的审计)→ 11:54:00.768 fault("controller disabled")(即FishEndCore内DestroyUI(:1312)同步触发OnDisable(:3203)→CancelReward)→ 11:54:00.780 `fishing-reward-segment {segment: ending}`(using块仍正常Dispose,证明CancelReward只Fault不抛出、故障确发生在"ending"段内部的自拆除)。这把记录文档中"瞬时禁用来源尚未定位"的旧假说否定掉:禁用源就是完成路径自身的 FishEndCore 拆除,方案的诊断成立。
## 二、hash核对表(10项)
工作目录 C:\projects\ft;算法 SHA256,大写hex;字节数一并核对。
| # | 文件 | 结果 | 字节数 |
|---|---|---|---|
| 1 | Client/Assets/Scripts/Services/UserService.KVFishingReward.cs | 一致(3104B335…4101) | 20005/20005 |
| 2 | Client/Assets/Scripts/Services/UserService.KVFishingRewardValidation.cs | 一致(14827CA2…C6EA) | 41479/41479 |
| 3 | Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs | 一致(930D1FF0…B4A) | 40556/40556 |
| 4 | Client/Assets/Scripts/GampPlay/FishingB/FishingUpdateSys.cs | 一致(937EF7EC…0CA3) | 130654/130654 |
| 5 | Client/Assets/Scripts/UI/FishingReward/FishingRewardPanel.cs | 一致(5289088D…9789) | 58984/58984 |
| 6 | CodexOutput/upserver/阶段4C2第二次真实尝试与收线取消记录-2026-09-24.md | 一致(4A4D735F…AAF1) | 5232/5232 |
| 7 | CodexOutput/upserver/阶段4C2收线取消缺陷最小修复方案-待评审-2026-09-24.md | 一致(47CF3AFA…3FB1) | 4410/4410 |
| 8 | KVFishingRewardTests/diagnostic-impl/run62216-summary.json | 一致(49736290…BEDC) | 1924/1924 |
| 9 | KVFishingRewardTests/diagnostic-impl/run62216-final-state.json | 一致(F927149D…C27) | 847/847 |
| 10 | KVGameplayLoopTests/runs/62216_639258472117601499/events.jsonl | 一致(7F637600…43A) | 4231/4231 |
**10/10 全部一致。** 该清单同时把5个源文件当前状态钉死,可作为实施后"其余文件字节不变"判据的比对基准。
## 三、六个审查重点逐项回答
### 重点1:守卫是否精确覆盖且仅覆盖"FishEndCore拆除窗口"
拟议守卫插入位置(身份守卫 :284 之后、`a.Cancelled=true` :285 之前)正确;用 `ReferenceEquals`(而非Unity伪null==)对已销毁托管引用判定也正确。四种情形逐条核对:
- **(a) 战斗中(Ending=false)控制器disable**:新守卫因 `a.Ending==false` 不生效 → 原逻辑 `a.Cancelled=true` 且 `Ready && !a.Confirmed` → Fault(KVFishingReward.cs:285-286)。真实放弃保护保留,结论成立。但注意:现有43组中没有任何用例钉住这条(见重点4/问题P2-1)。
- **(b) Ending后Confirmed前的控制器disable/destroy(缺陷窗口)**:守卫返回、不取消。run 62216 实证该窗口内控制器disable的唯一已知来源就是 FishEndCore 自身的 DestroyUI(:1312)(堆栈+events.jsonl时序,见第一节)。全库仅两处销毁 FishingPanel:FishEndCore:1312 与 FishingDuelAct.cs:168;后者为对决路径,而对决在准入处即被拒(CheckRewardEnvironment :117-119 Stop),rewardAction 不会存在,不构成豁免漏洞。
- **(c) Confirmed后控制器disable**:守卫因 `!a.Confirmed` 不满足而不适用 → 照旧 `a.Cancelled=true`,且 :286 的 `!a.Confirmed` 使其不Fault——与既有用例"confirmed-normal-cancel-no-second-exit"(Validation :120-122,断言 Cancelled && fishingAction.Cancelled && Faults==0 && Ready)完全一致。该用例中 `CancelReward(rewardAction.Controller,"normal disable")` 正是 Controller 身份、但 Confirmed=true,守卫不改变其期望。结论成立。
- **(d) Panel与Consumption.Actor路径**:守卫要求 `ReferenceEquals(actor,a.Controller)`。Controller 是 FishingUpdateSys(BeginRewardDrawing(this),FishingUpdateSys.cs:531),Panel 是 FishingRewardPanel 实例,Consumption.Actor 是 FishingBehaviorB(BeginFishing(this),FishingBehaviorB.cs:767)——三者在真实游戏与验证harness中均为互不相同对象,守卫对 (d) 永不触发。既有"actual-panel-disable-pending"(:229-242)与"cancel-after-consume-before-reward-confirmed"(:116-119)不受影响。结论成立。
**结论:精确覆盖且仅覆盖缺陷窗口,四情形全部与方案论断一致。**
### 重点2:FishEnd开始后的真实退出/放弃路径,守卫是否错误放行
Ending=true 之后会调用 `CancelReward(Controller)` 的路径,全库grep仅两条:FishingUpdateSys OnDisable(:3203) 与 OnDestroy(:3253)——即被豁免的恰是完成路径自拆除本身。真实放弃不依赖这两钩子:
- **合法放弃主通道**:`CancelFishing(action)` → `CancelReward(action.Actor)`(KVGameplayLoop.cs:195)。actor=FishingBehaviorB ≠ Controller → 守卫不豁免 → 照旧取消(未Confirmed则Fault)。调用方:FishingBehaviorB.OnDisable/OnDestroy(:2439"disabled"/:2465"destroyed",场景/演出对象拆除、编辑器停止时触发)、抛竿tween被杀(:41,仅战斗期)。
- **QuitAct排水**:QuitAct.cs:35-38 `CloseAsync()` → `Revoke()`(Ready=false,KVGameplayLoop.cs:278-279)→ 在途 ConfirmRewardAsync 的 `!RewardCurrent(a)` 检查(:257/:266)失败 → Stop/Fault。用户退出到主页/登出在ending窗口内仍被拦截。
- **Panel路径**:FishingRewardPanel OnDisable(:36)/OnDestroy(:1180),不受守卫影响(重点1(d))。
**结论:不存在只经 FishingUpdateSys disable/destroy 表达的真实放弃场景;守卫不会错误放行任何真实退出。** 残余风险提示(非本方案缺陷):Ending=false 期间若存在非退出性的"瞬时禁用"控制器(记录文档的旧假说,本次实证未支持、堆栈已否定),仍会按4C2原语义取消+Fault——维持原状,未被实证,不在本次最小修复范围。
### 重点3:OnDisable/OnDestroy两钩子覆盖与帧尾Destroy时序
- 两钩子(:3203/:3253)都传 `this`(Controller),守卫对两者同样生效——**都覆盖,且都应覆盖**:UIManager.DestroyUI 用 `GameObject.Destroy`(UIManager.cs:938,帧尾销毁)。run 62216 堆栈与events时序证明 OnDisable 在 DestroyUI 调用内同步触发(.768 fault 先于 .780 段Dispose);OnDestroy 延迟到帧尾,同一帧内 Confirmed 不可能为 true——确认必须经 ConfirmRewardAsync 的 FlushKVAsync+票据等待(:253-270,30秒预算的真实网络往返),而确认链在 FishEndCore 返回、ShowUI 面板 OnShow(:347 BindRewardPanel) 之后才启动,物理上晚于帧尾。故正常流中两钩子均落在 `Ending && !Confirmed` 窗口内,守卫全覆盖。
- **假想边界**(OnDestroy晚于Confirm完成):守卫因 Confirmed=true 不适用 → 落入原逻辑置 Cancelled、不Fault(:286)——与既有"confirmed-normal-cancel-no-second-exit"钉住的"确认后取消不二次退出"语义逐字一致,不产生新行为。结论:与既有语义一致。
### 重点4:新增验证用例设计充分性与既有43组168断言影响排查
- **红**(现状:pre-confirm控制器disable→Cancelled+Fault):既有代码路径 :284-286 直接可推,且 run 62216 已是实证红。**绿**(守卫后:不取消、Ready保持、后续确认展示可完成):守卫后 Cancelled=false,RewardCurrent(:188)链路完整,BindRewardPanel→reward段→ConfirmRewardAsync→PresentReward 均可走完(见重点5)。红绿主边界成立。
- **Confirmed后语义**:既有组 :120-122 原样保留即可覆盖,方案已声明。**Panel未决语义**:既有组 :229-242 覆盖。**Consumption.Actor路径**:既有组 :116-119 覆盖(该组若守卫被误写宽到Consumption.Actor会失败,恰好是防回归点)。
- **未覆盖的边界(问题P2-1)**:`Ending=false` 时 Controller disable 仍取消+Fault——方案的核心保留承诺之一,但既有43组与新增组都没有钉住。逐一排查验证中全部 CancelReward/CancelFishing 调用点(:118 actor=Consumption.Actor;:121 Controller但Confirmed=true;:237 panel),确无任何用例以 Controller 身份在 Ending=false 时调用 CancelReward。若守卫被误实现(如漏写 `a.Ending` 或写成 `!a.Confirmed` 即返回),全量验证仍会全绿。应在同一Validation改动里补一组对照(如 `battle-controller-disable-still-cancels`:不置Ending,CancelReward(Controller)→断言 Cancelled && Faults==1 && !Ready)。
- **既有43组168断言**:如上逐点排查,三个调用点的actor身份/时序均不落入守卫,**零期望变化**。43/168基线经最近全绿result.json核实。
### 重点5:守卫生效后 ShowKVRewardPanelAsync/BindRewardPanel 流程可否走通
可以。逐步核对:FishEnd :1254 BeginRewardEnding 置 Ending=true(:172)→ using BeginRewardSegment("ending") FishEndCore() → DestroyUI(:1312) → OnDisable → 守卫返回(Cancelled保持false)→ FishEndCore KV分支 :1355 提前return(由wrapper开UI)→ using正常Dispose(events.jsonl .780已证明)→ ShowKVRewardPanelAsync(:1265) → ShowUI → 面板OnShow(:340) → BindRewardPanel(:347):要求 RewardCurrent(:188,!Cancelled ✓ 且 FishingCurrent(Consumption) ✓) 与 a.Ending ✓ → Applying/Panel置位 → "reward"段写入(:351-356) → ConfirmKVRewardAsync(:25) → ConfirmRewardAsync(Sealed→票据→Confirmed :271)→ PresentReward(:276):要求 Confirmed ✓ → 展示。所有门条件在守卫下均满足;run 62216 同场景修复后将走到面板而非fault。
### 重点6:静态diff判据与验收组合是否足够
足够且可机械化执行:(1) 全量固定验证重跑是行为门;(2) Editor 0错误 + SERVICE_HOST exit 0 是编译门(KVFishingReward.cs 整文件在 `#if UNITY_EDITOR && !SERVICE_HOST` 内,两态都需验证);(3) 静态diff最小性可对照本审查第二节的hash清单执行——未改动文件(FishingUpdateSys.cs/FishingRewardPanel.cs/KVGameplayLoop.cs等)必须保持清单中的hash不变。建议补充(P3-2):实施记录中固化修改后两文件的SHA256,并归档新增组的红测失败证据(修复前运行result),使红绿两侧都有落盘凭证。
## 四、问题列表
**P1(阻塞):无。**
**P2(应修,随拟改动2同一Validation改动落实):**
- P2-1 新增验证组缺少"战斗中(Ending=false)控制器disable仍取消+Fault"的对照用例。这是方案明示保留的真实放弃保护,目前无任何固定验证钉住;缺失时守卫若被误写宽(漏 `a.Ending` 条件等)全量验证无法发现。补一组 `battle-controller-disable-still-cancels`(构造rewardAction不置Ending → CancelReward(rewardAction.Controller,…) → 断言 Cancelled && Faults==1 && !Ready)。
**P3(建议,不阻塞):**
- P3-1 绿用例中在守卫豁免第一次disable后,再补一次 `CancelReward(rewardAction.Controller,"controller destroyed")` 模拟OnDestroy第二钩子(:3253),钉住两钩子同被覆盖(与OnDisable走同一守卫分支,风险低,成本一行)。
- P3-2 验收判据补充:实施记录固化修改后 `UserService.KVFishingReward.cs` 与 Validation 两文件的SHA256;归档新增组在修复前的红测失败result。
- P3-3 方案文字"BeginRewardEnding 成功后"在验证harness中不可直接照做:harness 的 rewardPlan 无 Data,真实 BeginRewardEnding 的 CheckRewardContext 会NRE;应按 Batch() 模式直接置 `rewardAction.Ending=true`(Validation :64),或按 :253/:295 组模式构造 rewardPlan.Data。实现细节,不影响设计。
- P3-4 方案对 KVFishingReward.cs 的个别行号引用有漂移(RewardCurrent 实为 :188 非 :183;BeginRewardSegment 实为 :203 非 :198;ConfirmRewardAsync 实为 :242-275;CancelReward 为 :281-287)。关键锚点(守卫位置、OnDisable:3203、OnDestroy:3253、DestroyUI:1312、KVGameplayLoop:195)全部准确,hash绑定为准,不影响结论。
## 五、最终结论
**有条件批准实施。** 条件(1项,P2-1):拟改动2的新增验证组必须同时包含"Ending=false控制器disable仍取消+Fault"的对照用例,与主红绿用例同批落盘。其余按方案执行:拟改动1的一行守卫经四情形、真实退出路径、双钩子时序、面板流程、既有43组逐点排查均验证正确;拟改动2按P3-1/P3-3建议微调实现方式。验收按方案四条执行,并以本审查hash清单作为"其余文件字节不变"的比对基准。不构成4C2关闭。
## 六、边界声明
- 本审查为静态只读审查+离线hash/证据核对,未运行Unity、未执行任何固定验证、未连接正式/测试服务器与账号;不构成对修复后行为的运行验证。
- 本审查的"批准实施"仅授权方案限定的两处代码改动进入实施流程;修复后的真实收鱼闭环、新进程恢复仍需用户另行授权,4C2/完整4C/G4均不因此关闭。
- run 62216 消耗的10点体力不补发/不退款/不重放的既有约束不变。