16 KiB
阶段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):getterreturn 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是手工提取副本,其shimFishingData.fishItem声明为object且无 ID/SubType 成员(ConsumeControlTests.cs.txt:38)——改动后重新提取的slice引用fishItem?.ID/SubType将无法编译,必须同步给shim补一个带int ID; int SubType;的Item类型并重跑(属既有测试维护,不违反"不新建框架")。 - 批准范围:仅 FishingBehaviorB.cs 一处、仅原拒绝分支错误文字、不放宽准入、不新增日志点/框架/第二个Fault——与源码结构相容,可实施。
三、问题列表
P1(阻塞):无。
P2(应修,实施时必须处理):
- null文本插值陷阱:
$"{fishItem?.ID}"/$"{curFishData?.ID}"在对象为null时输出空串而非"null",无法兑现方案"null明确写null"。实现必须显式判null输出字面"null"(如fishItem==null?"null":fishItem.ID.ToString()或(object)fishItem?.ID ?? "null"),并在静态diff中可核对该处理存在。 - 提取测试需同步:
ConsumeControlSlice.cs.txt须从改后源码重新提取,且ConsumeControlTests.cs.txt:38的shimFishingData.fishItem(现为object)需补带ID/SubType的Item类型,否则验收第3项(既有消费入口提取测试)无法编译执行。此项属既有测试维护,不是新框架。
P3(建议):
- 验收"静态diff只有错误文字变化"建议写成可执行判据:diff仅涉及 FishingBehaviorB.cs 第51行字符串字面量,第49~50行谓词与文件其余部分字节级不变,无其他文件变更。
- 在方案或实施记录中注明诊断语义:六字段报告的是状态全集,可推出七条件各自真值,但不标识"哪个析取项最先短路"(SubType=21/22时条件2与6必然同真),避免后续读者误期待单一原因。
- 精确性备注:该门位于第48行
ClaimFishingConsumption(内存标记Consuming=true,非扣费、无审计事件)之后;"扣费前拦截"表述成立,但如后续文档复述顺序时可注明这一内存claim先于门发生。 - 两份102备份SHA256不同但45键逐值全同(仅顶层
at时间戳不同、字节数相同),建议在证据文档注明,避免后人把hash差异误读为数据变化。
四、最终结论
有条件批准实施。条件(全部满足后方可编码合入):
- 仅修改
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方式实现,禁止裸?.+插值依赖)。 - 不新增任何日志点/框架/文件/await/事件/第二个Fault;成功分支与其余门(offline/activity/bad-mag/context-change)文字不变。
- 验收按方案四项执行,其中提取测试按P2-2同步
ConsumeControlSlice.cs.txt与shim并重跑通过(10类前置门断言含player.Calls==0);静态diff按P3-1判据核对;Editor与SERVICE_HOST双编译通过。 - 编码完成后由主会话核hash、逐条裁决,再决定后续运行;本诊断不构成根因修复、不构成4C2验收、更不授权新的真实运行。
五、边界声明
- 本审查基于2026-09-24冻结的hash清单(18/18一致)与当刻工作区状态,只做只读核对;未运行Unity、未连接正式服务器或测试账号、未执行任何写操作类命令(Get-FileHash/ConvertFrom-Json/diff/sha256sum/wc均为只读)。
- 本审查不等于4C2关闭:4C2实施复核、真实收鱼奖励闭环、新进程恢复均未完成;4C2/完整4C/G4维持未关闭。
- 本审查不构成对任何新的真实运行的授权;新的真实运行仍需按记录要求先核对新进程、备份与实际操作范围,并向用户明确一次尝试。