Files
ft/CodexOutput/upserver/阶段4C2扣费前拦截诊断方案独立审查-2026-09-24.md

16 KiB
Raw Blame History

阶段4C2扣费前拦截诊断方案独立审查

2026-09-24。独立评审agent输出。审查对象:《阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md》(sha256 2877E083…F249)。本审查为只读审查,除本报告外未写任何文件,未修改任何共享文档/TODO/开发日志/生产代码,未运行Unity,未连接正式服务器或测试账号。

一、审查范围与方法

1.1 读过并核对的文件

  • 方案与记录:《阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md》、《阶段4C2首次真实尝试与扣费前拦截记录-2026-09-24.md》、《其他AI审查提示词-4C2扣费前拦截诊断-2026-09-24.md》、《接手入口.md》(第1~60行当前状态与硬约束部分及全文关键词检索)。
  • 生产源码:Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs(第1~100行、第195~210行、第795~810行)、Client/Assets/Scripts/GampPlay/FishingB/FishingData.cs(全文)、Client/Assets/Scripts/Tables/Item.cs(ID/SubType声明)、Client/Assets/Scripts/Tables/FishData.cs(ID声明)、Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs(第180~424行)、Client/Assets/Scripts/Services/UserService.KVFishingReward.cs(PrepareFishingReward相关行)、Client/Assets/Scripts/Services/UserService.KVFishingValidation.cs(全文,Editor固定测试)。
  • 测试与证据:KVFishingRewardTests/ConsumeControlSlice.cs.txt、ConsumeControlTests.cs.txt、ConsumeControlTests.csproj、KVGameplayLoopTests/runs/42620_639258392922105429/events.jsonl、KVFishingRewardTests/real-attempt-final-diff.json、real-after-attempt-errors.json、real-login-reconciliation.json、real-final-idle-state.json、real-precast-conditions-retry.json、两份102备份(094234Z/094501Z)。
  • 独立验证:用powershell Get-FileHash逐项计算18项SHA256(大写hex)与字节数;用ConvertFrom-Json对两份102备份做45键逐值比对;全库检索固定错误串"Only one ordinary paid fishing action"确认无逻辑依赖该文本。

1.2 hash核对结果表(基目录 C:\projects\ft,Get-FileHash SHA256 大写hex + 字节数)

# 文件 期望hash(清单) 实算hash 期望/实算字节 结论
1 Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs 0A76743E…9BE8 一致 99010/99010 一致
2 Client/Assets/Scripts/GampPlay/FishingB/FishingData.cs 8C7DC3D3…0A4 一致 8230/8230 一致
3 Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs 930D1FF0…B4A 一致 40556/40556 一致
4 Client/Assets/Scripts/Services/UserService.KVFishingReward.cs 3104B335…D101 一致 20005/20005 一致
5 CodexOutput/upserver/阶段4C2扣费前拦截诊断最小方案-待评审-2026-09-24.md 2877E083…F249 一致 3020/3020 一致
6 CodexOutput/upserver/阶段4C2首次真实尝试与扣费前拦截记录-2026-09-24.md 75613397…3EB6 一致 4464/4464 一致
7 CodexOutput/upserver/其他AI审查提示词-4C2扣费前拦截诊断-2026-09-24.md EADC12D4…994F 一致 896/896 一致
8 CodexOutput/upserver/接手入口.md 79A0D1AC…2F58 一致 57695/57695 一致
9 CodexOutput/upserver/KVFishingRewardTests/real-after-attempt-errors.json 657BF9AD…61B4 一致 3530/3530 一致
10 CodexOutput/upserver/KVFishingRewardTests/real-attempt-final-diff.json 0BA88ED4…3203 一致 397/397 一致
11 CodexOutput/upserver/KVFishingRewardTests/real-login-reconciliation.json 2B116C7F…7E33 一致 905/905 一致
12 CodexOutput/upserver/KVFishingRewardTests/real-final-idle-state.json 0636C241…9456 一致 830/830 一致
13 CodexOutput/upserver/KVFishingRewardTests/real-precast-conditions-retry.json AF2A05F8…B3F1 一致 1236/1236 一致
14 CodexOutput/upserver/KVGameplayLoopTests/runs/42620_639258392922105429/events.jsonl F6DC9FF1…0B03 一致 1872/1872 一致
15 CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T094234Z/102.json A9391829…0A23 一致 14351/14351 一致
16 CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T094501Z/102.json 05CB8DF2…AACE 一致 14351/14351 一致
17 CodexOutput/upserver/其他AI开发接手说明-4C2扣费前拦截-2026-09-24.md 1E632D50…90F5 一致 7360/7360 一致
18 CodexOutput/upserver/其他AI开发接手提示词-4C2-2026-09-24.md 981CB6C5…B2AC 一致 1189/1189 一致

汇总:18/18 一致(hash与字节数均一致),0项不一致。清单自述的"handoffUpdate:原16项复验一致"与本轮18项全对结果相容。

二、对方案末尾四个问题的逐项回答

问题1:现有证据是否只能定位到合并门,尚不能确定具体谓词?

结论:同意。证据只能证明命中 FishingBehaviorB.cs:49~51 的合并if,不能区分七个析取条件中的哪一项为真。

证据:

  • 拦截点与消息唯一:events.jsonl 第11行(09:44:05.1516474Z)kind=fault, message="Only one ordinary paid fishing action is permitted.";该字符串在生产代码中仅出现一处——FishingBehaviorB.cs:51 的 throw ...Stop("Only one ordinary paid fishing action is permitted.")(全库grep确认,无其他产生点)。
  • 栈帧与当前源码逐行对齐:real-after-attempt-errors.json 第12行栈显示 ConsumeBaitOnKVAsync d__4:MoveNext (at FishingBehaviorB.cs:51) ← Stop (at UserService.KVGameplayLoop.cs:280) ← Fault (at UserService.KVGameplayLoop.cs:287) ← ConsumeBait (at FishingBehaviorB.cs:801)。当前源码实算:51行=throw+Stop、KVGameplayLoop.cs:280=Stop定义、:287=Fault内Debug.LogError、FishingBehaviorB.cs:801=ConsumeBait的KV分支调用,全部吻合(FishingBehaviorB.cs hash已对上清单,无事后改动)。
  • 早于扣费/奖励:门在49~51行,PrepareFishingReward 在62行、ConsumeEnergyForKVLoopAsync 在63行;异常在51行抛出后直达70行catch,62~68行未执行。旁证:events.jsonl中无 fishing-reward-preflight(仅由 UserService.KVFishingReward.cs:144 在 PrepareFishingReward 内审计)、无 fishing-consume-confirmed(仅由 UserService.KVGameplayLoop.cs:241 在 ConfirmFishingAsync 内审计)、09:40:42之后再无 write-attempt/accepted。
  • 无法回溯谓词输入:该消息为七个条件共用,real-after-attempt-errors.json 与 real-final-idle-state.json 均只含同一句合并消息(另3条为无关的PlayFab域名解析旧错误);Fault 后 Ready=false(KVGameplayLoop.cs:283),FishingCurrent 恒false(:190-191),运行已closed(events.jsonl:12),拒绝瞬间的 fishingData 快照无处可取。
  • 45键全同成立:real-attempt-final-diff.json 记 changedAfterAttempt=[]、keyCount=45、paidConsumption=false、fishReward=false;本审查另用 ConvertFrom-Json 独立逐值比对两份备份,data 下45键全部相同、后备份无新增键,唯一差异是顶层 at 时间戳(这解释了两备份字节同为14351而SHA256不同),"逐值相同"表述准确。

问题2:六个字段是否足够唯一解释七条件,且不需新增状态?

结论:足够。七个谓词都是这六个字段值的纯函数,读出六个值即可逐一判定七条件的真值;不需要新增状态。

证据(源码定义与对应关系):

  • fishItem==null(条件1)⟺ fishItem?.ID 为null标记。fishItem 为公共字段:FishingData.cs:32。
  • IsGeneralFish()(条件2取反)= fishItem.SubType == 1:FishingData.cs:172-175。即 fishItem?.SubType==1 与该谓词完全等价(由于 || 短路,此谓词仅在 fishItem 非null时才被求值)。
  • IsFree(条件3):公共bool字段,FishingData.cs:111。
  • IsRetry(条件4):getter return RetryCount>0,FishingData.cs:113-116。
  • IsDuel(条件5):公共bool字段,FishingData.cs:112。
  • IsPackItemFish()(条件6)= fishItem.SubType==21 || fishItem.SubType==22:FishingData.cs:188-191;其中21=BarginPack(:184-187)、22=PiggyBank(:180-183)。即 fishItem?.SubType∈{21,22} 与该谓词完全等价。
  • curFishData==null(条件7)⟺ curFishData?.ID 为null标记。curFishData 为公共字段:FishingData.cs:27。

字段类型与getter无副作用(问题3一并回答):Item.ID、Item.SubType 均为 public int { get; private set; } 自动属性(Tables/Item.cs:54、:71);FishData.ID 同(Tables/FishData.cs:156);IsFree/IsDuel 为简单字段。所以六字段全部是只读当前对象的简单成员读取,无新增状态。

说明两点(非缺陷):(a) fishItem?.ID 对七条件的判定并非必需(SubType已覆盖条件2/6),但能唯一定位物品行便于后续对照表配置,保留合理;(b) 当 SubType=21/22 时条件2与条件6同时为真、IsFree等亦可与其它条件并存——诊断文字报告的是状态全集,能推出每个谓词的真值,但不标识"哪个析取项最先短路",这是合并门的固有属性,不影响定位。

问题3:仅改变拒绝消息是否保持时序/副作用边界,是否有null/日志异常风险?

结论:保持。存在一个必须在实现中显式处理的null文本陷阱(见P2-1),其余无风险。

证据:

  • 时序:改动只替换51行throw中Stop(...)的字符串字面量为插值字符串。C#求值顺序为"先求实参、再调Stop",插值发生在谓词判定之后、Stop/Fault之前,全程同步、无await、无事件、无随机/选鱼/存档调用;49~50行谓词与throw/Stop结构不动,时序不变。
  • 副作用:全部读取为自动属性/纯字段/纯getter(见问题2);fishingData 自身在49行已被解引用,故到51行时必非null(若为null,消息将是NRE文本而非当前消息,已被实际运行排除)。
  • 日志通道:消息经 Stop→Fault(KVGameplayLoop.cs:280、281-290)单通道写入 events.jsonl(:286 Audit、:294单行JSON追加)与Console(:287 LogError)及Error属性(real-final-idle-state.json可见);70行catch再调Fault时被 Interlocked.Exchange(ref failed,1)(:284)去重,不会产生第二个fault事件。插值内容只有int/bool与固定"null"文本,无引号/换行,不会破坏events.jsonl的JSON单行结构;全库无任何代码按该消息文本做逻辑分支或断言匹配(既有提取测试断言只查 Faults/Calls 计数),改文字不破坏既有行为。
  • null安全:fishItem?.ID、fishItem?.SubType、curFishData?.ID 用 ?. 安全;IsFree/IsRetry/IsDuel 在非null的fishingData上直读安全。唯一陷阱:C#插值中 int? 为null时输出空串而非"null"($"{fishItem?.ID}" 在fishItem为null时打印空),必须显式处理才能兑现方案"null明确写null"的承诺(实现例:fishItem==null?"null":fishItem.ID.ToString() 或 (object)fishItem?.ID ?? "null")。若不处理,条件1/条件7与"空显示"将混淆,直接削弱本方案唯一目的。

问题4:编译及既有入口验证是否足够,是否批准这一个文件、这一处修改?

结论:验收组合足够、批准范围正确,但验收第3项需按P2-2补一步(提取测试shim同步),故整体为有条件批准。

证据:

  • "静态diff只有错误文字变化":对纯文字改动是恰当且接近充分的验收;建议明确diff判据为——49~50行谓词与全文件其余部分字节级不变,仅51行字符串字面量变化(见P3-1)。
  • "Editor和SERVICE_HOST编译":该分支位于 #if UNITY_EDITOR && !SERVICE_HOST(FishingBehaviorB.cs:27),Editor编译直接覆盖改动;SERVICE_HOST编译不含该分支,作为全局回归无害,两者都应做。
  • "利用既有消费入口提取测试核对拒绝仍先于扣费":该测试真实存在且恰好覆盖此门——KVFishingRewardTests/ConsumeControlTests.cs.txt 第10~18行对 free/retry/duel/pack/special/missing-fish/missing-item 等逐项断言 player.Calls==0 && Continues==0 && Faults>0("pre-consumption gate"),即七条件每一项单独触发时扣费调用次数为0;断言不匹配消息文本,改文字不破坏断言。但注意:该测试编译的 ConsumeControlSlice.cs.txt 是手工提取副本,其shim FishingData.fishItem 声明为 object 且无 ID/SubType 成员(ConsumeControlTests.cs.txt:38)——改动后重新提取的slice引用 fishItem?.ID/SubType 将无法编译,必须同步给shim补一个带 int ID; int SubType; 的Item类型并重跑(属既有测试维护,不违反"不新建框架")。
  • 批准范围:仅 FishingBehaviorB.cs 一处、仅原拒绝分支错误文字、不放宽准入、不新增日志点/框架/第二个Fault——与源码结构相容,可实施。

三、问题列表

P1(阻塞):无。

P2(应修,实施时必须处理):

  1. null文本插值陷阱:$"{fishItem?.ID}"/$"{curFishData?.ID}" 在对象为null时输出空串而非"null",无法兑现方案"null明确写null"。实现必须显式判null输出字面"null"(如 fishItem==null?"null":fishItem.ID.ToString() 或 (object)fishItem?.ID ?? "null"),并在静态diff中可核对该处理存在。
  2. 提取测试需同步:ConsumeControlSlice.cs.txt 须从改后源码重新提取,且 ConsumeControlTests.cs.txt:38 的shim FishingData.fishItem(现为 object)需补带 ID/SubType 的Item类型,否则验收第3项(既有消费入口提取测试)无法编译执行。此项属既有测试维护,不是新框架。

P3(建议):

  1. 验收"静态diff只有错误文字变化"建议写成可执行判据:diff仅涉及 FishingBehaviorB.cs 第51行字符串字面量,第49~50行谓词与文件其余部分字节级不变,无其他文件变更。
  2. 在方案或实施记录中注明诊断语义:六字段报告的是状态全集,可推出七条件各自真值,但不标识"哪个析取项最先短路"(SubType=21/22时条件2与6必然同真),避免后续读者误期待单一原因。
  3. 精确性备注:该门位于第48行 ClaimFishingConsumption(内存标记Consuming=true,非扣费、无审计事件)之后;"扣费前拦截"表述成立,但如后续文档复述顺序时可注明这一内存claim先于门发生。
  4. 两份102备份SHA256不同但45键逐值全同(仅顶层 at 时间戳不同、字节数相同),建议在证据文档注明,避免后人把hash差异误读为数据变化。

四、最终结论

有条件批准实施。条件(全部满足后方可编码合入):

  1. 仅修改 Client/Assets/Scripts/GampPlay/FishingB/FishingBehaviorB.cs 第51行Stop(...)的字符串实参:保留第49~50行合并if谓词与throw/Stop调用结构字节级不变;错误文字含六字段(fishItem?.ID、fishItem?.SubType、curFishData?.ID、IsFree、IsRetry、IsDuel),null显式输出"null"(按P2-1方式实现,禁止裸 ?.+插值依赖)。
  2. 不新增任何日志点/框架/文件/await/事件/第二个Fault;成功分支与其余门(offline/activity/bad-mag/context-change)文字不变。
  3. 验收按方案四项执行,其中提取测试按P2-2同步 ConsumeControlSlice.cs.txt 与shim并重跑通过(10类前置门断言含 player.Calls==0);静态diff按P3-1判据核对;Editor与SERVICE_HOST双编译通过。
  4. 编码完成后由主会话核hash、逐条裁决,再决定后续运行;本诊断不构成根因修复、不构成4C2验收、更不授权新的真实运行。

五、边界声明

  • 本审查基于2026-09-24冻结的hash清单(18/18一致)与当刻工作区状态,只做只读核对;未运行Unity、未连接正式服务器或测试账号、未执行任何写操作类命令(Get-FileHash/ConvertFrom-Json/diff/sha256sum/wc均为只读)。
  • 本审查不等于4C2关闭:4C2实施复核、真实收鱼奖励闭环、新进程恢复均未完成;4C2/完整4C/G4维持未关闭。
  • 本审查不构成对任何新的真实运行的授权;新的真实运行仍需按记录要求先核对新进程、备份与实际操作范围,并向用户明确一次尝试。