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

15 KiB
Raw Blame History

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