# 阶段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点体力不补发/不退款/不重放的既有约束不变。